dara-core 1.19.1__py3-none-any.whl → 1.20.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. dara/core/__init__.py +1 -0
  2. dara/core/auth/basic.py +13 -7
  3. dara/core/auth/definitions.py +2 -2
  4. dara/core/auth/utils.py +1 -1
  5. dara/core/base_definitions.py +7 -42
  6. dara/core/data_utils.py +16 -17
  7. dara/core/definitions.py +8 -8
  8. dara/core/interactivity/__init__.py +4 -0
  9. dara/core/interactivity/actions.py +20 -22
  10. dara/core/interactivity/any_data_variable.py +7 -135
  11. dara/core/interactivity/any_variable.py +1 -1
  12. dara/core/interactivity/client_variable.py +71 -0
  13. dara/core/interactivity/data_variable.py +8 -266
  14. dara/core/interactivity/derived_data_variable.py +6 -290
  15. dara/core/interactivity/derived_variable.py +335 -201
  16. dara/core/interactivity/filtering.py +29 -2
  17. dara/core/interactivity/loop_variable.py +2 -2
  18. dara/core/interactivity/non_data_variable.py +5 -68
  19. dara/core/interactivity/plain_variable.py +87 -14
  20. dara/core/interactivity/server_variable.py +325 -0
  21. dara/core/interactivity/state_variable.py +2 -2
  22. dara/core/interactivity/switch_variable.py +15 -15
  23. dara/core/interactivity/tabular_variable.py +94 -0
  24. dara/core/interactivity/url_variable.py +10 -90
  25. dara/core/internal/cache_store/cache_store.py +5 -20
  26. dara/core/internal/dependency_resolution.py +27 -69
  27. dara/core/internal/devtools.py +10 -3
  28. dara/core/internal/execute_action.py +9 -3
  29. dara/core/internal/multi_resource_lock.py +70 -0
  30. dara/core/internal/normalization.py +0 -5
  31. dara/core/internal/pandas_utils.py +105 -3
  32. dara/core/internal/pool/definitions.py +1 -1
  33. dara/core/internal/pool/task_pool.py +1 -1
  34. dara/core/internal/registries.py +3 -2
  35. dara/core/internal/registry.py +1 -1
  36. dara/core/internal/registry_lookup.py +5 -3
  37. dara/core/internal/routing.py +52 -121
  38. dara/core/internal/store.py +2 -29
  39. dara/core/internal/tasks.py +372 -182
  40. dara/core/internal/utils.py +25 -3
  41. dara/core/internal/websocket.py +1 -1
  42. dara/core/js_tooling/js_utils.py +2 -0
  43. dara/core/logging.py +10 -6
  44. dara/core/persistence.py +26 -4
  45. dara/core/umd/dara.core.umd.js +751 -1386
  46. dara/core/visual/dynamic_component.py +10 -13
  47. {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/METADATA +10 -10
  48. {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/RECORD +51 -47
  49. {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/LICENSE +0 -0
  50. {dara_core-1.19.1.dist-info → dara_core-1.20.0.dist-info}/WHEEL +0 -0
  51. {dara_core-1.19.1.dist-info → dara_core-1.20.0.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(NonDataVariable):
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, NonDataVariable, Any]] = None
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, NonDataVariable, Any],
233
- value_map: Dict[Any, Any] | NonDataVariable,
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 NonDataVariable.
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 NonDataVariable
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, NonDataVariable):
266
+ if isinstance(v, ClientVariable):
267
267
  return v
268
- raise ValueError(f'value_map must be a dict or NonDataVariable, got {type(v)}')
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, NonDataVariable, Any],
274
- true_value: Union[Any, NonDataVariable],
275
- false_value: Union[Any, NonDataVariable],
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[NonDataVariable, Any],
350
- mapping: Union[Dict[Any, Any], NonDataVariable],
351
- default: Optional[Union[Any, NonDataVariable]] = None,
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 Any, Generic, Optional, TypeVar
20
+ from typing import Optional, TypeVar
21
21
 
22
- from pydantic import ConfigDict, SerializerFunctionWrapHandler, model_serializer
22
+ from pydantic import ConfigDict
23
+ from typing_extensions import deprecated
23
24
 
24
- from dara.core.interactivity.non_data_variable import NonDataVariable
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
- class UrlVariable(NonDataVariable, Generic[VariableType]):
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__(query=query, default=default, uid=uid)
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 PendingValue, resolve it
229
- if isinstance(prev_value, PendingValue):
230
- if error is not None:
231
- prev_value.error(error)
232
- else:
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 DataVariable, DerivedDataVariable, DerivedVariable
24
- from dara.core.interactivity.filtering import FilterQuery
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 ResolvedDerivedDataVariable(TypedDict):
40
- type: Literal['derived-data']
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 is_resolved_derived_data_variable(
66
- obj: Any,
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 is_resolved_data_variable(entry):
121
- return await _resolve_data_var(entry, store)
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 _resolve_data_var(data_variable_entry: ResolvedDataVariable, store: CacheStore):
149
+ async def _resolve_server_var(resolved_server_variable: ResolvedServerVariable) -> Any:
190
150
  """
191
- Resolve a data variable from the registry and get it's new value based on the dynamic variable mapping passed
192
- in.
151
+ Resolve a server variable.
193
152
 
194
- :param data_variable_entry: data var entry
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 data_variable_registry, utils_registry
156
+ from dara.core.internal.registries import server_variable_registry, utils_registry
198
157
 
199
158
  registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
200
- var = await registry_mgr.get(data_variable_registry, str(data_variable_entry.get('uid')))
201
- result = await DataVariable.get_value(var, store, data_variable_entry.get('filters', None))
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:
@@ -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 {'error': print_stacktrace(), 'time': str(datetime.now())}
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
- for key, value in values.items():
146
+ async def _resolve_kwarg(val: Any, key: str):
147
147
  typ = annotations.get(key)
148
- val = await resolve_dependency(value, store, task_mgr)
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
- return MetaTask(
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