dara-core 1.20.1a1__py3-none-any.whl → 1.20.1a2__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 +3 -0
  2. dara/core/actions.py +1 -2
  3. dara/core/auth/basic.py +22 -16
  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 +22 -64
  8. dara/core/cli.py +8 -7
  9. dara/core/configuration.py +5 -2
  10. dara/core/css.py +1 -2
  11. dara/core/data_utils.py +18 -19
  12. dara/core/defaults.py +6 -7
  13. dara/core/definitions.py +50 -19
  14. dara/core/http.py +7 -3
  15. dara/core/interactivity/__init__.py +6 -0
  16. dara/core/interactivity/actions.py +52 -50
  17. dara/core/interactivity/any_data_variable.py +7 -134
  18. dara/core/interactivity/any_variable.py +5 -8
  19. dara/core/interactivity/client_variable.py +71 -0
  20. dara/core/interactivity/data_variable.py +8 -266
  21. dara/core/interactivity/derived_data_variable.py +7 -290
  22. dara/core/interactivity/derived_variable.py +416 -176
  23. dara/core/interactivity/filtering.py +46 -27
  24. dara/core/interactivity/loop_variable.py +2 -2
  25. dara/core/interactivity/non_data_variable.py +5 -68
  26. dara/core/interactivity/plain_variable.py +89 -15
  27. dara/core/interactivity/server_variable.py +325 -0
  28. dara/core/interactivity/state_variable.py +69 -0
  29. dara/core/interactivity/switch_variable.py +19 -19
  30. dara/core/interactivity/tabular_variable.py +94 -0
  31. dara/core/interactivity/url_variable.py +10 -90
  32. dara/core/internal/cache_store/base_impl.py +2 -1
  33. dara/core/internal/cache_store/cache_store.py +22 -25
  34. dara/core/internal/cache_store/keep_all.py +4 -1
  35. dara/core/internal/cache_store/lru.py +5 -1
  36. dara/core/internal/cache_store/ttl.py +4 -1
  37. dara/core/internal/cgroup.py +1 -1
  38. dara/core/internal/dependency_resolution.py +60 -66
  39. dara/core/internal/devtools.py +12 -5
  40. dara/core/internal/download.py +13 -4
  41. dara/core/internal/encoder_registry.py +7 -7
  42. dara/core/internal/execute_action.py +13 -13
  43. dara/core/internal/hashing.py +1 -3
  44. dara/core/internal/import_discovery.py +3 -4
  45. dara/core/internal/multi_resource_lock.py +70 -0
  46. dara/core/internal/normalization.py +9 -18
  47. dara/core/internal/pandas_utils.py +107 -5
  48. dara/core/internal/pool/definitions.py +1 -1
  49. dara/core/internal/pool/task_pool.py +25 -16
  50. dara/core/internal/pool/utils.py +21 -18
  51. dara/core/internal/pool/worker.py +3 -2
  52. dara/core/internal/port_utils.py +1 -1
  53. dara/core/internal/registries.py +12 -6
  54. dara/core/internal/registry.py +4 -2
  55. dara/core/internal/registry_lookup.py +11 -5
  56. dara/core/internal/routing.py +109 -145
  57. dara/core/internal/scheduler.py +13 -8
  58. dara/core/internal/settings.py +2 -2
  59. dara/core/internal/store.py +2 -29
  60. dara/core/internal/tasks.py +379 -195
  61. dara/core/internal/utils.py +36 -13
  62. dara/core/internal/websocket.py +21 -20
  63. dara/core/js_tooling/js_utils.py +28 -26
  64. dara/core/js_tooling/templates/vite.config.template.ts +12 -3
  65. dara/core/logging.py +13 -12
  66. dara/core/main.py +14 -11
  67. dara/core/metrics/cache.py +1 -1
  68. dara/core/metrics/utils.py +3 -3
  69. dara/core/persistence.py +27 -5
  70. dara/core/umd/dara.core.umd.js +68291 -64718
  71. dara/core/visual/components/__init__.py +2 -2
  72. dara/core/visual/components/fallback.py +30 -4
  73. dara/core/visual/components/for_cmp.py +4 -1
  74. dara/core/visual/css/__init__.py +30 -31
  75. dara/core/visual/dynamic_component.py +31 -28
  76. dara/core/visual/progress_updater.py +4 -3
  77. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a2.dist-info}/METADATA +12 -11
  78. dara_core-1.20.1a2.dist-info/RECORD +119 -0
  79. dara_core-1.20.1a1.dist-info/RECORD +0 -114
  80. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a2.dist-info}/LICENSE +0 -0
  81. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a2.dist-info}/WHEEL +0 -0
  82. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a2.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
23
+ from typing import Any, List, Optional, Tuple, Union, cast, overload
24
24
 
25
25
  import numpy
26
- from pandas import DataFrame, Series # pylint: disable=unused-import
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'
@@ -157,11 +172,11 @@ def infer_column_type(series: Series) -> ColumnType:
157
172
  return ColumnType.CATEGORICAL
158
173
 
159
174
 
160
- def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, value: Any) -> Optional['Series[bool]']:
175
+ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, value: Any) -> Optional[Series]:
161
176
  """
162
177
  Convert a single filter to a Series[bool] for filtering
163
178
  """
164
- series = data[column]
179
+ series = cast(Series, data[column])
165
180
 
166
181
  # Contains is a special case, we always treat the column as a string
167
182
  if operator == QueryOperator.CONTAINS:
@@ -175,19 +190,15 @@ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, val
175
190
  return series.isin(value)
176
191
  # Converts date passed from frontend to the right format to compare with pandas
177
192
  if col_type == ColumnType.DATETIME:
178
- if isinstance(value, List):
179
- value = [parseISO(value[0]), parseISO(value[1])]
180
- else:
181
- value = parseISO(value)
193
+ value = [parseISO(value[0]), parseISO(value[1])] if isinstance(value, List) else parseISO(value)
182
194
  elif col_type == ColumnType.CATEGORICAL:
183
195
  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]
184
200
  else:
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)
201
+ value = float(value) if '.' in str(value) else int(value)
191
202
 
192
203
  if operator == QueryOperator.GT:
193
204
  return series > value
@@ -208,12 +219,14 @@ def _filter_to_series(data: DataFrame, column: str, operator: QueryOperator, val
208
219
  return None
209
220
 
210
221
 
211
- def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> 'Optional[Series[bool]]':
222
+ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> Optional[Series]:
212
223
  """
213
224
  Resolve a FilterQuery to a Series[bool] for filtering. Strips the internal column index from the query.
214
225
  """
215
226
  if isinstance(query, ValueQuery):
216
- return _filter_to_series(data, re.sub(COLUMN_PREFIX_REGEX, '', query.column, 1), query.operator, query.value)
227
+ return _filter_to_series(
228
+ data, re.sub(COLUMN_PREFIX_REGEX, repl='', string=query.column, count=1), query.operator, query.value
229
+ )
217
230
  elif isinstance(query, ClauseQuery):
218
231
  filters = None
219
232
 
@@ -222,15 +235,9 @@ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> 'Optional[Seri
222
235
 
223
236
  if resolved_clause is not None:
224
237
  if query.combinator == QueryCombinator.AND:
225
- if filters is None:
226
- filters = resolved_clause
227
- else:
228
- filters = filters & resolved_clause
238
+ filters = resolved_clause if filters is None else filters & resolved_clause
229
239
  elif query.combinator == QueryCombinator.OR:
230
- if filters is None:
231
- filters = resolved_clause
232
- else:
233
- filters = filters | resolved_clause
240
+ filters = resolved_clause if filters is None else filters | resolved_clause
234
241
  else:
235
242
  raise ValueError(f'Unknown combinator {query.combinator}')
236
243
 
@@ -239,6 +246,18 @@ def _resolve_filter_query(data: DataFrame, query: FilterQuery) -> 'Optional[Seri
239
246
  raise ValueError(f'Unknown query type {type(query)}')
240
247
 
241
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
+
242
261
  def apply_filters(
243
262
  data: Optional[DataFrame], filters: Optional[FilterQuery] = None, pagination: Optional[Pagination] = None
244
263
  ) -> Tuple[Optional[DataFrame], int]:
@@ -262,7 +281,7 @@ def apply_filters(
262
281
  if pagination is not None:
263
282
  # ON FETCHING SPECIFIC ROW
264
283
  if pagination.index is not None:
265
- return data[int(pagination.index) : int(pagination.index) + 1], total_count
284
+ return cast(DataFrame, data[int(pagination.index) : int(pagination.index) + 1]), total_count
266
285
 
267
286
  # SORT
268
287
  if pagination.orderBy is not None:
@@ -278,7 +297,7 @@ def apply_filters(
278
297
  if col == 'index':
279
298
  new_data = new_data.sort_index(ascending=ascending, inplace=False)
280
299
  else:
281
- new_data = new_data.sort_values(by=col, ascending=ascending, inplace=False)
300
+ new_data = new_data.sort_values(by=col, ascending=ascending, inplace=False) # type: ignore
282
301
 
283
302
  # PAGINATE
284
303
  start_index = pagination.offset if pagination.offset is not None else 0
@@ -286,4 +305,4 @@ def apply_filters(
286
305
 
287
306
  new_data = new_data.iloc[start_index:stop_index]
288
307
 
289
- return new_data, total_count
308
+ return cast(DataFrame, 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 .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,26 +31,25 @@ 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
 
42
42
  VariableType = TypeVar('VariableType')
43
43
  PersistenceStoreType_co = TypeVar('PersistenceStoreType_co', bound=PersistenceStore, covariant=True)
44
44
 
45
+
45
46
  # TODO: once Python supports a default value for a generic type properly we can make PersistenceStoreType a second generic param
46
- class Variable(NonDataVariable, Generic[VariableType]):
47
+ class Variable(ClientVariable, Generic[VariableType]):
47
48
  """
48
49
  A Variable represents a dynamic value in the system that can be read and written to by components and actions
49
50
  """
50
51
 
51
52
  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,6 +62,7 @@ class Variable(NonDataVariable, Generic[VariableType]):
62
62
  uid: Optional[str] = None,
63
63
  store: Optional[PersistenceStoreType_co] = None,
64
64
  nested: Optional[List[str]] = None,
65
+ **kwargs,
65
66
  ):
66
67
  """
67
68
  A Variable represents a dynamic value in the system that can be read and written to by components and actions
@@ -73,18 +74,32 @@ class Variable(NonDataVariable, Generic[VariableType]):
73
74
  """
74
75
  if nested is None:
75
76
  nested = []
76
- 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
+ }
77
84
 
78
85
  # If an override is active, run the kwargs through it
79
86
  override = VARIABLE_INIT_OVERRIDE.get()
80
87
  if override is not None:
81
88
  kwargs = override(kwargs)
82
89
 
83
- if kwargs.get('store') is not None and kwargs.get('persist_value'):
90
+ if kwargs.get('store') is not None and persist_value:
84
91
  # TODO: this is temporary, persist_value will eventually become a type of store
85
92
  raise ValueError('Cannot provide a Variable with both a store and persist_value set to True')
86
93
 
87
- super().__init__(**kwargs) # type: ignore
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
88
103
 
89
104
  if self.store:
90
105
  call_async(self.store.init, self)
@@ -115,7 +130,7 @@ class Variable(NonDataVariable, Generic[VariableType]):
115
130
  Override the init function of all Variables created within the context of this function.
116
131
 
117
132
  ```python
118
- with Variable.init_override(lambda kwargs: {**kwargs, 'persist_value': True}):
133
+ with Variable.init_override(lambda kwargs: {**kwargs, 'store': ...}):
119
134
  var = Variable()
120
135
  ```
121
136
 
@@ -252,12 +267,71 @@ class Variable(NonDataVariable, Generic[VariableType]):
252
267
 
253
268
  :param default: the initial value for the variable, defaults to None
254
269
  """
255
- if isinstance(other, DerivedDataVariable):
256
- raise ValueError(
257
- 'Cannot create a Variable from a DerivedDataVariable, only standard DerivedVariables are allowed'
258
- )
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.
259
316
 
260
- return cls(default=other) # type: ignore
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()
261
335
 
262
336
  @model_serializer(mode='wrap')
263
337
  def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict: