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.
@@ -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 Annotated, Any, Dict, List, Optional
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 Body, Depends, FastAPI, HTTPException, Request
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 print_stacktrace, send_error_for_session
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 create_router, error_decorator
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 WS_CHANNEL, WebsocketManager
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
- route_map = config.router.to_route_map()
401
-
402
- class ActionPayload(BaseModel):
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:
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: F403
2
2
  from .compat import *
3
3
  from .components import *
4
+ from .dependency_graph import *
4
5
  from .router import *
@@ -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: not implemented yet
55
- # prefetch: Literal['none', 'intent', 'render', 'viewport'] = 'none'
56
- # """
57
- # Defines the data and module prefetching behavior for the link.
58
- # - none default, no prefetching
59
- # - intent prefetches when the user hovers or focuses the link
60
- # - render — prefetches when the link renders
61
- # - viewport — prefetches when the link is in the viewport, very useful for mobile
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)
@@ -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
- if not props.get('name'):
255
- props['name'] = self.get_name()
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=_execute_route_func(self.content, self.full_path), on_load=self.on_load, definition=self
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=_execute_route_func(self.content, self.full_path), on_load=self.on_load, definition=self
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=_execute_route_func(self.content, self.full_path), definition=self
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()