dara-core 1.20.0a1__py3-none-any.whl → 1.20.1a1__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 +0 -3
- dara/core/actions.py +2 -1
- dara/core/auth/basic.py +16 -22
- dara/core/auth/definitions.py +2 -2
- dara/core/auth/routes.py +5 -5
- dara/core/auth/utils.py +5 -5
- dara/core/base_definitions.py +64 -22
- dara/core/cli.py +7 -8
- dara/core/configuration.py +2 -5
- dara/core/css.py +2 -1
- dara/core/data_utils.py +19 -18
- dara/core/defaults.py +7 -6
- dara/core/definitions.py +19 -50
- dara/core/http.py +3 -7
- dara/core/interactivity/__init__.py +0 -6
- dara/core/interactivity/actions.py +50 -52
- dara/core/interactivity/any_data_variable.py +134 -7
- dara/core/interactivity/any_variable.py +8 -5
- dara/core/interactivity/data_variable.py +266 -8
- dara/core/interactivity/derived_data_variable.py +290 -7
- dara/core/interactivity/derived_variable.py +174 -414
- dara/core/interactivity/filtering.py +27 -46
- dara/core/interactivity/loop_variable.py +2 -2
- dara/core/interactivity/non_data_variable.py +68 -5
- dara/core/interactivity/plain_variable.py +15 -89
- dara/core/interactivity/switch_variable.py +19 -19
- dara/core/interactivity/url_variable.py +90 -10
- dara/core/internal/cache_store/base_impl.py +1 -2
- dara/core/internal/cache_store/cache_store.py +25 -22
- dara/core/internal/cache_store/keep_all.py +1 -4
- dara/core/internal/cache_store/lru.py +1 -5
- dara/core/internal/cache_store/ttl.py +1 -4
- dara/core/internal/cgroup.py +1 -1
- dara/core/internal/dependency_resolution.py +66 -60
- dara/core/internal/devtools.py +5 -12
- dara/core/internal/download.py +4 -13
- dara/core/internal/encoder_registry.py +7 -7
- dara/core/internal/execute_action.py +13 -13
- dara/core/internal/hashing.py +3 -1
- dara/core/internal/import_discovery.py +4 -3
- dara/core/internal/normalization.py +18 -9
- dara/core/internal/pandas_utils.py +5 -107
- dara/core/internal/pool/definitions.py +1 -1
- dara/core/internal/pool/task_pool.py +16 -25
- dara/core/internal/pool/utils.py +18 -21
- dara/core/internal/pool/worker.py +2 -3
- dara/core/internal/port_utils.py +1 -1
- dara/core/internal/registries.py +6 -12
- dara/core/internal/registry.py +2 -4
- dara/core/internal/registry_lookup.py +5 -11
- dara/core/internal/routing.py +145 -109
- dara/core/internal/scheduler.py +8 -13
- dara/core/internal/settings.py +2 -2
- dara/core/internal/store.py +29 -2
- dara/core/internal/tasks.py +195 -379
- dara/core/internal/utils.py +13 -36
- dara/core/internal/websocket.py +20 -21
- dara/core/js_tooling/js_utils.py +26 -28
- dara/core/js_tooling/templates/vite.config.template.ts +3 -12
- dara/core/logging.py +12 -13
- dara/core/main.py +11 -14
- dara/core/metrics/cache.py +1 -1
- dara/core/metrics/utils.py +3 -3
- dara/core/persistence.py +5 -27
- dara/core/umd/dara.core.umd.js +55425 -59091
- dara/core/visual/components/__init__.py +2 -2
- dara/core/visual/components/fallback.py +4 -30
- dara/core/visual/components/for_cmp.py +1 -4
- dara/core/visual/css/__init__.py +31 -30
- dara/core/visual/dynamic_component.py +28 -31
- dara/core/visual/progress_updater.py +3 -4
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/METADATA +11 -12
- dara_core-1.20.1a1.dist-info/RECORD +114 -0
- dara/core/interactivity/client_variable.py +0 -71
- dara/core/interactivity/server_variable.py +0 -325
- dara/core/interactivity/state_variable.py +0 -69
- dara/core/interactivity/tabular_variable.py +0 -94
- dara/core/internal/multi_resource_lock.py +0 -70
- dara_core-1.20.0a1.dist-info/RECORD +0 -119
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/LICENSE +0 -0
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/WHEEL +0 -0
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/entry_points.txt +0 -0
|
@@ -19,24 +19,19 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
21
|
import uuid
|
|
22
|
-
from collections.abc import Awaitable
|
|
23
22
|
from inspect import Parameter, signature
|
|
24
23
|
from typing import (
|
|
25
24
|
Any,
|
|
25
|
+
Awaitable,
|
|
26
26
|
Callable,
|
|
27
27
|
Generic,
|
|
28
28
|
List,
|
|
29
29
|
Optional,
|
|
30
|
-
Protocol,
|
|
31
|
-
Tuple,
|
|
32
30
|
TypeVar,
|
|
33
31
|
Union,
|
|
34
32
|
cast,
|
|
35
33
|
)
|
|
36
34
|
|
|
37
|
-
import anyio
|
|
38
|
-
from cachetools import LRUCache
|
|
39
|
-
from pandas import DataFrame
|
|
40
35
|
from pydantic import (
|
|
41
36
|
ConfigDict,
|
|
42
37
|
Field,
|
|
@@ -45,7 +40,7 @@ from pydantic import (
|
|
|
45
40
|
field_validator,
|
|
46
41
|
model_serializer,
|
|
47
42
|
)
|
|
48
|
-
from typing_extensions import TypedDict
|
|
43
|
+
from typing_extensions import TypedDict
|
|
49
44
|
|
|
50
45
|
from dara.core.base_definitions import (
|
|
51
46
|
BaseCachePolicy,
|
|
@@ -53,17 +48,14 @@ from dara.core.base_definitions import (
|
|
|
53
48
|
Cache,
|
|
54
49
|
CacheArgType,
|
|
55
50
|
CachedRegistryEntry,
|
|
56
|
-
NonTabularDataError,
|
|
57
51
|
PendingTask,
|
|
52
|
+
PendingValue,
|
|
58
53
|
)
|
|
59
54
|
from dara.core.interactivity.actions import TriggerVariable, assert_no_context
|
|
60
55
|
from dara.core.interactivity.any_variable import AnyVariable
|
|
61
|
-
from dara.core.interactivity.
|
|
62
|
-
from dara.core.interactivity.filtering import FilterQuery, Pagination, apply_filters
|
|
56
|
+
from dara.core.interactivity.non_data_variable import NonDataVariable
|
|
63
57
|
from dara.core.internal.cache_store import CacheStore
|
|
64
58
|
from dara.core.internal.encoder_registry import deserialize
|
|
65
|
-
from dara.core.internal.multi_resource_lock import MultiResourceLock
|
|
66
|
-
from dara.core.internal.pandas_utils import DataResponse, append_index, build_data_response
|
|
67
59
|
from dara.core.internal.tasks import MetaTask, Task, TaskManager
|
|
68
60
|
from dara.core.internal.utils import get_cache_scope, run_user_handler
|
|
69
61
|
from dara.core.logging import dev_logger, eng_logger
|
|
@@ -71,44 +63,13 @@ from dara.core.metrics import RUNTIME_METRICS_TRACKER
|
|
|
71
63
|
|
|
72
64
|
VariableType = TypeVar('VariableType')
|
|
73
65
|
|
|
74
|
-
# Static lock for all DV computations, keyed by cache_key
|
|
75
|
-
# Explicitly not re-entrant, this prevents variable loops
|
|
76
|
-
DV_LOCK = MultiResourceLock()
|
|
77
|
-
|
|
78
|
-
# Global set to track force keys that have been encountered
|
|
79
|
-
# LRU with 2048 entries should be sufficient to not drop in-progress force keys
|
|
80
|
-
# but also not have to worry about memory leaks
|
|
81
|
-
_force_keys_seen: LRUCache[str, bool] = LRUCache(maxsize=2048)
|
|
82
|
-
|
|
83
|
-
VALUE_MISSING = object()
|
|
84
|
-
"""
|
|
85
|
-
Sentinel value to indicate that a value is missing from the cache
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
66
|
|
|
89
67
|
class DerivedVariableResult(TypedDict):
|
|
90
68
|
cache_key: str
|
|
91
69
|
value: Union[Any, BaseTask]
|
|
92
70
|
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
class FilterResolver(Protocol):
|
|
96
|
-
async def __call__(
|
|
97
|
-
self, data: Any, filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
|
|
98
|
-
) -> Tuple[DataFrame, int]: ...
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
async def default_filter_resolver(
|
|
102
|
-
data: Any, filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
|
|
103
|
-
) -> Tuple[DataFrame, int]:
|
|
104
|
-
if not isinstance(data, DataFrame):
|
|
105
|
-
raise NonTabularDataError(
|
|
106
|
-
f'Default filter resolver expects a DataFrame to be returned from the DerivedVariable function, got {type(data)}'
|
|
107
|
-
)
|
|
108
|
-
return apply_filters(data, filters, pagination)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
72
|
+
class DerivedVariable(NonDataVariable, Generic[VariableType]):
|
|
112
73
|
"""
|
|
113
74
|
A DerivedVariable allows a value to be derived (via a function) from the current value of a set of other
|
|
114
75
|
variables with a python function. This is one of two primary ways that python logic can be embedded into the
|
|
@@ -117,61 +78,6 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
117
78
|
DerivedVariables can be chained together to form complex data flows whilst keeping everything organized and
|
|
118
79
|
structured in an easy to follow way. DerivedVariable results are cached automatically and will only be
|
|
119
80
|
recalculated when necessary.
|
|
120
|
-
|
|
121
|
-
As a special case, DerivedVariables can be used for tabular data and retrieving its slice as a DataFrame. This functionality
|
|
122
|
-
is utilized by e.g. the built-in Table component. By default, when passing a DerivedVariable to a Table component, Dara
|
|
123
|
-
expects the resolver function to return a DataFrame or None. This behaviour can be customized by providing a custom `filter_resolver`.
|
|
124
|
-
This function will be invoked with the result of the main DerivedVariable function, as well as filters and pagination. It can be used
|
|
125
|
-
to e.g. retrieve a slice of data from an API endpoint or a database instead of retrieving the entire dataset and filtering it in-memory.
|
|
126
|
-
|
|
127
|
-
```python
|
|
128
|
-
from typing import Optional
|
|
129
|
-
import httpx
|
|
130
|
-
import pandas as pd
|
|
131
|
-
from dara.core import DerivedVariable, Variable
|
|
132
|
-
from dara.core.interactivity.filtering import FilterQuery, Pagination
|
|
133
|
-
|
|
134
|
-
# Custom filter resolver for API-based filtering
|
|
135
|
-
async def api_filter_resolver(data, filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None):
|
|
136
|
-
async with httpx.AsyncClient() as client:
|
|
137
|
-
# in this case data is a string url
|
|
138
|
-
response = await client.get(data, params={
|
|
139
|
-
# translates filters/pagination to API-specific query params
|
|
140
|
-
'filters': filters.dict() if filters else {},
|
|
141
|
-
'offset': pagination.offset if pagination else 0,
|
|
142
|
-
'limit': pagination.limit if pagination else 50
|
|
143
|
-
})
|
|
144
|
-
data = response.json()
|
|
145
|
-
# conform to the filter resolver API, return a tuple of (DataFrame, total_count)
|
|
146
|
-
return pd.DataFrame(data['results']), data['total_count']
|
|
147
|
-
|
|
148
|
-
# DerivedVariable with custom filtering
|
|
149
|
-
user_params = Variable({'dataset': 'experiments'})
|
|
150
|
-
derived_data = DerivedVariable(
|
|
151
|
-
lambda params: f"https://api.example.com/data/{params['dataset']}",
|
|
152
|
-
variables=[user_params],
|
|
153
|
-
filter_resolver=api_filter_resolver
|
|
154
|
-
)
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
:param func: the function to derive a new value from the input variables.
|
|
158
|
-
:param variables: a set of input variables that will be passed to the deriving function
|
|
159
|
-
:param cache: whether to cache the result, defaults to global caching. Other options are to cache per user
|
|
160
|
-
session, per user or to not cache at all
|
|
161
|
-
:param run_as_task: whether to run the calculation in a separate process, recommended for any CPU intensive
|
|
162
|
-
tasks, defaults to False
|
|
163
|
-
:param polling_interval: an optional polling interval for the DerivedVariable. Setting this will cause the
|
|
164
|
-
component to poll the backend and refresh itself every n seconds.
|
|
165
|
-
:param filter_resolver: an optional function to resolve the filter query for the derived variable. This can be
|
|
166
|
-
used to customize the way tabular data is resolved. This is invoked with the result of the main DerivedVariable function,
|
|
167
|
-
as well as filters and pagination. The function should return a DataFrame and total count.
|
|
168
|
-
:param deps: an optional array of variables, specifying which dependant variables changing should trigger a
|
|
169
|
-
recalculation of the derived variable
|
|
170
|
-
- `deps = None` - `func` is ran everytime (default behaviour),
|
|
171
|
-
- `deps = []` - `func` is ran once on initial startup,
|
|
172
|
-
- `deps = [var1, var2]` - `func` is ran whenever one of these vars changes
|
|
173
|
-
- `deps = [var1.get('nested_property')]` - `func` is ran only when the nested property changes, other changes to the variable are ignored
|
|
174
|
-
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
175
81
|
"""
|
|
176
82
|
|
|
177
83
|
cache: Optional[BaseCachePolicy]
|
|
@@ -180,11 +86,11 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
180
86
|
deps: Optional[List[AnyVariable]] = Field(validate_default=True)
|
|
181
87
|
nested: List[str] = Field(default_factory=list)
|
|
182
88
|
uid: str
|
|
183
|
-
model_config = ConfigDict(extra='forbid', use_enum_values=True
|
|
89
|
+
model_config = ConfigDict(extra='forbid', use_enum_values=True)
|
|
184
90
|
|
|
185
91
|
def __init__(
|
|
186
92
|
self,
|
|
187
|
-
func:
|
|
93
|
+
func: Callable[..., VariableType] | Callable[..., Awaitable[VariableType]],
|
|
188
94
|
variables: List[AnyVariable],
|
|
189
95
|
cache: Optional[CacheArgType] = Cache.Type.GLOBAL,
|
|
190
96
|
run_as_task: bool = False,
|
|
@@ -192,26 +98,36 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
192
98
|
deps: Optional[List[AnyVariable]] = None,
|
|
193
99
|
uid: Optional[str] = None,
|
|
194
100
|
nested: Optional[List[str]] = None,
|
|
195
|
-
filter_resolver: Optional[FilterResolver] = None,
|
|
196
101
|
_get_value: Optional[Callable[..., Awaitable[Any]]] = None,
|
|
197
|
-
_get_tabular_data: Optional[Callable[..., Union[Awaitable[DataResponse], Awaitable[MetaTask]]]] = None,
|
|
198
102
|
):
|
|
103
|
+
"""
|
|
104
|
+
A DerivedVariable allows a value to be derived (via a function) from the current value of a set of other
|
|
105
|
+
variables with a python function. This is one of two primary ways that python logic can be embedded into the
|
|
106
|
+
application (the other being the @py_component decorator).
|
|
107
|
+
|
|
108
|
+
DerivedVariables can be chained together to form complex data flows whilst keeping everything organized and
|
|
109
|
+
structured in an easy to follow way. DerivedVariable results are cached automatically and will only be
|
|
110
|
+
recalculated when necessary.
|
|
111
|
+
|
|
112
|
+
:param func: the function to derive a new value from the input variables.
|
|
113
|
+
:param variables: a set of input variables that will be passed to the deriving function
|
|
114
|
+
:param cache: whether to cache the result, defaults to global caching. Other options are to cache per user
|
|
115
|
+
session, per user or to not cache at all
|
|
116
|
+
:param run_as_task: whether to run the calculation in a separate process, recommended for any CPU intensive
|
|
117
|
+
tasks, defaults to False
|
|
118
|
+
:param polling_interval: an optional polling interval for the DerivedVariable. Setting this will cause the
|
|
119
|
+
component to poll the backend and refresh itself every n seconds.
|
|
120
|
+
:param deps: an optional array of variables, specifying which dependant variables changing should trigger a
|
|
121
|
+
recalculation of the derived variable
|
|
122
|
+
- `deps = None` - `func` is ran everytime (default behaviour),
|
|
123
|
+
- `deps = []` - `func` is ran once on initial startup,
|
|
124
|
+
- `deps = [var1, var2]` - `func` is ran whenever one of these vars changes
|
|
125
|
+
- `deps = [var1.get('nested_property')]` - `func` is ran only when the nested property changes, other changes to the variable are ignored
|
|
126
|
+
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
127
|
+
"""
|
|
199
128
|
if nested is None:
|
|
200
129
|
nested = []
|
|
201
130
|
|
|
202
|
-
# Validate that StateVariables are not used as inputs
|
|
203
|
-
from dara.core.interactivity.state_variable import StateVariable
|
|
204
|
-
|
|
205
|
-
for var in variables:
|
|
206
|
-
if isinstance(var, StateVariable):
|
|
207
|
-
raise ValueError(
|
|
208
|
-
'StateVariable cannot be used as input to DerivedVariable. '
|
|
209
|
-
'StateVariables are internal variables for tracking DerivedVariable states '
|
|
210
|
-
'and using them as inputs would create complex dependencies that are '
|
|
211
|
-
'difficult to debug. Consider using the parent DerivedVariable directly instead,'
|
|
212
|
-
' or use the StateVariable with an If component or SwitchVariable.'
|
|
213
|
-
)
|
|
214
|
-
|
|
215
131
|
if cache is not None:
|
|
216
132
|
cache = Cache.Policy.from_arg(cache)
|
|
217
133
|
|
|
@@ -230,12 +146,7 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
230
146
|
raise RuntimeError('run_as_task is not supported within a Jupyter environment')
|
|
231
147
|
|
|
232
148
|
super().__init__(
|
|
233
|
-
cache=cache,
|
|
234
|
-
uid=uid,
|
|
235
|
-
variables=variables,
|
|
236
|
-
polling_interval=polling_interval,
|
|
237
|
-
deps=deps,
|
|
238
|
-
nested=nested,
|
|
149
|
+
cache=cache, uid=uid, variables=variables, polling_interval=polling_interval, deps=deps, nested=nested
|
|
239
150
|
)
|
|
240
151
|
|
|
241
152
|
# Import the registry of variables and register the function at import
|
|
@@ -254,14 +165,12 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
254
165
|
DerivedVariableRegistryEntry(
|
|
255
166
|
cache=cache,
|
|
256
167
|
func=func,
|
|
257
|
-
filter_resolver=filter_resolver,
|
|
258
168
|
polling_interval=polling_interval,
|
|
259
169
|
run_as_task=run_as_task,
|
|
260
170
|
uid=str(self.uid),
|
|
261
171
|
variables=variables,
|
|
262
172
|
deps=deps_indexes,
|
|
263
173
|
get_value=_get_value or DerivedVariable.get_value,
|
|
264
|
-
get_tabular_data=_get_tabular_data or DerivedVariable.get_tabular_data,
|
|
265
174
|
),
|
|
266
175
|
)
|
|
267
176
|
|
|
@@ -289,39 +198,6 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
289
198
|
assert_no_context('ctx.trigger')
|
|
290
199
|
return TriggerVariable(variable=self, force=force)
|
|
291
200
|
|
|
292
|
-
@property
|
|
293
|
-
def is_loading(self):
|
|
294
|
-
"""
|
|
295
|
-
Get a StateVariable that tracks the loading state of this DerivedVariable.
|
|
296
|
-
|
|
297
|
-
:return: StateVariable that is True when this DerivedVariable is loading, False otherwise
|
|
298
|
-
"""
|
|
299
|
-
from dara.core.interactivity.state_variable import StateVariable
|
|
300
|
-
|
|
301
|
-
return StateVariable(parent_variable=self, property_name='loading')
|
|
302
|
-
|
|
303
|
-
@property
|
|
304
|
-
def has_error(self):
|
|
305
|
-
"""
|
|
306
|
-
Get a StateVariable that tracks the error state of this DerivedVariable.
|
|
307
|
-
|
|
308
|
-
:return: StateVariable that is True when this DerivedVariable has an error, False otherwise
|
|
309
|
-
"""
|
|
310
|
-
from dara.core.interactivity.state_variable import StateVariable
|
|
311
|
-
|
|
312
|
-
return StateVariable(parent_variable=self, property_name='error')
|
|
313
|
-
|
|
314
|
-
@property
|
|
315
|
-
def has_value(self):
|
|
316
|
-
"""
|
|
317
|
-
Get a StateVariable that tracks whether this DerivedVariable has a resolved value.
|
|
318
|
-
|
|
319
|
-
:return: StateVariable that is True when this DerivedVariable has a value, False otherwise
|
|
320
|
-
"""
|
|
321
|
-
from dara.core.interactivity.state_variable import StateVariable
|
|
322
|
-
|
|
323
|
-
return StateVariable(parent_variable=self, property_name='hasValue')
|
|
324
|
-
|
|
325
201
|
@staticmethod
|
|
326
202
|
def _get_cache_key(*args, uid: str, deps: Optional[List[int]] = None):
|
|
327
203
|
"""
|
|
@@ -332,17 +208,15 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
332
208
|
:param uid: uid of a DerivedVariable
|
|
333
209
|
:param deps: list of indexes of dependencies
|
|
334
210
|
"""
|
|
335
|
-
from dara.core.internal.dependency_resolution import clean_force_key
|
|
336
|
-
|
|
337
211
|
key = f'{uid}'
|
|
338
212
|
|
|
339
213
|
filtered_args = [arg for idx, arg in enumerate(args) if idx in deps] if deps is not None else args
|
|
340
214
|
|
|
341
|
-
for
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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}'
|
|
346
220
|
return key
|
|
347
221
|
|
|
348
222
|
@staticmethod
|
|
@@ -395,8 +269,7 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
395
269
|
if not latest_value_registry.has(var_entry.uid):
|
|
396
270
|
# Keep latest entry per scope (user,session); if cache_type is None, use GLOBAL
|
|
397
271
|
reg_entry = LatestValueRegistryEntry(
|
|
398
|
-
uid=var_entry.uid,
|
|
399
|
-
cache=Cache.Policy.MostRecent(cache_type=cache_type or Cache.Type.GLOBAL),
|
|
272
|
+
uid=var_entry.uid, cache=Cache.Policy.MostRecent(cache_type=cache_type or Cache.Type.GLOBAL)
|
|
400
273
|
)
|
|
401
274
|
latest_value_registry.register(var_entry.uid, reg_entry)
|
|
402
275
|
else:
|
|
@@ -412,8 +285,7 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
412
285
|
store: CacheStore,
|
|
413
286
|
task_mgr: TaskManager,
|
|
414
287
|
args: List[Any],
|
|
415
|
-
|
|
416
|
-
_pin_result: bool = False,
|
|
288
|
+
force: bool = False,
|
|
417
289
|
) -> DerivedVariableResult:
|
|
418
290
|
"""
|
|
419
291
|
Get the value of this DerivedVariable. This method will check the main app store for an appropriate response
|
|
@@ -424,19 +296,11 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
424
296
|
:param store: the store instance to check for cached values
|
|
425
297
|
:param task_mgr: task manager instance
|
|
426
298
|
:param args: the arguments to call the underlying function with
|
|
427
|
-
:param
|
|
428
|
-
:param _pin_result: whether to pin the result in the store, used internally by derived data variables
|
|
299
|
+
:param force: whether to ignore cache
|
|
429
300
|
"""
|
|
430
|
-
# dynamic import due to circular import
|
|
431
|
-
from dara.core.internal.dependency_resolution import (
|
|
432
|
-
is_forced,
|
|
433
|
-
resolve_dependency,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
301
|
assert var_entry.func is not None, 'DerivedVariable function is not defined'
|
|
437
302
|
|
|
438
|
-
|
|
439
|
-
_uid_short = f'{var_entry.uid[:3]}..{var_entry.uid[-3:]}'
|
|
303
|
+
histogram = RUNTIME_METRICS_TRACKER.get_dv_histogram(var_entry.uid)
|
|
440
304
|
|
|
441
305
|
if var_entry.run_as_task:
|
|
442
306
|
from dara.core.internal.registries import utils_registry
|
|
@@ -446,272 +310,175 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
446
310
|
'Task module is not configured. Set config.task_module path to a tasks.py module to run a derived variable as task.'
|
|
447
311
|
)
|
|
448
312
|
|
|
449
|
-
|
|
450
|
-
|
|
313
|
+
with histogram.time():
|
|
314
|
+
# Shortened UID used for logging
|
|
315
|
+
_uid_short = f'{var_entry.uid[:3]}..{var_entry.uid[-3:]}'
|
|
451
316
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
histogram = RUNTIME_METRICS_TRACKER.get_dv_histogram(var_entry.uid)
|
|
317
|
+
# Extract and process nested derived variables
|
|
318
|
+
values = []
|
|
455
319
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
320
|
+
# dynamic import due to circular import
|
|
321
|
+
from dara.core.internal.dependency_resolution import (
|
|
322
|
+
is_resolved_derived_variable,
|
|
323
|
+
resolve_dependency,
|
|
324
|
+
)
|
|
459
325
|
|
|
460
|
-
|
|
461
|
-
f'Derived Variable {_uid_short} get_value',
|
|
462
|
-
{'uid': var_entry.uid, 'args': args},
|
|
463
|
-
)
|
|
326
|
+
eng_logger.info(f'Derived Variable {_uid_short} get_value', {'uid': var_entry.uid, 'args': args})
|
|
464
327
|
|
|
465
|
-
|
|
466
|
-
|
|
328
|
+
for val in args:
|
|
329
|
+
# Don't force nested DVs
|
|
330
|
+
if is_resolved_derived_variable(val):
|
|
331
|
+
val['force'] = False
|
|
467
332
|
|
|
468
|
-
|
|
469
|
-
|
|
333
|
+
var_value = await resolve_dependency(val, store, task_mgr)
|
|
334
|
+
values.append(var_value)
|
|
470
335
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
values[index] = var_value
|
|
336
|
+
eng_logger.debug(
|
|
337
|
+
f'DerivedVariable {_uid_short}', 'resolved arguments', {'values': values, 'uid': var_entry.uid}
|
|
338
|
+
)
|
|
475
339
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
340
|
+
# Loop over the passed arguments and if the expected type is a BaseModel and arg is a dict then convert the dict
|
|
341
|
+
# to an instance of the BaseModel class.
|
|
342
|
+
parsed_args = DerivedVariable._restore_pydantic_models(var_entry.func, *values)
|
|
479
343
|
|
|
480
|
-
|
|
481
|
-
f'DerivedVariable {_uid_short}',
|
|
482
|
-
'resolved arguments',
|
|
483
|
-
{'values': values, 'uid': var_entry.uid},
|
|
484
|
-
)
|
|
344
|
+
dev_logger.debug(f'DerivedVariable {_uid_short}', 'executing', {'args': parsed_args, 'uid': var_entry.uid})
|
|
485
345
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
parsed_args = DerivedVariable._restore_pydantic_models(var_entry.func, *values)
|
|
346
|
+
# Check if there are any Tasks to be run in the args
|
|
347
|
+
has_tasks = any(isinstance(arg, BaseTask) for arg in parsed_args)
|
|
489
348
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
'executing',
|
|
493
|
-
{'args': parsed_args, 'uid': var_entry.uid},
|
|
494
|
-
)
|
|
349
|
+
cache_key = DerivedVariable._get_cache_key(*args, uid=var_entry.uid, deps=var_entry.deps)
|
|
350
|
+
await DerivedVariable.add_latest_value(store, var_entry, cache_key)
|
|
495
351
|
|
|
496
|
-
|
|
497
|
-
has_tasks = any(isinstance(arg, BaseTask) for arg in parsed_args)
|
|
498
|
-
|
|
499
|
-
await DerivedVariable.add_latest_value(store, var_entry, cache_key)
|
|
500
|
-
|
|
501
|
-
cache_type = var_entry.cache
|
|
502
|
-
|
|
503
|
-
# Handle force key tracking to prevent double execution
|
|
504
|
-
effective_force = force_key is not None
|
|
505
|
-
if force_key is not None:
|
|
506
|
-
if force_key in _force_keys_seen:
|
|
507
|
-
# This force key has been seen before, don't force again
|
|
508
|
-
effective_force = False
|
|
509
|
-
eng_logger.debug(
|
|
510
|
-
f'DerivedVariable {_uid_short} force key already seen, using cached value',
|
|
511
|
-
extra={'uid': var_entry.uid, 'force_key': force_key},
|
|
512
|
-
)
|
|
513
|
-
else:
|
|
514
|
-
# First time seeing this force key, add it to the set
|
|
515
|
-
_force_keys_seen[force_key] = True
|
|
516
|
-
eng_logger.debug(
|
|
517
|
-
f'DerivedVariable {_uid_short} new force key, will force recalculation',
|
|
518
|
-
extra={'uid': var_entry.uid, 'force_key': force_key},
|
|
519
|
-
)
|
|
352
|
+
cache_type = var_entry.cache
|
|
520
353
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
354
|
+
# If deps is not None, force session use
|
|
355
|
+
# Note: this is temporarily commented out as no tests were broken by removing it;
|
|
356
|
+
# once we find what scenario this fixes, we should add a test to cover that scenario and move this snippet
|
|
357
|
+
# to constructors of DerivedVariable and DerivedDataVariable
|
|
358
|
+
# if cache_type == CacheType.GLOBAL and (var_entry.deps is not None and len(var_entry.deps) > 0):
|
|
359
|
+
# cache_type = CacheType.SESSION
|
|
360
|
+
|
|
361
|
+
eng_logger.debug(
|
|
362
|
+
f'DerivedVariable {_uid_short}',
|
|
363
|
+
f'using cache: {cache_type}',
|
|
364
|
+
{'uid': var_entry.uid},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
ignore_cache = (
|
|
368
|
+
var_entry.cache is None
|
|
369
|
+
or var_entry.polling_interval
|
|
370
|
+
or DerivedVariable.check_polling(var_entry.variables)
|
|
371
|
+
)
|
|
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
|
+
)
|
|
379
|
+
|
|
380
|
+
# If it's a PendingTask then return that task so it can be awaited later by a MetaTask
|
|
381
|
+
if isinstance(value, PendingTask):
|
|
382
|
+
eng_logger.info(
|
|
383
|
+
f'DerivedVariable {_uid_short} waiting for pending task',
|
|
384
|
+
{'uid': var_entry.uid, 'pending_task': value.task_id},
|
|
525
385
|
)
|
|
386
|
+
value.add_subscriber()
|
|
387
|
+
return {'cache_key': cache_key, 'value': value}
|
|
526
388
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
ignore_cache = (
|
|
533
|
-
var_entry.cache is None
|
|
534
|
-
or var_entry.polling_interval
|
|
535
|
-
or DerivedVariable.check_polling(var_entry.variables)
|
|
536
|
-
or effective_force
|
|
537
|
-
or has_forced_child
|
|
389
|
+
# If it's a PendingValue then wait for the value and return it
|
|
390
|
+
if isinstance(value, PendingValue):
|
|
391
|
+
eng_logger.info(
|
|
392
|
+
f'DerivedVariable {_uid_short} waiting for pending value',
|
|
393
|
+
{'uid': var_entry.uid, 'pending_value': value},
|
|
538
394
|
)
|
|
539
|
-
|
|
540
|
-
try:
|
|
541
|
-
value = await store.get(var_entry, key=cache_key, raise_for_missing=True)
|
|
542
|
-
eng_logger.debug(
|
|
543
|
-
f'DerivedVariable {_uid_short}',
|
|
544
|
-
'retrieved value from cache',
|
|
545
|
-
{'uid': var_entry.uid, 'cached_value': value},
|
|
546
|
-
)
|
|
547
|
-
except KeyError:
|
|
548
|
-
eng_logger.debug(
|
|
549
|
-
f'DerivedVariable {_uid_short}',
|
|
550
|
-
'no value found in cache',
|
|
551
|
-
{'uid': var_entry.uid},
|
|
552
|
-
)
|
|
553
|
-
# key error means no entry found;
|
|
554
|
-
# this lets us distinguish from a None value stored and not found
|
|
555
|
-
|
|
556
|
-
# If it's a PendingTask then return that task so it can be awaited later by a MetaTask
|
|
557
|
-
if isinstance(value, PendingTask):
|
|
558
|
-
eng_logger.info(
|
|
559
|
-
f'DerivedVariable {_uid_short} waiting for pending task',
|
|
560
|
-
{'uid': var_entry.uid, 'pending_task': value.task_id},
|
|
561
|
-
)
|
|
562
|
-
return {'cache_key': cache_key, 'value': value}
|
|
395
|
+
return {'cache_key': cache_key, 'value': await store.get_or_wait(var_entry, key=cache_key)}
|
|
563
396
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
extra_notify_channels = [
|
|
580
|
-
channel
|
|
581
|
-
for arg in parsed_args
|
|
582
|
-
if isinstance(arg, BaseTask)
|
|
583
|
-
for channel in arg.notify_channels
|
|
584
|
-
]
|
|
585
|
-
eng_logger.debug(
|
|
586
|
-
f'DerivedVariable {_uid_short}',
|
|
587
|
-
'running has tasks',
|
|
588
|
-
{'uid': var_entry.uid, 'task_id': task_id},
|
|
589
|
-
)
|
|
590
|
-
meta_task = MetaTask(
|
|
591
|
-
var_entry.func,
|
|
592
|
-
parsed_args,
|
|
593
|
-
notify_channels=list(set(extra_notify_channels)),
|
|
594
|
-
process_as_task=var_entry.run_as_task,
|
|
595
|
-
cache_key=cache_key,
|
|
596
|
-
task_id=task_id,
|
|
597
|
-
reg_entry=var_entry, # task results are set as the DV result
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
# Immediately store the pending task in the store
|
|
601
|
-
pending_task = task_mgr.register_task(meta_task)
|
|
602
|
-
await store.set(var_entry, key=cache_key, value=pending_task, pin=_pin_result)
|
|
603
|
-
|
|
604
|
-
return {'cache_key': cache_key, 'value': meta_task}
|
|
605
|
-
|
|
606
|
-
task_id = f'{var_uid}_Task_{str(uuid.uuid4())}'
|
|
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:
|
|
400
|
+
eng_logger.info(
|
|
401
|
+
f'DerivedVariable {_uid_short} returning cached value directly',
|
|
402
|
+
{'uid': var_entry.uid, 'cached_value': value},
|
|
403
|
+
)
|
|
404
|
+
return {'cache_key': cache_key, 'value': value}
|
|
405
|
+
|
|
406
|
+
# Setup pending task if it needs it and then return the task
|
|
407
|
+
if var_entry.run_as_task or has_tasks:
|
|
408
|
+
var_uid = var_entry.uid or str(uuid.uuid4())
|
|
409
|
+
|
|
410
|
+
if has_tasks:
|
|
411
|
+
task_id = f'{var_uid}_MetaTask_{str(uuid.uuid4())}'
|
|
607
412
|
|
|
413
|
+
extra_notify_channels = [
|
|
414
|
+
channel for arg in parsed_args if isinstance(arg, BaseTask) for channel in arg.notify_channels
|
|
415
|
+
]
|
|
608
416
|
eng_logger.debug(
|
|
609
417
|
f'DerivedVariable {_uid_short}',
|
|
610
|
-
'running
|
|
418
|
+
'running has tasks',
|
|
611
419
|
{'uid': var_entry.uid, 'task_id': task_id},
|
|
612
420
|
)
|
|
613
|
-
|
|
421
|
+
meta_task = MetaTask(
|
|
614
422
|
var_entry.func,
|
|
615
423
|
parsed_args,
|
|
424
|
+
notify_channels=list(set(extra_notify_channels)),
|
|
425
|
+
process_as_task=var_entry.run_as_task,
|
|
616
426
|
cache_key=cache_key,
|
|
617
427
|
task_id=task_id,
|
|
618
428
|
reg_entry=var_entry, # task results are set as the DV result
|
|
619
429
|
)
|
|
620
430
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return {'cache_key': cache_key, 'value': task}
|
|
626
|
-
|
|
627
|
-
try:
|
|
628
|
-
result = await run_user_handler(var_entry.func, args=parsed_args)
|
|
629
|
-
except Exception:
|
|
630
|
-
# Delete the store value so subsequent requests recalculate instaed
|
|
631
|
-
if var_entry.cache is not None:
|
|
632
|
-
await store.delete(var_entry, key=cache_key)
|
|
633
|
-
raise
|
|
634
|
-
|
|
635
|
-
# If a task is returned then ensure we register it
|
|
636
|
-
if isinstance(result, BaseTask):
|
|
637
|
-
eng_logger.info(
|
|
638
|
-
f'DerivedVariable {_uid_short} returning task as a result',
|
|
639
|
-
{'uid': var_entry.uid, 'task_id': result.task_id},
|
|
640
|
-
)
|
|
641
|
-
# Make sure cache settings are set on the task
|
|
642
|
-
result.cache_key = cache_key
|
|
643
|
-
result.reg_entry = var_entry
|
|
431
|
+
return {'cache_key': cache_key, 'value': meta_task}
|
|
432
|
+
|
|
433
|
+
task_id = f'{var_uid}_Task_{str(uuid.uuid4())}'
|
|
644
434
|
|
|
645
|
-
|
|
435
|
+
eng_logger.debug(
|
|
436
|
+
f'DerivedVariable {_uid_short}',
|
|
437
|
+
'running as a task',
|
|
438
|
+
{'uid': var_entry.uid, 'task_id': task_id},
|
|
439
|
+
)
|
|
440
|
+
task = Task(
|
|
441
|
+
var_entry.func,
|
|
442
|
+
parsed_args,
|
|
443
|
+
cache_key=cache_key,
|
|
444
|
+
task_id=task_id,
|
|
445
|
+
reg_entry=var_entry, # task results are set as the DV result
|
|
446
|
+
)
|
|
447
|
+
return {'cache_key': cache_key, 'value': task}
|
|
646
448
|
|
|
647
|
-
|
|
449
|
+
# only set pending value if cache is not None, otherwise subsequent requests calculate the value again
|
|
450
|
+
if var_entry.cache is not None:
|
|
451
|
+
await store.set_pending(var_entry, key=cache_key)
|
|
648
452
|
|
|
649
|
-
|
|
453
|
+
try:
|
|
454
|
+
result = await run_user_handler(var_entry.func, args=parsed_args)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
# Set the store value to None before raising, so subsequent requests don't hang on a PendingValue
|
|
650
457
|
if var_entry.cache is not None:
|
|
651
|
-
await store.set(var_entry, key=cache_key, value=
|
|
458
|
+
await store.set(var_entry, key=cache_key, value=None, error=e)
|
|
459
|
+
raise
|
|
652
460
|
|
|
461
|
+
# If a task is returned then update pending value to pending task and return it
|
|
462
|
+
if isinstance(result, BaseTask):
|
|
653
463
|
eng_logger.info(
|
|
654
|
-
f'DerivedVariable {_uid_short} returning result',
|
|
655
|
-
{'uid': var_entry.uid, '
|
|
464
|
+
f'DerivedVariable {_uid_short} returning task as a result',
|
|
465
|
+
{'uid': var_entry.uid, 'task_id': result.task_id},
|
|
656
466
|
)
|
|
657
|
-
|
|
467
|
+
# Make sure cache settings are set on the task
|
|
468
|
+
result.cache_key = cache_key
|
|
469
|
+
result.reg_entry = var_entry
|
|
658
470
|
|
|
659
|
-
|
|
660
|
-
async def _filter_data(
|
|
661
|
-
cls,
|
|
662
|
-
data: Union[DataFrame, Any, None],
|
|
663
|
-
filter_resolver: FilterResolver,
|
|
664
|
-
filters: Optional[FilterQuery] = None,
|
|
665
|
-
pagination: Optional[Pagination] = None,
|
|
666
|
-
) -> DataResponse:
|
|
667
|
-
if data is None:
|
|
668
|
-
return DataResponse(data=None, count=0, schema=None)
|
|
669
|
-
|
|
670
|
-
# silently add the index column for DataFrame values
|
|
671
|
-
# User resolver could technically not be returning a DataFrame
|
|
672
|
-
if isinstance(data, DataFrame):
|
|
673
|
-
data = append_index(data)
|
|
674
|
-
|
|
675
|
-
# Filtering part
|
|
676
|
-
data, count = await filter_resolver(data, filters, pagination)
|
|
677
|
-
return build_data_response(data, count)
|
|
471
|
+
return {'cache_key': cache_key, 'value': result}
|
|
678
472
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
var_entry: DerivedVariableRegistryEntry,
|
|
683
|
-
store: CacheStore,
|
|
684
|
-
task_mgr: TaskManager,
|
|
685
|
-
args: List[Any],
|
|
686
|
-
force_key: Optional[str] = None,
|
|
687
|
-
pagination: Optional[Pagination] = None,
|
|
688
|
-
filters: Optional[FilterQuery] = None,
|
|
689
|
-
) -> Union[MetaTask, DataResponse]:
|
|
690
|
-
"""
|
|
691
|
-
Get filtered tabular data from the underlying derived variable.
|
|
473
|
+
# only set the value if cache is not None, otherwise subsequent requests calculate the value again
|
|
474
|
+
if var_entry.cache is not None:
|
|
475
|
+
await store.set(var_entry, key=cache_key, value=result)
|
|
692
476
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
filter_resolver = var_entry.filter_resolver or default_filter_resolver
|
|
697
|
-
result = await cls.get_value(var_entry, store, task_mgr, args, force_key)
|
|
698
|
-
|
|
699
|
-
if isinstance(result['value'], BaseTask):
|
|
700
|
-
task_id = f'{var_entry.uid}_Filter_MetaTask_{str(uuid.uuid4())}'
|
|
701
|
-
task = MetaTask(
|
|
702
|
-
cls._filter_data,
|
|
703
|
-
task_id=task_id,
|
|
704
|
-
kwargs={
|
|
705
|
-
'data': result['value'],
|
|
706
|
-
'filters': filters,
|
|
707
|
-
'pagination': pagination,
|
|
708
|
-
'filter_resolver': filter_resolver,
|
|
709
|
-
},
|
|
477
|
+
eng_logger.info(
|
|
478
|
+
f'DerivedVariable {_uid_short} returning result',
|
|
479
|
+
{'uid': var_entry.uid, 'result': result},
|
|
710
480
|
)
|
|
711
|
-
|
|
712
|
-
return task
|
|
713
|
-
|
|
714
|
-
return await cls._filter_data(result['value'], filter_resolver, filters, pagination)
|
|
481
|
+
return {'cache_key': cache_key, 'value': result}
|
|
715
482
|
|
|
716
483
|
@classmethod
|
|
717
484
|
def check_polling(cls, variables: List[AnyVariable]):
|
|
@@ -725,25 +492,18 @@ class DerivedVariable(ClientVariable, Generic[VariableType]):
|
|
|
725
492
|
@model_serializer(mode='wrap')
|
|
726
493
|
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
727
494
|
parent_dict = nxt(self)
|
|
728
|
-
return {
|
|
729
|
-
**parent_dict,
|
|
730
|
-
'__typename': 'DerivedVariable',
|
|
731
|
-
'uid': str(parent_dict['uid']),
|
|
732
|
-
}
|
|
495
|
+
return {**parent_dict, '__typename': 'DerivedVariable', 'uid': str(parent_dict['uid'])}
|
|
733
496
|
|
|
734
497
|
|
|
735
498
|
class DerivedVariableRegistryEntry(CachedRegistryEntry):
|
|
736
499
|
deps: Optional[List[int]]
|
|
737
500
|
func: Optional[Callable[..., Any]]
|
|
738
|
-
filter_resolver: Optional[FilterResolver]
|
|
739
501
|
run_as_task: bool
|
|
740
502
|
variables: List[AnyVariable]
|
|
741
503
|
polling_interval: Optional[int]
|
|
742
504
|
get_value: Callable[..., Awaitable[Any]]
|
|
743
505
|
"""Handler to get the value of the derived variable. Defaults to DerivedVariable.get_value, should match the signature"""
|
|
744
|
-
|
|
745
|
-
"""Handler to get the tabular data of the derived variable. Defaults to DerivedVariable.get_tabular_data, should match the signature"""
|
|
746
|
-
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
506
|
+
model_config = ConfigDict(extra='forbid')
|
|
747
507
|
|
|
748
508
|
|
|
749
509
|
class LatestValueRegistryEntry(CachedRegistryEntry):
|