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.
Files changed (52) hide show
  1. dara/core/__init__.py +1 -0
  2. dara/core/auth/basic.py +13 -7
  3. dara/core/auth/definitions.py +2 -2
  4. dara/core/auth/utils.py +1 -1
  5. dara/core/base_definitions.py +7 -42
  6. dara/core/data_utils.py +16 -17
  7. dara/core/definitions.py +8 -8
  8. dara/core/interactivity/__init__.py +6 -0
  9. dara/core/interactivity/actions.py +26 -22
  10. dara/core/interactivity/any_data_variable.py +7 -135
  11. dara/core/interactivity/any_variable.py +1 -1
  12. dara/core/interactivity/client_variable.py +71 -0
  13. dara/core/interactivity/data_variable.py +8 -266
  14. dara/core/interactivity/derived_data_variable.py +6 -290
  15. dara/core/interactivity/derived_variable.py +379 -199
  16. dara/core/interactivity/filtering.py +29 -2
  17. dara/core/interactivity/loop_variable.py +2 -2
  18. dara/core/interactivity/non_data_variable.py +5 -68
  19. dara/core/interactivity/plain_variable.py +87 -14
  20. dara/core/interactivity/server_variable.py +325 -0
  21. dara/core/interactivity/state_variable.py +69 -0
  22. dara/core/interactivity/switch_variable.py +15 -15
  23. dara/core/interactivity/tabular_variable.py +94 -0
  24. dara/core/interactivity/url_variable.py +10 -90
  25. dara/core/internal/cache_store/cache_store.py +5 -20
  26. dara/core/internal/dependency_resolution.py +27 -69
  27. dara/core/internal/devtools.py +10 -3
  28. dara/core/internal/execute_action.py +9 -3
  29. dara/core/internal/multi_resource_lock.py +70 -0
  30. dara/core/internal/normalization.py +0 -5
  31. dara/core/internal/pandas_utils.py +105 -3
  32. dara/core/internal/pool/definitions.py +1 -1
  33. dara/core/internal/pool/task_pool.py +9 -6
  34. dara/core/internal/pool/utils.py +19 -14
  35. dara/core/internal/registries.py +3 -2
  36. dara/core/internal/registry.py +1 -1
  37. dara/core/internal/registry_lookup.py +5 -3
  38. dara/core/internal/routing.py +52 -121
  39. dara/core/internal/store.py +2 -29
  40. dara/core/internal/tasks.py +372 -182
  41. dara/core/internal/utils.py +25 -3
  42. dara/core/internal/websocket.py +1 -1
  43. dara/core/js_tooling/js_utils.py +2 -0
  44. dara/core/logging.py +10 -6
  45. dara/core/persistence.py +26 -4
  46. dara/core/umd/dara.core.umd.js +1082 -1464
  47. dara/core/visual/dynamic_component.py +17 -13
  48. {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/METADATA +11 -11
  49. {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/RECORD +52 -47
  50. {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/LICENSE +0 -0
  51. {dara_core-1.19.0.dist-info → dara_core-1.20.0a1.dist-info}/WHEEL +0 -0
  52. {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 # noqa: F401
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 .non_data_variable import NonDataVariable
5
+ from .client_variable import ClientVariable
6
6
 
7
7
 
8
- class LoopVariable(NonDataVariable):
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
- http://www.apache.org/licenses/LICENSE-2.0
3
+ from .client_variable import * # noqa: F403
10
4
 
11
- Unless required by applicable law or agreed to in writing, software
12
- distributed under the License is distributed on an "AS IS" BASIS,
13
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
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.derived_data_variable import DerivedDataVariable
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(NonDataVariable, Generic[VariableType]):
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 = {'default': default, 'persist_value': persist_value, 'uid': uid, 'store': store, 'nested': nested}
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 kwargs.get('persist_value'):
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, 'persist_value': True}):
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)