dara-core 1.19.0__py3-none-any.whl → 1.20.0a1__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 +1 -0
- dara/core/auth/basic.py +13 -7
- dara/core/auth/definitions.py +2 -2
- dara/core/auth/utils.py +1 -1
- dara/core/base_definitions.py +7 -42
- dara/core/data_utils.py +16 -17
- dara/core/definitions.py +8 -8
- dara/core/interactivity/__init__.py +6 -0
- dara/core/interactivity/actions.py +26 -22
- dara/core/interactivity/any_data_variable.py +7 -135
- dara/core/interactivity/any_variable.py +1 -1
- dara/core/interactivity/client_variable.py +71 -0
- dara/core/interactivity/data_variable.py +8 -266
- dara/core/interactivity/derived_data_variable.py +6 -290
- dara/core/interactivity/derived_variable.py +379 -199
- dara/core/interactivity/filtering.py +29 -2
- dara/core/interactivity/loop_variable.py +2 -2
- dara/core/interactivity/non_data_variable.py +5 -68
- dara/core/interactivity/plain_variable.py +87 -14
- dara/core/interactivity/server_variable.py +325 -0
- dara/core/interactivity/state_variable.py +69 -0
- dara/core/interactivity/switch_variable.py +15 -15
- dara/core/interactivity/tabular_variable.py +94 -0
- dara/core/interactivity/url_variable.py +10 -90
- dara/core/internal/cache_store/cache_store.py +5 -20
- dara/core/internal/dependency_resolution.py +27 -69
- dara/core/internal/devtools.py +10 -3
- dara/core/internal/execute_action.py +9 -3
- dara/core/internal/multi_resource_lock.py +70 -0
- dara/core/internal/normalization.py +0 -5
- dara/core/internal/pandas_utils.py +105 -3
- dara/core/internal/pool/definitions.py +1 -1
- dara/core/internal/pool/task_pool.py +9 -6
- dara/core/internal/pool/utils.py +19 -14
- dara/core/internal/registries.py +3 -2
- dara/core/internal/registry.py +1 -1
- dara/core/internal/registry_lookup.py +5 -3
- dara/core/internal/routing.py +52 -121
- dara/core/internal/store.py +2 -29
- dara/core/internal/tasks.py +372 -182
- dara/core/internal/utils.py +25 -3
- dara/core/internal/websocket.py +1 -1
- dara/core/js_tooling/js_utils.py +2 -0
- dara/core/logging.py +10 -6
- dara/core/persistence.py +26 -4
- dara/core/umd/dara.core.umd.js +1082 -1464
- dara/core/visual/dynamic_component.py +17 -13
- {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/METADATA +11 -11
- {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/RECORD +52 -47
- {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/LICENSE +0 -0
- {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/WHEEL +0 -0
- {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/entry_points.txt +0 -0
|
@@ -20,10 +20,11 @@ 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, cast
|
|
23
|
+
from typing import Any, List, Optional, Tuple, Union, cast, overload
|
|
24
24
|
|
|
25
25
|
import numpy
|
|
26
|
-
from pandas import DataFrame, Series
|
|
26
|
+
from pandas import DataFrame, Series
|
|
27
|
+
from pydantic import field_validator # noqa: F401
|
|
27
28
|
|
|
28
29
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
29
30
|
from dara.core.logging import dev_logger
|
|
@@ -31,6 +32,13 @@ from dara.core.logging import dev_logger
|
|
|
31
32
|
COLUMN_PREFIX_REGEX = re.compile(r'__(?:col|index)__\d+__')
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def clean_column_name(col: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Cleans a column name by removing the index or col prefix
|
|
38
|
+
"""
|
|
39
|
+
return re.sub(COLUMN_PREFIX_REGEX, '', col)
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
class Pagination(BaseModel):
|
|
35
43
|
"""
|
|
36
44
|
Model representing pagination to be applied to a dataset.
|
|
@@ -44,6 +52,13 @@ class Pagination(BaseModel):
|
|
|
44
52
|
orderBy: Optional[str] = None
|
|
45
53
|
index: Optional[str] = None
|
|
46
54
|
|
|
55
|
+
@field_validator('orderBy', mode='before')
|
|
56
|
+
@classmethod
|
|
57
|
+
def clean_order_by(cls, order_by):
|
|
58
|
+
if order_by is None:
|
|
59
|
+
return None
|
|
60
|
+
return clean_column_name(order_by)
|
|
61
|
+
|
|
47
62
|
|
|
48
63
|
class QueryCombinator(str, Enum):
|
|
49
64
|
AND = 'AND'
|
|
@@ -231,6 +246,18 @@ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> Optional[Serie
|
|
|
231
246
|
raise ValueError(f'Unknown query type {type(query)}')
|
|
232
247
|
|
|
233
248
|
|
|
249
|
+
@overload
|
|
250
|
+
def apply_filters(
|
|
251
|
+
data: DataFrame, filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
|
|
252
|
+
) -> Tuple[DataFrame, int]: ...
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@overload
|
|
256
|
+
def apply_filters(
|
|
257
|
+
data: None, filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
|
|
258
|
+
) -> Tuple[None, int]: ...
|
|
259
|
+
|
|
260
|
+
|
|
234
261
|
def apply_filters(
|
|
235
262
|
data: Optional[DataFrame], filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
|
|
236
263
|
) -> Tuple[Optional[DataFrame], int]:
|
|
@@ -2,10 +2,10 @@ from typing import List, Optional
|
|
|
2
2
|
|
|
3
3
|
from pydantic import Field, SerializerFunctionWrapHandler, model_serializer
|
|
4
4
|
|
|
5
|
-
from .
|
|
5
|
+
from .client_variable import ClientVariable
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class LoopVariable(
|
|
8
|
+
class LoopVariable(ClientVariable):
|
|
9
9
|
"""
|
|
10
10
|
A LoopVariable is a type of variable that represents an item in a list.
|
|
11
11
|
It should be constructed using a parent Variable's `.list_item` property.
|
|
@@ -1,71 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
Copyright 2023 Impulse Innovations Limited
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
you may not use this file except in compliance with the License.
|
|
7
|
-
You may obtain a copy of the License at
|
|
1
|
+
from typing_extensions import TypeAlias
|
|
8
2
|
|
|
9
|
-
|
|
3
|
+
from .client_variable import * # noqa: F403
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
See the License for the specific language governing permissions and
|
|
15
|
-
limitations under the License.
|
|
5
|
+
NonDataVariable: TypeAlias = ClientVariable # noqa: F405
|
|
6
|
+
"""
|
|
7
|
+
Deprecated alias for ClientVariable
|
|
16
8
|
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import abc
|
|
21
|
-
from typing import Optional
|
|
22
|
-
|
|
23
|
-
from dara.core.interactivity.any_variable import AnyVariable
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class NonDataVariable(AnyVariable, abc.ABC):
|
|
27
|
-
"""
|
|
28
|
-
NonDataVariable represents any variable that is not specifically designed to hold datasets (i.e. Variable, DerivedVariable, UrlVariable)
|
|
29
|
-
|
|
30
|
-
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
uid: str
|
|
34
|
-
|
|
35
|
-
def __init__(self, uid: Optional[str] = None, **kwargs) -> None:
|
|
36
|
-
super().__init__(uid=uid, **kwargs)
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def list_item(self):
|
|
40
|
-
"""
|
|
41
|
-
Get a LoopVariable that represents the current item in the list.
|
|
42
|
-
Should only be used in conjunction with the `For` component.
|
|
43
|
-
|
|
44
|
-
Note that it is a type of a Variable so it can be used in places where a regular Variable is expected.
|
|
45
|
-
|
|
46
|
-
By default, the entire list item is used as the item.
|
|
47
|
-
|
|
48
|
-
`LoopVariable` supports nested property access using `get` or index access i.e. `[]`.
|
|
49
|
-
You can mix and match those two methods to access nested properties as they are equivalent.
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
my_list = Variable(['foo', 'bar', 'baz'])
|
|
53
|
-
|
|
54
|
-
# Represents the entire item in the list
|
|
55
|
-
my_list.list_item
|
|
56
|
-
|
|
57
|
-
my_list_of_objects = Variable([
|
|
58
|
-
{'id': 1, 'name': 'John', 'data': {'city': 'London', 'country': 'UK'}},
|
|
59
|
-
{'id': 2, 'name': 'Jane', 'data': {'city': 'Paris', 'country': 'France'}},
|
|
60
|
-
])
|
|
61
|
-
|
|
62
|
-
# Represents the item 'name' property
|
|
63
|
-
my_list_of_objects.list_item['name']
|
|
64
|
-
|
|
65
|
-
# Represents the item 'data.country' property
|
|
66
|
-
my_list_of_objects.list_item.get('data')['country']
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
from .loop_variable import LoopVariable
|
|
70
|
-
|
|
71
|
-
return LoopVariable()
|
|
@@ -17,9 +17,10 @@ limitations under the License.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import warnings
|
|
20
21
|
from contextlib import contextmanager
|
|
21
22
|
from contextvars import ContextVar
|
|
22
|
-
from typing import Any, Callable, Generic, List, Optional, TypeVar
|
|
23
|
+
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
|
|
23
24
|
|
|
24
25
|
from fastapi.encoders import jsonable_encoder
|
|
25
26
|
from pydantic import (
|
|
@@ -30,12 +31,11 @@ from pydantic import (
|
|
|
30
31
|
model_serializer,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
|
-
from dara.core.interactivity.
|
|
34
|
+
from dara.core.interactivity.client_variable import ClientVariable
|
|
34
35
|
from dara.core.interactivity.derived_variable import DerivedVariable
|
|
35
|
-
from dara.core.interactivity.non_data_variable import NonDataVariable
|
|
36
36
|
from dara.core.internal.utils import call_async
|
|
37
37
|
from dara.core.logging import dev_logger
|
|
38
|
-
from dara.core.persistence import PersistenceStore
|
|
38
|
+
from dara.core.persistence import BackendStore, BrowserStore, PersistenceStore
|
|
39
39
|
|
|
40
40
|
VARIABLE_INIT_OVERRIDE = ContextVar[Optional[Callable[[dict], dict]]]('VARIABLE_INIT_OVERRIDE', default=None)
|
|
41
41
|
|
|
@@ -44,13 +44,12 @@ PersistenceStoreType_co = TypeVar('PersistenceStoreType_co', bound=PersistenceSt
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
# TODO: once Python supports a default value for a generic type properly we can make PersistenceStoreType a second generic param
|
|
47
|
-
class Variable(
|
|
47
|
+
class Variable(ClientVariable, Generic[VariableType]):
|
|
48
48
|
"""
|
|
49
49
|
A Variable represents a dynamic value in the system that can be read and written to by components and actions
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
52
|
default: Optional[VariableType] = None
|
|
53
|
-
persist_value: bool = False
|
|
54
53
|
store: Optional[PersistenceStore] = None
|
|
55
54
|
uid: str
|
|
56
55
|
nested: List[str] = Field(default_factory=list)
|
|
@@ -63,6 +62,7 @@ class Variable(NonDataVariable, Generic[VariableType]):
|
|
|
63
62
|
uid: Optional[str] = None,
|
|
64
63
|
store: Optional[PersistenceStoreType_co] = None,
|
|
65
64
|
nested: Optional[List[str]] = None,
|
|
65
|
+
**kwargs,
|
|
66
66
|
):
|
|
67
67
|
"""
|
|
68
68
|
A Variable represents a dynamic value in the system that can be read and written to by components and actions
|
|
@@ -74,17 +74,31 @@ class Variable(NonDataVariable, Generic[VariableType]):
|
|
|
74
74
|
"""
|
|
75
75
|
if nested is None:
|
|
76
76
|
nested = []
|
|
77
|
-
kwargs = {
|
|
77
|
+
kwargs = {
|
|
78
|
+
'default': default,
|
|
79
|
+
'uid': uid,
|
|
80
|
+
'store': store,
|
|
81
|
+
'nested': nested,
|
|
82
|
+
**kwargs,
|
|
83
|
+
}
|
|
78
84
|
|
|
79
85
|
# If an override is active, run the kwargs through it
|
|
80
86
|
override = VARIABLE_INIT_OVERRIDE.get()
|
|
81
87
|
if override is not None:
|
|
82
88
|
kwargs = override(kwargs)
|
|
83
89
|
|
|
84
|
-
if kwargs.get('store') is not None and
|
|
90
|
+
if kwargs.get('store') is not None and persist_value:
|
|
85
91
|
# TODO: this is temporary, persist_value will eventually become a type of store
|
|
86
92
|
raise ValueError('Cannot provide a Variable with both a store and persist_value set to True')
|
|
87
93
|
|
|
94
|
+
if persist_value:
|
|
95
|
+
warnings.warn(
|
|
96
|
+
'`persist_value` is deprecated and will be removed in a future version. Use `store=dara.core.persistence.BrowserStore(...)` instead.',
|
|
97
|
+
DeprecationWarning,
|
|
98
|
+
stacklevel=2,
|
|
99
|
+
)
|
|
100
|
+
kwargs['store'] = BrowserStore()
|
|
101
|
+
|
|
88
102
|
super().__init__(**kwargs) # type: ignore
|
|
89
103
|
|
|
90
104
|
if self.store:
|
|
@@ -116,7 +130,7 @@ class Variable(NonDataVariable, Generic[VariableType]):
|
|
|
116
130
|
Override the init function of all Variables created within the context of this function.
|
|
117
131
|
|
|
118
132
|
```python
|
|
119
|
-
with Variable.init_override(lambda kwargs: {**kwargs, '
|
|
133
|
+
with Variable.init_override(lambda kwargs: {**kwargs, 'store': ...}):
|
|
120
134
|
var = Variable()
|
|
121
135
|
```
|
|
122
136
|
|
|
@@ -253,13 +267,72 @@ class Variable(NonDataVariable, Generic[VariableType]):
|
|
|
253
267
|
|
|
254
268
|
:param default: the initial value for the variable, defaults to None
|
|
255
269
|
"""
|
|
256
|
-
if isinstance(other, DerivedDataVariable):
|
|
257
|
-
raise ValueError(
|
|
258
|
-
'Cannot create a Variable from a DerivedDataVariable, only standard DerivedVariables are allowed'
|
|
259
|
-
)
|
|
260
|
-
|
|
261
270
|
return cls(default=other) # type: ignore
|
|
262
271
|
|
|
272
|
+
async def write(self, value: Any, notify=True, ignore_channel: Optional[str] = None):
|
|
273
|
+
"""
|
|
274
|
+
Persist a value to the variable's BackendStore.
|
|
275
|
+
Raises an error if the variable does not have a BackendStore attached.
|
|
276
|
+
|
|
277
|
+
If scope='user', the value is written for the current user so the method can only
|
|
278
|
+
be used in authenticated contexts.
|
|
279
|
+
|
|
280
|
+
:param value: value to write
|
|
281
|
+
:param notify: whether to broadcast the new value to clients
|
|
282
|
+
:param ignore_channel: if passed, ignore the specified websocket channel when broadcasting
|
|
283
|
+
"""
|
|
284
|
+
assert isinstance(self.store, BackendStore), 'This method can only be used with a BackendStore'
|
|
285
|
+
return await self.store.write(value, notify=notify, ignore_channel=ignore_channel)
|
|
286
|
+
|
|
287
|
+
async def write_partial(self, data: Union[List[Dict[str, Any]], Any], notify: bool = True):
|
|
288
|
+
"""
|
|
289
|
+
Apply partial updates to the variable's BackendStore using JSON Patch operations or automatic diffing.
|
|
290
|
+
Raises an error if the variable does not have a BackendStore attached.
|
|
291
|
+
|
|
292
|
+
If scope='user', the patches are applied for the current user so the method can only
|
|
293
|
+
be used in authenticated contexts.
|
|
294
|
+
|
|
295
|
+
:param data: Either a list of JSON patch operations (RFC 6902) or a full object to diff against current value
|
|
296
|
+
:param notify: whether to broadcast the patches to clients
|
|
297
|
+
"""
|
|
298
|
+
assert isinstance(self.store, BackendStore), 'This method can only be used with a BackendStore'
|
|
299
|
+
return await self.store.write_partial(data, notify=notify)
|
|
300
|
+
|
|
301
|
+
async def read(self):
|
|
302
|
+
"""
|
|
303
|
+
Read a value from the variable's BackendStore.
|
|
304
|
+
Raises an error if the variable does not have a BackendStore attached.
|
|
305
|
+
|
|
306
|
+
If scope='user', the value is read for the current user so the method can only
|
|
307
|
+
be used in authenticated contexts.
|
|
308
|
+
"""
|
|
309
|
+
assert isinstance(self.store, BackendStore), 'This method can only be used with a BackendStore'
|
|
310
|
+
return await self.store.read()
|
|
311
|
+
|
|
312
|
+
async def delete(self, notify=True):
|
|
313
|
+
"""
|
|
314
|
+
Delete the persisted value from the variable's BackendStore.
|
|
315
|
+
Raises an error if the variable does not have a BackendStore attached.
|
|
316
|
+
|
|
317
|
+
If scope='user', the value is deleted for the current user so the method can only
|
|
318
|
+
be used in authenticated contexts.
|
|
319
|
+
|
|
320
|
+
:param notify: whether to broadcast that the value was deleted to clients
|
|
321
|
+
"""
|
|
322
|
+
assert isinstance(self.store, BackendStore), 'This method can only be used with a BackendStore'
|
|
323
|
+
return await self.store.delete(notify=notify)
|
|
324
|
+
|
|
325
|
+
async def get_all(self) -> Dict[str, Any]:
|
|
326
|
+
"""
|
|
327
|
+
Get all the values from the variable's BackendStore as a dictionary of key-value pairs.
|
|
328
|
+
Raises an error if the variable does not have a BackendStore attached.
|
|
329
|
+
|
|
330
|
+
For global scope, the dictionary contains a single key-value pair `{'global': value}`.
|
|
331
|
+
For user scope, the dictionary contains a key-value pair for each user `{'user1': value1, 'user2': value2, ...}`.
|
|
332
|
+
"""
|
|
333
|
+
assert isinstance(self.store, BackendStore), 'This method can only be used with a BackendStore'
|
|
334
|
+
return await self.store.get_all()
|
|
335
|
+
|
|
263
336
|
@model_serializer(mode='wrap')
|
|
264
337
|
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
265
338
|
parent_dict = nxt(self)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any, DefaultDict, Dict, Literal, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, SerializerFunctionWrapHandler, model_serializer
|
|
7
|
+
|
|
8
|
+
from dara.core.auth.definitions import USER
|
|
9
|
+
from dara.core.base_definitions import CachedRegistryEntry, NonTabularDataError
|
|
10
|
+
from dara.core.interactivity.filtering import FilterQuery, Pagination, apply_filters, coerce_to_filter_query
|
|
11
|
+
from dara.core.internal.pandas_utils import DataResponse, append_index, build_data_response
|
|
12
|
+
from dara.core.internal.utils import call_async
|
|
13
|
+
from dara.core.internal.websocket import ServerMessagePayload, WebsocketManager
|
|
14
|
+
|
|
15
|
+
from .any_variable import AnyVariable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServerVariableMessage(ServerMessagePayload):
|
|
19
|
+
typ: Literal['ServerVariable'] = Field(alias='__type', default='ServerVariable')
|
|
20
|
+
uid: str
|
|
21
|
+
sequence_number: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ServerBackend(BaseModel, abc.ABC):
|
|
25
|
+
scope: Literal['global', 'user']
|
|
26
|
+
|
|
27
|
+
@abc.abstractmethod
|
|
28
|
+
async def write(self, key: str, value: Any):
|
|
29
|
+
"""
|
|
30
|
+
Persist a value
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@abc.abstractmethod
|
|
34
|
+
async def read(self, key: str) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Read a value
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
async def read_filtered(
|
|
41
|
+
self, key: str, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
42
|
+
) -> Tuple[Optional[DataFrame], int]:
|
|
43
|
+
"""
|
|
44
|
+
Read a value
|
|
45
|
+
:param filters: filters to apply
|
|
46
|
+
:param pagination: pagination to apply
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
async def get_sequence_number(self, key: str) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Get the sequence number for a given key
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MemoryBackend(ServerBackend):
|
|
57
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
|
58
|
+
sequence_number: DefaultDict[str, int] = Field(default_factory=lambda: defaultdict(int))
|
|
59
|
+
|
|
60
|
+
def __init__(self, scope: Literal['user', 'global'] = 'user'):
|
|
61
|
+
super().__init__(scope=scope)
|
|
62
|
+
|
|
63
|
+
async def write(self, key: str, value: Any):
|
|
64
|
+
self.data[key] = value
|
|
65
|
+
self.sequence_number[key] += 1
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
async def read(self, key: str) -> Any:
|
|
69
|
+
return self.data.get(key)
|
|
70
|
+
|
|
71
|
+
async def read_filtered(
|
|
72
|
+
self, key: str, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
73
|
+
) -> Tuple[Optional[DataFrame], int]:
|
|
74
|
+
dataset = self.data.get(key)
|
|
75
|
+
|
|
76
|
+
# print user-friendly error message if the data is not a DataFrame
|
|
77
|
+
# most likely due to user passing a non-tabular server variable to e.g. a Table
|
|
78
|
+
if dataset is not None and not isinstance(dataset, DataFrame):
|
|
79
|
+
raise NonTabularDataError(
|
|
80
|
+
f'Failed to retrieve ServerVariable tabular data, expected pandas.DataFrame, got {type(dataset)}'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
dataset = append_index(dataset)
|
|
84
|
+
return apply_filters(dataset, coerce_to_filter_query(filters), pagination)
|
|
85
|
+
|
|
86
|
+
async def get_sequence_number(self, key: str) -> int:
|
|
87
|
+
return self.sequence_number[key]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ServerVariable(AnyVariable):
|
|
91
|
+
"""
|
|
92
|
+
A ServerVariable represents server-side data that is synchronized with the client.
|
|
93
|
+
|
|
94
|
+
Unlike Variables with BackendStore (which are client state persisted on server),
|
|
95
|
+
ServerVariable holds data that originates and is managed on the server, with
|
|
96
|
+
clients receiving reactive updates when the data changes.
|
|
97
|
+
|
|
98
|
+
ServerVariable can store any Python object, including non-serializable data like
|
|
99
|
+
database connections, ML models, or complex objects.
|
|
100
|
+
|
|
101
|
+
However, when used with components expecting tabular data (like Table), the data must be
|
|
102
|
+
serializable or a NonTabularDataError will be raised. The default backend implementation
|
|
103
|
+
expects the tabular data to be a pandas DataFrame. To support other data types, you can
|
|
104
|
+
implement a custom backend that translates the data into a filtered DataFrame in the
|
|
105
|
+
`read_filtered` method.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import pandas as pd
|
|
109
|
+
from dara.core import ServerVariable, action
|
|
110
|
+
from dara.core.interactivity.server_variable import ServerBackend
|
|
111
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
112
|
+
|
|
113
|
+
# Basic usage with DataFrame
|
|
114
|
+
data = ServerVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
115
|
+
|
|
116
|
+
# Non-serializable data (ML model)
|
|
117
|
+
model = ServerVariable(trained_sklearn_model, scope='global')
|
|
118
|
+
|
|
119
|
+
# Custom backend
|
|
120
|
+
class DatabaseBackend(ServerBackend):
|
|
121
|
+
# ... implements all the methods as DB operations
|
|
122
|
+
|
|
123
|
+
data = ServerVariable(backend=DatabaseBackend(...))
|
|
124
|
+
|
|
125
|
+
# User-specific data
|
|
126
|
+
user_prefs = ServerVariable(scope='user')
|
|
127
|
+
|
|
128
|
+
@action
|
|
129
|
+
async def on_click(ctx):
|
|
130
|
+
# write to the data for the user who initiated the action
|
|
131
|
+
await user_prefs.write('dark')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
:param default: Initial value for the variable (global scope only)
|
|
135
|
+
:param backend: Custom backend for data storage and retrieval
|
|
136
|
+
:param scope: 'global' (shared across all users) or 'user' (per-user data)
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
backend: ServerBackend = Field(exclude=True)
|
|
140
|
+
scope: Literal['user', 'global']
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
default: Optional[Any] = None,
|
|
145
|
+
backend: Optional[ServerBackend] = None,
|
|
146
|
+
scope: Literal['user', 'global'] = 'global',
|
|
147
|
+
uid: Optional[str] = None,
|
|
148
|
+
**kwargs,
|
|
149
|
+
) -> None:
|
|
150
|
+
from dara.core.internal.registries import server_variable_registry
|
|
151
|
+
|
|
152
|
+
if backend is None:
|
|
153
|
+
backend = MemoryBackend(scope=scope)
|
|
154
|
+
|
|
155
|
+
if default is not None:
|
|
156
|
+
assert scope == 'global', (
|
|
157
|
+
'ServerVariable can only be used with global scope, cannot initialize user-specific values'
|
|
158
|
+
)
|
|
159
|
+
call_async(backend.write, 'global', default)
|
|
160
|
+
|
|
161
|
+
super().__init__(uid=uid, backend=backend, scope=scope, **kwargs)
|
|
162
|
+
|
|
163
|
+
var_entry = ServerVariableRegistryEntry(uid=str(self.uid), backend=backend)
|
|
164
|
+
server_variable_registry.register(str(self.uid), var_entry)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
async def get_value(cls, entry: 'ServerVariableRegistryEntry'):
|
|
168
|
+
"""
|
|
169
|
+
Internal method to get the value of a server variable based in its registry entry.
|
|
170
|
+
"""
|
|
171
|
+
key = cls.get_key(entry.backend.scope)
|
|
172
|
+
return await entry.backend.read(key)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
async def write_value(cls, entry: 'ServerVariableRegistryEntry', value: Any):
|
|
176
|
+
"""
|
|
177
|
+
Internal method to write the value of a server variable based in its registry entry.
|
|
178
|
+
"""
|
|
179
|
+
key = cls.get_key(entry.backend.scope)
|
|
180
|
+
await entry.backend.write(key, value)
|
|
181
|
+
await cls._notify(entry.uid, key, entry.backend)
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
async def get_sequence_number(cls, entry: 'ServerVariableRegistryEntry'):
|
|
185
|
+
"""
|
|
186
|
+
Internal method to get the sequence number of a server variable based in its registry entry.
|
|
187
|
+
"""
|
|
188
|
+
key = cls.get_key(entry.backend.scope)
|
|
189
|
+
return await entry.backend.get_sequence_number(key)
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
async def get_tabular_data(
|
|
193
|
+
cls,
|
|
194
|
+
entry: 'ServerVariableRegistryEntry',
|
|
195
|
+
filters: Optional[FilterQuery] = None,
|
|
196
|
+
pagination: Optional[Pagination] = None,
|
|
197
|
+
) -> DataResponse:
|
|
198
|
+
"""
|
|
199
|
+
Internal method to get tabular data from the backend
|
|
200
|
+
"""
|
|
201
|
+
key = cls.get_key(entry.backend.scope)
|
|
202
|
+
data, count = await entry.backend.read_filtered(key, filters, pagination)
|
|
203
|
+
if data is None:
|
|
204
|
+
return DataResponse(data=None, count=0, schema=None)
|
|
205
|
+
return build_data_response(data, count)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def get_key(cls, scope: Literal['global', 'user']):
|
|
209
|
+
"""
|
|
210
|
+
Resolve the key for the given scope
|
|
211
|
+
|
|
212
|
+
:param scope: the scope to resolve the key for
|
|
213
|
+
"""
|
|
214
|
+
if scope == 'global':
|
|
215
|
+
return 'global'
|
|
216
|
+
|
|
217
|
+
user = USER.get()
|
|
218
|
+
|
|
219
|
+
if user:
|
|
220
|
+
return user.identity_id
|
|
221
|
+
|
|
222
|
+
raise ValueError('User not found when trying to compute the key for a user-scoped store')
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def key(self):
|
|
226
|
+
"""
|
|
227
|
+
Current key for the backend
|
|
228
|
+
"""
|
|
229
|
+
return self.get_key(self.scope)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
async def _notify(cls, uid: str, key: str, backend: ServerBackend):
|
|
233
|
+
"""
|
|
234
|
+
Internal method to notify clients of a change in the value
|
|
235
|
+
|
|
236
|
+
:param uid: the uid of the variable
|
|
237
|
+
:param key: the key for the backend
|
|
238
|
+
:param backend: the backend instance
|
|
239
|
+
"""
|
|
240
|
+
from dara.core.internal.registries import utils_registry
|
|
241
|
+
|
|
242
|
+
ws_mgr: WebsocketManager = utils_registry.get('WebsocketManager')
|
|
243
|
+
|
|
244
|
+
message = ServerVariableMessage(uid=uid, sequence_number=await backend.get_sequence_number(key))
|
|
245
|
+
|
|
246
|
+
if backend.scope == 'global':
|
|
247
|
+
return await ws_mgr.broadcast(message)
|
|
248
|
+
|
|
249
|
+
user = USER.get()
|
|
250
|
+
assert user is not None, 'User not found when trying to send notification'
|
|
251
|
+
user_id = user.identity_id
|
|
252
|
+
return await ws_mgr.send_message_to_user(user_id, message)
|
|
253
|
+
|
|
254
|
+
def update(self, value: Any):
|
|
255
|
+
"""
|
|
256
|
+
Create an action to update the value of this Variable to a provided value.
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import pandas as pd
|
|
260
|
+
from dara.core import ServerVariable
|
|
261
|
+
from dara.components import Button
|
|
262
|
+
|
|
263
|
+
data = ServerVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
264
|
+
|
|
265
|
+
Button(
|
|
266
|
+
'Empty Data',
|
|
267
|
+
onclick=data.update(None),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
"""
|
|
272
|
+
from dara.core.interactivity.actions import UpdateVariableImpl
|
|
273
|
+
|
|
274
|
+
return UpdateVariableImpl(variable=self, value=value)
|
|
275
|
+
|
|
276
|
+
def reset(self):
|
|
277
|
+
raise NotImplementedError('ServerVariable does not support reset')
|
|
278
|
+
|
|
279
|
+
async def read(self):
|
|
280
|
+
"""
|
|
281
|
+
Read the current value from the backend.
|
|
282
|
+
Depending on the scope, the value will be global or user-specific.
|
|
283
|
+
"""
|
|
284
|
+
return await self.backend.read(self.key)
|
|
285
|
+
|
|
286
|
+
async def write(self, value: Any):
|
|
287
|
+
"""
|
|
288
|
+
Write a new value to the backend.
|
|
289
|
+
Depending on the scope, the value will be global or user-specific.
|
|
290
|
+
|
|
291
|
+
:param value: the new value to write
|
|
292
|
+
"""
|
|
293
|
+
value = await self.backend.write(self.key, value)
|
|
294
|
+
await self._notify(self.uid, self.key, self.backend)
|
|
295
|
+
return value
|
|
296
|
+
|
|
297
|
+
async def read_filtered(
|
|
298
|
+
self, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Read a filtered value from the backend.
|
|
302
|
+
Depending on the scope, the value will be global or user-specific.
|
|
303
|
+
|
|
304
|
+
:param filters: the filters to apply
|
|
305
|
+
:param pagination: the pagination to apply
|
|
306
|
+
"""
|
|
307
|
+
return await self.backend.read_filtered(self.key, filters, pagination)
|
|
308
|
+
|
|
309
|
+
@model_serializer(mode='wrap')
|
|
310
|
+
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
311
|
+
parent_dict = nxt(self)
|
|
312
|
+
return {**parent_dict, '__typename': 'ServerVariable', 'uid': str(parent_dict['uid'])}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ServerVariableRegistryEntry(CachedRegistryEntry):
|
|
316
|
+
"""
|
|
317
|
+
Registry entry for ServerVariable.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
backend: ServerBackend
|
|
321
|
+
"""
|
|
322
|
+
Backend instance
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|