dara-core 1.19.1__py3-none-any.whl → 1.20.0__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 +4 -0
- dara/core/interactivity/actions.py +20 -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 +335 -201
- 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 +2 -2
- 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 +1 -1
- 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 +751 -1386
- dara/core/visual/dynamic_component.py +10 -13
- {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/METADATA +10 -10
- {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/RECORD +51 -47
- {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/LICENSE +0 -0
- {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/WHEEL +0 -0
- {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
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
|
|
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.
|
|
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 ClientVariable(AnyVariable, abc.ABC):
|
|
27
|
+
"""
|
|
28
|
+
Client represents any variable which can be ordinarily serialized to the client
|
|
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,40 +17,15 @@ limitations under the License.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
import
|
|
21
|
-
from typing import Optional, Union, cast
|
|
20
|
+
from typing import Optional
|
|
22
21
|
|
|
23
|
-
from anyio.abc import TaskGroup
|
|
24
22
|
from pandas import DataFrame
|
|
25
|
-
from pandas.io.json._table_schema import build_table_schema
|
|
26
|
-
from pydantic import (
|
|
27
|
-
BaseModel,
|
|
28
|
-
ConfigDict,
|
|
29
|
-
SerializerFunctionWrapHandler,
|
|
30
|
-
model_serializer,
|
|
31
|
-
)
|
|
32
23
|
|
|
33
|
-
from dara.core.base_definitions import
|
|
34
|
-
from dara.core.interactivity.
|
|
35
|
-
AnyDataVariable,
|
|
36
|
-
DataFrameSchema,
|
|
37
|
-
DataVariableRegistryEntry,
|
|
38
|
-
)
|
|
39
|
-
from dara.core.interactivity.filtering import (
|
|
40
|
-
FilterQuery,
|
|
41
|
-
Pagination,
|
|
42
|
-
apply_filters,
|
|
43
|
-
coerce_to_filter_query,
|
|
44
|
-
)
|
|
45
|
-
from dara.core.internal.cache_store import CacheStore
|
|
46
|
-
from dara.core.internal.hashing import hash_object
|
|
47
|
-
from dara.core.internal.pandas_utils import append_index, df_convert_to_internal
|
|
48
|
-
from dara.core.internal.utils import call_async
|
|
49
|
-
from dara.core.internal.websocket import WebsocketManager
|
|
50
|
-
from dara.core.logging import eng_logger
|
|
24
|
+
from dara.core.base_definitions import Cache, CacheArgType
|
|
25
|
+
from dara.core.interactivity.server_variable import ServerVariable
|
|
51
26
|
|
|
52
27
|
|
|
53
|
-
class DataVariable(
|
|
28
|
+
class DataVariable(ServerVariable):
|
|
54
29
|
"""
|
|
55
30
|
DataVariable represents a variable that is specifically designed to hold datasets.
|
|
56
31
|
|
|
@@ -69,11 +44,6 @@ class DataVariable(AnyDataVariable):
|
|
|
69
44
|
>>> UpdateVariable(func=some_data_transformation, variable=session_data)
|
|
70
45
|
"""
|
|
71
46
|
|
|
72
|
-
uid: str
|
|
73
|
-
filters: Optional[FilterQuery] = None
|
|
74
|
-
cache: Optional[BaseCachePolicy] = None
|
|
75
|
-
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True, use_enum_values=True)
|
|
76
|
-
|
|
77
47
|
def __init__(
|
|
78
48
|
self,
|
|
79
49
|
data: Optional[DataFrame] = None,
|
|
@@ -89,239 +59,11 @@ class DataVariable(AnyDataVariable):
|
|
|
89
59
|
:param filters: a dictionary of filters to apply to the data
|
|
90
60
|
:param cache: how to cache the result; 'user' per user, 'session' per session, 'global' for all users
|
|
91
61
|
"""
|
|
92
|
-
|
|
62
|
+
cache_policy = Cache.Policy.from_arg(cache)
|
|
93
63
|
|
|
94
|
-
if data is not None and
|
|
64
|
+
if data is not None and cache_policy.cache_type is not Cache.Type.GLOBAL:
|
|
95
65
|
raise ValueError('Data cannot be cached per session or per user if provided upfront')
|
|
96
66
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# Register the variable with the dataset
|
|
100
|
-
from dara.core.internal.registries import data_variable_registry
|
|
101
|
-
|
|
102
|
-
var_entry = DataVariableRegistryEntry(
|
|
103
|
-
cache=cache,
|
|
104
|
-
uid=str(self.uid),
|
|
105
|
-
type='plain',
|
|
106
|
-
get_data=DataVariable.get_value,
|
|
107
|
-
get_total_count=DataVariable.get_total_count,
|
|
108
|
-
get_schema=DataVariable.get_schema,
|
|
109
|
-
)
|
|
110
|
-
data_variable_registry.register(
|
|
111
|
-
str(self.uid),
|
|
112
|
-
var_entry,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
# Put the data entry into the store if not empty (so cache='global')
|
|
116
|
-
# We don't create an entry in a different case since session key will be global anyway at this point
|
|
117
|
-
if data is not None:
|
|
118
|
-
from dara.core.internal.registries import utils_registry
|
|
119
|
-
|
|
120
|
-
store: CacheStore = utils_registry.get('Store')
|
|
121
|
-
call_async(self._update, var_entry, store, data)
|
|
122
|
-
|
|
123
|
-
@staticmethod
|
|
124
|
-
def _get_cache_key(uid: str) -> str:
|
|
125
|
-
"""
|
|
126
|
-
Get a unique cache key for the data variable.
|
|
127
|
-
|
|
128
|
-
:param uid: uid of the DataVariable
|
|
129
|
-
"""
|
|
130
|
-
return f'data-{uid}'
|
|
131
|
-
|
|
132
|
-
@staticmethod
|
|
133
|
-
def _get_schema_cache_key(uid: str) -> str:
|
|
134
|
-
"""
|
|
135
|
-
Get a unique cache key for the data variable's schema.
|
|
136
|
-
|
|
137
|
-
:param uid: uid of the DataVariable
|
|
138
|
-
"""
|
|
139
|
-
return f'schema-{uid}'
|
|
140
|
-
|
|
141
|
-
@classmethod
|
|
142
|
-
def _get_count_cache_key(cls, uid: str, filters: Optional[Union[FilterQuery, dict]]) -> str:
|
|
143
|
-
return f'{cls._get_cache_key(uid)}_{hash_object(filters)}'
|
|
144
|
-
|
|
145
|
-
@classmethod
|
|
146
|
-
async def _update(cls, var_entry: DataVariableRegistryEntry, store: CacheStore, data: Optional[DataFrame]):
|
|
147
|
-
"""
|
|
148
|
-
Internal helper which updates the data variable entry in store.
|
|
149
|
-
|
|
150
|
-
TODO: for now data is always kept in store, in the future depending on the size data might be cached on disk
|
|
151
|
-
"""
|
|
152
|
-
await store.set(var_entry, key=cls._get_cache_key(var_entry.uid), value=DataStoreEntry(data=append_index(data)))
|
|
153
|
-
|
|
154
|
-
@classmethod
|
|
155
|
-
def update_value(cls, var_entry: DataVariableRegistryEntry, store: CacheStore, data: Optional[DataFrame]):
|
|
156
|
-
"""
|
|
157
|
-
Update the data entry and notify all clients about the update.
|
|
158
|
-
|
|
159
|
-
:param var_entry: the entry in the registry
|
|
160
|
-
:param data: the data to update
|
|
161
|
-
:param store: store instance
|
|
162
|
-
"""
|
|
163
|
-
from dara.core.internal.registries import utils_registry
|
|
164
|
-
|
|
165
|
-
ws_mgr: WebsocketManager = utils_registry.get('WebsocketManager')
|
|
166
|
-
task_group: TaskGroup = utils_registry.get('TaskGroup')
|
|
167
|
-
|
|
168
|
-
# Update store
|
|
169
|
-
task_group.start_soon(cls._update, var_entry, store, data)
|
|
170
|
-
|
|
171
|
-
# Broadcast the update to all clients
|
|
172
|
-
task_group.start_soon(
|
|
173
|
-
ws_mgr.broadcast,
|
|
174
|
-
{
|
|
175
|
-
'data_id': str(var_entry.uid),
|
|
176
|
-
},
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
@classmethod
|
|
180
|
-
async def get_value(
|
|
181
|
-
cls,
|
|
182
|
-
var_entry: DataVariableRegistryEntry,
|
|
183
|
-
store: CacheStore,
|
|
184
|
-
filters: Optional[Union[FilterQuery, dict]] = None,
|
|
185
|
-
pagination: Optional[Pagination] = None,
|
|
186
|
-
format_for_display: bool = False,
|
|
187
|
-
) -> Optional[DataFrame]:
|
|
188
|
-
"""
|
|
189
|
-
Get the value of this DataVariable.
|
|
190
|
-
"""
|
|
191
|
-
_uid_short = f'{var_entry.uid[:3]}..{var_entry.uid[-3:]}'
|
|
192
|
-
eng_logger.info(
|
|
193
|
-
f'Data Variable {_uid_short} get_value',
|
|
194
|
-
{'uid': var_entry.uid, 'filters': filters, 'pagination': pagination},
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
cache_key = cls._get_cache_key(var_entry.uid)
|
|
198
|
-
entry = await store.get(var_entry, key=cache_key)
|
|
199
|
-
|
|
200
|
-
eng_logger.debug(
|
|
201
|
-
f'Data Variable {_uid_short}',
|
|
202
|
-
'retrieved from cache',
|
|
203
|
-
{
|
|
204
|
-
'uid': var_entry.uid,
|
|
205
|
-
'size': len(entry.data.index) if entry is not None and entry.data is not None else 0,
|
|
206
|
-
},
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if entry is None:
|
|
210
|
-
await asyncio.gather(
|
|
211
|
-
store.set(var_entry, key=cls._get_count_cache_key(var_entry.uid, filters), value=0, pin=True),
|
|
212
|
-
store.set(var_entry, key=cls._get_schema_cache_key(var_entry.uid), value=None, pin=True),
|
|
213
|
-
)
|
|
214
|
-
return None
|
|
215
|
-
|
|
216
|
-
data = None
|
|
217
|
-
|
|
218
|
-
if entry.data is not None:
|
|
219
|
-
filtered_data, count = apply_filters(entry.data, coerce_to_filter_query(filters), pagination)
|
|
220
|
-
if format_for_display and filtered_data is not None:
|
|
221
|
-
filtered_data = filtered_data.copy()
|
|
222
|
-
for col in filtered_data.columns:
|
|
223
|
-
if filtered_data[col].dtype == 'object':
|
|
224
|
-
# We need to convert all values to string to avoid issues with displaying data in the Table component, for example when displaying datetime and number objects in the same column
|
|
225
|
-
filtered_data.loc[:, col] = filtered_data[col].apply(str)
|
|
226
|
-
data = filtered_data
|
|
227
|
-
# Store count for given filters and schema
|
|
228
|
-
await asyncio.gather(
|
|
229
|
-
store.set(var_entry, key=cls._get_count_cache_key(var_entry.uid, filters), value=count, pin=True),
|
|
230
|
-
store.set(
|
|
231
|
-
var_entry,
|
|
232
|
-
key=cls._get_schema_cache_key(var_entry.uid),
|
|
233
|
-
value=build_table_schema(df_convert_to_internal(entry.data)),
|
|
234
|
-
pin=True,
|
|
235
|
-
),
|
|
236
|
-
)
|
|
237
|
-
else:
|
|
238
|
-
await asyncio.gather(
|
|
239
|
-
store.set(var_entry, key=cls._get_count_cache_key(var_entry.uid, filters), value=0, pin=True),
|
|
240
|
-
store.set(var_entry, key=cls._get_schema_cache_key(var_entry.uid), value=None, pin=True),
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
# TODO: once path is supported, stream&filter from disk
|
|
244
|
-
if entry.path:
|
|
245
|
-
raise NotImplementedError('DataVariable.get_value() does not support disk caching yet')
|
|
246
|
-
|
|
247
|
-
eng_logger.info(
|
|
248
|
-
f'Data Variable {_uid_short} returning filtered data',
|
|
249
|
-
{'uid': var_entry.uid, 'size': len(data.index) if data is not None else 0},
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
return data
|
|
253
|
-
|
|
254
|
-
@classmethod
|
|
255
|
-
async def get_total_count(
|
|
256
|
-
cls, var_entry: DataVariableRegistryEntry, store: CacheStore, filters: Optional[FilterQuery]
|
|
257
|
-
):
|
|
258
|
-
"""
|
|
259
|
-
Get total count of the data variable.
|
|
260
|
-
|
|
261
|
-
:param var_entry: variable entry
|
|
262
|
-
:param store: store
|
|
263
|
-
:param filters: filters to get count for
|
|
264
|
-
"""
|
|
265
|
-
cache_key = cls._get_count_cache_key(var_entry.uid, filters)
|
|
266
|
-
entry = await store.get(var_entry, key=cache_key, unpin=True)
|
|
267
|
-
|
|
268
|
-
if entry is None:
|
|
269
|
-
raise ValueError('Requested count for filter setup which has not been performed yet')
|
|
270
|
-
|
|
271
|
-
return entry
|
|
272
|
-
|
|
273
|
-
@classmethod
|
|
274
|
-
async def get_schema(cls, var_entry: DataVariableRegistryEntry, store: CacheStore):
|
|
275
|
-
"""
|
|
276
|
-
Get the schema of the data variable.
|
|
277
|
-
|
|
278
|
-
:param var_entry: variable entry
|
|
279
|
-
:param store: store
|
|
280
|
-
"""
|
|
281
|
-
cache_key = cls._get_schema_cache_key(var_entry.uid)
|
|
282
|
-
entry = await store.get(var_entry, key=cache_key, unpin=True)
|
|
283
|
-
|
|
284
|
-
return cast(DataFrameSchema, entry)
|
|
285
|
-
|
|
286
|
-
def reset(self):
|
|
287
|
-
raise NotImplementedError('DataVariable cannot be reset')
|
|
288
|
-
|
|
289
|
-
def update(self, value: Optional[DataFrame]):
|
|
290
|
-
"""
|
|
291
|
-
Create an action to update the value of this Variable to a provided value.
|
|
292
|
-
|
|
293
|
-
```python
|
|
294
|
-
import pandas as pd
|
|
295
|
-
from dara.core import DataVariable
|
|
296
|
-
from dara.components import Button
|
|
297
|
-
|
|
298
|
-
data = DataVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
299
|
-
|
|
300
|
-
Button(
|
|
301
|
-
'Empty Data',
|
|
302
|
-
onclick=data.update(None),
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
```
|
|
306
|
-
"""
|
|
307
|
-
from dara.core.interactivity.actions import UpdateVariableImpl
|
|
308
|
-
|
|
309
|
-
return UpdateVariableImpl(variable=self, value=value)
|
|
310
|
-
|
|
311
|
-
@model_serializer(mode='wrap')
|
|
312
|
-
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
313
|
-
parent_dict = nxt(self)
|
|
314
|
-
if 'data' in parent_dict:
|
|
315
|
-
parent_dict.pop('data') # make sure data is not included in the serialised dict
|
|
316
|
-
return {**parent_dict, '__typename': 'DataVariable', 'uid': str(parent_dict['uid'])}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
class DataStoreEntry(BaseModel):
|
|
320
|
-
"""
|
|
321
|
-
Entry in the cache store for a DataVariable.
|
|
322
|
-
Can either be a DataFrame or a path to a file on disk.
|
|
323
|
-
"""
|
|
67
|
+
scope = 'global' if cache_policy.cache_type == Cache.Type.GLOBAL else 'user'
|
|
324
68
|
|
|
325
|
-
|
|
326
|
-
path: Optional[str] = None
|
|
327
|
-
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
69
|
+
super().__init__(scope=scope, uid=uid, default=data, **kwargs)
|