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 +8 -42
- dara/core/configuration.py +33 -4
- dara/core/defaults.py +9 -0
- dara/core/definitions.py +21 -34
- dara/core/interactivity/actions.py +29 -28
- dara/core/interactivity/switch_variable.py +2 -2
- dara/core/internal/execute_action.py +75 -6
- dara/core/internal/routing.py +9 -46
- dara/core/jinja/index.html +97 -1
- dara/core/jinja/index_autojs.html +116 -10
- dara/core/js_tooling/js_utils.py +35 -14
- dara/core/main.py +212 -89
- dara/core/router/__init__.py +4 -0
- dara/core/router/compat.py +77 -0
- dara/core/router/components.py +109 -0
- dara/core/router/router.py +868 -0
- dara/core/umd/{dara.core.umd.js → dara.core.umd.cjs} +30052 -17828
- dara/core/umd/style.css +52 -9
- dara/core/visual/components/__init__.py +19 -11
- dara/core/visual/components/menu.py +4 -0
- dara/core/visual/components/menu_link.py +36 -0
- dara/core/visual/components/powered_by_causalens.py +9 -0
- dara/core/visual/components/sidebar_frame.py +1 -0
- {dara_core-1.20.3.dist-info → dara_core-1.21.0.dist-info}/METADATA +10 -10
- {dara_core-1.20.3.dist-info → dara_core-1.21.0.dist-info}/RECORD +28 -22
- {dara_core-1.20.3.dist-info → dara_core-1.21.0.dist-info}/LICENSE +0 -0
- {dara_core-1.20.3.dist-info → dara_core-1.21.0.dist-info}/WHEEL +0 -0
- {dara_core-1.20.3.dist-info → dara_core-1.21.0.dist-info}/entry_points.txt +0 -0
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
|
|
25
|
-
from dara.core.css import
|
|
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.
|
|
29
|
-
from dara.core.visual.
|
|
30
|
-
from dara.core.visual.
|
|
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
|
dara/core/configuration.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
368
|
-
config.add_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(
|
|
421
|
-
config.add_page(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
987
|
-
config.add_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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
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('
|
|
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
|
|
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(
|
|
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
|
-
|
|
75
|
-
|
|
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(
|
|
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,
|