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.
- dara/core/__init__.py +0 -3
- dara/core/actions.py +2 -1
- dara/core/auth/basic.py +16 -22
- dara/core/auth/definitions.py +2 -2
- dara/core/auth/routes.py +5 -5
- dara/core/auth/utils.py +5 -5
- dara/core/base_definitions.py +64 -22
- dara/core/cli.py +7 -8
- dara/core/configuration.py +2 -5
- dara/core/css.py +2 -1
- dara/core/data_utils.py +19 -18
- dara/core/defaults.py +7 -6
- dara/core/definitions.py +19 -50
- dara/core/http.py +3 -7
- dara/core/interactivity/__init__.py +0 -6
- dara/core/interactivity/actions.py +50 -52
- dara/core/interactivity/any_data_variable.py +134 -7
- dara/core/interactivity/any_variable.py +8 -5
- dara/core/interactivity/data_variable.py +266 -8
- dara/core/interactivity/derived_data_variable.py +290 -7
- dara/core/interactivity/derived_variable.py +174 -414
- dara/core/interactivity/filtering.py +27 -46
- dara/core/interactivity/loop_variable.py +2 -2
- dara/core/interactivity/non_data_variable.py +68 -5
- dara/core/interactivity/plain_variable.py +15 -89
- dara/core/interactivity/switch_variable.py +19 -19
- dara/core/interactivity/url_variable.py +90 -10
- dara/core/internal/cache_store/base_impl.py +1 -2
- dara/core/internal/cache_store/cache_store.py +25 -22
- dara/core/internal/cache_store/keep_all.py +1 -4
- dara/core/internal/cache_store/lru.py +1 -5
- dara/core/internal/cache_store/ttl.py +1 -4
- dara/core/internal/cgroup.py +1 -1
- dara/core/internal/dependency_resolution.py +66 -60
- dara/core/internal/devtools.py +5 -12
- dara/core/internal/download.py +4 -13
- dara/core/internal/encoder_registry.py +7 -7
- dara/core/internal/execute_action.py +13 -13
- dara/core/internal/hashing.py +3 -1
- dara/core/internal/import_discovery.py +4 -3
- dara/core/internal/normalization.py +18 -9
- dara/core/internal/pandas_utils.py +5 -107
- dara/core/internal/pool/definitions.py +1 -1
- dara/core/internal/pool/task_pool.py +16 -25
- dara/core/internal/pool/utils.py +18 -21
- dara/core/internal/pool/worker.py +2 -3
- dara/core/internal/port_utils.py +1 -1
- dara/core/internal/registries.py +6 -12
- dara/core/internal/registry.py +2 -4
- dara/core/internal/registry_lookup.py +5 -11
- dara/core/internal/routing.py +145 -109
- dara/core/internal/scheduler.py +8 -13
- dara/core/internal/settings.py +2 -2
- dara/core/internal/store.py +29 -2
- dara/core/internal/tasks.py +195 -379
- dara/core/internal/utils.py +13 -36
- dara/core/internal/websocket.py +20 -21
- dara/core/js_tooling/js_utils.py +26 -28
- dara/core/js_tooling/templates/vite.config.template.ts +3 -12
- dara/core/logging.py +12 -13
- dara/core/main.py +11 -14
- dara/core/metrics/cache.py +1 -1
- dara/core/metrics/utils.py +3 -3
- dara/core/persistence.py +5 -27
- dara/core/umd/dara.core.umd.js +55425 -59091
- dara/core/visual/components/__init__.py +2 -2
- dara/core/visual/components/fallback.py +4 -30
- dara/core/visual/components/for_cmp.py +1 -4
- dara/core/visual/css/__init__.py +31 -30
- dara/core/visual/dynamic_component.py +28 -31
- dara/core/visual/progress_updater.py +3 -4
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/METADATA +11 -12
- dara_core-1.20.1a1.dist-info/RECORD +114 -0
- dara/core/interactivity/client_variable.py +0 -71
- dara/core/interactivity/server_variable.py +0 -325
- dara/core/interactivity/state_variable.py +0 -69
- dara/core/interactivity/tabular_variable.py +0 -94
- dara/core/internal/multi_resource_lock.py +0 -70
- dara_core-1.20.0a1.dist-info/RECORD +0 -119
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/LICENSE +0 -0
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/WHEEL +0 -0
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/entry_points.txt +0 -0
|
@@ -1,11 +1,138 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Impulse Innovations Limited
|
|
2
3
|
|
|
3
|
-
from dara.core.interactivity.any_variable import AnyVariable
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
11
16
|
"""
|
|
17
|
+
|
|
18
|
+
import abc
|
|
19
|
+
import io
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any, Awaitable, Callable, Literal, Optional, TypedDict, Union, cast
|
|
22
|
+
|
|
23
|
+
import pandas
|
|
24
|
+
from fastapi import UploadFile
|
|
25
|
+
from pydantic import ConfigDict
|
|
26
|
+
|
|
27
|
+
from dara.core.base_definitions import CachedRegistryEntry, UploadResolverDef
|
|
28
|
+
from dara.core.interactivity.any_variable import AnyVariable
|
|
29
|
+
from dara.core.interactivity.filtering import FilterQuery
|
|
30
|
+
from dara.core.internal.cache_store.cache_store import CacheStore
|
|
31
|
+
from dara.core.internal.registry_lookup import RegistryLookup
|
|
32
|
+
from dara.core.internal.utils import run_user_handler
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AnyDataVariable(AnyVariable, abc.ABC):
|
|
36
|
+
"""
|
|
37
|
+
AnyDataVariable represents any variable that is specifically designed to hold datasets (i.e. DataVariable, DerivedDataVariable)
|
|
38
|
+
|
|
39
|
+
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
40
|
+
:param filters: a dictionary of filters to apply to the data
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
uid: str
|
|
44
|
+
filters: Optional[FilterQuery] = None
|
|
45
|
+
|
|
46
|
+
def __init__(self, uid: Optional[str] = None, **kwargs) -> None:
|
|
47
|
+
super().__init__(uid=uid, **kwargs)
|
|
48
|
+
|
|
49
|
+
def filter(self, filters: FilterQuery):
|
|
50
|
+
return self.copy(update={'filters': filters}, deep=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FieldType(TypedDict):
|
|
54
|
+
name: Union[str, tuple[str, ...]]
|
|
55
|
+
type: Literal['integer', 'number', 'boolean', 'datetime', 'duration', 'any', 'str']
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DataFrameSchema(TypedDict):
|
|
59
|
+
fields: list[FieldType]
|
|
60
|
+
primaryKey: list[str]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DataVariableRegistryEntry(CachedRegistryEntry):
|
|
64
|
+
"""
|
|
65
|
+
Registry entry for DataVariable.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
type: Literal['plain', 'derived']
|
|
69
|
+
get_data: Callable[..., Awaitable[Any]]
|
|
70
|
+
"""Handler to get the data from the data variable. Defaults to DataVariable.get_value for type=plain, and DerivedDataVariable.get_data for type=derived"""
|
|
71
|
+
|
|
72
|
+
get_total_count: Callable[..., Awaitable[int]]
|
|
73
|
+
"""Handler to get the total number of rows in the data variable. Defaults to DataVariable.get_total_count for type=plain, and DerivedDataVariable.get_total_count for type=derived"""
|
|
74
|
+
|
|
75
|
+
get_schema: Callable[..., Awaitable[DataFrameSchema]]
|
|
76
|
+
"""Handler to get the schema for data variable. Defaults to DataVariable.get_schema for type=plain, and DerivedDataVariable.get_schema for type=derived"""
|
|
77
|
+
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def upload(data: UploadFile, data_uid: Optional[str] = None, resolver_id: Optional[str] = None):
|
|
81
|
+
"""
|
|
82
|
+
Handler for uploading data.
|
|
83
|
+
|
|
84
|
+
:param data: the file to upload
|
|
85
|
+
:param data_uid: optional uid of the data variable to upload to
|
|
86
|
+
:param resolver_id: optional id of the upload resolver to use, falls back to default handlers for csv/xlsx
|
|
87
|
+
"""
|
|
88
|
+
from dara.core.interactivity.data_variable import DataVariable
|
|
89
|
+
from dara.core.internal.registries import (
|
|
90
|
+
data_variable_registry,
|
|
91
|
+
upload_resolver_registry,
|
|
92
|
+
utils_registry,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
store: CacheStore = utils_registry.get('Store')
|
|
96
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
97
|
+
|
|
98
|
+
if data.filename is None:
|
|
99
|
+
raise ValueError('Filename not provided')
|
|
100
|
+
|
|
101
|
+
variable = None
|
|
102
|
+
|
|
103
|
+
_name, file_type = os.path.splitext(data.filename)
|
|
104
|
+
|
|
105
|
+
if data_uid is not None:
|
|
106
|
+
try:
|
|
107
|
+
variable = await registry_mgr.get(data_variable_registry, data_uid)
|
|
108
|
+
except KeyError:
|
|
109
|
+
raise ValueError(f'Data Variable {data_uid} does not exist')
|
|
110
|
+
|
|
111
|
+
if variable.type == 'derived':
|
|
112
|
+
raise ValueError('Cannot upload data to DerivedDataVariable')
|
|
113
|
+
|
|
114
|
+
content = cast(bytes, await data.read())
|
|
115
|
+
|
|
116
|
+
resolver = None
|
|
117
|
+
|
|
118
|
+
# If Id is provided, lookup the definition from registry
|
|
119
|
+
if resolver_id is not None:
|
|
120
|
+
resolver_def: UploadResolverDef = await registry_mgr.get(upload_resolver_registry, resolver_id)
|
|
121
|
+
resolver = resolver_def.resolver
|
|
122
|
+
|
|
123
|
+
if resolver:
|
|
124
|
+
content = await run_user_handler(handler=resolver, args=(content, data.filename))
|
|
125
|
+
# If resolver is not provided, follow roughly the cl_dataset_parser logic
|
|
126
|
+
elif file_type == '.xlsx':
|
|
127
|
+
file_object_xlsx = io.BytesIO(content)
|
|
128
|
+
content = pandas.read_excel(file_object_xlsx, index_col=None)
|
|
129
|
+
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
130
|
+
else:
|
|
131
|
+
# default to csv
|
|
132
|
+
file_object_csv = io.StringIO(content.decode('utf-8'))
|
|
133
|
+
content = pandas.read_csv(file_object_csv, index_col=0)
|
|
134
|
+
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
135
|
+
|
|
136
|
+
# If a data variable is provided, update it with the new content
|
|
137
|
+
if variable:
|
|
138
|
+
DataVariable.update_value(variable, store, content)
|
|
@@ -30,8 +30,9 @@ from fastapi.encoders import jsonable_encoder
|
|
|
30
30
|
from pydantic import ConfigDict
|
|
31
31
|
|
|
32
32
|
from dara.core.auth.definitions import SESSION_ID, USER, UserData
|
|
33
|
-
from dara.core.base_definitions import BaseTask
|
|
33
|
+
from dara.core.base_definitions import BaseTask
|
|
34
34
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
35
|
+
from dara.core.base_definitions import PendingTask
|
|
35
36
|
from dara.core.interactivity.condition import Condition, Operator
|
|
36
37
|
from dara.core.internal.cache_store import CacheStore
|
|
37
38
|
from dara.core.internal.tasks import TaskManager
|
|
@@ -111,7 +112,7 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
|
|
|
111
112
|
user_identity = None
|
|
112
113
|
|
|
113
114
|
if current_user is not None:
|
|
114
|
-
user_identity = current_user.identity_id
|
|
115
|
+
user_identity = current_user.identity_id or current_user.identity_name
|
|
115
116
|
elif isinstance(auth_config, BasicAuthConfig):
|
|
116
117
|
# basic auth - assume it's the single existing user
|
|
117
118
|
user_identity = list(auth_config.users.keys())[0]
|
|
@@ -203,6 +204,7 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
|
|
|
203
204
|
|
|
204
205
|
for session, channels in session_channels.items():
|
|
205
206
|
for ws in channels:
|
|
207
|
+
|
|
206
208
|
raw_result = raw_results[ws]
|
|
207
209
|
# Skip values from clients where the variable is not registered
|
|
208
210
|
if raw_result == NOT_REGISTERED:
|
|
@@ -236,12 +238,12 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
|
|
|
236
238
|
|
|
237
239
|
# If we're returning multiple values, in Jupyter environments print an explainer
|
|
238
240
|
try:
|
|
239
|
-
from IPython import get_ipython
|
|
241
|
+
from IPython import get_ipython
|
|
240
242
|
except ImportError:
|
|
241
243
|
pass
|
|
242
244
|
else:
|
|
243
245
|
if get_ipython() is not None:
|
|
244
|
-
from IPython.display import HTML, display
|
|
246
|
+
from IPython.display import HTML, display
|
|
245
247
|
|
|
246
248
|
display(
|
|
247
249
|
HTML(
|
|
@@ -270,7 +272,7 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
|
|
|
270
272
|
return results
|
|
271
273
|
|
|
272
274
|
|
|
273
|
-
class AnyVariable(BaseModel, abc.ABC):
|
|
275
|
+
class AnyVariable(BaseModel, abc.ABC):
|
|
274
276
|
"""
|
|
275
277
|
Base class for all variables. Used for typing to specify that any variable can be provided.
|
|
276
278
|
"""
|
|
@@ -280,6 +282,7 @@ class AnyVariable(BaseModel, abc.ABC): # noqa: PLW1641 # we override equals to
|
|
|
280
282
|
uid: str
|
|
281
283
|
|
|
282
284
|
def __init__(self, uid: Optional[str] = None, **kwargs) -> None:
|
|
285
|
+
|
|
283
286
|
new_uid = uid
|
|
284
287
|
if new_uid is None:
|
|
285
288
|
new_uid = str(uuid.uuid4())
|
|
@@ -17,15 +17,40 @@ limitations under the License.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
import asyncio
|
|
21
|
+
from typing import Optional, Union, cast
|
|
21
22
|
|
|
23
|
+
from anyio.abc import TaskGroup
|
|
22
24
|
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
|
+
)
|
|
23
32
|
|
|
24
|
-
from dara.core.base_definitions import Cache, CacheArgType
|
|
25
|
-
from dara.core.interactivity.
|
|
33
|
+
from dara.core.base_definitions import BaseCachePolicy, Cache, CacheArgType
|
|
34
|
+
from dara.core.interactivity.any_data_variable import (
|
|
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
|
|
26
51
|
|
|
27
52
|
|
|
28
|
-
class DataVariable(
|
|
53
|
+
class DataVariable(AnyDataVariable):
|
|
29
54
|
"""
|
|
30
55
|
DataVariable represents a variable that is specifically designed to hold datasets.
|
|
31
56
|
|
|
@@ -44,6 +69,11 @@ class DataVariable(ServerVariable):
|
|
|
44
69
|
>>> UpdateVariable(func=some_data_transformation, variable=session_data)
|
|
45
70
|
"""
|
|
46
71
|
|
|
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
|
+
|
|
47
77
|
def __init__(
|
|
48
78
|
self,
|
|
49
79
|
data: Optional[DataFrame] = None,
|
|
@@ -59,11 +89,239 @@ class DataVariable(ServerVariable):
|
|
|
59
89
|
:param filters: a dictionary of filters to apply to the data
|
|
60
90
|
:param cache: how to cache the result; 'user' per user, 'session' per session, 'global' for all users
|
|
61
91
|
"""
|
|
62
|
-
|
|
92
|
+
cache = Cache.Policy.from_arg(cache)
|
|
63
93
|
|
|
64
|
-
if data is not None and
|
|
94
|
+
if data is not None and cache.cache_type is not Cache.Type.GLOBAL:
|
|
65
95
|
raise ValueError('Data cannot be cached per session or per user if provided upfront')
|
|
66
96
|
|
|
67
|
-
|
|
97
|
+
super().__init__(cache=cache, uid=uid, **kwargs)
|
|
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
|
+
"""
|
|
68
324
|
|
|
69
|
-
|
|
325
|
+
data: Optional[DataFrame] = None
|
|
326
|
+
path: Optional[str] = None
|
|
327
|
+
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|