dara-core 1.19.1__py3-none-any.whl → 1.20.0a1__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 +333 -199
- 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 +742 -1381
- dara/core/visual/dynamic_component.py +10 -13
- {dara_core-1.19.1.dist-info → dara_core-1.20.0a1.dist-info}/METADATA +10 -10
- {dara_core-1.19.1.dist-info → dara_core-1.20.0a1.dist-info}/RECORD +51 -47
- {dara_core-1.19.1.dist-info → dara_core-1.20.0a1.dist-info}/LICENSE +0 -0
- {dara_core-1.19.1.dist-info → dara_core-1.20.0a1.dist-info}/WHEEL +0 -0
- {dara_core-1.19.1.dist-info → dara_core-1.20.0a1.dist-info}/entry_points.txt +0 -0
|
@@ -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.
|
|
@@ -222,15 +222,15 @@ class SwitchVariable(NonDataVariable):
|
|
|
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,26 +253,26 @@ 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
|
"""
|
|
@@ -346,9 +346,9 @@ 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
|
"""
|
|
@@ -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)
|
|
@@ -17,16 +17,21 @@ limitations under the License.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import
|
|
20
|
+
from typing import Optional, TypeVar
|
|
21
21
|
|
|
22
|
-
from pydantic import ConfigDict
|
|
22
|
+
from pydantic import ConfigDict
|
|
23
|
+
from typing_extensions import deprecated
|
|
23
24
|
|
|
24
|
-
from dara.core.interactivity.
|
|
25
|
+
from dara.core.interactivity.plain_variable import Variable
|
|
26
|
+
from dara.core.persistence import QueryParamStore
|
|
25
27
|
|
|
26
28
|
VariableType = TypeVar('VariableType')
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
@deprecated(
|
|
32
|
+
'UrlVariable is deprecated and will be removed in a future version. Use dara.core.interactivity.plain_variable.Variable with dara.core.persistence.QueryParamStore instead'
|
|
33
|
+
)
|
|
34
|
+
class UrlVariable(Variable[VariableType]):
|
|
30
35
|
"""
|
|
31
36
|
A UrlVariable is very similar to a normal Variable however rather than it's state being stored in the memory of
|
|
32
37
|
the client it's value is stored in the url of page as a query parameter. This is very useful for parameterizing
|
|
@@ -48,89 +53,4 @@ class UrlVariable(NonDataVariable, Generic[VariableType]):
|
|
|
48
53
|
:param default: the initial value for the variable, defaults to None
|
|
49
54
|
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
50
55
|
"""
|
|
51
|
-
super().__init__(
|
|
52
|
-
|
|
53
|
-
def sync(self):
|
|
54
|
-
"""
|
|
55
|
-
Create an action to synchronise the value of this UrlVariable with input value sent from the component.
|
|
56
|
-
|
|
57
|
-
```python
|
|
58
|
-
|
|
59
|
-
from dara.core import UrlVariable
|
|
60
|
-
from dara.components import Select
|
|
61
|
-
|
|
62
|
-
var = UrlVariable('first', query='num')
|
|
63
|
-
another_var = UrlVariable('second', query='num_two')
|
|
64
|
-
|
|
65
|
-
Select(
|
|
66
|
-
value=var,
|
|
67
|
-
items=['first', 'second', 'third'],
|
|
68
|
-
onchange=another_var.sync(),
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
"""
|
|
73
|
-
from dara.core.interactivity.actions import (
|
|
74
|
-
UpdateVariableImpl,
|
|
75
|
-
assert_no_context,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
assert_no_context('ctx.update')
|
|
79
|
-
return UpdateVariableImpl(variable=self, value=UpdateVariableImpl.INPUT)
|
|
80
|
-
|
|
81
|
-
def toggle(self):
|
|
82
|
-
"""
|
|
83
|
-
Create an action to toggle the value of this UrlVariable. Note this only works for boolean variables.
|
|
84
|
-
|
|
85
|
-
```python
|
|
86
|
-
|
|
87
|
-
from dara.core import UrlVariable
|
|
88
|
-
from dara.components import Button
|
|
89
|
-
|
|
90
|
-
var = UrlVariable(True, query='show')
|
|
91
|
-
|
|
92
|
-
Button(
|
|
93
|
-
'Toggle',
|
|
94
|
-
onclick=var.toggle(),
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
"""
|
|
99
|
-
from dara.core.interactivity.actions import (
|
|
100
|
-
UpdateVariableImpl,
|
|
101
|
-
assert_no_context,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
assert_no_context('ctx.update')
|
|
105
|
-
return UpdateVariableImpl(variable=self, value=UpdateVariableImpl.TOGGLE)
|
|
106
|
-
|
|
107
|
-
def update(self, value: Any):
|
|
108
|
-
"""
|
|
109
|
-
Create an action to update the value of this UrlVariable to a provided value.
|
|
110
|
-
|
|
111
|
-
```python
|
|
112
|
-
|
|
113
|
-
from dara.core import UrlVariable
|
|
114
|
-
from dara.components import Button
|
|
115
|
-
|
|
116
|
-
show = UrlVariable(True, query='show')
|
|
117
|
-
|
|
118
|
-
Button(
|
|
119
|
-
'Hide',
|
|
120
|
-
onclick=show.update(False),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
"""
|
|
125
|
-
from dara.core.interactivity.actions import (
|
|
126
|
-
UpdateVariableImpl,
|
|
127
|
-
assert_no_context,
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
assert_no_context('ctx.update')
|
|
131
|
-
return UpdateVariableImpl(variable=self, value=value)
|
|
132
|
-
|
|
133
|
-
@model_serializer(mode='wrap')
|
|
134
|
-
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
135
|
-
parent_dict = nxt(self)
|
|
136
|
-
return {**parent_dict, '__typename': 'UrlVariable', 'uid': str(parent_dict['uid'])}
|
|
56
|
+
super().__init__(default=default, uid=uid, store=QueryParamStore(query=query), query=query)
|
|
@@ -6,7 +6,6 @@ from dara.core.base_definitions import (
|
|
|
6
6
|
LruCachePolicy,
|
|
7
7
|
MostRecentCachePolicy,
|
|
8
8
|
PendingTask,
|
|
9
|
-
PendingValue,
|
|
10
9
|
TTLCachePolicy,
|
|
11
10
|
)
|
|
12
11
|
from dara.core.internal.cache_store.base_impl import CacheStoreImpl, PolicyT
|
|
@@ -189,9 +188,6 @@ class CacheStore:
|
|
|
189
188
|
|
|
190
189
|
value = await self.get(registry_entry, key)
|
|
191
190
|
|
|
192
|
-
if isinstance(value, PendingValue):
|
|
193
|
-
return await value.wait()
|
|
194
|
-
|
|
195
191
|
if isinstance(value, PendingTask):
|
|
196
192
|
return await value.run()
|
|
197
193
|
|
|
@@ -202,7 +198,6 @@ class CacheStore:
|
|
|
202
198
|
registry_entry: CachedRegistryEntry,
|
|
203
199
|
key: str,
|
|
204
200
|
value: Any,
|
|
205
|
-
error: Optional[Exception] = None,
|
|
206
201
|
pin: bool = False,
|
|
207
202
|
):
|
|
208
203
|
"""
|
|
@@ -225,12 +220,11 @@ class CacheStore:
|
|
|
225
220
|
|
|
226
221
|
prev_value = await registry_store.get(key)
|
|
227
222
|
|
|
228
|
-
# If previous value was a
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
prev_value.resolve(value)
|
|
223
|
+
# If the previous value was a PendingTask, resolve it with the new value
|
|
224
|
+
# This handles cache-coordinated tasks (e.g., DerivedVariables) where PendingTasks
|
|
225
|
+
# are stored in cache to coordinate multiple callers with the same cache key
|
|
226
|
+
if isinstance(prev_value, PendingTask):
|
|
227
|
+
prev_value.resolve(value)
|
|
234
228
|
|
|
235
229
|
# Update size
|
|
236
230
|
self._update_size(prev_value, value)
|
|
@@ -240,15 +234,6 @@ class CacheStore:
|
|
|
240
234
|
|
|
241
235
|
return value
|
|
242
236
|
|
|
243
|
-
async def set_pending(self, registry_entry: CachedRegistryEntry, key: str):
|
|
244
|
-
"""
|
|
245
|
-
Set a pending value for the given registry entry and cache key.
|
|
246
|
-
|
|
247
|
-
:param registry_entry: The registry entry to store the value for.
|
|
248
|
-
:param key: The key of the entry to set.
|
|
249
|
-
"""
|
|
250
|
-
return await self.set(registry_entry, key, PendingValue())
|
|
251
|
-
|
|
252
237
|
async def clear(self):
|
|
253
238
|
"""
|
|
254
239
|
Empty all stores.
|
|
@@ -20,10 +20,9 @@ from typing import Any, List, Literal, Optional, Union
|
|
|
20
20
|
from typing_extensions import TypedDict, TypeGuard
|
|
21
21
|
|
|
22
22
|
from dara.core.base_definitions import BaseTask, PendingTask
|
|
23
|
-
from dara.core.interactivity import
|
|
24
|
-
from dara.core.interactivity.
|
|
23
|
+
from dara.core.interactivity import DerivedVariable
|
|
24
|
+
from dara.core.interactivity.server_variable import ServerVariable
|
|
25
25
|
from dara.core.internal.cache_store import CacheStore
|
|
26
|
-
from dara.core.internal.pandas_utils import remove_index
|
|
27
26
|
from dara.core.internal.registry_lookup import RegistryLookup
|
|
28
27
|
from dara.core.internal.tasks import TaskManager
|
|
29
28
|
from dara.core.logging import dev_logger
|
|
@@ -36,18 +35,10 @@ class ResolvedDerivedVariable(TypedDict):
|
|
|
36
35
|
force_key: Optional[str]
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
class
|
|
40
|
-
type: Literal['
|
|
41
|
-
uid: str
|
|
42
|
-
values: List[Any]
|
|
43
|
-
filters: Optional[Union[FilterQuery, dict]]
|
|
44
|
-
force_key: Optional[str]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class ResolvedDataVariable(TypedDict):
|
|
48
|
-
filters: Optional[Union[FilterQuery, dict]]
|
|
49
|
-
type: Literal['data']
|
|
38
|
+
class ResolvedServerVariable(TypedDict):
|
|
39
|
+
type: Literal['server']
|
|
50
40
|
uid: str
|
|
41
|
+
sequence_number: int
|
|
51
42
|
|
|
52
43
|
|
|
53
44
|
class ResolvedSwitchVariable(TypedDict):
|
|
@@ -62,20 +53,24 @@ def is_resolved_derived_variable(obj: Any) -> TypeGuard[ResolvedDerivedVariable]
|
|
|
62
53
|
return isinstance(obj, dict) and 'uid' in obj and obj.get('type') == 'derived'
|
|
63
54
|
|
|
64
55
|
|
|
65
|
-
def
|
|
66
|
-
obj
|
|
67
|
-
) -> TypeGuard[ResolvedDerivedDataVariable]:
|
|
68
|
-
return isinstance(obj, dict) and 'uid' in obj and obj.get('type') == 'derived-data'
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def is_resolved_data_variable(obj: Any) -> TypeGuard[ResolvedDataVariable]:
|
|
72
|
-
return isinstance(obj, dict) and 'uid' in obj and obj.get('type') == 'data'
|
|
56
|
+
def is_resolved_server_variable(obj: Any) -> TypeGuard[ResolvedServerVariable]:
|
|
57
|
+
return isinstance(obj, dict) and 'uid' in obj and obj.get('type') == 'server'
|
|
73
58
|
|
|
74
59
|
|
|
75
60
|
def is_resolved_switch_variable(obj: Any) -> TypeGuard[ResolvedSwitchVariable]:
|
|
76
61
|
return isinstance(obj, dict) and 'uid' in obj and obj.get('type') == 'switch'
|
|
77
62
|
|
|
78
63
|
|
|
64
|
+
def is_forced(value: Any) -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Whether a value is a Derived(Data)Variable with a force_key or any of its values are forced
|
|
67
|
+
"""
|
|
68
|
+
if not is_resolved_derived_variable(value):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
return value.get('force_key') is not None or any(is_forced(v) for v in value.get('values', []))
|
|
72
|
+
|
|
73
|
+
|
|
79
74
|
def clean_force_key(value: Any) -> Any:
|
|
80
75
|
"""
|
|
81
76
|
Clean an argument to a value to remove force keys
|
|
@@ -84,6 +79,8 @@ def clean_force_key(value: Any) -> Any:
|
|
|
84
79
|
return value
|
|
85
80
|
|
|
86
81
|
if isinstance(value, dict):
|
|
82
|
+
# clone the dict to avoid mutating the original
|
|
83
|
+
value = value.copy()
|
|
87
84
|
# Remove force key from the value
|
|
88
85
|
value.pop('force_key', None)
|
|
89
86
|
return {k: clean_force_key(v) for k, v in value.items()}
|
|
@@ -94,8 +91,6 @@ def clean_force_key(value: Any) -> Any:
|
|
|
94
91
|
|
|
95
92
|
async def resolve_dependency(
|
|
96
93
|
entry: Union[
|
|
97
|
-
ResolvedDerivedDataVariable,
|
|
98
|
-
ResolvedDataVariable,
|
|
99
94
|
ResolvedDerivedVariable,
|
|
100
95
|
ResolvedSwitchVariable,
|
|
101
96
|
Any,
|
|
@@ -111,14 +106,11 @@ async def resolve_dependency(
|
|
|
111
106
|
:param store: store instance
|
|
112
107
|
:param task_mgr: task manager instance
|
|
113
108
|
"""
|
|
114
|
-
if is_resolved_derived_data_variable(entry):
|
|
115
|
-
return await _resolve_derived_data_var(entry, store, task_mgr)
|
|
116
|
-
|
|
117
109
|
if is_resolved_derived_variable(entry):
|
|
118
110
|
return await _resolve_derived_var(entry, store, task_mgr)
|
|
119
111
|
|
|
120
|
-
if
|
|
121
|
-
return await
|
|
112
|
+
if is_resolved_server_variable(entry):
|
|
113
|
+
return await _resolve_server_var(entry)
|
|
122
114
|
|
|
123
115
|
if is_resolved_switch_variable(entry):
|
|
124
116
|
return await _resolve_switch_var(entry, store, task_mgr)
|
|
@@ -126,38 +118,6 @@ async def resolve_dependency(
|
|
|
126
118
|
return entry
|
|
127
119
|
|
|
128
120
|
|
|
129
|
-
async def _resolve_derived_data_var(entry: ResolvedDerivedDataVariable, store: CacheStore, task_mgr: TaskManager):
|
|
130
|
-
"""
|
|
131
|
-
Resolve a derived data variable from the registry
|
|
132
|
-
|
|
133
|
-
:param entry: derived data variable entry
|
|
134
|
-
:param store: store instance to use for caching
|
|
135
|
-
:param task_mgr: task manager instance
|
|
136
|
-
"""
|
|
137
|
-
from dara.core.internal.registries import (
|
|
138
|
-
data_variable_registry,
|
|
139
|
-
derived_variable_registry,
|
|
140
|
-
utils_registry,
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
144
|
-
dv_var = await registry_mgr.get(derived_variable_registry, str(entry.get('uid')))
|
|
145
|
-
data_var = await registry_mgr.get(data_variable_registry, str(entry.get('uid')))
|
|
146
|
-
|
|
147
|
-
input_values: List[Any] = entry.get('values', [])
|
|
148
|
-
|
|
149
|
-
result = await DerivedDataVariable.resolve_value(
|
|
150
|
-
data_entry=data_var,
|
|
151
|
-
dv_entry=dv_var,
|
|
152
|
-
store=store,
|
|
153
|
-
task_mgr=task_mgr,
|
|
154
|
-
args=input_values,
|
|
155
|
-
filters=entry.get('filters', None),
|
|
156
|
-
force_key=entry.get('force_key'),
|
|
157
|
-
)
|
|
158
|
-
return remove_index(result)
|
|
159
|
-
|
|
160
|
-
|
|
161
121
|
async def _resolve_derived_var(
|
|
162
122
|
derived_variable_entry: ResolvedDerivedVariable,
|
|
163
123
|
store: CacheStore,
|
|
@@ -186,20 +146,18 @@ async def _resolve_derived_var(
|
|
|
186
146
|
return result['value']
|
|
187
147
|
|
|
188
148
|
|
|
189
|
-
async def
|
|
149
|
+
async def _resolve_server_var(resolved_server_variable: ResolvedServerVariable) -> Any:
|
|
190
150
|
"""
|
|
191
|
-
Resolve a
|
|
192
|
-
in.
|
|
151
|
+
Resolve a server variable.
|
|
193
152
|
|
|
194
|
-
:param
|
|
153
|
+
:param server_variable_entry: server var entry
|
|
195
154
|
:param store: the store instance to use for caching
|
|
196
155
|
"""
|
|
197
|
-
from dara.core.internal.registries import
|
|
156
|
+
from dara.core.internal.registries import server_variable_registry, utils_registry
|
|
198
157
|
|
|
199
158
|
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return remove_index(result)
|
|
159
|
+
server_var_entry = await registry_mgr.get(server_variable_registry, resolved_server_variable['uid'])
|
|
160
|
+
return await ServerVariable.get_value(server_var_entry)
|
|
203
161
|
|
|
204
162
|
|
|
205
163
|
def _normalize_lookup_key(value: Any) -> str:
|
dara/core/internal/devtools.py
CHANGED
|
@@ -21,15 +21,19 @@ import sys
|
|
|
21
21
|
import traceback
|
|
22
22
|
from contextlib import contextmanager
|
|
23
23
|
from datetime import datetime
|
|
24
|
+
from typing import Optional
|
|
24
25
|
|
|
25
26
|
from dara.core.internal.websocket import WebsocketManager
|
|
26
27
|
from dara.core.logging import eng_logger
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def print_stacktrace():
|
|
30
|
+
def print_stacktrace(err: Optional[BaseException] = None) -> str:
|
|
30
31
|
"""
|
|
31
32
|
Prints out the current stack trace. Will also extract any exceptions and print them at the end.
|
|
32
33
|
"""
|
|
34
|
+
if err is not None:
|
|
35
|
+
return ''.join(traceback.format_exception(type(err), err, err.__traceback__))
|
|
36
|
+
|
|
33
37
|
exc = sys.exc_info()[0]
|
|
34
38
|
stack = traceback.extract_stack()[:-1]
|
|
35
39
|
|
|
@@ -55,11 +59,14 @@ def handle_system_exit(error_msg: str):
|
|
|
55
59
|
raise InterruptedError(error_msg) from e
|
|
56
60
|
|
|
57
61
|
|
|
58
|
-
def get_error_for_channel() -> dict:
|
|
62
|
+
def get_error_for_channel(err: Optional[BaseException] = None) -> dict:
|
|
59
63
|
"""
|
|
60
64
|
Get error from current stacktrace to send to the client
|
|
61
65
|
"""
|
|
62
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
'error': print_stacktrace(err),
|
|
68
|
+
'time': str(datetime.now()),
|
|
69
|
+
}
|
|
63
70
|
|
|
64
71
|
|
|
65
72
|
async def send_error_for_session(ws_mgr: WebsocketManager, session_id: str):
|
|
@@ -143,11 +143,15 @@ async def execute_action(
|
|
|
143
143
|
if values is not None:
|
|
144
144
|
annotations = action.__annotations__
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
async def _resolve_kwarg(val: Any, key: str):
|
|
147
147
|
typ = annotations.get(key)
|
|
148
|
-
val = await resolve_dependency(
|
|
148
|
+
val = await resolve_dependency(val, store, task_mgr)
|
|
149
149
|
resolved_kwargs[key] = deserialize(val, typ)
|
|
150
150
|
|
|
151
|
+
async with anyio.create_task_group() as tg:
|
|
152
|
+
for key, value in values.items():
|
|
153
|
+
tg.start_soon(_resolve_kwarg, value, key)
|
|
154
|
+
|
|
151
155
|
# Merge resolved dynamic kwargs with static kwargs received
|
|
152
156
|
resolved_kwargs = {**resolved_kwargs, **static_kwargs}
|
|
153
157
|
|
|
@@ -171,9 +175,11 @@ async def execute_action(
|
|
|
171
175
|
|
|
172
176
|
# Note: no associated registry entry, the result are not persisted in cache
|
|
173
177
|
# Return a metatask which, when all dependencies are ready, will stream the action results to the frontend
|
|
174
|
-
|
|
178
|
+
meta_task = MetaTask(
|
|
175
179
|
process_result=_stream_action, args=[action, ctx], kwargs=resolved_kwargs, notify_channels=notify_channels
|
|
176
180
|
)
|
|
181
|
+
task_mgr.register_task(meta_task)
|
|
182
|
+
return meta_task
|
|
177
183
|
|
|
178
184
|
# No tasks - run directly as an asyncio task and return the execution id
|
|
179
185
|
# Originally used to use FastAPI BackgroundTasks, but these ended up causing a blocking behavior that blocked some
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
|
|
4
|
+
import anyio
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MultiResourceLock:
|
|
8
|
+
"""
|
|
9
|
+
A class that manages multiple named locks for concurrent access to shared resources.
|
|
10
|
+
|
|
11
|
+
This class allows for acquiring and releasing locks on named resources, ensuring
|
|
12
|
+
that only one task can access a specific resource at a time. It automatically
|
|
13
|
+
creates locks for new resources and cleans them up when they're no longer in use.
|
|
14
|
+
|
|
15
|
+
:reentrant:
|
|
16
|
+
If True a task can acquire the same resource more than once; every
|
|
17
|
+
subsequent acquire of an already-held lock is a no-op. If False the
|
|
18
|
+
second attempt raises ``RuntimeError``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self._locks: dict[str, anyio.Lock] = {}
|
|
23
|
+
self._waiters = Counter[str]()
|
|
24
|
+
self._cleanup_lock = anyio.Lock()
|
|
25
|
+
|
|
26
|
+
def is_locked(self, resource_name: str) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Check if a lock for the specified resource is currently held.
|
|
29
|
+
|
|
30
|
+
:param resource_name (str): The name of the resource to check.
|
|
31
|
+
:return: True if the lock is held, False otherwise.
|
|
32
|
+
"""
|
|
33
|
+
return resource_name in self._locks and self._locks[resource_name].locked()
|
|
34
|
+
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def acquire(self, resource_name: str):
|
|
37
|
+
"""
|
|
38
|
+
Acquire a lock for the specified resource.
|
|
39
|
+
|
|
40
|
+
This method is an async context manager that acquires a lock for the given
|
|
41
|
+
resource name. If the lock doesn't exist, it creates one. It also keeps
|
|
42
|
+
track of waiters to ensure proper cleanup when the resource is no longer in use.
|
|
43
|
+
|
|
44
|
+
:param resource_name (str): The name of the resource to lock.
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
```python
|
|
48
|
+
async with multi_lock.acquire_lock("resource_a"):
|
|
49
|
+
# Critical section for "resource_a"
|
|
50
|
+
...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Note:
|
|
54
|
+
The lock is automatically released when exiting the context manager.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
async with self._cleanup_lock:
|
|
58
|
+
if resource_name not in self._locks:
|
|
59
|
+
self._locks[resource_name] = anyio.Lock()
|
|
60
|
+
self._waiters[resource_name] += 1
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
async with self._locks[resource_name]:
|
|
64
|
+
yield
|
|
65
|
+
finally:
|
|
66
|
+
async with self._cleanup_lock:
|
|
67
|
+
self._waiters[resource_name] -= 1
|
|
68
|
+
if self._waiters[resource_name] <= 0:
|
|
69
|
+
del self._waiters[resource_name]
|
|
70
|
+
del self._locks[resource_name]
|
|
@@ -31,7 +31,6 @@ from typing import (
|
|
|
31
31
|
from typing_extensions import TypedDict, TypeGuard
|
|
32
32
|
|
|
33
33
|
from dara.core.base_definitions import DaraBaseModel as BaseModel
|
|
34
|
-
from dara.core.internal.hashing import hash_object
|
|
35
34
|
|
|
36
35
|
JsonLike = Union[Mapping, List]
|
|
37
36
|
|
|
@@ -81,10 +80,6 @@ def _get_identifier(obj: Referrable) -> str:
|
|
|
81
80
|
nested = ','.join(cast(List[str], obj['nested']))
|
|
82
81
|
identifier = f'{identifier}:{nested}'
|
|
83
82
|
|
|
84
|
-
if _is_referrable_with_filters(obj):
|
|
85
|
-
filter_hash = hash_object(obj['filters'])
|
|
86
|
-
identifier = f'{identifier}:{filter_hash}'
|
|
87
|
-
|
|
88
83
|
return identifier
|
|
89
84
|
|
|
90
85
|
|