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.
Files changed (82) hide show
  1. dara/core/__init__.py +0 -3
  2. dara/core/actions.py +2 -1
  3. dara/core/auth/basic.py +16 -22
  4. dara/core/auth/definitions.py +2 -2
  5. dara/core/auth/routes.py +5 -5
  6. dara/core/auth/utils.py +5 -5
  7. dara/core/base_definitions.py +64 -22
  8. dara/core/cli.py +7 -8
  9. dara/core/configuration.py +2 -5
  10. dara/core/css.py +2 -1
  11. dara/core/data_utils.py +19 -18
  12. dara/core/defaults.py +7 -6
  13. dara/core/definitions.py +19 -50
  14. dara/core/http.py +3 -7
  15. dara/core/interactivity/__init__.py +0 -6
  16. dara/core/interactivity/actions.py +50 -52
  17. dara/core/interactivity/any_data_variable.py +134 -7
  18. dara/core/interactivity/any_variable.py +8 -5
  19. dara/core/interactivity/data_variable.py +266 -8
  20. dara/core/interactivity/derived_data_variable.py +290 -7
  21. dara/core/interactivity/derived_variable.py +174 -414
  22. dara/core/interactivity/filtering.py +27 -46
  23. dara/core/interactivity/loop_variable.py +2 -2
  24. dara/core/interactivity/non_data_variable.py +68 -5
  25. dara/core/interactivity/plain_variable.py +15 -89
  26. dara/core/interactivity/switch_variable.py +19 -19
  27. dara/core/interactivity/url_variable.py +90 -10
  28. dara/core/internal/cache_store/base_impl.py +1 -2
  29. dara/core/internal/cache_store/cache_store.py +25 -22
  30. dara/core/internal/cache_store/keep_all.py +1 -4
  31. dara/core/internal/cache_store/lru.py +1 -5
  32. dara/core/internal/cache_store/ttl.py +1 -4
  33. dara/core/internal/cgroup.py +1 -1
  34. dara/core/internal/dependency_resolution.py +66 -60
  35. dara/core/internal/devtools.py +5 -12
  36. dara/core/internal/download.py +4 -13
  37. dara/core/internal/encoder_registry.py +7 -7
  38. dara/core/internal/execute_action.py +13 -13
  39. dara/core/internal/hashing.py +3 -1
  40. dara/core/internal/import_discovery.py +4 -3
  41. dara/core/internal/normalization.py +18 -9
  42. dara/core/internal/pandas_utils.py +5 -107
  43. dara/core/internal/pool/definitions.py +1 -1
  44. dara/core/internal/pool/task_pool.py +16 -25
  45. dara/core/internal/pool/utils.py +18 -21
  46. dara/core/internal/pool/worker.py +2 -3
  47. dara/core/internal/port_utils.py +1 -1
  48. dara/core/internal/registries.py +6 -12
  49. dara/core/internal/registry.py +2 -4
  50. dara/core/internal/registry_lookup.py +5 -11
  51. dara/core/internal/routing.py +145 -109
  52. dara/core/internal/scheduler.py +8 -13
  53. dara/core/internal/settings.py +2 -2
  54. dara/core/internal/store.py +29 -2
  55. dara/core/internal/tasks.py +195 -379
  56. dara/core/internal/utils.py +13 -36
  57. dara/core/internal/websocket.py +20 -21
  58. dara/core/js_tooling/js_utils.py +26 -28
  59. dara/core/js_tooling/templates/vite.config.template.ts +3 -12
  60. dara/core/logging.py +12 -13
  61. dara/core/main.py +11 -14
  62. dara/core/metrics/cache.py +1 -1
  63. dara/core/metrics/utils.py +3 -3
  64. dara/core/persistence.py +5 -27
  65. dara/core/umd/dara.core.umd.js +55425 -59091
  66. dara/core/visual/components/__init__.py +2 -2
  67. dara/core/visual/components/fallback.py +4 -30
  68. dara/core/visual/components/for_cmp.py +1 -4
  69. dara/core/visual/css/__init__.py +31 -30
  70. dara/core/visual/dynamic_component.py +28 -31
  71. dara/core/visual/progress_updater.py +3 -4
  72. {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/METADATA +11 -12
  73. dara_core-1.20.1a1.dist-info/RECORD +114 -0
  74. dara/core/interactivity/client_variable.py +0 -71
  75. dara/core/interactivity/server_variable.py +0 -325
  76. dara/core/interactivity/state_variable.py +0 -69
  77. dara/core/interactivity/tabular_variable.py +0 -94
  78. dara/core/internal/multi_resource_lock.py +0 -70
  79. dara_core-1.20.0a1.dist-info/RECORD +0 -119
  80. {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/LICENSE +0 -0
  81. {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/WHEEL +0 -0
  82. {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/entry_points.txt +0 -0
@@ -20,11 +20,10 @@ from __future__ import annotations
20
20
  import re
21
21
  from datetime import datetime, timezone
22
22
  from enum import Enum
23
- from typing import Any, List, Optional, Tuple, Union, cast, overload
23
+ from typing import Any, List, Optional, Tuple, Union
24
24
 
25
25
  import numpy
26
- from pandas import DataFrame, Series
27
- from pydantic import field_validator # noqa: F401
26
+ from pandas import DataFrame, Series # pylint: disable=unused-import
28
27
 
29
28
  from dara.core.base_definitions import DaraBaseModel as BaseModel
30
29
  from dara.core.logging import dev_logger
@@ -32,13 +31,6 @@ from dara.core.logging import dev_logger
32
31
  COLUMN_PREFIX_REGEX = re.compile(r'__(?:col|index)__\d+__')
33
32
 
34
33
 
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
-
42
34
  class Pagination(BaseModel):
43
35
  """
44
36
  Model representing pagination to be applied to a dataset.
@@ -52,13 +44,6 @@ class Pagination(BaseModel):
52
44
  orderBy: Optional[str] = None
53
45
  index: Optional[str] = None
54
46
 
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
-
62
47
 
63
48
  class QueryCombinator(str, Enum):
64
49
  AND = 'AND'
@@ -172,11 +157,11 @@ def infer_column_type(series: Series) -> ColumnType:
172
157
  return ColumnType.CATEGORICAL
173
158
 
174
159
 
175
- def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, value: Any) -> Optional[Series]:
160
+ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, value: Any) -> Optional['Series[bool]']:
176
161
  """
177
162
  Convert a single filter to a Series[bool] for filtering
178
163
  """
179
- series = cast(Series, data[column])
164
+ series = data[column]
180
165
 
181
166
  # Contains is a special case, we always treat the column as a string
182
167
  if operator == QueryOperator.CONTAINS:
@@ -190,15 +175,19 @@ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, val
190
175
  return series.isin(value)
191
176
  # Converts date passed from frontend to the right format to compare with pandas
192
177
  if col_type == ColumnType.DATETIME:
193
- value = [parseISO(value[0]), parseISO(value[1])] if isinstance(value, List) else parseISO(value)
178
+ if isinstance(value, List):
179
+ value = [parseISO(value[0]), parseISO(value[1])]
180
+ else:
181
+ value = parseISO(value)
194
182
  elif col_type == ColumnType.CATEGORICAL:
195
183
  value = str(value)
196
- elif isinstance(value, List):
197
- lower_bound = float(value[0]) if '.' in str(value[0]) else int(value[0])
198
- upper_bound = float(value[1]) if '.' in str(value[1]) else int(value[1])
199
- value = [lower_bound, upper_bound]
200
184
  else:
201
- value = float(value) if '.' in str(value) else int(value)
185
+ if isinstance(value, List):
186
+ lower_bound = float(value[0]) if '.' in str(value[0]) else int(value[0])
187
+ upper_bound = float(value[1]) if '.' in str(value[1]) else int(value[1])
188
+ value = [lower_bound, upper_bound]
189
+ else:
190
+ value = float(value) if '.' in str(value) else int(value)
202
191
 
203
192
  if operator == QueryOperator.GT:
204
193
  return series > value
@@ -219,14 +208,12 @@ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, val
219
208
  return None
220
209
 
221
210
 
222
- def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> Optional[Series]:
211
+ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> 'Optional[Series[bool]]':
223
212
  """
224
213
  Resolve a FilterQuery to a Series[bool] for filtering. Strips the internal column index from the query.
225
214
  """
226
215
  if isinstance(query, ValueQuery):
227
- return _filter_to_series(
228
- data, re.sub(COLUMN_PREFIX_REGEX, repl='', string=query.column, count=1), query.operator, query.value
229
- )
216
+ return _filter_to_series(data, re.sub(COLUMN_PREFIX_REGEX, '', query.column, 1), query.operator, query.value)
230
217
  elif isinstance(query, ClauseQuery):
231
218
  filters = None
232
219
 
@@ -235,9 +222,15 @@ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> Optional[Serie
235
222
 
236
223
  if resolved_clause is not None:
237
224
  if query.combinator == QueryCombinator.AND:
238
- filters = resolved_clause if filters is None else filters & resolved_clause
225
+ if filters is None:
226
+ filters = resolved_clause
227
+ else:
228
+ filters = filters & resolved_clause
239
229
  elif query.combinator == QueryCombinator.OR:
240
- filters = resolved_clause if filters is None else filters | resolved_clause
230
+ if filters is None:
231
+ filters = resolved_clause
232
+ else:
233
+ filters = filters | resolved_clause
241
234
  else:
242
235
  raise ValueError(f'Unknown combinator {query.combinator}')
243
236
 
@@ -246,18 +239,6 @@ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> Optional[Serie
246
239
  raise ValueError(f'Unknown query type {type(query)}')
247
240
 
248
241
 
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
-
261
242
  def apply_filters(
262
243
  data: Optional[DataFrame], filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
263
244
  ) -> Tuple[Optional[DataFrame], int]:
@@ -281,7 +262,7 @@ def apply_filters(
281
262
  if pagination is not None:
282
263
  # ON FETCHING SPECIFIC ROW
283
264
  if pagination.index is not None:
284
- return cast(DataFrame, data[int(pagination.index) : int(pagination.index) + 1]), total_count
265
+ return data[int(pagination.index) : int(pagination.index) + 1], total_count
285
266
 
286
267
  # SORT
287
268
  if pagination.orderBy is not None:
@@ -297,7 +278,7 @@ def apply_filters(
297
278
  if col == 'index':
298
279
  new_data = new_data.sort_index(ascending=ascending, inplace=False)
299
280
  else:
300
- new_data = new_data.sort_values(by=col, ascending=ascending, inplace=False) # type: ignore
281
+ new_data = new_data.sort_values(by=col, ascending=ascending, inplace=False)
301
282
 
302
283
  # PAGINATE
303
284
  start_index = pagination.offset if pagination.offset is not None else 0
@@ -305,4 +286,4 @@ def apply_filters(
305
286
 
306
287
  new_data = new_data.iloc[start_index:stop_index]
307
288
 
308
- return cast(DataFrame, new_data), total_count
289
+ return new_data, total_count
@@ -2,10 +2,10 @@ from typing import List, Optional
2
2
 
3
3
  from pydantic import Field, SerializerFunctionWrapHandler, model_serializer
4
4
 
5
- from .client_variable import ClientVariable
5
+ from .non_data_variable import NonDataVariable
6
6
 
7
7
 
8
- class LoopVariable(ClientVariable):
8
+ class LoopVariable(NonDataVariable):
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,8 +1,71 @@
1
- from typing_extensions import TypeAlias
1
+ """
2
+ Copyright 2023 Impulse Innovations Limited
2
3
 
3
- from .client_variable import * # noqa: F403
4
4
 
5
- NonDataVariable: TypeAlias = ClientVariable # noqa: F405
6
- """
7
- Deprecated alias for ClientVariable
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
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
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.
8
16
  """
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,10 +17,9 @@ limitations under the License.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- import warnings
21
20
  from contextlib import contextmanager
22
21
  from contextvars import ContextVar
23
- from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
22
+ from typing import Any, Callable, Generic, List, Optional, TypeVar
24
23
 
25
24
  from fastapi.encoders import jsonable_encoder
26
25
  from pydantic import (
@@ -31,25 +30,26 @@ from pydantic import (
31
30
  model_serializer,
32
31
  )
33
32
 
34
- from dara.core.interactivity.client_variable import ClientVariable
33
+ from dara.core.interactivity.derived_data_variable import DerivedDataVariable
35
34
  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 BackendStore, BrowserStore, PersistenceStore
38
+ from dara.core.persistence import PersistenceStore
39
39
 
40
40
  VARIABLE_INIT_OVERRIDE = ContextVar[Optional[Callable[[dict], dict]]]('VARIABLE_INIT_OVERRIDE', default=None)
41
41
 
42
42
  VariableType = TypeVar('VariableType')
43
43
  PersistenceStoreType_co = TypeVar('PersistenceStoreType_co', bound=PersistenceStore, covariant=True)
44
44
 
45
-
46
45
  # TODO: once Python supports a default value for a generic type properly we can make PersistenceStoreType a second generic param
47
- class Variable(ClientVariable, Generic[VariableType]):
46
+ class Variable(NonDataVariable, Generic[VariableType]):
48
47
  """
49
48
  A Variable represents a dynamic value in the system that can be read and written to by components and actions
50
49
  """
51
50
 
52
51
  default: Optional[VariableType] = None
52
+ persist_value: bool = False
53
53
  store: Optional[PersistenceStore] = None
54
54
  uid: str
55
55
  nested: List[str] = Field(default_factory=list)
@@ -62,7 +62,6 @@ class Variable(ClientVariable, Generic[VariableType]):
62
62
  uid: Optional[str] = None,
63
63
  store: Optional[PersistenceStoreType_co] = None,
64
64
  nested: Optional[List[str]] = None,
65
- **kwargs,
66
65
  ):
67
66
  """
68
67
  A Variable represents a dynamic value in the system that can be read and written to by components and actions
@@ -74,32 +73,18 @@ class Variable(ClientVariable, Generic[VariableType]):
74
73
  """
75
74
  if nested is None:
76
75
  nested = []
77
- kwargs = {
78
- 'default': default,
79
- 'uid': uid,
80
- 'store': store,
81
- 'nested': nested,
82
- **kwargs,
83
- }
76
+ kwargs = {'default': default, 'persist_value': persist_value, 'uid': uid, 'store': store, 'nested': nested}
84
77
 
85
78
  # If an override is active, run the kwargs through it
86
79
  override = VARIABLE_INIT_OVERRIDE.get()
87
80
  if override is not None:
88
81
  kwargs = override(kwargs)
89
82
 
90
- if kwargs.get('store') is not None and persist_value:
83
+ if kwargs.get('store') is not None and kwargs.get('persist_value'):
91
84
  # TODO: this is temporary, persist_value will eventually become a type of store
92
85
  raise ValueError('Cannot provide a Variable with both a store and persist_value set to True')
93
86
 
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
-
102
- super().__init__(**kwargs) # type: ignore
87
+ super().__init__(**kwargs) # type: ignore
103
88
 
104
89
  if self.store:
105
90
  call_async(self.store.init, self)
@@ -130,7 +115,7 @@ class Variable(ClientVariable, Generic[VariableType]):
130
115
  Override the init function of all Variables created within the context of this function.
131
116
 
132
117
  ```python
133
- with Variable.init_override(lambda kwargs: {**kwargs, 'store': ...}):
118
+ with Variable.init_override(lambda kwargs: {**kwargs, 'persist_value': True}):
134
119
  var = Variable()
135
120
  ```
136
121
 
@@ -267,71 +252,12 @@ class Variable(ClientVariable, Generic[VariableType]):
267
252
 
268
253
  :param default: the initial value for the variable, defaults to None
269
254
  """
270
- return cls(default=other) # type: ignore
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.
255
+ if isinstance(other, DerivedDataVariable):
256
+ raise ValueError(
257
+ 'Cannot create a Variable from a DerivedDataVariable, only standard DerivedVariables are allowed'
258
+ )
329
259
 
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()
260
+ return cls(default=other) # type: ignore
335
261
 
336
262
  @model_serializer(mode='wrap')
337
263
  def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
@@ -21,11 +21,11 @@ from typing import Any, Dict, Optional, Union
21
21
 
22
22
  from pydantic import SerializerFunctionWrapHandler, field_validator, model_serializer
23
23
 
24
- from dara.core.interactivity.client_variable import ClientVariable
25
24
  from dara.core.interactivity.condition import Condition
25
+ from dara.core.interactivity.non_data_variable import NonDataVariable
26
26
 
27
27
 
28
- class SwitchVariable(ClientVariable):
28
+ class SwitchVariable(NonDataVariable):
29
29
  """
30
30
  A SwitchVariable represents a conditional value that switches between
31
31
  different values based on a condition or variable value.
@@ -216,21 +216,21 @@ class SwitchVariable(ClientVariable):
216
216
  Key Serialization:
217
217
  When using mappings with SwitchVariable, be aware that JavaScript object keys
218
218
  are always strings. The system automatically converts lookup keys to strings:
219
- - Python: `{True: 'admin', False: 'user'}`
220
- - JavaScript: `{"true": "admin", "false": "user"}`
219
+ - Python: {True: 'admin', False: 'user'}
220
+ - JavaScript: {"true": "admin", "false": "user"}
221
221
  - Boolean values are converted to lowercase strings ("true"/"false")
222
222
  - Other values use standard string conversion to match JavaScript's String() behavior
223
223
  """
224
224
 
225
- value: Optional[Union[Condition, ClientVariable, Any]] = None
225
+ value: Optional[Union[Condition, NonDataVariable, Any]] = None
226
226
  # must be typed as any, otherwise pydantic is trying to instantiate the variables incorrectly
227
227
  value_map: Optional[Any] = None
228
228
  default: Optional[Any] = None
229
229
 
230
230
  def __init__(
231
231
  self,
232
- value: Union[Condition, ClientVariable, Any],
233
- value_map: Dict[Any, Any] | ClientVariable,
232
+ value: Union[Condition, NonDataVariable, Any],
233
+ value_map: Dict[Any, Any] | NonDataVariable,
234
234
  default: Optional[Any] = None,
235
235
  uid: Optional[str] = None,
236
236
  ):
@@ -253,28 +253,28 @@ class SwitchVariable(ClientVariable):
253
253
  @classmethod
254
254
  def validate_value_map(cls, v):
255
255
  """
256
- Validate that value_map is either a dict or a ClientVariable.
256
+ Validate that value_map is either a dict or a NonDataVariable.
257
257
 
258
258
  :param v: The value to validate
259
259
  :return: The validated value
260
- :raises ValueError: If value_map is not a dict or ClientVariable
260
+ :raises ValueError: If value_map is not a dict or NonDataVariable
261
261
  """
262
262
  if v is None:
263
263
  return v
264
264
  if isinstance(v, dict):
265
265
  return v
266
- if isinstance(v, ClientVariable):
266
+ if isinstance(v, NonDataVariable):
267
267
  return v
268
- raise ValueError(f'value_map must be a dict or ClientVariable, got {type(v)}')
268
+ raise ValueError(f'value_map must be a dict or NonDataVariable, got {type(v)}')
269
269
 
270
270
  @classmethod
271
271
  def when(
272
272
  cls,
273
- condition: Union[Condition, ClientVariable, Any],
274
- true_value: Union[Any, ClientVariable],
275
- false_value: Union[Any, ClientVariable],
273
+ condition: Union[Condition, NonDataVariable, Any],
274
+ true_value: Union[Any, NonDataVariable],
275
+ false_value: Union[Any, NonDataVariable],
276
276
  uid: Optional[str] = None,
277
- ) -> SwitchVariable:
277
+ ) -> 'SwitchVariable':
278
278
  """
279
279
  Create a SwitchVariable for boolean conditions.
280
280
 
@@ -346,11 +346,11 @@ class SwitchVariable(ClientVariable):
346
346
  @classmethod
347
347
  def match(
348
348
  cls,
349
- value: Union[ClientVariable, Any],
350
- mapping: Union[Dict[Any, Any], ClientVariable],
351
- default: Optional[Union[Any, ClientVariable]] = None,
349
+ value: Union[NonDataVariable, Any],
350
+ mapping: Union[Dict[Any, Any], NonDataVariable],
351
+ default: Optional[Union[Any, NonDataVariable]] = None,
352
352
  uid: Optional[str] = None,
353
- ) -> SwitchVariable:
353
+ ) -> 'SwitchVariable':
354
354
  """
355
355
  Create a SwitchVariable with a custom mapping.
356
356
 
@@ -17,21 +17,16 @@ limitations under the License.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from typing import Optional, TypeVar
20
+ from typing import Any, Generic, Optional, TypeVar
21
21
 
22
- from pydantic import ConfigDict
23
- from typing_extensions import deprecated
22
+ from pydantic import ConfigDict, SerializerFunctionWrapHandler, model_serializer
24
23
 
25
- from dara.core.interactivity.plain_variable import Variable
26
- from dara.core.persistence import QueryParamStore
24
+ from dara.core.interactivity.non_data_variable import NonDataVariable
27
25
 
28
26
  VariableType = TypeVar('VariableType')
29
27
 
30
28
 
31
- @deprecated(
32
- 'UrlVariable is deprecated and will be removed in a future version. Use dara.core.interactivity.plain_variable.Variable with dara.core.persistence.QueryParamStore instead'
33
- )
34
- class UrlVariable(Variable[VariableType]):
29
+ class UrlVariable(NonDataVariable, Generic[VariableType]):
35
30
  """
36
31
  A UrlVariable is very similar to a normal Variable however rather than it's state being stored in the memory of
37
32
  the client it's value is stored in the url of page as a query parameter. This is very useful for parameterizing
@@ -53,4 +48,89 @@ class UrlVariable(Variable[VariableType]):
53
48
  :param default: the initial value for the variable, defaults to None
54
49
  :param uid: the unique identifier for this variable; if not provided a random one is generated
55
50
  """
56
- super().__init__(default=default, uid=uid, store=QueryParamStore(query=query), query=query)
51
+ super().__init__(query=query, default=default, uid=uid)
52
+
53
+ def sync(self):
54
+ """
55
+ Create an action to synchronise the value of this UrlVariable with input value sent from the component.
56
+
57
+ ```python
58
+
59
+ from dara.core import UrlVariable
60
+ from dara.components import Select
61
+
62
+ var = UrlVariable('first', query='num')
63
+ another_var = UrlVariable('second', query='num_two')
64
+
65
+ Select(
66
+ value=var,
67
+ items=['first', 'second', 'third'],
68
+ onchange=another_var.sync(),
69
+ )
70
+
71
+ ```
72
+ """
73
+ from dara.core.interactivity.actions import (
74
+ UpdateVariableImpl,
75
+ assert_no_context,
76
+ )
77
+
78
+ assert_no_context('ctx.update')
79
+ return UpdateVariableImpl(variable=self, value=UpdateVariableImpl.INPUT)
80
+
81
+ def toggle(self):
82
+ """
83
+ Create an action to toggle the value of this UrlVariable. Note this only works for boolean variables.
84
+
85
+ ```python
86
+
87
+ from dara.core import UrlVariable
88
+ from dara.components import Button
89
+
90
+ var = UrlVariable(True, query='show')
91
+
92
+ Button(
93
+ 'Toggle',
94
+ onclick=var.toggle(),
95
+ )
96
+
97
+ ```
98
+ """
99
+ from dara.core.interactivity.actions import (
100
+ UpdateVariableImpl,
101
+ assert_no_context,
102
+ )
103
+
104
+ assert_no_context('ctx.update')
105
+ return UpdateVariableImpl(variable=self, value=UpdateVariableImpl.TOGGLE)
106
+
107
+ def update(self, value: Any):
108
+ """
109
+ Create an action to update the value of this UrlVariable to a provided value.
110
+
111
+ ```python
112
+
113
+ from dara.core import UrlVariable
114
+ from dara.components import Button
115
+
116
+ show = UrlVariable(True, query='show')
117
+
118
+ Button(
119
+ 'Hide',
120
+ onclick=show.update(False),
121
+ )
122
+
123
+ ```
124
+ """
125
+ from dara.core.interactivity.actions import (
126
+ UpdateVariableImpl,
127
+ assert_no_context,
128
+ )
129
+
130
+ assert_no_context('ctx.update')
131
+ return UpdateVariableImpl(variable=self, value=value)
132
+
133
+ @model_serializer(mode='wrap')
134
+ def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
135
+ parent_dict = nxt(self)
136
+ return {**parent_dict, '__typename': 'UrlVariable', 'uid': str(parent_dict['uid'])}
@@ -19,13 +19,12 @@ class CacheStoreImpl(abc.ABC, Generic[PolicyT]):
19
19
  """
20
20
 
21
21
  @abc.abstractmethod
22
- async def get(self, key: str, unpin: bool = False, raise_for_missing: bool = False) -> Any:
22
+ async def get(self, key: str, unpin: bool = False) -> Any:
23
23
  """
24
24
  Retrieve an entry from the cache.
25
25
 
26
26
  :param key: The key of the entry to retrieve.
27
27
  :param unpin: If true, the entry will be unpinned if it is pinned.
28
- :param raise_for_missing: If true, an exception will be raised if the entry is not found
29
28
  """
30
29
 
31
30
  @abc.abstractmethod