dara-core 1.19.0__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 +6 -0
- dara/core/interactivity/actions.py +26 -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 +381 -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 +69 -0
- 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 +9 -6
- dara/core/internal/pool/utils.py +19 -14
- 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 +1091 -1469
- dara/core/visual/dynamic_component.py +17 -13
- {dara_core-1.19.0.dist-info → dara_core-1.20.0.dist-info}/METADATA +11 -11
- {dara_core-1.19.0.dist-info → dara_core-1.20.0.dist-info}/RECORD +52 -47
- {dara_core-1.19.0.dist-info → dara_core-1.20.0.dist-info}/LICENSE +0 -0
- {dara_core-1.19.0.dist-info → dara_core-1.20.0.dist-info}/WHEEL +0 -0
- {dara_core-1.19.0.dist-info → dara_core-1.20.0.dist-info}/entry_points.txt +0 -0
dara/core/__init__.py
CHANGED
dara/core/auth/basic.py
CHANGED
|
@@ -25,7 +25,6 @@ from dara.core.auth.definitions import (
|
|
|
25
25
|
EXPIRED_TOKEN_ERROR,
|
|
26
26
|
INVALID_CREDENTIALS_ERROR,
|
|
27
27
|
INVALID_TOKEN_ERROR,
|
|
28
|
-
JWT_ALGO,
|
|
29
28
|
SESSION_ID,
|
|
30
29
|
USER,
|
|
31
30
|
AuthError,
|
|
@@ -35,7 +34,6 @@ from dara.core.auth.definitions import (
|
|
|
35
34
|
UserData,
|
|
36
35
|
)
|
|
37
36
|
from dara.core.auth.utils import decode_token, sign_jwt
|
|
38
|
-
from dara.core.internal.settings import get_settings
|
|
39
37
|
|
|
40
38
|
DefaultAuthLogin = AuthComponent(js_module='@darajs/core', py_module='dara.core', js_name='DefaultAuthLogin')
|
|
41
39
|
|
|
@@ -69,7 +67,7 @@ class BaseBasicAuthConfig(BaseAuthConfig):
|
|
|
69
67
|
|
|
70
68
|
return {
|
|
71
69
|
'token': sign_jwt(
|
|
72
|
-
identity_id=
|
|
70
|
+
identity_id=body.username,
|
|
73
71
|
identity_name=body.username,
|
|
74
72
|
identity_email=None,
|
|
75
73
|
groups=[],
|
|
@@ -87,6 +85,7 @@ class BaseBasicAuthConfig(BaseAuthConfig):
|
|
|
87
85
|
SESSION_ID.set(decoded.session_id)
|
|
88
86
|
USER.set(
|
|
89
87
|
UserData(
|
|
88
|
+
identity_id=decoded.identity_name,
|
|
90
89
|
identity_name=decoded.identity_name,
|
|
91
90
|
)
|
|
92
91
|
)
|
|
@@ -127,7 +126,7 @@ class DefaultAuthConfig(BaseAuthConfig):
|
|
|
127
126
|
|
|
128
127
|
In default auth a new token is returned every time.
|
|
129
128
|
"""
|
|
130
|
-
token = sign_jwt(identity_id=
|
|
129
|
+
token = sign_jwt(identity_id='user', identity_name='user', identity_email=None, groups=[])
|
|
131
130
|
return {'token': token}
|
|
132
131
|
|
|
133
132
|
def verify_token(self, token: str) -> TokenData:
|
|
@@ -139,9 +138,16 @@ class DefaultAuthConfig(BaseAuthConfig):
|
|
|
139
138
|
:param token: the token to verify
|
|
140
139
|
"""
|
|
141
140
|
try:
|
|
142
|
-
decoded =
|
|
143
|
-
SESSION_ID.set(decoded.
|
|
144
|
-
|
|
141
|
+
decoded = decode_token(token)
|
|
142
|
+
SESSION_ID.set(decoded.session_id)
|
|
143
|
+
# Implicit auth assumes used by one user so all users are the same
|
|
144
|
+
USER.set(
|
|
145
|
+
UserData(
|
|
146
|
+
identity_id=decoded.identity_id,
|
|
147
|
+
identity_name=decoded.identity_id,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
return decoded
|
|
145
151
|
except jwt.ExpiredSignatureError as e:
|
|
146
152
|
raise AuthError(EXPIRED_TOKEN_ERROR, 401) from e
|
|
147
153
|
except jwt.DecodeError as e:
|
dara/core/auth/definitions.py
CHANGED
|
@@ -39,7 +39,7 @@ class TokenData(BaseModel):
|
|
|
39
39
|
|
|
40
40
|
session_id: str
|
|
41
41
|
exp: Union[float, int, datetime]
|
|
42
|
-
identity_id:
|
|
42
|
+
identity_id: str
|
|
43
43
|
identity_name: str
|
|
44
44
|
identity_email: Optional[str] = None
|
|
45
45
|
id_token: Optional[str] = None
|
|
@@ -56,7 +56,7 @@ class UserData(BaseModel):
|
|
|
56
56
|
:param groups: list of groups user belongs to
|
|
57
57
|
"""
|
|
58
58
|
|
|
59
|
-
identity_id:
|
|
59
|
+
identity_id: str
|
|
60
60
|
identity_name: str
|
|
61
61
|
identity_email: Optional[str] = None
|
|
62
62
|
groups: Optional[List[str]] = []
|
dara/core/auth/utils.py
CHANGED
dara/core/base_definitions.py
CHANGED
|
@@ -304,6 +304,12 @@ class CachedRegistryEntry(BaseModel):
|
|
|
304
304
|
return f'{self.__class__.__name__}(cache={self.cache}, uid={self.uid})'
|
|
305
305
|
|
|
306
306
|
|
|
307
|
+
class NonTabularDataError(Exception):
|
|
308
|
+
"""
|
|
309
|
+
Raised when trying to interpret a non-tabular variable as tabular
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
|
|
307
313
|
class BaseTaskMessage(BaseModel):
|
|
308
314
|
task_id: str
|
|
309
315
|
|
|
@@ -407,7 +413,7 @@ class PendingTask(BaseTask):
|
|
|
407
413
|
self.cancel_scope.cancel()
|
|
408
414
|
await self.task_def.cancel()
|
|
409
415
|
|
|
410
|
-
self.error =
|
|
416
|
+
self.error = anyio.get_cancelled_exc_class()()
|
|
411
417
|
self.event.set()
|
|
412
418
|
|
|
413
419
|
def add_subscriber(self):
|
|
@@ -432,47 +438,6 @@ class PendingTask(BaseTask):
|
|
|
432
438
|
return self.result
|
|
433
439
|
|
|
434
440
|
|
|
435
|
-
class PendingValue:
|
|
436
|
-
"""
|
|
437
|
-
An internal class that's used to represent a pending value. Holds a future object that can be awaited by
|
|
438
|
-
multiple consumers.
|
|
439
|
-
"""
|
|
440
|
-
|
|
441
|
-
def __init__(self):
|
|
442
|
-
self.event = anyio.Event()
|
|
443
|
-
self._value = None
|
|
444
|
-
self._error = None
|
|
445
|
-
|
|
446
|
-
async def wait(self):
|
|
447
|
-
"""
|
|
448
|
-
Wait for the underlying event to be set
|
|
449
|
-
"""
|
|
450
|
-
# Waiting in chunks as otherwise Jupyter blocks the event loop
|
|
451
|
-
while not self.event.is_set():
|
|
452
|
-
await anyio.sleep(0.01)
|
|
453
|
-
if self._error:
|
|
454
|
-
raise self._error
|
|
455
|
-
return self._value
|
|
456
|
-
|
|
457
|
-
def resolve(self, value: Any):
|
|
458
|
-
"""
|
|
459
|
-
Resolve the pending state and send values to the waiting code
|
|
460
|
-
|
|
461
|
-
:param value: the value to resolve as the result
|
|
462
|
-
"""
|
|
463
|
-
self._value = value
|
|
464
|
-
self.event.set()
|
|
465
|
-
|
|
466
|
-
def error(self, exc: Exception):
|
|
467
|
-
"""
|
|
468
|
-
Resolve the pending state with an error and send it to the waiting code
|
|
469
|
-
|
|
470
|
-
:param exc: exception to resolve as the result
|
|
471
|
-
"""
|
|
472
|
-
self._error = exc
|
|
473
|
-
self.event.set()
|
|
474
|
-
|
|
475
|
-
|
|
476
441
|
class AnnotatedAction(BaseModel):
|
|
477
442
|
"""
|
|
478
443
|
Represents a single call to an @action-annotated action.
|
dara/core/data_utils.py
CHANGED
|
@@ -25,10 +25,9 @@ from pandas import DataFrame
|
|
|
25
25
|
from dara.core.base_definitions import CacheType
|
|
26
26
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
27
27
|
from dara.core.interactivity import (
|
|
28
|
-
|
|
28
|
+
ClientVariable,
|
|
29
29
|
DerivedVariable,
|
|
30
30
|
DownloadContent,
|
|
31
|
-
NonDataVariable,
|
|
32
31
|
SideEffect,
|
|
33
32
|
Variable,
|
|
34
33
|
)
|
|
@@ -172,7 +171,7 @@ class DataFactory(BaseModel):
|
|
|
172
171
|
|
|
173
172
|
def list_datasets_var(
|
|
174
173
|
self,
|
|
175
|
-
cache: Union[CacheType,
|
|
174
|
+
cache: Union[CacheType, ClientVariable] = CacheType.GLOBAL,
|
|
176
175
|
polling_interval: Optional[int] = None,
|
|
177
176
|
) -> DerivedVariable[List[str]]:
|
|
178
177
|
"""
|
|
@@ -181,7 +180,7 @@ class DataFactory(BaseModel):
|
|
|
181
180
|
:param cache: cache type to get the list of datasets for
|
|
182
181
|
:param polling_interval: optional polling_interval in seconds for the derived variable
|
|
183
182
|
"""
|
|
184
|
-
cache_var = cache if isinstance(cache,
|
|
183
|
+
cache_var = cache if isinstance(cache, ClientVariable) else Variable(cache)
|
|
185
184
|
|
|
186
185
|
return DerivedVariable(
|
|
187
186
|
lambda cache_val: self.file_store.list_files(cache_val or CacheType.GLOBAL),
|
|
@@ -289,21 +288,21 @@ class DataFactory(BaseModel):
|
|
|
289
288
|
|
|
290
289
|
def read_dataset_var(
|
|
291
290
|
self,
|
|
292
|
-
name: Union[str,
|
|
293
|
-
cache: Union[CacheType,
|
|
291
|
+
name: Union[str, ClientVariable],
|
|
292
|
+
cache: Union[CacheType, ClientVariable] = CacheType.GLOBAL,
|
|
294
293
|
polling_interval: Optional[int] = None,
|
|
295
|
-
) ->
|
|
294
|
+
) -> DerivedVariable:
|
|
296
295
|
"""
|
|
297
|
-
Create a
|
|
296
|
+
Create a DerivedVariable which reads a specific dataset from disk
|
|
298
297
|
|
|
299
298
|
:param name: name of the dataset
|
|
300
299
|
:param cache: cache to get the dataset for
|
|
301
300
|
:param polling_interval: optional polling interval in seconds for the derived variable
|
|
302
301
|
"""
|
|
303
|
-
name_var = name if isinstance(name,
|
|
304
|
-
cache_var = cache if isinstance(cache,
|
|
302
|
+
name_var = name if isinstance(name, ClientVariable) else Variable(name)
|
|
303
|
+
cache_var = cache if isinstance(cache, ClientVariable) else Variable(cache)
|
|
305
304
|
|
|
306
|
-
return
|
|
305
|
+
return DerivedVariable(
|
|
307
306
|
self.read_dataset,
|
|
308
307
|
variables=[name_var, cache_var],
|
|
309
308
|
cache=CacheType.SESSION,
|
|
@@ -320,7 +319,7 @@ class DataFactory(BaseModel):
|
|
|
320
319
|
self.file_store.delete_file(cache, name)
|
|
321
320
|
|
|
322
321
|
def delete_dataset_action(
|
|
323
|
-
self, name: Union[str,
|
|
322
|
+
self, name: Union[str, ClientVariable], cache: Union[CacheType, ClientVariable] = CacheType.GLOBAL
|
|
324
323
|
):
|
|
325
324
|
"""
|
|
326
325
|
Get a SideEffect action which deletes a given dataset
|
|
@@ -328,13 +327,13 @@ class DataFactory(BaseModel):
|
|
|
328
327
|
:param name: name of the dataset
|
|
329
328
|
:param cache: cache to remove the daatset for
|
|
330
329
|
"""
|
|
331
|
-
name_var = name if isinstance(name,
|
|
332
|
-
cache_var = cache if isinstance(cache,
|
|
330
|
+
name_var = name if isinstance(name, ClientVariable) else Variable(name)
|
|
331
|
+
cache_var = cache if isinstance(cache, ClientVariable) else Variable(cache)
|
|
333
332
|
|
|
334
333
|
return SideEffect(lambda ctx: self.delete_dataset(ctx.extras[0], ctx.extras[1]), extras=[name_var, cache_var])
|
|
335
334
|
|
|
336
335
|
def download_dataset_action(
|
|
337
|
-
self, name: Union[str,
|
|
336
|
+
self, name: Union[str, ClientVariable], cache: Union[CacheType, ClientVariable] = CacheType.GLOBAL
|
|
338
337
|
):
|
|
339
338
|
"""
|
|
340
339
|
Get a DownloadContent action which downloads a dataset with a given name as a .csv
|
|
@@ -342,8 +341,8 @@ class DataFactory(BaseModel):
|
|
|
342
341
|
:param name: name of the dataset to download
|
|
343
342
|
:param cache: cache to download dataset for
|
|
344
343
|
"""
|
|
345
|
-
name_var = name if isinstance(name,
|
|
346
|
-
cache_var = cache if isinstance(cache,
|
|
344
|
+
name_var = name if isinstance(name, ClientVariable) else Variable(name)
|
|
345
|
+
cache_var = cache if isinstance(cache, ClientVariable) else Variable(cache)
|
|
347
346
|
|
|
348
347
|
def _resolver(ctx: DownloadContent.Ctx): # type: ignore
|
|
349
348
|
ds_name, sel_cache = ctx.extras # type: ignore
|
dara/core/definitions.py
CHANGED
|
@@ -84,22 +84,22 @@ class ErrorHandlingConfig(BaseModel):
|
|
|
84
84
|
raw_css: Optional[Any] = None
|
|
85
85
|
"""
|
|
86
86
|
Raw styling to apply to the displayed error boundary.
|
|
87
|
-
Accepts a CSSProperties, dict, str, or
|
|
87
|
+
Accepts a CSSProperties, dict, str, or ClientVariable.
|
|
88
88
|
"""
|
|
89
89
|
|
|
90
90
|
@field_validator('raw_css', mode='before')
|
|
91
91
|
@classmethod
|
|
92
92
|
def validate_raw_css(cls, value):
|
|
93
|
-
from dara.core.interactivity.
|
|
93
|
+
from dara.core.interactivity.client_variable import ClientVariable
|
|
94
94
|
|
|
95
95
|
if value is None:
|
|
96
96
|
return None
|
|
97
|
-
if isinstance(value, (str,
|
|
97
|
+
if isinstance(value, (str, ClientVariable, CSSProperties)):
|
|
98
98
|
return value
|
|
99
99
|
if isinstance(value, dict):
|
|
100
100
|
return {_kebab_to_camel(k): v for k, v in value.items()}
|
|
101
101
|
|
|
102
|
-
raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or
|
|
102
|
+
raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or ClientVariable, got {type(value)}')
|
|
103
103
|
|
|
104
104
|
def model_dump(self, *args, **kwargs):
|
|
105
105
|
result = super().model_dump(*args, **kwargs)
|
|
@@ -145,7 +145,7 @@ class ComponentInstance(BaseModel):
|
|
|
145
145
|
"""
|
|
146
146
|
Raw styling to apply to the component.
|
|
147
147
|
Can be an dict/CSSProperties instance representing the `styles` tag, a string injected directly into the CSS of the wrapping component,
|
|
148
|
-
or a
|
|
148
|
+
or a ClientVariable resoling to either of the above.
|
|
149
149
|
|
|
150
150
|
```python
|
|
151
151
|
|
|
@@ -216,7 +216,7 @@ class ComponentInstance(BaseModel):
|
|
|
216
216
|
@field_validator('raw_css', mode='before')
|
|
217
217
|
@classmethod
|
|
218
218
|
def parse_css(cls, css: Optional[Any]):
|
|
219
|
-
from dara.core.interactivity.
|
|
219
|
+
from dara.core.interactivity.client_variable import ClientVariable
|
|
220
220
|
|
|
221
221
|
if css is None:
|
|
222
222
|
return None
|
|
@@ -225,10 +225,10 @@ class ComponentInstance(BaseModel):
|
|
|
225
225
|
if isinstance(css, dict):
|
|
226
226
|
return {_kebab_to_camel(k): v for k, v in css.items()}
|
|
227
227
|
|
|
228
|
-
if isinstance(css, (
|
|
228
|
+
if isinstance(css, (ClientVariable, CSSProperties, str)):
|
|
229
229
|
return css
|
|
230
230
|
|
|
231
|
-
raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or
|
|
231
|
+
raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or ClientVariable, got {type(css)}')
|
|
232
232
|
|
|
233
233
|
@classmethod
|
|
234
234
|
def isinstance(cls, obj: Any) -> bool:
|
|
@@ -36,12 +36,15 @@ from dara.core.interactivity.actions import (
|
|
|
36
36
|
)
|
|
37
37
|
from dara.core.interactivity.any_data_variable import AnyDataVariable
|
|
38
38
|
from dara.core.interactivity.any_variable import AnyVariable
|
|
39
|
+
from dara.core.interactivity.client_variable import ClientVariable
|
|
39
40
|
from dara.core.interactivity.condition import Condition, Operator
|
|
40
41
|
from dara.core.interactivity.data_variable import DataVariable
|
|
41
42
|
from dara.core.interactivity.derived_data_variable import DerivedDataVariable
|
|
42
43
|
from dara.core.interactivity.derived_variable import DerivedVariable
|
|
43
44
|
from dara.core.interactivity.non_data_variable import NonDataVariable
|
|
44
45
|
from dara.core.interactivity.plain_variable import Variable
|
|
46
|
+
from dara.core.interactivity.server_variable import ServerVariable
|
|
47
|
+
from dara.core.interactivity.state_variable import StateVariable
|
|
45
48
|
from dara.core.interactivity.switch_variable import SwitchVariable
|
|
46
49
|
from dara.core.interactivity.url_variable import UrlVariable
|
|
47
50
|
|
|
@@ -50,9 +53,11 @@ __all__ = [
|
|
|
50
53
|
'ActionCtx',
|
|
51
54
|
'AnyVariable',
|
|
52
55
|
'AnyDataVariable',
|
|
56
|
+
'ClientVariable',
|
|
53
57
|
'DataVariable',
|
|
54
58
|
'NonDataVariable',
|
|
55
59
|
'Variable',
|
|
60
|
+
'StateVariable',
|
|
56
61
|
'SwitchVariable',
|
|
57
62
|
'DerivedVariable',
|
|
58
63
|
'DerivedDataVariable',
|
|
@@ -67,6 +72,7 @@ __all__ = [
|
|
|
67
72
|
'TriggerVariable',
|
|
68
73
|
'UpdateVariable',
|
|
69
74
|
'UpdateVariableImpl',
|
|
75
|
+
'ServerVariable',
|
|
70
76
|
'SideEffect',
|
|
71
77
|
'Condition',
|
|
72
78
|
'Operator',
|
|
@@ -44,7 +44,7 @@ import anyio
|
|
|
44
44
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
45
45
|
from pandas import DataFrame
|
|
46
46
|
from pydantic import ConfigDict
|
|
47
|
-
from typing_extensions import deprecated
|
|
47
|
+
from typing_extensions import TypeAlias, deprecated
|
|
48
48
|
|
|
49
49
|
from dara.core.base_definitions import (
|
|
50
50
|
ActionDef,
|
|
@@ -54,7 +54,8 @@ from dara.core.base_definitions import (
|
|
|
54
54
|
TaskProgressUpdate,
|
|
55
55
|
)
|
|
56
56
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
57
|
-
from dara.core.interactivity.
|
|
57
|
+
from dara.core.interactivity.server_variable import ServerVariable
|
|
58
|
+
from dara.core.interactivity.state_variable import StateVariable
|
|
58
59
|
from dara.core.internal.download import generate_download_code
|
|
59
60
|
from dara.core.internal.registry_lookup import RegistryLookup
|
|
60
61
|
from dara.core.internal.utils import run_user_handler
|
|
@@ -64,10 +65,8 @@ if TYPE_CHECKING:
|
|
|
64
65
|
from dara.core.interactivity import (
|
|
65
66
|
AnyVariable,
|
|
66
67
|
DerivedVariable,
|
|
67
|
-
UrlVariable,
|
|
68
68
|
Variable,
|
|
69
69
|
)
|
|
70
|
-
from dara.core.internal.cache_store import CacheStore
|
|
71
70
|
|
|
72
71
|
|
|
73
72
|
class ActionInputs(BaseModel):
|
|
@@ -125,7 +124,7 @@ class UpdateVariableImpl(ActionImpl):
|
|
|
125
124
|
|
|
126
125
|
py_name = 'UpdateVariable'
|
|
127
126
|
|
|
128
|
-
variable: Union[Variable,
|
|
127
|
+
variable: Union[Variable, ServerVariable]
|
|
129
128
|
value: Any
|
|
130
129
|
|
|
131
130
|
INPUT: ClassVar[str] = '__dara_input__'
|
|
@@ -135,19 +134,18 @@ class UpdateVariableImpl(ActionImpl):
|
|
|
135
134
|
"""Special value for `value` that will toggle the variable value"""
|
|
136
135
|
|
|
137
136
|
async def execute(self, ctx: ActionCtx) -> Any:
|
|
138
|
-
if isinstance(self.variable,
|
|
137
|
+
if isinstance(self.variable, ServerVariable):
|
|
139
138
|
# Update on the backend
|
|
140
139
|
from dara.core.internal.registries import (
|
|
141
|
-
|
|
140
|
+
server_variable_registry,
|
|
142
141
|
utils_registry,
|
|
143
142
|
)
|
|
144
143
|
|
|
145
|
-
store: CacheStore = utils_registry.get('Store')
|
|
146
144
|
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
147
145
|
|
|
148
|
-
var_entry = await registry_mgr.get(
|
|
149
|
-
|
|
150
|
-
# Don't notify frontend explicitly, all clients will be notified by
|
|
146
|
+
var_entry = await registry_mgr.get(server_variable_registry, self.variable.uid)
|
|
147
|
+
await ServerVariable.write_value(var_entry, self.value)
|
|
148
|
+
# Don't notify frontend explicitly, all clients will be notified by write_value above
|
|
151
149
|
return None
|
|
152
150
|
|
|
153
151
|
# for non-data variables just ping frontend with the new value
|
|
@@ -172,7 +170,7 @@ class UpdateVariable(AnnotatedAction):
|
|
|
172
170
|
@deprecated: Passing in resolvers is deprecated, use `ctx.update` in an `@action` or `UpdateVariableImpl` instead.
|
|
173
171
|
`UpdateVariableImpl` will be renamed to `UpdateVariable` in Dara 2.0.
|
|
174
172
|
|
|
175
|
-
The UpdateVariable action can be passed to any `ComponentInstance` prop accepting an action and trigger the update of a Variable
|
|
173
|
+
The UpdateVariable action can be passed to any `ComponentInstance` prop accepting an action and trigger the update of a Variable or ServerVariable.
|
|
176
174
|
The resolver function takes a Context param which will feed the `inputs`: `old` and `new` as well as any `extras` passed through.
|
|
177
175
|
|
|
178
176
|
Below an example of how a resolver might look:
|
|
@@ -240,13 +238,13 @@ class UpdateVariable(AnnotatedAction):
|
|
|
240
238
|
|
|
241
239
|
Ctx: ClassVar[type[UpdateVariableContext]] = UpdateVariableContext
|
|
242
240
|
|
|
243
|
-
variable: Union[Variable,
|
|
241
|
+
variable: Union[Variable, ServerVariable]
|
|
244
242
|
extras: Optional[List[AnyVariable]]
|
|
245
243
|
|
|
246
244
|
def __init__(
|
|
247
245
|
self,
|
|
248
246
|
resolver: Callable[[UpdateVariableContext], Any],
|
|
249
|
-
variable: Union[Variable,
|
|
247
|
+
variable: Union[Variable, ServerVariable],
|
|
250
248
|
extras: Optional[List[AnyVariable]] = None,
|
|
251
249
|
):
|
|
252
250
|
"""
|
|
@@ -628,13 +626,13 @@ def DownloadContent(
|
|
|
628
626
|
|
|
629
627
|
```python
|
|
630
628
|
|
|
631
|
-
from dara.core import action, ConfigurationBuilder,
|
|
629
|
+
from dara.core import action, ConfigurationBuilder, ServerVariable, DownloadContent
|
|
632
630
|
from dara.components.components import Button, Stack
|
|
633
631
|
|
|
634
632
|
|
|
635
633
|
# generate data, alternatively you could load it from a file
|
|
636
634
|
df = pandas.DataFrame(data={'x': [1, 2, 3], 'y':[4, 5, 6]})
|
|
637
|
-
my_var =
|
|
635
|
+
my_var = ServerVariable(df)
|
|
638
636
|
|
|
639
637
|
config = ConfigurationBuilder()
|
|
640
638
|
|
|
@@ -826,12 +824,12 @@ class ActionCtx:
|
|
|
826
824
|
self._on_action = _on_action
|
|
827
825
|
|
|
828
826
|
@overload
|
|
829
|
-
async def update(self, variable:
|
|
827
|
+
async def update(self, variable: ServerVariable, value: Optional[DataFrame]): ...
|
|
830
828
|
|
|
831
829
|
@overload
|
|
832
|
-
async def update(self, variable:
|
|
830
|
+
async def update(self, variable: Variable[VariableT], value: VariableT): ...
|
|
833
831
|
|
|
834
|
-
async def update(self, variable: Union[Variable,
|
|
832
|
+
async def update(self, variable: Union[Variable, ServerVariable], value: Any):
|
|
835
833
|
"""
|
|
836
834
|
Update a given variable to provided value.
|
|
837
835
|
|
|
@@ -1114,12 +1112,12 @@ class ActionCtx:
|
|
|
1114
1112
|
|
|
1115
1113
|
```python
|
|
1116
1114
|
|
|
1117
|
-
from dara.core import action, ConfigurationBuilder,
|
|
1115
|
+
from dara.core import action, ConfigurationBuilder, ServerVariable
|
|
1118
1116
|
from dara.components.components import Button, Stack
|
|
1119
1117
|
|
|
1120
1118
|
# generate data, alternatively you could load it from a file
|
|
1121
1119
|
df = pandas.DataFrame(data={'x': [1, 2, 3], 'y':[4, 5, 6]})
|
|
1122
|
-
my_var =
|
|
1120
|
+
my_var = ServerVariable(df)
|
|
1123
1121
|
|
|
1124
1122
|
config = ConfigurationBuilder()
|
|
1125
1123
|
|
|
@@ -1245,6 +1243,7 @@ class ActionCtx:
|
|
|
1245
1243
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
1246
1244
|
|
|
1247
1245
|
task = Task(func=func, args=args, kwargs=kwargs, on_progress=on_progress)
|
|
1246
|
+
task_mgr.register_task(task)
|
|
1248
1247
|
pending_task = await task_mgr.run_task(task)
|
|
1249
1248
|
return await pending_task.value()
|
|
1250
1249
|
|
|
@@ -1329,7 +1328,7 @@ class action:
|
|
|
1329
1328
|
```
|
|
1330
1329
|
"""
|
|
1331
1330
|
|
|
1332
|
-
Ctx:
|
|
1331
|
+
Ctx: TypeAlias = ActionCtx
|
|
1333
1332
|
|
|
1334
1333
|
def __init__(self, func: Callable[..., Any]):
|
|
1335
1334
|
from dara.core.internal.execute_action import execute_action
|
|
@@ -1456,6 +1455,11 @@ class action:
|
|
|
1456
1455
|
dynamic_kwargs: Dict[str, AnyVariable] = {}
|
|
1457
1456
|
static_kwargs: Dict[str, Any] = {}
|
|
1458
1457
|
for key, kwarg in all_kwargs.items():
|
|
1458
|
+
if isinstance(kwarg, StateVariable):
|
|
1459
|
+
raise ValueError(
|
|
1460
|
+
'StateVariable cannot be used as input to actions. '
|
|
1461
|
+
"StateVariables are internal variables for tracking DerivedVariable ephemeral client state shouldn't be used as action payloads."
|
|
1462
|
+
)
|
|
1459
1463
|
if isinstance(kwarg, AnyVariable):
|
|
1460
1464
|
dynamic_kwargs[key] = kwarg
|
|
1461
1465
|
else:
|
|
@@ -1,139 +1,11 @@
|
|
|
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 abc
|
|
19
|
-
import io
|
|
20
|
-
import os
|
|
21
|
-
from collections.abc import Awaitable
|
|
22
|
-
from typing import Any, Callable, Literal, Optional, TypedDict, Union, cast
|
|
23
|
-
|
|
24
|
-
import pandas
|
|
25
|
-
from fastapi import UploadFile
|
|
26
|
-
from pydantic import ConfigDict
|
|
1
|
+
from typing_extensions import TypeAlias
|
|
27
2
|
|
|
28
|
-
from dara.core.base_definitions import CachedRegistryEntry, UploadResolverDef
|
|
29
3
|
from dara.core.interactivity.any_variable import AnyVariable
|
|
30
|
-
from dara.core.interactivity.filtering import FilterQuery
|
|
31
|
-
from dara.core.internal.cache_store.cache_store import CacheStore
|
|
32
|
-
from dara.core.internal.registry_lookup import RegistryLookup
|
|
33
|
-
from dara.core.internal.utils import run_user_handler
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class AnyDataVariable(AnyVariable, abc.ABC):
|
|
37
|
-
"""
|
|
38
|
-
AnyDataVariable represents any variable that is specifically designed to hold datasets (i.e. DataVariable, DerivedDataVariable)
|
|
39
|
-
|
|
40
|
-
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
41
|
-
:param filters: a dictionary of filters to apply to the data
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
uid: str
|
|
45
|
-
filters: Optional[FilterQuery] = None
|
|
46
|
-
|
|
47
|
-
def __init__(self, uid: Optional[str] = None, **kwargs) -> None:
|
|
48
|
-
super().__init__(uid=uid, **kwargs)
|
|
49
|
-
|
|
50
|
-
def filter(self, filters: FilterQuery):
|
|
51
|
-
return self.copy(update={'filters': filters}, deep=True)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class FieldType(TypedDict):
|
|
55
|
-
name: Union[str, tuple[str, ...]]
|
|
56
|
-
type: Literal['integer', 'number', 'boolean', 'datetime', 'duration', 'any', 'str']
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class DataFrameSchema(TypedDict):
|
|
60
|
-
fields: list[FieldType]
|
|
61
|
-
primaryKey: list[str]
|
|
62
|
-
|
|
63
4
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Registry entry for DataVariable.
|
|
67
|
-
"""
|
|
5
|
+
# re-export for backwards compatibility
|
|
6
|
+
from .tabular_variable import * # noqa: F403
|
|
68
7
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
get_total_count: Callable[..., Awaitable[int]]
|
|
74
|
-
"""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"""
|
|
75
|
-
|
|
76
|
-
get_schema: Callable[..., Awaitable[DataFrameSchema]]
|
|
77
|
-
"""Handler to get the schema for data variable. Defaults to DataVariable.get_schema for type=plain, and DerivedDataVariable.get_schema for type=derived"""
|
|
78
|
-
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
async def upload(data: UploadFile, data_uid: Optional[str] = None, resolver_id: Optional[str] = None):
|
|
82
|
-
"""
|
|
83
|
-
Handler for uploading data.
|
|
84
|
-
|
|
85
|
-
:param data: the file to upload
|
|
86
|
-
:param data_uid: optional uid of the data variable to upload to
|
|
87
|
-
:param resolver_id: optional id of the upload resolver to use, falls back to default handlers for csv/xlsx
|
|
88
|
-
"""
|
|
89
|
-
from dara.core.interactivity.data_variable import DataVariable
|
|
90
|
-
from dara.core.internal.registries import (
|
|
91
|
-
data_variable_registry,
|
|
92
|
-
upload_resolver_registry,
|
|
93
|
-
utils_registry,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
store: CacheStore = utils_registry.get('Store')
|
|
97
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
98
|
-
|
|
99
|
-
if data.filename is None:
|
|
100
|
-
raise ValueError('Filename not provided')
|
|
101
|
-
|
|
102
|
-
variable = None
|
|
103
|
-
|
|
104
|
-
_name, file_type = os.path.splitext(data.filename)
|
|
105
|
-
|
|
106
|
-
if data_uid is not None:
|
|
107
|
-
try:
|
|
108
|
-
variable = await registry_mgr.get(data_variable_registry, data_uid)
|
|
109
|
-
except KeyError as e:
|
|
110
|
-
raise ValueError(f'Data Variable {data_uid} does not exist') from e
|
|
111
|
-
|
|
112
|
-
if variable.type == 'derived':
|
|
113
|
-
raise ValueError('Cannot upload data to DerivedDataVariable')
|
|
114
|
-
|
|
115
|
-
content = cast(bytes, await data.read())
|
|
116
|
-
|
|
117
|
-
resolver = None
|
|
118
|
-
|
|
119
|
-
# If Id is provided, lookup the definition from registry
|
|
120
|
-
if resolver_id is not None:
|
|
121
|
-
resolver_def: UploadResolverDef = await registry_mgr.get(upload_resolver_registry, resolver_id)
|
|
122
|
-
resolver = resolver_def.resolver
|
|
123
|
-
|
|
124
|
-
if resolver:
|
|
125
|
-
content = await run_user_handler(handler=resolver, args=(content, data.filename))
|
|
126
|
-
# If resolver is not provided, follow roughly the cl_dataset_parser logic
|
|
127
|
-
elif file_type == '.xlsx':
|
|
128
|
-
file_object_xlsx = io.BytesIO(content)
|
|
129
|
-
content = pandas.read_excel(file_object_xlsx, index_col=None)
|
|
130
|
-
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
131
|
-
else:
|
|
132
|
-
# default to csv
|
|
133
|
-
file_object_csv = io.StringIO(content.decode('utf-8'))
|
|
134
|
-
content = pandas.read_csv(file_object_csv, index_col=0)
|
|
135
|
-
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
136
|
-
|
|
137
|
-
# If a data variable is provided, update it with the new content
|
|
138
|
-
if variable:
|
|
139
|
-
DataVariable.update_value(variable, store, content)
|
|
8
|
+
AnyDataVariable: TypeAlias = AnyVariable
|
|
9
|
+
"""
|
|
10
|
+
Deprecated alias. Tabular variables are now DerivedVariable or ServerVariable
|
|
11
|
+
"""
|
|
@@ -111,7 +111,7 @@ async def get_current_value(variable: dict, timeout: float = 3, raw: bool = Fals
|
|
|
111
111
|
user_identity = None
|
|
112
112
|
|
|
113
113
|
if current_user is not None:
|
|
114
|
-
user_identity = current_user.identity_id
|
|
114
|
+
user_identity = current_user.identity_id
|
|
115
115
|
elif isinstance(auth_config, BasicAuthConfig):
|
|
116
116
|
# basic auth - assume it's the single existing user
|
|
117
117
|
user_identity = list(auth_config.users.keys())[0]
|