dara-core 1.21.0__py3-none-any.whl → 1.21.1__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/defaults.py +1 -3
- dara/core/definitions.py +1 -1
- dara/core/interactivity/plain_variable.py +6 -2
- dara/core/internal/routing.py +528 -319
- dara/core/internal/tasks.py +1 -1
- dara/core/main.py +8 -83
- dara/core/persistence.py +6 -2
- dara/core/router/__init__.py +1 -0
- dara/core/router/components.py +43 -9
- dara/core/router/dependency_graph.py +62 -0
- dara/core/router/router.py +24 -5
- dara/core/umd/dara.core.umd.cjs +61046 -57648
- dara/core/visual/components/__init__.py +0 -3
- dara/core/visual/components/menu_link.py +0 -35
- dara/core/visual/dynamic_component.py +1 -1
- {dara_core-1.21.0.dist-info → dara_core-1.21.1.dist-info}/METADATA +10 -10
- {dara_core-1.21.0.dist-info → dara_core-1.21.1.dist-info}/RECORD +20 -19
- {dara_core-1.21.0.dist-info → dara_core-1.21.1.dist-info}/LICENSE +0 -0
- {dara_core-1.21.0.dist-info → dara_core-1.21.1.dist-info}/WHEEL +0 -0
- {dara_core-1.21.0.dist-info → dara_core-1.21.1.dist-info}/entry_points.txt +0 -0
dara/core/internal/tasks.py
CHANGED
|
@@ -526,7 +526,7 @@ class TaskManager:
|
|
|
526
526
|
"""
|
|
527
527
|
# the result is not deleted, the results are kept in an LRU cache
|
|
528
528
|
# which will clean up older entries
|
|
529
|
-
return await self.store.get(TaskResultEntry, key=task_id)
|
|
529
|
+
return await self.store.get(TaskResultEntry, key=task_id, raise_for_missing=True)
|
|
530
530
|
|
|
531
531
|
async def set_result(self, task_id: str, value: Any):
|
|
532
532
|
"""
|
dara/core/main.py
CHANGED
|
@@ -20,27 +20,22 @@ import json
|
|
|
20
20
|
import os
|
|
21
21
|
import sys
|
|
22
22
|
import traceback
|
|
23
|
-
from collections.abc import Mapping
|
|
24
23
|
from concurrent.futures import ThreadPoolExecutor
|
|
25
24
|
from contextlib import asynccontextmanager
|
|
26
25
|
from importlib.util import find_spec
|
|
27
26
|
from inspect import iscoroutine
|
|
28
27
|
from pathlib import Path
|
|
29
|
-
from typing import
|
|
30
|
-
from urllib.parse import unquote
|
|
28
|
+
from typing import Optional
|
|
31
29
|
|
|
32
30
|
from anyio import create_task_group
|
|
33
|
-
from fastapi import
|
|
34
|
-
from fastapi import Path as PathParam
|
|
31
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
35
32
|
from fastapi.encoders import ENCODERS_BY_TYPE, jsonable_encoder
|
|
36
33
|
from fastapi.staticfiles import StaticFiles
|
|
37
34
|
from prometheus_client import start_http_server
|
|
38
|
-
from pydantic import BaseModel, Field
|
|
39
35
|
from starlette.responses import FileResponse
|
|
40
36
|
from starlette.templating import Jinja2Templates, _TemplateResponse
|
|
41
37
|
|
|
42
38
|
from dara.core.auth import auth_router
|
|
43
|
-
from dara.core.auth.routes import verify_session
|
|
44
39
|
from dara.core.configuration import Configuration, ConfigurationBuilder
|
|
45
40
|
from dara.core.defaults import (
|
|
46
41
|
blank_template,
|
|
@@ -51,30 +46,26 @@ from dara.core.defaults import (
|
|
|
51
46
|
from dara.core.internal.cache_store import CacheStore
|
|
52
47
|
from dara.core.internal.cgroup import get_cpu_count, set_memory_limit
|
|
53
48
|
from dara.core.internal.custom_response import CustomResponse
|
|
54
|
-
from dara.core.internal.devtools import
|
|
49
|
+
from dara.core.internal.devtools import send_error_for_session
|
|
55
50
|
from dara.core.internal.encoder_registry import encoder_registry
|
|
56
|
-
from dara.core.internal.execute_action import CURRENT_ACTION_ID, execute_action_sync
|
|
57
|
-
from dara.core.internal.normalization import NormalizedPayload, denormalize, normalize
|
|
58
51
|
from dara.core.internal.pool import TaskPool
|
|
59
52
|
from dara.core.internal.registries import (
|
|
60
53
|
action_def_registry,
|
|
61
|
-
action_registry,
|
|
62
54
|
auth_registry,
|
|
63
55
|
component_registry,
|
|
64
56
|
config_registry,
|
|
65
57
|
custom_ws_handlers_registry,
|
|
66
58
|
latest_value_registry,
|
|
67
59
|
sessions_registry,
|
|
68
|
-
static_kwargs_registry,
|
|
69
60
|
utils_registry,
|
|
70
61
|
websocket_registry,
|
|
71
62
|
)
|
|
72
63
|
from dara.core.internal.registry_lookup import RegistryLookup
|
|
73
|
-
from dara.core.internal.routing import
|
|
64
|
+
from dara.core.internal.routing import core_api_router, create_loader_route, error_decorator
|
|
74
65
|
from dara.core.internal.settings import get_settings
|
|
75
66
|
from dara.core.internal.tasks import TaskManager
|
|
76
67
|
from dara.core.internal.utils import enforce_sso, import_config
|
|
77
|
-
from dara.core.internal.websocket import
|
|
68
|
+
from dara.core.internal.websocket import WebsocketManager
|
|
78
69
|
from dara.core.js_tooling.js_utils import (
|
|
79
70
|
BuildCache,
|
|
80
71
|
BuildMode,
|
|
@@ -352,9 +343,6 @@ def _start_application(config: Configuration):
|
|
|
352
343
|
"""
|
|
353
344
|
return {'status': 'ok'}
|
|
354
345
|
|
|
355
|
-
# Register the core routes of the application
|
|
356
|
-
core_api_router = create_router(config)
|
|
357
|
-
|
|
358
346
|
# Start metrics server in a daemon thread
|
|
359
347
|
if os.environ.get('DARA_DISABLE_METRICS') != 'TRUE' and os.environ.get('DARA_TEST_FLAG', None) is None:
|
|
360
348
|
port = int(os.environ.get('DARA_METRICS_PORT', '10000'))
|
|
@@ -397,72 +385,9 @@ def _start_application(config: Configuration):
|
|
|
397
385
|
dev_logger.info('Registering pages:')
|
|
398
386
|
# TODO: convert this to use the logger?
|
|
399
387
|
config.router.print_route_tree()
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
uid: str
|
|
404
|
-
definition_uid: str
|
|
405
|
-
values: NormalizedPayload[Mapping[str, Any]]
|
|
406
|
-
|
|
407
|
-
class RouteDataRequestBody(BaseModel):
|
|
408
|
-
action_payloads: List[ActionPayload] = Field(default_factory=list)
|
|
409
|
-
ws_channel: Optional[str] = None
|
|
410
|
-
params: Dict[str, str] = Field(default_factory=dict)
|
|
411
|
-
|
|
412
|
-
@app.post('/api/core/route/{route_id}', dependencies=[Depends(verify_session)])
|
|
413
|
-
async def get_route_data(route_id: Annotated[str, PathParam()], body: Annotated[RouteDataRequestBody, Body()]):
|
|
414
|
-
# unquote route_id since it can be url-encoded
|
|
415
|
-
route_id = unquote(route_id)
|
|
416
|
-
|
|
417
|
-
# TODO: This will accept DV inputs etc and stream those back
|
|
418
|
-
route_data = route_map.get(route_id)
|
|
419
|
-
|
|
420
|
-
if route_data is None:
|
|
421
|
-
raise HTTPException(status_code=404, detail=f'Route {route_id} not found')
|
|
422
|
-
|
|
423
|
-
action_results: Dict[str, Any] = {}
|
|
424
|
-
|
|
425
|
-
if len(body.action_payloads) > 0:
|
|
426
|
-
store: CacheStore = utils_registry.get('Store')
|
|
427
|
-
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
428
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
429
|
-
|
|
430
|
-
# Ws channel can be null for top-level layouts rendered above the ws client
|
|
431
|
-
if body.ws_channel is not None:
|
|
432
|
-
WS_CHANNEL.set(body.ws_channel)
|
|
433
|
-
|
|
434
|
-
# Run actions in order to guarantee execution order
|
|
435
|
-
for action_payload in body.action_payloads:
|
|
436
|
-
action_def = await registry_mgr.get(action_registry, action_payload.definition_uid)
|
|
437
|
-
static_kwargs = await registry_mgr.get(static_kwargs_registry, action_payload.uid)
|
|
438
|
-
|
|
439
|
-
CURRENT_ACTION_ID.set(action_payload.uid)
|
|
440
|
-
values = denormalize(action_payload.values.data, action_payload.values.lookup)
|
|
441
|
-
try:
|
|
442
|
-
action_results[action_payload.uid] = await execute_action_sync(
|
|
443
|
-
action_def,
|
|
444
|
-
inp={'params': body.params, 'route': route_data.definition},
|
|
445
|
-
values=values,
|
|
446
|
-
static_kwargs=static_kwargs,
|
|
447
|
-
store=store,
|
|
448
|
-
task_mgr=task_mgr,
|
|
449
|
-
)
|
|
450
|
-
except BaseException as e:
|
|
451
|
-
assert route_data.definition is not None
|
|
452
|
-
route_path = route_data.definition.full_path
|
|
453
|
-
action_name = str(action_def.resolver)
|
|
454
|
-
raise HTTPException(
|
|
455
|
-
status_code=500,
|
|
456
|
-
detail={
|
|
457
|
-
'error': str(e),
|
|
458
|
-
'stacktrace': print_stacktrace(e),
|
|
459
|
-
'path': route_path,
|
|
460
|
-
'action_name': action_name,
|
|
461
|
-
},
|
|
462
|
-
) from e
|
|
463
|
-
|
|
464
|
-
normalized_template, lookup = normalize(jsonable_encoder(route_data.content))
|
|
465
|
-
return {'template': {'data': normalized_template, 'lookup': lookup}, 'action_results': action_results}
|
|
388
|
+
|
|
389
|
+
# Add the page data loader route
|
|
390
|
+
create_loader_route(config, app)
|
|
466
391
|
|
|
467
392
|
# Catch-all, must add after adding the last api route
|
|
468
393
|
@app.get('/api/{rest_of_path:path}')
|
dara/core/persistence.py
CHANGED
|
@@ -415,7 +415,7 @@ class BackendStore(PersistenceStore):
|
|
|
415
415
|
|
|
416
416
|
await self.backend.subscribe(_on_value)
|
|
417
417
|
|
|
418
|
-
async def write_partial(self, data: Union[List[Dict[str, Any]], Any], notify: bool = True):
|
|
418
|
+
async def write_partial(self, data: Union[List[Dict[str, Any]], Any], notify: bool = True, in_place: bool = False):
|
|
419
419
|
"""
|
|
420
420
|
Apply partial updates to the store using JSON Patch operations or automatic diffing.
|
|
421
421
|
|
|
@@ -424,6 +424,10 @@ class BackendStore(PersistenceStore):
|
|
|
424
424
|
|
|
425
425
|
:param data: Either a list of JSON patch operations (RFC 6902) or a full object to diff against current value
|
|
426
426
|
:param notify: whether to broadcast the patches to clients
|
|
427
|
+
:param in_place: whether to apply the patches in-place or return a new value.
|
|
428
|
+
When set to True, the value will be mutated when applying the patches rather than deep-cloned.
|
|
429
|
+
This is recommended when the updated value is large and deep-cloning it can be expensive; however, users should exercise
|
|
430
|
+
caution when using the option as previous results retrieved from `store.read()` will potentially be mutated, depending on the backend used.
|
|
427
431
|
"""
|
|
428
432
|
if self.readonly:
|
|
429
433
|
raise ValueError('Cannot write to a read-only store')
|
|
@@ -451,7 +455,7 @@ class BackendStore(PersistenceStore):
|
|
|
451
455
|
|
|
452
456
|
# Apply patches to current value
|
|
453
457
|
try:
|
|
454
|
-
updated_value = jsonpatch.apply_patch(current_value, patches)
|
|
458
|
+
updated_value = jsonpatch.apply_patch(current_value, patches, in_place=in_place)
|
|
455
459
|
except (jsonpatch.InvalidJsonPatch, jsonpatch.JsonPatchException) as e:
|
|
456
460
|
raise ValueError(f'Invalid JSON patch operation: {e}') from e
|
|
457
461
|
else:
|
dara/core/router/__init__.py
CHANGED
dara/core/router/components.py
CHANGED
|
@@ -51,15 +51,14 @@ class Link(StyledComponentInstance):
|
|
|
51
51
|
- with `end=True`, be considered inactive because of the missing '123' part
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
|
-
# TODO:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# """
|
|
54
|
+
# TODO: consider making it True by default in Dara v2
|
|
55
|
+
prefetch: bool = False
|
|
56
|
+
"""
|
|
57
|
+
Whether to prefetch the navigation data when user intends to navigate to the link.
|
|
58
|
+
When set to True, whenever the user hovers or focuses the link, the navigation data will be prefetched
|
|
59
|
+
and cached for a short period of time to speed up navigation.
|
|
60
|
+
Defaults to False.
|
|
61
|
+
"""
|
|
63
62
|
|
|
64
63
|
relative: Literal['route', 'path'] = 'route'
|
|
65
64
|
"""
|
|
@@ -107,3 +106,38 @@ class Link(StyledComponentInstance):
|
|
|
107
106
|
if 'children' not in kwargs:
|
|
108
107
|
kwargs['children'] = components
|
|
109
108
|
super().__init__(**kwargs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
MenuLinkDef = JsComponentDef(name='MenuLink', js_module='@darajs/core', py_module='dara.core')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MenuLink(Link):
|
|
115
|
+
"""
|
|
116
|
+
Styled version of Link component, ready to be used with e.g. the built-in SideBarFrame component.
|
|
117
|
+
Accepts all the same props as the Link component, can be used as a drop-in replacement.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from dara.core.visual.components import MenuLink, SideBarFrame
|
|
121
|
+
from dara.core.router import Router, Outlet
|
|
122
|
+
|
|
123
|
+
router = Router()
|
|
124
|
+
|
|
125
|
+
def RootLayout():
|
|
126
|
+
routes = router.get_navigable_routes()
|
|
127
|
+
|
|
128
|
+
return SideBarFrame(
|
|
129
|
+
side_bar=Stack(
|
|
130
|
+
*[MenuLink(Text(path['name']), to=path['path']) for path in routes],
|
|
131
|
+
),
|
|
132
|
+
content=Outlet(),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
root = router.add_layout(content=RootLayout)
|
|
136
|
+
```
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, *children: ComponentInstance, **kwargs):
|
|
140
|
+
els = list(children)
|
|
141
|
+
if 'children' not in kwargs:
|
|
142
|
+
kwargs['children'] = els
|
|
143
|
+
super().__init__(**kwargs)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, SerializeAsAny
|
|
4
|
+
|
|
5
|
+
from dara.core.definitions import ComponentInstance
|
|
6
|
+
from dara.core.interactivity.derived_variable import DerivedVariable
|
|
7
|
+
from dara.core.visual.dynamic_component import PyComponentInstance
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependencyGraph(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Data structure representing dependencies for derived state on a page
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
derived_variables: SerializeAsAny[Dict[str, DerivedVariable]] = Field(default_factory=dict)
|
|
16
|
+
"""
|
|
17
|
+
Map of DerivedVariable instances
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
py_components: SerializeAsAny[Dict[str, PyComponentInstance]] = Field(default_factory=dict)
|
|
21
|
+
"""
|
|
22
|
+
Map of PyComponentInstance instances
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def from_component(component: ComponentInstance) -> 'DependencyGraph':
|
|
27
|
+
"""
|
|
28
|
+
Create a DependencyGraph from a ComponentInstance
|
|
29
|
+
"""
|
|
30
|
+
graph = DependencyGraph()
|
|
31
|
+
_analyze_component_dependencies(component, graph)
|
|
32
|
+
return graph
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _analyze_component_dependencies(component: ComponentInstance, graph: DependencyGraph) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Recursively analyze a component tree to build a dependency graph of DerivedVariables and PyComponentInstances.
|
|
38
|
+
"""
|
|
39
|
+
# The component itself is a PyComponentInstance
|
|
40
|
+
if isinstance(component, PyComponentInstance):
|
|
41
|
+
if component.uid not in graph.py_components:
|
|
42
|
+
graph.py_components[component.uid] = component
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# otherwise check each field
|
|
46
|
+
for attr in component.model_fields_set:
|
|
47
|
+
value = getattr(component, attr, None)
|
|
48
|
+
|
|
49
|
+
# Handle encountered variables and py_components
|
|
50
|
+
if isinstance(value, DerivedVariable) and value.uid not in graph.derived_variables:
|
|
51
|
+
graph.derived_variables[value.uid] = value
|
|
52
|
+
elif isinstance(value, PyComponentInstance) and value.uid not in graph.py_components:
|
|
53
|
+
graph.py_components[value.uid] = value
|
|
54
|
+
# Recursion cases:
|
|
55
|
+
# component instances
|
|
56
|
+
elif isinstance(value, ComponentInstance):
|
|
57
|
+
_analyze_component_dependencies(value, graph)
|
|
58
|
+
# component lists
|
|
59
|
+
elif isinstance(value, list):
|
|
60
|
+
for item in value:
|
|
61
|
+
if isinstance(item, ComponentInstance):
|
|
62
|
+
_analyze_component_dependencies(item, graph)
|
dara/core/router/router.py
CHANGED
|
@@ -20,6 +20,8 @@ from dara.core.definitions import ComponentInstance
|
|
|
20
20
|
from dara.core.interactivity import Variable
|
|
21
21
|
from dara.core.persistence import PersistenceStore # noqa: F401
|
|
22
22
|
|
|
23
|
+
from .dependency_graph import DependencyGraph
|
|
24
|
+
|
|
23
25
|
# Matches :param or :param? (captures the name without the colon)
|
|
24
26
|
PARAM_REGEX = re.compile(r':([\w-]+)\??')
|
|
25
27
|
|
|
@@ -86,6 +88,7 @@ class RouteData(BaseModel):
|
|
|
86
88
|
content: Optional[ComponentInstance] = None
|
|
87
89
|
on_load: Optional[Action] = None
|
|
88
90
|
definition: Optional['BaseRoute'] = None
|
|
91
|
+
dependency_graph: Optional[DependencyGraph] = Field(default=None, exclude=True)
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
class BaseRoute(BaseModel):
|
|
@@ -251,8 +254,10 @@ class BaseRoute(BaseModel):
|
|
|
251
254
|
props['__typename'] = self.__class__.__name__
|
|
252
255
|
props['full_path'] = self.full_path
|
|
253
256
|
props['id'] = quote(self.get_identifier())
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
props['name'] = self.get_name()
|
|
258
|
+
|
|
259
|
+
assert self.compiled_data is not None
|
|
260
|
+
props['dependency_graph'] = self.compiled_data.dependency_graph
|
|
256
261
|
return props
|
|
257
262
|
|
|
258
263
|
|
|
@@ -484,8 +489,13 @@ class IndexRoute(BaseRoute):
|
|
|
484
489
|
|
|
485
490
|
def compile(self):
|
|
486
491
|
super().compile()
|
|
492
|
+
content = _execute_route_func(self.content, self.full_path)
|
|
493
|
+
|
|
494
|
+
# Analyze component dependencies
|
|
495
|
+
dependency_graph = DependencyGraph.from_component(content)
|
|
496
|
+
|
|
487
497
|
self.compiled_data = RouteData(
|
|
488
|
-
content=
|
|
498
|
+
content=content, on_load=self.on_load, definition=self, dependency_graph=dependency_graph
|
|
489
499
|
)
|
|
490
500
|
|
|
491
501
|
|
|
@@ -505,8 +515,13 @@ class PageRoute(BaseRoute, HasChildRoutes):
|
|
|
505
515
|
|
|
506
516
|
def compile(self):
|
|
507
517
|
super().compile()
|
|
518
|
+
content = _execute_route_func(self.content, self.full_path)
|
|
519
|
+
|
|
520
|
+
# Analyze component dependencies
|
|
521
|
+
dependency_graph = DependencyGraph.from_component(content)
|
|
522
|
+
|
|
508
523
|
self.compiled_data = RouteData(
|
|
509
|
-
content=
|
|
524
|
+
content=content, on_load=self.on_load, definition=self, dependency_graph=dependency_graph
|
|
510
525
|
)
|
|
511
526
|
for child in self.children:
|
|
512
527
|
child.compile()
|
|
@@ -550,8 +565,12 @@ class LayoutRoute(BaseRoute, HasChildRoutes):
|
|
|
550
565
|
|
|
551
566
|
def compile(self):
|
|
552
567
|
super().compile()
|
|
568
|
+
|
|
569
|
+
content = _execute_route_func(self.content, self.full_path)
|
|
570
|
+
dependency_graph = DependencyGraph.from_component(content)
|
|
571
|
+
|
|
553
572
|
self.compiled_data = RouteData(
|
|
554
|
-
on_load=self.on_load, content=
|
|
573
|
+
on_load=self.on_load, content=content, definition=self, dependency_graph=dependency_graph
|
|
555
574
|
)
|
|
556
575
|
for child in self.children:
|
|
557
576
|
child.compile()
|