dara-core 1.20.1a1__py3-none-any.whl → 1.20.1a3__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 +3 -0
- dara/core/actions.py +1 -2
- dara/core/auth/basic.py +22 -16
- 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 +22 -64
- dara/core/cli.py +8 -7
- dara/core/configuration.py +5 -2
- dara/core/css.py +1 -2
- dara/core/data_utils.py +18 -19
- dara/core/defaults.py +6 -7
- dara/core/definitions.py +50 -19
- dara/core/http.py +7 -3
- dara/core/interactivity/__init__.py +6 -0
- dara/core/interactivity/actions.py +52 -50
- dara/core/interactivity/any_data_variable.py +7 -134
- dara/core/interactivity/any_variable.py +5 -8
- dara/core/interactivity/client_variable.py +71 -0
- dara/core/interactivity/data_variable.py +8 -266
- dara/core/interactivity/derived_data_variable.py +7 -290
- dara/core/interactivity/derived_variable.py +416 -176
- dara/core/interactivity/filtering.py +46 -27
- dara/core/interactivity/loop_variable.py +2 -2
- dara/core/interactivity/non_data_variable.py +5 -68
- dara/core/interactivity/plain_variable.py +89 -15
- dara/core/interactivity/server_variable.py +325 -0
- dara/core/interactivity/state_variable.py +69 -0
- dara/core/interactivity/switch_variable.py +19 -19
- dara/core/interactivity/tabular_variable.py +94 -0
- dara/core/interactivity/url_variable.py +10 -90
- dara/core/internal/cache_store/base_impl.py +2 -1
- dara/core/internal/cache_store/cache_store.py +22 -25
- dara/core/internal/cache_store/keep_all.py +4 -1
- dara/core/internal/cache_store/lru.py +5 -1
- dara/core/internal/cache_store/ttl.py +4 -1
- dara/core/internal/cgroup.py +1 -1
- dara/core/internal/dependency_resolution.py +60 -66
- dara/core/internal/devtools.py +12 -5
- dara/core/internal/download.py +13 -4
- dara/core/internal/encoder_registry.py +7 -7
- dara/core/internal/execute_action.py +13 -13
- dara/core/internal/hashing.py +1 -3
- dara/core/internal/import_discovery.py +3 -4
- dara/core/internal/multi_resource_lock.py +70 -0
- dara/core/internal/normalization.py +9 -18
- dara/core/internal/pandas_utils.py +107 -5
- dara/core/internal/pool/definitions.py +1 -1
- dara/core/internal/pool/task_pool.py +25 -16
- dara/core/internal/pool/utils.py +21 -18
- dara/core/internal/pool/worker.py +3 -2
- dara/core/internal/port_utils.py +1 -1
- dara/core/internal/registries.py +12 -6
- dara/core/internal/registry.py +4 -2
- dara/core/internal/registry_lookup.py +11 -5
- dara/core/internal/routing.py +109 -145
- dara/core/internal/scheduler.py +13 -8
- dara/core/internal/settings.py +2 -2
- dara/core/internal/store.py +2 -29
- dara/core/internal/tasks.py +379 -195
- dara/core/internal/utils.py +36 -13
- dara/core/internal/websocket.py +21 -20
- dara/core/js_tooling/js_utils.py +28 -26
- dara/core/js_tooling/templates/vite.config.template.ts +12 -3
- dara/core/logging.py +13 -12
- dara/core/main.py +14 -11
- dara/core/metrics/cache.py +1 -1
- dara/core/metrics/utils.py +3 -3
- dara/core/persistence.py +27 -5
- dara/core/umd/dara.core.umd.js +68291 -64718
- dara/core/visual/components/__init__.py +2 -2
- dara/core/visual/components/fallback.py +30 -4
- dara/core/visual/components/for_cmp.py +4 -1
- dara/core/visual/css/__init__.py +30 -31
- dara/core/visual/dynamic_component.py +31 -28
- dara/core/visual/progress_updater.py +4 -3
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/METADATA +12 -11
- dara_core-1.20.1a3.dist-info/RECORD +119 -0
- dara_core-1.20.1a1.dist-info/RECORD +0 -114
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/LICENSE +0 -0
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/WHEEL +0 -0
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any, DefaultDict, Dict, Literal, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, SerializerFunctionWrapHandler, model_serializer
|
|
7
|
+
|
|
8
|
+
from dara.core.auth.definitions import USER
|
|
9
|
+
from dara.core.base_definitions import CachedRegistryEntry, NonTabularDataError
|
|
10
|
+
from dara.core.interactivity.filtering import FilterQuery, Pagination, apply_filters, coerce_to_filter_query
|
|
11
|
+
from dara.core.internal.pandas_utils import DataResponse, append_index, build_data_response
|
|
12
|
+
from dara.core.internal.utils import call_async
|
|
13
|
+
from dara.core.internal.websocket import ServerMessagePayload, WebsocketManager
|
|
14
|
+
|
|
15
|
+
from .any_variable import AnyVariable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServerVariableMessage(ServerMessagePayload):
|
|
19
|
+
typ: Literal['ServerVariable'] = Field(alias='__type', default='ServerVariable')
|
|
20
|
+
uid: str
|
|
21
|
+
sequence_number: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ServerBackend(BaseModel, abc.ABC):
|
|
25
|
+
scope: Literal['global', 'user']
|
|
26
|
+
|
|
27
|
+
@abc.abstractmethod
|
|
28
|
+
async def write(self, key: str, value: Any):
|
|
29
|
+
"""
|
|
30
|
+
Persist a value
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@abc.abstractmethod
|
|
34
|
+
async def read(self, key: str) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Read a value
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@abc.abstractmethod
|
|
40
|
+
async def read_filtered(
|
|
41
|
+
self, key: str, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
42
|
+
) -> Tuple[Optional[DataFrame], int]:
|
|
43
|
+
"""
|
|
44
|
+
Read a value
|
|
45
|
+
:param filters: filters to apply
|
|
46
|
+
:param pagination: pagination to apply
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
async def get_sequence_number(self, key: str) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Get the sequence number for a given key
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MemoryBackend(ServerBackend):
|
|
57
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
|
58
|
+
sequence_number: DefaultDict[str, int] = Field(default_factory=lambda: defaultdict(int))
|
|
59
|
+
|
|
60
|
+
def __init__(self, scope: Literal['user', 'global'] = 'user'):
|
|
61
|
+
super().__init__(scope=scope)
|
|
62
|
+
|
|
63
|
+
async def write(self, key: str, value: Any):
|
|
64
|
+
self.data[key] = value
|
|
65
|
+
self.sequence_number[key] += 1
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
async def read(self, key: str) -> Any:
|
|
69
|
+
return self.data.get(key)
|
|
70
|
+
|
|
71
|
+
async def read_filtered(
|
|
72
|
+
self, key: str, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
73
|
+
) -> Tuple[Optional[DataFrame], int]:
|
|
74
|
+
dataset = self.data.get(key)
|
|
75
|
+
|
|
76
|
+
# print user-friendly error message if the data is not a DataFrame
|
|
77
|
+
# most likely due to user passing a non-tabular server variable to e.g. a Table
|
|
78
|
+
if dataset is not None and not isinstance(dataset, DataFrame):
|
|
79
|
+
raise NonTabularDataError(
|
|
80
|
+
f'Failed to retrieve ServerVariable tabular data, expected pandas.DataFrame, got {type(dataset)}'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
dataset = append_index(dataset)
|
|
84
|
+
return apply_filters(dataset, coerce_to_filter_query(filters), pagination)
|
|
85
|
+
|
|
86
|
+
async def get_sequence_number(self, key: str) -> int:
|
|
87
|
+
return self.sequence_number[key]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ServerVariable(AnyVariable):
|
|
91
|
+
"""
|
|
92
|
+
A ServerVariable represents server-side data that is synchronized with the client.
|
|
93
|
+
|
|
94
|
+
Unlike Variables with BackendStore (which are client state persisted on server),
|
|
95
|
+
ServerVariable holds data that originates and is managed on the server, with
|
|
96
|
+
clients receiving reactive updates when the data changes.
|
|
97
|
+
|
|
98
|
+
ServerVariable can store any Python object, including non-serializable data like
|
|
99
|
+
database connections, ML models, or complex objects.
|
|
100
|
+
|
|
101
|
+
However, when used with components expecting tabular data (like Table), the data must be
|
|
102
|
+
serializable or a NonTabularDataError will be raised. The default backend implementation
|
|
103
|
+
expects the tabular data to be a pandas DataFrame. To support other data types, you can
|
|
104
|
+
implement a custom backend that translates the data into a filtered DataFrame in the
|
|
105
|
+
`read_filtered` method.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import pandas as pd
|
|
109
|
+
from dara.core import ServerVariable, action
|
|
110
|
+
from dara.core.interactivity.server_variable import ServerBackend
|
|
111
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
112
|
+
|
|
113
|
+
# Basic usage with DataFrame
|
|
114
|
+
data = ServerVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
115
|
+
|
|
116
|
+
# Non-serializable data (ML model)
|
|
117
|
+
model = ServerVariable(trained_sklearn_model, scope='global')
|
|
118
|
+
|
|
119
|
+
# Custom backend
|
|
120
|
+
class DatabaseBackend(ServerBackend):
|
|
121
|
+
# ... implements all the methods as DB operations
|
|
122
|
+
|
|
123
|
+
data = ServerVariable(backend=DatabaseBackend(...))
|
|
124
|
+
|
|
125
|
+
# User-specific data
|
|
126
|
+
user_prefs = ServerVariable(scope='user')
|
|
127
|
+
|
|
128
|
+
@action
|
|
129
|
+
async def on_click(ctx):
|
|
130
|
+
# write to the data for the user who initiated the action
|
|
131
|
+
await user_prefs.write('dark')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
:param default: Initial value for the variable (global scope only)
|
|
135
|
+
:param backend: Custom backend for data storage and retrieval
|
|
136
|
+
:param scope: 'global' (shared across all users) or 'user' (per-user data)
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
backend: ServerBackend = Field(exclude=True)
|
|
140
|
+
scope: Literal['user', 'global']
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
default: Optional[Any] = None,
|
|
145
|
+
backend: Optional[ServerBackend] = None,
|
|
146
|
+
scope: Literal['user', 'global'] = 'global',
|
|
147
|
+
uid: Optional[str] = None,
|
|
148
|
+
**kwargs,
|
|
149
|
+
) -> None:
|
|
150
|
+
from dara.core.internal.registries import server_variable_registry
|
|
151
|
+
|
|
152
|
+
if backend is None:
|
|
153
|
+
backend = MemoryBackend(scope=scope)
|
|
154
|
+
|
|
155
|
+
if default is not None:
|
|
156
|
+
assert scope == 'global', (
|
|
157
|
+
'ServerVariable can only be used with global scope, cannot initialize user-specific values'
|
|
158
|
+
)
|
|
159
|
+
call_async(backend.write, 'global', default)
|
|
160
|
+
|
|
161
|
+
super().__init__(uid=uid, backend=backend, scope=scope, **kwargs)
|
|
162
|
+
|
|
163
|
+
var_entry = ServerVariableRegistryEntry(uid=str(self.uid), backend=backend)
|
|
164
|
+
server_variable_registry.register(str(self.uid), var_entry)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
async def get_value(cls, entry: 'ServerVariableRegistryEntry'):
|
|
168
|
+
"""
|
|
169
|
+
Internal method to get the value of a server variable based in its registry entry.
|
|
170
|
+
"""
|
|
171
|
+
key = cls.get_key(entry.backend.scope)
|
|
172
|
+
return await entry.backend.read(key)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
async def write_value(cls, entry: 'ServerVariableRegistryEntry', value: Any):
|
|
176
|
+
"""
|
|
177
|
+
Internal method to write the value of a server variable based in its registry entry.
|
|
178
|
+
"""
|
|
179
|
+
key = cls.get_key(entry.backend.scope)
|
|
180
|
+
await entry.backend.write(key, value)
|
|
181
|
+
await cls._notify(entry.uid, key, entry.backend)
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
async def get_sequence_number(cls, entry: 'ServerVariableRegistryEntry'):
|
|
185
|
+
"""
|
|
186
|
+
Internal method to get the sequence number of a server variable based in its registry entry.
|
|
187
|
+
"""
|
|
188
|
+
key = cls.get_key(entry.backend.scope)
|
|
189
|
+
return await entry.backend.get_sequence_number(key)
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
async def get_tabular_data(
|
|
193
|
+
cls,
|
|
194
|
+
entry: 'ServerVariableRegistryEntry',
|
|
195
|
+
filters: Optional[FilterQuery] = None,
|
|
196
|
+
pagination: Optional[Pagination] = None,
|
|
197
|
+
) -> DataResponse:
|
|
198
|
+
"""
|
|
199
|
+
Internal method to get tabular data from the backend
|
|
200
|
+
"""
|
|
201
|
+
key = cls.get_key(entry.backend.scope)
|
|
202
|
+
data, count = await entry.backend.read_filtered(key, filters, pagination)
|
|
203
|
+
if data is None:
|
|
204
|
+
return DataResponse(data=None, count=0, schema=None)
|
|
205
|
+
return build_data_response(data, count)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def get_key(cls, scope: Literal['global', 'user']):
|
|
209
|
+
"""
|
|
210
|
+
Resolve the key for the given scope
|
|
211
|
+
|
|
212
|
+
:param scope: the scope to resolve the key for
|
|
213
|
+
"""
|
|
214
|
+
if scope == 'global':
|
|
215
|
+
return 'global'
|
|
216
|
+
|
|
217
|
+
user = USER.get()
|
|
218
|
+
|
|
219
|
+
if user:
|
|
220
|
+
return user.identity_id
|
|
221
|
+
|
|
222
|
+
raise ValueError('User not found when trying to compute the key for a user-scoped store')
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def key(self):
|
|
226
|
+
"""
|
|
227
|
+
Current key for the backend
|
|
228
|
+
"""
|
|
229
|
+
return self.get_key(self.scope)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
async def _notify(cls, uid: str, key: str, backend: ServerBackend):
|
|
233
|
+
"""
|
|
234
|
+
Internal method to notify clients of a change in the value
|
|
235
|
+
|
|
236
|
+
:param uid: the uid of the variable
|
|
237
|
+
:param key: the key for the backend
|
|
238
|
+
:param backend: the backend instance
|
|
239
|
+
"""
|
|
240
|
+
from dara.core.internal.registries import utils_registry
|
|
241
|
+
|
|
242
|
+
ws_mgr: WebsocketManager = utils_registry.get('WebsocketManager')
|
|
243
|
+
|
|
244
|
+
message = ServerVariableMessage(uid=uid, sequence_number=await backend.get_sequence_number(key))
|
|
245
|
+
|
|
246
|
+
if backend.scope == 'global':
|
|
247
|
+
return await ws_mgr.broadcast(message)
|
|
248
|
+
|
|
249
|
+
user = USER.get()
|
|
250
|
+
assert user is not None, 'User not found when trying to send notification'
|
|
251
|
+
user_id = user.identity_id
|
|
252
|
+
return await ws_mgr.send_message_to_user(user_id, message)
|
|
253
|
+
|
|
254
|
+
def update(self, value: Any):
|
|
255
|
+
"""
|
|
256
|
+
Create an action to update the value of this Variable to a provided value.
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
import pandas as pd
|
|
260
|
+
from dara.core import ServerVariable
|
|
261
|
+
from dara.components import Button
|
|
262
|
+
|
|
263
|
+
data = ServerVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
264
|
+
|
|
265
|
+
Button(
|
|
266
|
+
'Empty Data',
|
|
267
|
+
onclick=data.update(None),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
"""
|
|
272
|
+
from dara.core.interactivity.actions import UpdateVariableImpl
|
|
273
|
+
|
|
274
|
+
return UpdateVariableImpl(variable=self, value=value)
|
|
275
|
+
|
|
276
|
+
def reset(self):
|
|
277
|
+
raise NotImplementedError('ServerVariable does not support reset')
|
|
278
|
+
|
|
279
|
+
async def read(self):
|
|
280
|
+
"""
|
|
281
|
+
Read the current value from the backend.
|
|
282
|
+
Depending on the scope, the value will be global or user-specific.
|
|
283
|
+
"""
|
|
284
|
+
return await self.backend.read(self.key)
|
|
285
|
+
|
|
286
|
+
async def write(self, value: Any):
|
|
287
|
+
"""
|
|
288
|
+
Write a new value to the backend.
|
|
289
|
+
Depending on the scope, the value will be global or user-specific.
|
|
290
|
+
|
|
291
|
+
:param value: the new value to write
|
|
292
|
+
"""
|
|
293
|
+
value = await self.backend.write(self.key, value)
|
|
294
|
+
await self._notify(self.uid, self.key, self.backend)
|
|
295
|
+
return value
|
|
296
|
+
|
|
297
|
+
async def read_filtered(
|
|
298
|
+
self, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Read a filtered value from the backend.
|
|
302
|
+
Depending on the scope, the value will be global or user-specific.
|
|
303
|
+
|
|
304
|
+
:param filters: the filters to apply
|
|
305
|
+
:param pagination: the pagination to apply
|
|
306
|
+
"""
|
|
307
|
+
return await self.backend.read_filtered(self.key, filters, pagination)
|
|
308
|
+
|
|
309
|
+
@model_serializer(mode='wrap')
|
|
310
|
+
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
311
|
+
parent_dict = nxt(self)
|
|
312
|
+
return {**parent_dict, '__typename': 'ServerVariable', 'uid': str(parent_dict['uid'])}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ServerVariableRegistryEntry(CachedRegistryEntry):
|
|
316
|
+
"""
|
|
317
|
+
Registry entry for ServerVariable.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
backend: ServerBackend
|
|
321
|
+
"""
|
|
322
|
+
Backend instance
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
from typing import TYPE_CHECKING, Optional
|
|
21
|
+
|
|
22
|
+
from pydantic import SerializerFunctionWrapHandler, model_serializer
|
|
23
|
+
from typing_extensions import Literal
|
|
24
|
+
|
|
25
|
+
from dara.core.interactivity.client_variable import ClientVariable
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from dara.core.interactivity.derived_variable import DerivedVariable
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StateVariable(ClientVariable):
|
|
32
|
+
"""
|
|
33
|
+
A StateVariable is an internal variable type used to track client-side state of other variables.
|
|
34
|
+
It is not meant to be created directly by users, but rather returned by properties like
|
|
35
|
+
DerivedVariable.is_loading, DerivedVariable.has_error, etc.
|
|
36
|
+
|
|
37
|
+
This variable tracks the state of a parent DerivedVariable and maps to specific properties
|
|
38
|
+
like loading state, error state, etc.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
parent_variable: 'DerivedVariable'
|
|
42
|
+
property_name: Literal['loading', 'error', 'hasValue']
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
parent_variable: 'DerivedVariable',
|
|
47
|
+
property_name: Literal['loading', 'error', 'hasValue'],
|
|
48
|
+
uid: Optional[str] = None,
|
|
49
|
+
**kwargs,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize a StateVariable.
|
|
53
|
+
|
|
54
|
+
:param parent_variable: The DerivedVariable this StateVariable tracks
|
|
55
|
+
:param property_name: The property name this StateVariable represents ('loading', 'error', etc.)
|
|
56
|
+
:param uid: Optional unique identifier; if not provided, one is generated
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(uid=uid, parent_variable=parent_variable, property_name=property_name, **kwargs)
|
|
59
|
+
|
|
60
|
+
@model_serializer(mode='wrap')
|
|
61
|
+
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
62
|
+
parent_dict = nxt(self)
|
|
63
|
+
return {
|
|
64
|
+
**parent_dict,
|
|
65
|
+
'__typename': 'StateVariable',
|
|
66
|
+
'uid': str(parent_dict['uid']),
|
|
67
|
+
'parent_variable': self.parent_variable,
|
|
68
|
+
'property_name': self.property_name,
|
|
69
|
+
}
|
|
@@ -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
|
|
24
25
|
from dara.core.interactivity.condition import Condition
|
|
25
|
-
from dara.core.interactivity.non_data_variable import NonDataVariable
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class SwitchVariable(
|
|
28
|
+
class SwitchVariable(ClientVariable):
|
|
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(NonDataVariable):
|
|
|
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,
|
|
225
|
+
value: Optional[Union[Condition, ClientVariable, 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,
|
|
233
|
-
value_map: Dict[Any, Any] |
|
|
232
|
+
value: Union[Condition, ClientVariable, Any],
|
|
233
|
+
value_map: Dict[Any, Any] | ClientVariable,
|
|
234
234
|
default: Optional[Any] = None,
|
|
235
235
|
uid: Optional[str] = None,
|
|
236
236
|
):
|
|
@@ -253,28 +253,28 @@ class SwitchVariable(NonDataVariable):
|
|
|
253
253
|
@classmethod
|
|
254
254
|
def validate_value_map(cls, v):
|
|
255
255
|
"""
|
|
256
|
-
Validate that value_map is either a dict or a
|
|
256
|
+
Validate that value_map is either a dict or a ClientVariable.
|
|
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
|
|
260
|
+
:raises ValueError: If value_map is not a dict or ClientVariable
|
|
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,
|
|
266
|
+
if isinstance(v, ClientVariable):
|
|
267
267
|
return v
|
|
268
|
-
raise ValueError(f'value_map must be a dict or
|
|
268
|
+
raise ValueError(f'value_map must be a dict or ClientVariable, got {type(v)}')
|
|
269
269
|
|
|
270
270
|
@classmethod
|
|
271
271
|
def when(
|
|
272
272
|
cls,
|
|
273
|
-
condition: Union[Condition,
|
|
274
|
-
true_value: Union[Any,
|
|
275
|
-
false_value: Union[Any,
|
|
273
|
+
condition: Union[Condition, ClientVariable, Any],
|
|
274
|
+
true_value: Union[Any, ClientVariable],
|
|
275
|
+
false_value: Union[Any, ClientVariable],
|
|
276
276
|
uid: Optional[str] = None,
|
|
277
|
-
) ->
|
|
277
|
+
) -> SwitchVariable:
|
|
278
278
|
"""
|
|
279
279
|
Create a SwitchVariable for boolean conditions.
|
|
280
280
|
|
|
@@ -346,11 +346,11 @@ class SwitchVariable(NonDataVariable):
|
|
|
346
346
|
@classmethod
|
|
347
347
|
def match(
|
|
348
348
|
cls,
|
|
349
|
-
value: Union[
|
|
350
|
-
mapping: Union[Dict[Any, Any],
|
|
351
|
-
default: Optional[Union[Any,
|
|
349
|
+
value: Union[ClientVariable, Any],
|
|
350
|
+
mapping: Union[Dict[Any, Any], ClientVariable],
|
|
351
|
+
default: Optional[Union[Any, ClientVariable]] = None,
|
|
352
352
|
uid: Optional[str] = None,
|
|
353
|
-
) ->
|
|
353
|
+
) -> SwitchVariable:
|
|
354
354
|
"""
|
|
355
355
|
Create a SwitchVariable with a custom mapping.
|
|
356
356
|
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
import io
|
|
19
|
+
import os
|
|
20
|
+
from typing import Literal, Optional, TypedDict, Union, cast
|
|
21
|
+
|
|
22
|
+
import pandas
|
|
23
|
+
from fastapi import UploadFile
|
|
24
|
+
|
|
25
|
+
from dara.core.base_definitions import UploadResolverDef
|
|
26
|
+
from dara.core.internal.registry_lookup import RegistryLookup
|
|
27
|
+
from dara.core.internal.utils import run_user_handler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FieldType(TypedDict):
|
|
31
|
+
name: Union[str, tuple[str, ...]]
|
|
32
|
+
type: Literal['integer', 'number', 'boolean', 'datetime', 'duration', 'any', 'str']
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DataFrameSchema(TypedDict):
|
|
36
|
+
fields: list[FieldType]
|
|
37
|
+
primaryKey: list[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def upload(data: UploadFile, data_uid: Optional[str] = None, resolver_id: Optional[str] = None):
|
|
41
|
+
"""
|
|
42
|
+
Handler for uploading data.
|
|
43
|
+
|
|
44
|
+
:param data: the file to upload
|
|
45
|
+
:param data_uid: optional uid of the data variable to upload to
|
|
46
|
+
:param resolver_id: optional id of the upload resolver to use, falls back to default handlers for csv/xlsx
|
|
47
|
+
"""
|
|
48
|
+
from dara.core.interactivity.server_variable import ServerVariable
|
|
49
|
+
from dara.core.internal.registries import (
|
|
50
|
+
server_variable_registry,
|
|
51
|
+
upload_resolver_registry,
|
|
52
|
+
utils_registry,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
56
|
+
|
|
57
|
+
if data.filename is None:
|
|
58
|
+
raise ValueError('Filename not provided')
|
|
59
|
+
|
|
60
|
+
variable_entry = None
|
|
61
|
+
|
|
62
|
+
_name, file_type = os.path.splitext(data.filename)
|
|
63
|
+
|
|
64
|
+
if data_uid is not None:
|
|
65
|
+
try:
|
|
66
|
+
variable_entry = await registry_mgr.get(server_variable_registry, data_uid)
|
|
67
|
+
except KeyError as e:
|
|
68
|
+
raise ValueError(f'Data Variable {data_uid} does not exist') from e
|
|
69
|
+
|
|
70
|
+
content = cast(bytes, await data.read())
|
|
71
|
+
|
|
72
|
+
resolver = None
|
|
73
|
+
|
|
74
|
+
# If Id is provided, lookup the definition from registry
|
|
75
|
+
if resolver_id is not None:
|
|
76
|
+
resolver_def: UploadResolverDef = await registry_mgr.get(upload_resolver_registry, resolver_id)
|
|
77
|
+
resolver = resolver_def.resolver
|
|
78
|
+
|
|
79
|
+
if resolver:
|
|
80
|
+
content = await run_user_handler(handler=resolver, args=(content, data.filename))
|
|
81
|
+
# If resolver is not provided, follow roughly the cl_dataset_parser logic
|
|
82
|
+
elif file_type == '.xlsx':
|
|
83
|
+
file_object_xlsx = io.BytesIO(content)
|
|
84
|
+
content = pandas.read_excel(file_object_xlsx, index_col=None)
|
|
85
|
+
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
86
|
+
else:
|
|
87
|
+
# default to csv
|
|
88
|
+
file_object_csv = io.StringIO(content.decode('utf-8'))
|
|
89
|
+
content = pandas.read_csv(file_object_csv, index_col=0)
|
|
90
|
+
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
91
|
+
|
|
92
|
+
# If a server variable is provided, update it with the new content
|
|
93
|
+
if variable_entry:
|
|
94
|
+
await ServerVariable.write_value(variable_entry, content)
|