dara-core 1.20.3__py3-none-any.whl → 1.21.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 CHANGED
@@ -16,61 +16,27 @@ limitations under the License.
16
16
  """
17
17
  # ruff: noqa: F403, F405
18
18
 
19
+ import inspect
19
20
  from importlib.metadata import version
20
21
 
21
22
  from pydantic import BaseModel
22
23
 
23
24
  from dara.core.base_definitions import *
24
- from dara.core.configuration import ConfigurationBuilder
25
- from dara.core.css import CSSProperties, get_icon
25
+ from dara.core.configuration import *
26
+ from dara.core.css import *
26
27
  from dara.core.definitions import *
27
28
  from dara.core.interactivity import *
28
- from dara.core.visual.components import Fallback
29
- from dara.core.visual.dynamic_component import py_component
30
- from dara.core.visual.progress_updater import ProgressUpdater, track_progress
29
+ from dara.core.router import *
30
+ from dara.core.visual.components import *
31
+ from dara.core.visual.dynamic_component import *
32
+ from dara.core.visual.progress_updater import *
31
33
 
32
34
  __version__ = version('dara-core')
33
35
 
34
36
 
35
- # Top-level imports for most commonly used APIs for ease of use
36
-
37
- __all__ = [
38
- 'action',
39
- 'ActionCtx',
40
- 'ConfigurationBuilder',
41
- 'DerivedVariable',
42
- 'DerivedDataVariable',
43
- 'DataVariable',
44
- 'ServerVariable',
45
- 'UrlVariable',
46
- 'Cache',
47
- 'CacheType',
48
- 'Variable',
49
- 'py_component',
50
- 'DownloadVariable',
51
- 'DownloadContent',
52
- 'NavigateTo',
53
- 'Notify',
54
- 'ResetVariables',
55
- 'SideEffect',
56
- 'TriggerVariable',
57
- 'UpdateVariable',
58
- 'get_icon',
59
- 'CSSProperties',
60
- 'ProgressUpdater',
61
- 'track_progress',
62
- 'ComponentInstance',
63
- 'StyledComponentInstance',
64
- 'ErrorHandlingConfig',
65
- 'Fallback',
66
- 'UpdateVariableImpl',
67
- 'DownloadContentImpl',
68
- 'NavigateToImpl',
69
- ]
70
-
71
37
  for symbol in list(globals().values()):
72
38
  try:
73
- if issubclass(symbol, BaseModel) and symbol is not BaseModel:
39
+ if inspect.isclass(symbol) and issubclass(symbol, BaseModel) and symbol is not BaseModel:
74
40
  symbol.model_rebuild()
75
41
  except Exception as e:
76
42
  from dara.core.logging import dev_logger
@@ -36,6 +36,7 @@ from typing import (
36
36
  from fastapi.middleware import Middleware
37
37
  from pydantic import ConfigDict
38
38
  from starlette.middleware.base import BaseHTTPMiddleware
39
+ from typing_extensions import deprecated
39
40
 
40
41
  from dara.core.auth.base import BaseAuthConfig
41
42
  from dara.core.auth.basic import DefaultAuthConfig
@@ -63,6 +64,7 @@ from dara.core.internal.import_discovery import (
63
64
  from dara.core.internal.registry_lookup import CustomRegistryLookup
64
65
  from dara.core.internal.scheduler import ScheduledJob, ScheduledJobFactory
65
66
  from dara.core.logging import dev_logger
67
+ from dara.core.router import Router
66
68
  from dara.core.visual.components import RawString
67
69
  from dara.core.visual.themes import BaseTheme, ThemeDef
68
70
 
@@ -80,6 +82,7 @@ class Configuration(BaseModel):
80
82
  module_dependencies: Dict[str, str]
81
83
  live_reload: bool
82
84
  powered_by_causalens: bool
85
+ router: Router
83
86
  pages: Dict[str, Page]
84
87
  routes: Set[ApiRoute]
85
88
  scheduled_jobs: List[Tuple[Union[ScheduledJob, ScheduledJobFactory], Callable, Optional[List[Any]]]] = []
@@ -157,12 +160,13 @@ class ConfigurationBuilder:
157
160
  _custom_encoders: Dict[Type[Any], Encoder]
158
161
  _middlewares: List[Middleware]
159
162
  routes: Set[ApiRoute]
163
+ router: Router
160
164
  static_files_dir: str
161
165
  scheduled_jobs: List[Tuple[Union[ScheduledJob, ScheduledJobFactory], Callable, Optional[List[Any]]]] = []
162
166
  startup_functions: List[Callable]
163
167
  context_components: List[ComponentInstance]
164
168
  task_module: Optional[str]
165
- template: str
169
+ _template: str
166
170
  theme: BaseTheme
167
171
  title: str
168
172
 
@@ -175,13 +179,14 @@ class ConfigurationBuilder:
175
179
  self._errors = []
176
180
  self.enable_devtools = False
177
181
  self.live_reload = False
178
- self.powered_by_causalens = False
182
+ self._powered_by_causalens = False
179
183
  self._package_tags_processors = []
180
184
  self._template_extra_js = ''
181
185
  self._pages = {}
182
186
  self._template_renderers = {}
183
187
  self._endpoint_configurations = []
184
188
  self.routes = set()
189
+ self.router = Router()
185
190
  self.static_files_dir = os.path.join(pathlib.Path().parent.parent.absolute(), 'dist')
186
191
  self._static_folders = []
187
192
  self._middlewares = []
@@ -193,10 +198,31 @@ class ConfigurationBuilder:
193
198
  self._custom_ws_handlers = {}
194
199
  self._custom_encoders = {}
195
200
 
196
- self.template = 'default'
201
+ self._template = 'default'
197
202
  self.theme = BaseTheme(main='light')
198
203
  self.title = 'decisionApp'
199
204
 
205
+ @property
206
+ @deprecated('Use `config.router` instead and set a root layout route.')
207
+ def template(self):
208
+ return self._template
209
+
210
+ @template.setter
211
+ @deprecated('Use `config.router` instead and set a root layout route.')
212
+ def template(self, value):
213
+ self._template = value
214
+
215
+ @property
216
+ def powered_by_causalens(self):
217
+ return self._powered_by_causalens
218
+
219
+ @powered_by_causalens.setter
220
+ @deprecated(
221
+ 'Pass `powered_by_causalens=` kwarg to SideBarFrame directly, or use the `dara.core.visual.components.PoweredByCausalens` component'
222
+ )
223
+ def powered_by_causalens(self, value):
224
+ self._powered_by_causalens = value
225
+
200
226
  def add_action(self, action: Type[ActionImpl], local: bool = False):
201
227
  """
202
228
  Register an Action with the application.
@@ -380,6 +406,7 @@ class ConfigurationBuilder:
380
406
  """
381
407
  self._package_tags_processors.append(processor)
382
408
 
409
+ @deprecated('Use `config.router.add_page` instead.')
383
410
  def add_page(
384
411
  self,
385
412
  name: str,
@@ -436,6 +463,7 @@ class ConfigurationBuilder:
436
463
  self._pages[name] = page
437
464
  return page
438
465
 
466
+ @deprecated('Use `config.router.add_layout` and set a root layout route instead.')
439
467
  def add_template_renderer(self, name: str, template_renderer: Callable[..., Template]) -> str:
440
468
  """
441
469
  Add a new template renderer that can be selected by name as part of the configuration. By default calling this
@@ -570,10 +598,11 @@ class ConfigurationBuilder:
570
598
  enable_devtools=self.enable_devtools,
571
599
  module_dependencies=self._module_dependencies,
572
600
  live_reload=self.live_reload,
601
+ pages=self._pages,
573
602
  powered_by_causalens=self.powered_by_causalens,
574
603
  package_tag_processors=self._package_tags_processors,
575
- pages=self._pages,
576
604
  routes=self.routes,
605
+ router=self.router,
577
606
  static_files_dir=self.static_files_dir,
578
607
  scheduled_jobs=self.scheduled_jobs,
579
608
  startup_functions=self.startup_functions,
dara/core/defaults.py CHANGED
@@ -34,6 +34,7 @@ from dara.core.interactivity.actions import (
34
34
  UpdateVariableDef,
35
35
  )
36
36
  from dara.core.internal.cache_store import CacheStore
37
+ from dara.core.router import Link, LinkDef, Outlet, OutletDef
37
38
  from dara.core.visual.components import (
38
39
  DefaultFallbackDef,
39
40
  DynamicComponent,
@@ -43,6 +44,10 @@ from dara.core.visual.components import (
43
44
  ForDef,
44
45
  Menu,
45
46
  MenuDef,
47
+ MenuLink,
48
+ MenuLinkDef,
49
+ PoweredByCausalens,
50
+ PoweredByCausalensDef,
46
51
  ProgressTracker,
47
52
  ProgressTrackerDef,
48
53
  RouterContent,
@@ -74,6 +79,7 @@ INITIAL_CORE_INTERNALS = {'Store': _store}
74
79
  CORE_COMPONENTS: Dict[str, ComponentTypeAnnotation] = {
75
80
  DynamicComponent.__name__: DynamicComponentDef,
76
81
  Menu.__name__: MenuDef,
82
+ MenuLink.__name__: MenuLinkDef,
77
83
  ProgressTracker.__name__: ProgressTrackerDef,
78
84
  RouterContent.__name__: RouterContentDef,
79
85
  SideBarFrame.__name__: SideBarFrameDef,
@@ -82,6 +88,9 @@ CORE_COMPONENTS: Dict[str, ComponentTypeAnnotation] = {
82
88
  cast(str, Fallback.Row.py_component): RowFallbackDef,
83
89
  cast(str, Fallback.Custom.py_component): CustomFallbackDef,
84
90
  For.__name__: ForDef,
91
+ Link.__name__: LinkDef,
92
+ Outlet.__name__: OutletDef,
93
+ PoweredByCausalens.__name__: PoweredByCausalensDef,
85
94
  }
86
95
 
87
96
  # These actions are provided by the core JS of this module
dara/core/definitions.py CHANGED
@@ -38,12 +38,14 @@ from typing import (
38
38
  from fastapi.encoders import jsonable_encoder
39
39
  from fastapi.params import Depends
40
40
  from pydantic import (
41
+ BeforeValidator,
41
42
  ConfigDict,
42
43
  Field,
43
44
  SerializerFunctionWrapHandler,
44
45
  field_validator,
45
46
  model_serializer,
46
47
  )
48
+ from typing_extensions import Annotated
47
49
 
48
50
  from dara.core.base_definitions import Action, ComponentType
49
51
  from dara.core.base_definitions import DaraBaseModel as BaseModel
@@ -74,6 +76,22 @@ def _kebab_to_camel(string: str):
74
76
  return chunks[0] + ''.join([chunk[0].upper() + chunk[1:].lower() for chunk in chunks[1:]])
75
77
 
76
78
 
79
+ def transform_raw_css(value: Any):
80
+ """
81
+ Transform and validate a raw_css value.
82
+ """
83
+ from dara.core.interactivity.client_variable import ClientVariable
84
+
85
+ if value is None:
86
+ return None
87
+ if isinstance(value, (str, ClientVariable, CSSProperties)):
88
+ return value
89
+ if isinstance(value, dict):
90
+ return {_kebab_to_camel(k): v for k, v in value.items()}
91
+
92
+ raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or ClientVariable, got {type(value)}')
93
+
94
+
77
95
  class ErrorHandlingConfig(BaseModel):
78
96
  title: str = DEFAULT_ERROR_TITLE
79
97
  """Title to display in the error boundary"""
@@ -81,26 +99,12 @@ class ErrorHandlingConfig(BaseModel):
81
99
  description: str = DEFAULT_ERROR_DESCRIPTION
82
100
  """Description to display in the error boundary"""
83
101
 
84
- raw_css: Optional[Any] = None
102
+ raw_css: Annotated[Optional[Any], BeforeValidator(transform_raw_css)] = None
85
103
  """
86
104
  Raw styling to apply to the displayed error boundary.
87
105
  Accepts a CSSProperties, dict, str, or ClientVariable.
88
106
  """
89
107
 
90
- @field_validator('raw_css', mode='before')
91
- @classmethod
92
- def validate_raw_css(cls, value):
93
- from dara.core.interactivity.client_variable import ClientVariable
94
-
95
- if value is None:
96
- return None
97
- if isinstance(value, (str, ClientVariable, CSSProperties)):
98
- return value
99
- if isinstance(value, dict):
100
- return {_kebab_to_camel(k): v for k, v in value.items()}
101
-
102
- raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or ClientVariable, got {type(value)}')
103
-
104
108
  def model_dump(self, *args, **kwargs):
105
109
  result = super().model_dump(*args, **kwargs)
106
110
 
@@ -141,7 +145,7 @@ class ComponentInstance(BaseModel):
141
145
  required_routes: ClassVar[List[ApiRoute]] = []
142
146
  """List of routes the component depends on. Will be implicitly added to the app if this component is used"""
143
147
 
144
- raw_css: Optional[Any] = None
148
+ raw_css: Annotated[Optional[Any], BeforeValidator(transform_raw_css)] = None
145
149
  """
146
150
  Raw styling to apply to the component.
147
151
  Can be an dict/CSSProperties instance representing the `styles` tag, a string injected directly into the CSS of the wrapping component,
@@ -183,7 +187,7 @@ class ComponentInstance(BaseModel):
183
187
  """
184
188
  An optional unique identifier for the component, defaults to None
185
189
 
186
- This has no runtime effect and are intended to help identify components with human-readable names in the serialized trees, not in the DOM
190
+ This is intended to help identify components with human-readable names in the serialized trees, and is also set as the `id` attribute of the DOM element
187
191
  """
188
192
 
189
193
  for_: Optional[str] = None
@@ -213,23 +217,6 @@ class ComponentInstance(BaseModel):
213
217
 
214
218
  raise ValueError(f'fallback must be a BaseFallback or ComponentInstance, got {type(fallback)}')
215
219
 
216
- @field_validator('raw_css', mode='before')
217
- @classmethod
218
- def parse_css(cls, css: Optional[Any]):
219
- from dara.core.interactivity.client_variable import ClientVariable
220
-
221
- if css is None:
222
- return None
223
-
224
- # If it's a plain dict, change kebab case to camel case
225
- if isinstance(css, dict):
226
- return {_kebab_to_camel(k): v for k, v in css.items()}
227
-
228
- if isinstance(css, (ClientVariable, CSSProperties, str)):
229
- return css
230
-
231
- raise ValueError(f'raw_css must be a CSSProperties, dict, str, None or ClientVariable, got {type(css)}')
232
-
233
220
  @classmethod
234
221
  def isinstance(cls, obj: Any) -> bool:
235
222
  return isinstance(obj, cls)
@@ -325,7 +325,7 @@ class TriggerVariable(ActionImpl):
325
325
  )
326
326
 
327
327
 
328
- config.add_page(name='Trigger Variable', content=test_page())
328
+ config.router.add_page(path='trigger', content=test_page)
329
329
 
330
330
  ```
331
331
  """
@@ -364,8 +364,8 @@ class NavigateToImpl(ActionImpl):
364
364
  )
365
365
 
366
366
 
367
- config.add_page(name='Test Page', content=test_page())
368
- config.add_page(name='Another Page', content=another_page())
367
+ config.router.add_page(path='test-page', content=test_page)
368
+ config.router.add_page(path='another-page', content=another_page)
369
369
 
370
370
  ```
371
371
  """
@@ -373,7 +373,7 @@ class NavigateToImpl(ActionImpl):
373
373
  py_name = 'NavigateTo'
374
374
 
375
375
  url: Optional[str] = None
376
- new_tab: bool
376
+ new_tab: bool = False
377
377
 
378
378
 
379
379
  @deprecated('Use @action or `NavigateToImpl` for simple cases')
@@ -417,8 +417,8 @@ def NavigateTo(
417
417
  )
418
418
 
419
419
 
420
- config.add_page(name='Test Page', content=test_page(), icon=get_icon('shield-dog'))
421
- config.add_page(name='Another Page', content=another_page(), icon=get_icon('shield-cat'))
420
+ config.router.add_page(path='test-page', content=test_page)
421
+ config.router.add_page(path='another-page', content=another_page)
422
422
 
423
423
  ```
424
424
  """
@@ -473,7 +473,7 @@ def Logout():
473
473
  return Stack(Button('Logout', onclick=Logout()))
474
474
 
475
475
 
476
- config.add_page(name='Logout Page', content=test_page())
476
+ config.router.add_page(path='logout-page', content=test_page)
477
477
 
478
478
  ```
479
479
  """
@@ -511,7 +511,7 @@ class ResetVariables(ActionImpl):
511
511
  )
512
512
 
513
513
 
514
- config.add_page(name='ResetVariable', content=test_page(), icon=get_icon('shrimp'))
514
+ config.router.add_page(path='reset-variable', content=test_page)
515
515
 
516
516
  ```
517
517
 
@@ -565,7 +565,7 @@ class Notify(ActionImpl):
565
565
  )
566
566
 
567
567
 
568
- config.add_page(name='Notify Example', content=test_page())
568
+ config.router.add_page(path='notify-example', content=test_page)
569
569
 
570
570
  ```
571
571
  """
@@ -600,7 +600,7 @@ class DownloadContentImpl(ActionImpl):
600
600
  )
601
601
 
602
602
 
603
- config.add_page(name='Download Content', content=test_page)
603
+ config.router.add_page(path='download-content', content=test_page)
604
604
 
605
605
  ```
606
606
  """
@@ -650,14 +650,14 @@ def DownloadContent(
650
650
 
651
651
 
652
652
  def test_page():
653
- return Stack(
654
- Button(
655
- 'Download File', onclick=DownloadContent(resolver=return_csv, extras=[my_var], cleanup_file=False)
656
- ),
657
- )
653
+ return Stack(
654
+ Button(
655
+ 'Download File', onclick=DownloadContent(resolver=return_csv, extras=[my_var], cleanup_file=False)
656
+ ),
657
+ )
658
658
 
659
659
 
660
- config.add_page(name='Download Content', content=test_page)
660
+ config.router.add_page(path='download-content', content=test_page)
661
661
 
662
662
  ```
663
663
  """
@@ -715,9 +715,9 @@ class DownloadVariable(ActionImpl):
715
715
  )
716
716
 
717
717
 
718
- config.add_page(name='Download Variable', content=test_page())
718
+ config.router.add_page(path='download-variable', content=test_page)
719
719
 
720
- ```
720
+ ```
721
721
  """
722
722
 
723
723
  variable: AnyVariable
@@ -764,7 +764,8 @@ def SideEffect(
764
764
  return Stack(Select(value=Variable(3), items=[3, 4, 5], onchange=SideEffect(side_effect, extras=[x, y, z])))
765
765
 
766
766
 
767
- config.add_page(name='SideEffect', content=test_page(), icon=get_icon('kiwi-bird'))
767
+ config.router.add_page(path='side-effect', content=test_page)
768
+
768
769
  ```
769
770
  """
770
771
 
@@ -937,7 +938,7 @@ class ActionCtx:
937
938
  )
938
939
 
939
940
 
940
- config.add_page(name='Trigger Variable', content=test_page())
941
+ config.router.add_page(path='trigger-variable', content=test_page)
941
942
 
942
943
  ```
943
944
 
@@ -983,8 +984,8 @@ class ActionCtx:
983
984
  )
984
985
 
985
986
 
986
- config.add_page(name='Test Page', content=test_page())
987
- config.add_page(name='Another Page', content=another_page())
987
+ config.router.add_page(path='test-page', content=test_page)
988
+ config.router.add_page(path='another-page', content=another_page)
988
989
 
989
990
  ```
990
991
 
@@ -1014,7 +1015,7 @@ class ActionCtx:
1014
1015
  return Stack(Button('Logout', onclick=logout()))
1015
1016
 
1016
1017
 
1017
- config.add_page(name='Logout Page', content=test_page())
1018
+ config.router.add_page(path='logout-page', content=test_page)
1018
1019
 
1019
1020
  ```
1020
1021
  """
@@ -1052,7 +1053,7 @@ class ActionCtx:
1052
1053
  )
1053
1054
 
1054
1055
 
1055
- config.add_page(name='Notify Example', content=test_page())
1056
+ config.router.add_page(path='notify-example', content=test_page)
1056
1057
 
1057
1058
  ```
1058
1059
 
@@ -1097,7 +1098,7 @@ class ActionCtx:
1097
1098
  )
1098
1099
 
1099
1100
 
1100
- config.add_page(name='ResetVariable', content=test_page(), icon=get_icon('shrimp'))
1101
+ config.router.add_page(path='reset-variable', content=test_page)
1101
1102
 
1102
1103
  ```
1103
1104
 
@@ -1141,7 +1142,7 @@ class ActionCtx:
1141
1142
  )
1142
1143
 
1143
1144
 
1144
- config.add_page(name='Download Content', content=test_page)
1145
+ config.router.add_page(path='download-content', content=test_page)
1145
1146
 
1146
1147
  ```
1147
1148
 
@@ -1180,7 +1181,7 @@ class ActionCtx:
1180
1181
  )
1181
1182
 
1182
1183
 
1183
- config.add_page(name='Download Variable', content=test_page())
1184
+ config.router.add_page(path='download-variable', content=test_page)
1184
1185
 
1185
1186
  ```
1186
1187
 
@@ -1229,7 +1230,7 @@ class ActionCtx:
1229
1230
  def task_page():
1230
1231
  return Stack(Text('Status display:'), Text(text=status), Button('Run', onclick=my_task()))
1231
1232
 
1232
- config.add_page(name='task', content=task_page())
1233
+ config.router.add_page(path='task', content=task_page)
1233
1234
  ```
1234
1235
 
1235
1236
  :param func: the function to run as a task
@@ -67,7 +67,7 @@ class SwitchVariable(ClientVariable):
67
67
  title='Admin Panel Demo'
68
68
  )
69
69
 
70
- config.add_page('Admin Demo', content=page_content)
70
+ config.router.add_page(path='admin', content=page_content)
71
71
  ```
72
72
 
73
73
  Value mapping with defaults:
@@ -109,7 +109,7 @@ class SwitchVariable(ClientVariable):
109
109
  title='Role Permissions'
110
110
  )
111
111
 
112
- config.add_page('Permissions Demo', content=page_content)
112
+ config.router.add_page(path='permissions', content=page_content)
113
113
  ```
114
114
 
115
115
  Complex conditions:
@@ -20,7 +20,8 @@ from __future__ import annotations
20
20
  import asyncio
21
21
  from collections.abc import Mapping
22
22
  from contextvars import ContextVar
23
- from typing import Any, Callable, Optional, Union
23
+ from functools import partial
24
+ from typing import Any, Callable, Literal, Optional, Union
24
25
 
25
26
  import anyio
26
27
 
@@ -42,13 +43,16 @@ from dara.core.logging import dev_logger
42
43
  CURRENT_ACTION_ID = ContextVar('current_action_id', default='')
43
44
 
44
45
 
45
- async def _execute_action(handler: Callable, ctx: ActionCtx, values: Mapping[str, Any]):
46
+ async def _execute_action(
47
+ handler: Callable, ctx: ActionCtx, values: Mapping[str, Any], _on_error: Literal['raise', 'notify'] = 'notify'
48
+ ):
46
49
  """
47
50
  Execute the action handler within the given action context, handling any exceptions that occur.
48
51
 
49
52
  :param handler: the action handler to execute
50
53
  :param ctx: the action context to use
51
54
  :param values: the resolved values to pass to the handler
55
+ :param _on_error: whether to raise or notify on errors
52
56
  """
53
57
  bound_arg = None
54
58
  kwarg_names = list(values.keys())
@@ -71,13 +75,18 @@ async def _execute_action(handler: Callable, ctx: ActionCtx, values: Mapping[str
71
75
  try:
72
76
  return await run_user_handler(handler, args=args, kwargs=parsed_values)
73
77
  except Exception as e:
74
- dev_logger.error('Error executing action', e)
75
- await ctx.notify('An error occurred while executing the action', 'Error', 'ERROR')
78
+ if _on_error == 'raise':
79
+ raise
80
+ elif _on_error == 'notify':
81
+ dev_logger.error('Error executing action', e)
82
+ await ctx.notify('An error occurred while executing the action', 'Error', 'ERROR')
76
83
  finally:
77
84
  await ctx._end_execution()
78
85
 
79
86
 
80
- async def _stream_action(handler: Callable, ctx: ActionCtx, **values: Mapping[str, Any]):
87
+ async def _stream_action(
88
+ handler: Callable, ctx: ActionCtx, _on_error: Literal['raise', 'notify'] = 'notify', **values: Mapping[str, Any]
89
+ ):
81
90
  """
82
91
  Run the action handler and stream the results to the frontend.
83
92
  Executes two tasks in parallel:
@@ -91,13 +100,73 @@ async def _stream_action(handler: Callable, ctx: ActionCtx, **values: Mapping[st
91
100
  try:
92
101
  async with anyio.create_task_group() as tg:
93
102
  # Execute the handler and a stream consumer in parallel
94
- tg.start_soon(_execute_action, handler, ctx, values)
103
+ tg.start_soon(partial(_execute_action, _on_error=_on_error), handler, ctx, values)
95
104
  tg.start_soon(ctx._handle_results)
96
105
  finally:
97
106
  # None is treated as a sentinel value to stop waiting for new actions to come in on the client
98
107
  await ctx._on_action(None)
99
108
 
100
109
 
110
+ async def execute_action_sync(
111
+ action_def: ActionResolverDef,
112
+ inp: Any,
113
+ values: Mapping[str, Any],
114
+ static_kwargs: Mapping[str, Any],
115
+ store: CacheStore,
116
+ task_mgr: TaskManager,
117
+ ):
118
+ """
119
+ Execute an action until completion.
120
+ Used for executing `on_load` route actions.
121
+
122
+ :param action_def: resolver definition
123
+ :param inp: input to the action
124
+ :param values: values from the frontend
125
+ :param static_kwargs: mapping of var names to current values for static arguments
126
+ :param store: store instance
127
+ :param task_mgr: task manager instance - task are not supported here but passed for compat
128
+ """
129
+ action = action_def.resolver
130
+ assert action is not None, 'Action resolver must be defined'
131
+
132
+ results = []
133
+
134
+ # Construct a context which handles action messages by accumulating them in an array
135
+ async def handle_action(act_impl: Optional[ActionImpl]):
136
+ if act_impl is not None:
137
+ results.append(act_impl)
138
+
139
+ ctx = ActionCtx(inp, handle_action)
140
+ ACTION_CONTEXT.set(ctx)
141
+
142
+ resolved_kwargs = {}
143
+
144
+ if values is not None:
145
+ annotations = action.__annotations__
146
+
147
+ async def _resolve_kwarg(val: Any, key: str):
148
+ typ = annotations.get(key)
149
+ val = await resolve_dependency(val, store, task_mgr)
150
+ resolved_kwargs[key] = deserialize(val, typ)
151
+
152
+ async with anyio.create_task_group() as tg:
153
+ for key, value in values.items():
154
+ tg.start_soon(_resolve_kwarg, value, key)
155
+
156
+ # Merge resolved dynamic kwargs with static kwargs received
157
+ resolved_kwargs = {**resolved_kwargs, **static_kwargs}
158
+
159
+ # Disallow tasks here
160
+ has_tasks = any(isinstance(extra, BaseTask) for extra in resolved_kwargs.values())
161
+ if has_tasks:
162
+ raise ValueError('This action does not support tasks')
163
+
164
+ # Run until completion, raising on errors
165
+ await _stream_action(action, ctx, _on_error='raise', **resolved_kwargs)
166
+
167
+ return results
168
+
169
+
101
170
  async def execute_action(
102
171
  action_def: ActionResolverDef,
103
172
  inp: Any,