dara-core 1.21.11__py3-none-any.whl → 1.21.13__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.
@@ -31,6 +31,7 @@ from typing import (
31
31
  ClassVar,
32
32
  Dict,
33
33
  List,
34
+ Literal,
34
35
  Optional,
35
36
  Tuple,
36
37
  Union,
@@ -438,6 +439,60 @@ class PendingTask(BaseTask):
438
439
  return self.result
439
440
 
440
441
 
442
+ class RouterPath(BaseModel):
443
+ path: Optional[str] = None
444
+ """
445
+ A URL pathname, beginning with '/'.
446
+ """
447
+
448
+ search: Optional[str] = None
449
+ """
450
+ A URL search string, beginning with '?'.
451
+ """
452
+
453
+ hash: Optional[str] = None
454
+ """
455
+ A URL hash string, beginning with '#'.
456
+ """
457
+
458
+
459
+ class NavigateOptions(BaseModel):
460
+ """
461
+ Options for the navigate action
462
+ """
463
+
464
+ replace: bool = False
465
+ """
466
+ Replaces the current entry in the history stack instead of pushing a new one.
467
+
468
+ ```
469
+ # with a history stack like this
470
+ A -> B
471
+
472
+ # normal link click pushes a new entry
473
+ A -> B -> C
474
+
475
+ # but with `replace`, B is replaced by C
476
+ A -> C
477
+ ```
478
+ """
479
+
480
+ relative: Literal['route', 'path'] = 'route'
481
+ """
482
+ Defines the relative path behavior for the link.
483
+
484
+ ```python
485
+ NavigateOptions(to='..') # default, relative='route'
486
+ NavigateOptions(to='..', relative='path')
487
+ ```
488
+
489
+ Consider a route hierarchy where a parent route pattern is "blog" and a child route pattern is "blog/:slug/edit".
490
+ - route — default, resolves the link relative to the route pattern. In the example above, a relative link of "..." will remove both :slug/edit segments back to "/blog".
491
+ - path — relative to the path so "..." will only remove one URL segment up to "/blog/:slug"
492
+ Note that index routes and layout routes do not have paths so they are not included in the relative path calculation.
493
+ """
494
+
495
+
441
496
  class AnnotatedAction(BaseModel):
442
497
  """
443
498
  Represents a single call to an @action-annotated action.
@@ -51,6 +51,8 @@ from dara.core.base_definitions import (
51
51
  ActionImpl,
52
52
  ActionResolverDef,
53
53
  AnnotatedAction,
54
+ NavigateOptions,
55
+ RouterPath,
54
56
  TaskProgressUpdate,
55
57
  )
56
58
  from dara.core.base_definitions import DaraBaseModel as BaseModel
@@ -372,9 +374,15 @@ class NavigateToImpl(ActionImpl):
372
374
 
373
375
  py_name = 'NavigateTo'
374
376
 
375
- url: Optional[str] = None
377
+ url: Union[str, RouterPath]
378
+
376
379
  new_tab: bool = False
377
380
 
381
+ options: Optional[NavigateOptions] = None
382
+ """
383
+ Options for relative navigations
384
+ """
385
+
378
386
 
379
387
  @deprecated('Use @action or `NavigateToImpl` for simple cases')
380
388
  def NavigateTo(
@@ -947,7 +955,9 @@ class ActionCtx:
947
955
  """
948
956
  return await TriggerVariable(variable=variable, force=force).execute(self)
949
957
 
950
- async def navigate(self, url: str, new_tab: bool = False):
958
+ async def navigate(
959
+ self, url: Union[str, RouterPath], new_tab: bool = False, options: Optional[NavigateOptions] = None
960
+ ):
951
961
  """
952
962
  Navigate to a given url
953
963
 
@@ -991,8 +1001,9 @@ class ActionCtx:
991
1001
 
992
1002
  :param url: the url to navigate to
993
1003
  :param new_tab: whether to open the url in a new tab, defaults to False
1004
+ :param options: options for relative navigations, defaults to None
994
1005
  """
995
- return await NavigateToImpl(url=url, new_tab=new_tab).execute(self)
1006
+ return await NavigateToImpl(url=url, new_tab=new_tab, options=options).execute(self)
996
1007
 
997
1008
  async def logout(self):
998
1009
  """
@@ -68,7 +68,7 @@ def store_in_shared_memory(content: Any) -> SharedMemoryPointer:
68
68
  data_size = len(pickled_args)
69
69
 
70
70
  shared_mem = SharedMemory(create=True, size=data_size)
71
-
71
+ assert shared_mem.buf
72
72
  shared_mem.buf[0:data_size] = pickled_args
73
73
  shared_mem.close()
74
74
 
@@ -88,6 +88,7 @@ def read_from_shared_memory(pointer: SharedMemoryPointer) -> Any:
88
88
 
89
89
  # Read from memory
90
90
  shared_mem = SharedMemory(name=shared_mem_name)
91
+ assert shared_mem.buf
91
92
  data = shared_mem.buf[:data_size]
92
93
 
93
94
  # Unpickle and deepcopy
@@ -19,7 +19,7 @@ import contextlib
19
19
  import inspect
20
20
  import math
21
21
  from collections.abc import Awaitable
22
- from typing import Any, Callable, Dict, List, Optional, Set, Union, overload
22
+ from typing import Any, Callable, Dict, List, Optional, Set, Union, cast, overload
23
23
 
24
24
  from anyio import (
25
25
  BrokenResourceError,
@@ -453,8 +453,8 @@ class TaskManager:
453
453
 
454
454
  # Notify channels that this specific task was cancelled
455
455
  if notify:
456
- await self._send_notification_for_pending_task(
457
- pending_task=pending_task,
456
+ await self._send_notification_for_task(
457
+ task=pending_task,
458
458
  messages=[{'status': 'CANCELED', 'task_id': task_id_to_cancel}],
459
459
  )
460
460
 
@@ -556,12 +556,14 @@ class TaskManager:
556
556
 
557
557
  return task_ids
558
558
 
559
- async def _multicast_notification(self, task_id: str, messages: List[dict]):
559
+ async def _multicast_notification(self, task_id: str, messages: List[dict], variable_task_id: bool = True):
560
560
  """
561
561
  Send notifications to all task IDs that are related to a given task
562
562
 
563
563
  :param task: the task the notifications are related to
564
564
  :param messages: List of message dictionaries to send to all related tasks
565
+ :param variable_task_id: whether the task_id in messages should be replaced with the task being notified
566
+ or if False, the task_id should be left as is
565
567
  """
566
568
  # prevent cancellation, we need the notifications to be sent
567
569
  with CancelScope(shield=True):
@@ -581,28 +583,33 @@ class TaskManager:
581
583
  if pending_task_id not in self.tasks:
582
584
  continue
583
585
  pending_task = self.tasks[pending_task_id]
584
- task_tg.start_soon(self._send_notification_for_pending_task, pending_task, messages)
586
+ task_tg.start_soon(self._send_notification_for_task, pending_task, messages, variable_task_id)
585
587
 
586
- async def _send_notification_for_pending_task(self, pending_task: PendingTask, messages: List[dict]):
588
+ async def _send_notification_for_task(self, task: BaseTask, messages: List[dict], variable_task_id: bool = True):
587
589
  """
588
590
  Send notifications for a specific PendingTask
589
591
 
590
592
  :param pending_task: The PendingTask to send notifications for
591
593
  :param messages: The messages to send
594
+ :param variable_task_id: whether the task_id in messages should be replaced with the task being notified
592
595
  """
593
596
  # Collect channels for this PendingTask
594
- channels_to_notify = set(pending_task.notify_channels)
595
- channels_to_notify.update(pending_task.task_def.notify_channels)
597
+ channels_to_notify = set(task.notify_channels)
598
+ if isinstance(task, PendingTask):
599
+ channels_to_notify.update(task.task_def.notify_channels)
596
600
 
597
601
  if not channels_to_notify:
598
602
  return
599
603
 
600
- # Send to all channels for this PendingTask in parallel
604
+ # Send to all channels for this Task in parallel
601
605
  async def _send_to_channel(channel: str):
602
606
  async with create_task_group() as channel_tg:
603
607
  for message in messages:
604
- # Create message with this PendingTask's task_id (if message has task_id)
605
- message_for_task = {**message, 'task_id': pending_task.task_id} if 'task_id' in message else message
608
+ if variable_task_id:
609
+ # Create message with this Task's task_id (if message has task_id)
610
+ message_for_task = {**message, 'task_id': task.task_id} if 'task_id' in message else message
611
+ else:
612
+ message_for_task = message
606
613
  channel_tg.start_soon(self.ws_manager.send_message, channel, message_for_task)
607
614
 
608
615
  async with create_task_group() as channel_tg:
@@ -633,13 +640,18 @@ class TaskManager:
633
640
  async for message in receive_stream:
634
641
  if isinstance(message, TaskProgressUpdate):
635
642
  # Send progress notifications to related tasks
636
- progress_message = {
637
- 'task_id': message.task_id, # Will be updated per task ID in multicast
638
- 'status': 'PROGRESS',
639
- 'progress': message.progress,
640
- 'message': message.message,
641
- }
642
- await self._multicast_notification(message.task_id, [progress_message])
643
+ await self._multicast_notification(
644
+ task_id=message.task_id,
645
+ messages=[
646
+ {
647
+ # Will be updated per task ID in multicast
648
+ 'task_id': message.task_id,
649
+ 'status': 'PROGRESS',
650
+ 'progress': message.progress,
651
+ 'message': message.message,
652
+ }
653
+ ],
654
+ )
643
655
  if isinstance(task, Task) and task.on_progress:
644
656
  await run_user_handler(task.on_progress, args=(message,))
645
657
  elif isinstance(message, TaskResult):
@@ -659,10 +671,17 @@ class TaskManager:
659
671
  # Set final result
660
672
  await self.set_result(message.task_id, message.result)
661
673
 
662
- # Notify all PendingTasks that depend on this specific task
674
+ # NOTE: this is still multicasting to all related channels to ensure
675
+ # all interested parties are notified, but notably we use `variable_task_id=False`
676
+ # to only notify about the task that actually completed,
677
+ # as opposed to e.g. progress, errors and cancellations which
678
+ # we 'bubble up' by updating the task_id in the message to the respective related task
663
679
  await self._multicast_notification(
664
680
  task_id=message.task_id,
665
- messages=[{'result': message.result, 'status': 'COMPLETE', 'task_id': message.task_id}],
681
+ messages=[
682
+ {'result': message.result, 'status': 'COMPLETE', 'task_id': message.task_id},
683
+ ],
684
+ variable_task_id=False,
666
685
  )
667
686
 
668
687
  # Remove the task from the registered tasks - it finished running
@@ -726,7 +745,8 @@ class TaskManager:
726
745
  eng_logger.info(f'TaskManager finished task {task.task_id}', {'result': result})
727
746
  finally:
728
747
  with CancelScope(shield=True):
729
- # pyright: ignore[reportUnreachable]
748
+ # cast explicitly as otherwise pyright thinks it's always None here
749
+ task_error = cast(Optional[ExceptionGroup], task_error)
730
750
  if task_error is not None:
731
751
  err = task_error
732
752
  # Mark pending task as failed
@@ -755,8 +775,8 @@ class TaskManager:
755
775
  error = get_error_for_channel(err)
756
776
  message = {'status': 'ERROR', 'task_id': task.task_id, 'error': error['error']}
757
777
  # Notify about this task failing, and a server broadcast error
758
- await self._send_notification_for_pending_task(
759
- pending_task=pending_task,
778
+ await self._send_notification_for_task(
779
+ task=pending_task,
760
780
  messages=[message, error],
761
781
  )
762
782
  # notify related tasks
@@ -1,27 +1,16 @@
1
1
  from typing import Annotated, Any, Literal, Optional, Union
2
2
 
3
- from pydantic import BaseModel, BeforeValidator
4
-
5
- from dara.core.definitions import ComponentInstance, JsComponentDef, StyledComponentInstance, transform_raw_css
3
+ from pydantic import BeforeValidator
4
+
5
+ from dara.core.base_definitions import RouterPath
6
+ from dara.core.definitions import (
7
+ ComponentInstance,
8
+ JsComponentDef,
9
+ StyledComponentInstance,
10
+ transform_raw_css,
11
+ )
6
12
  from dara.core.interactivity import ClientVariable
7
-
8
-
9
- class RouterPath(BaseModel):
10
- path: str
11
- """
12
- A URL pathname, beginning with '/'.
13
- """
14
-
15
- search: Optional[str] = None
16
- """
17
- A URL search string, beginning with '?'.
18
- """
19
-
20
- hash: Optional[str] = None
21
- """
22
- A URL hash string, beginning with '#'.
23
- """
24
-
13
+ from dara.core.visual.components import RawString
25
14
 
26
15
  OutletDef = JsComponentDef(name='Outlet', js_module='@darajs/core', py_module='dara.core')
27
16
 
@@ -52,8 +41,35 @@ class Navigate(ComponentInstance):
52
41
  to: Union[str, RouterPath, ClientVariable]
53
42
 
54
43
  replace: bool = False
44
+ """
45
+ Replaces the current entry in the history stack instead of pushing a new one.
46
+
47
+ ```
48
+ # with a history stack like this
49
+ A -> B
50
+
51
+ # normal link click pushes a new entry
52
+ A -> B -> C
53
+
54
+ # but with `replace`, B is replaced by C
55
+ A -> C
56
+ ```
57
+ """
55
58
 
56
59
  relative: Literal['route', 'path'] = 'route'
60
+ """
61
+ Defines the relative path behavior for the link.
62
+
63
+ ```python
64
+ Navigate(to='..') # default, relative='route'
65
+ Navigate(to='..', relative='path')
66
+ ```
67
+
68
+ Consider a route hierarchy where a parent route pattern is "blog" and a child route pattern is "blog/:slug/edit".
69
+ - route — default, resolves the link relative to the route pattern. In the example above, a relative link of "..." will remove both :slug/edit segments back to "/blog".
70
+ - path — relative to the path so "..." will only remove one URL segment up to "/blog/:slug"
71
+ Note that index routes and layout routes do not have paths so they are not included in the relative path calculation.
72
+ """
57
73
 
58
74
 
59
75
  LinkDef = JsComponentDef(name='Link', js_module='@darajs/core', py_module='dara.core')
@@ -121,15 +137,23 @@ class Link(StyledComponentInstance):
121
137
  Can be a string or RouterPath object
122
138
  """
123
139
 
140
+ # core anchor element attributes
141
+ target: Optional[str] = None
142
+ download: Optional[str] = None
143
+ rel: Optional[str] = None
144
+ referrer_policy: Optional[str] = None
145
+
124
146
  active_css: Annotated[Optional[Any], BeforeValidator(transform_raw_css)] = None
125
147
  inactive_css: Annotated[Optional[Any], BeforeValidator(transform_raw_css)] = None
126
148
 
127
- # TODO: add scroll restoration if it works?
128
-
129
- def __init__(self, *children: ComponentInstance, **kwargs):
149
+ def __init__(self, *children: Union[str, ComponentInstance], **kwargs):
130
150
  components = list(children)
131
151
  if 'children' not in kwargs:
132
152
  kwargs['children'] = components
153
+
154
+ if len(kwargs['children']) > 0:
155
+ kwargs['children'] = [RawString(content=x) if isinstance(x, str) else x for x in kwargs['children']]
156
+
133
157
  super().__init__(**kwargs)
134
158
 
135
159
 
@@ -51420,7 +51420,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
51420
51420
  }
51421
51421
  function matchRoutesImpl(routes, locationArg, basename, allowPartial) {
51422
51422
  let location2 = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
51423
- let pathname = stripBasename(location2.pathname || "/", basename);
51423
+ let pathname = stripBasename$1(location2.pathname || "/", basename);
51424
51424
  if (pathname == null) {
51425
51425
  return null;
51426
51426
  }
@@ -51712,7 +51712,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
51712
51712
  return value;
51713
51713
  }
51714
51714
  }
51715
- function stripBasename(pathname, basename) {
51715
+ function stripBasename$1(pathname, basename) {
51716
51716
  if (basename === "/") return pathname;
51717
51717
  if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
51718
51718
  return null;
@@ -51911,8 +51911,8 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
51911
51911
  reset: void 0,
51912
51912
  location: void 0
51913
51913
  };
51914
- var ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
51915
- var isAbsoluteUrl = (url) => ABSOLUTE_URL_REGEX.test(url);
51914
+ var ABSOLUTE_URL_REGEX$1 = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
51915
+ var isAbsoluteUrl = (url) => ABSOLUTE_URL_REGEX$1.test(url);
51916
51916
  var defaultMapRouteProperties = (route) => ({
51917
51917
  hasErrorBoundary: Boolean(route.hasErrorBoundary)
51918
51918
  });
@@ -53207,7 +53207,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
53207
53207
  const url = createBrowserURLImpl(location2, true);
53208
53208
  isDocumentReload = // Hard reload if it's an absolute URL to a new origin
53209
53209
  url.origin !== routerWindow.location.origin || // Hard reload if it's an absolute URL that does not match our basename
53210
- stripBasename(url.pathname, basename) == null;
53210
+ stripBasename$1(url.pathname, basename) == null;
53211
53211
  }
53212
53212
  if (isDocumentReload) {
53213
53213
  if (replace22) {
@@ -54311,7 +54311,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
54311
54311
  let path = resolveTo(
54312
54312
  to ? to : ".",
54313
54313
  getResolveToMatches(contextualMatches),
54314
- stripBasename(location2.pathname, basename) || location2.pathname,
54314
+ stripBasename$1(location2.pathname, basename) || location2.pathname,
54315
54315
  relative === "path"
54316
54316
  );
54317
54317
  if (to == null) {
@@ -55344,7 +55344,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
55344
55344
  if (isAbsoluteUrl(location2)) {
55345
55345
  let normalizedLocation = location2;
55346
55346
  let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
55347
- let isSameBasename = stripBasename(url.pathname, basename) != null;
55347
+ let isSameBasename = stripBasename$1(url.pathname, basename) != null;
55348
55348
  if (url.origin === currentUrl.origin && isSameBasename) {
55349
55349
  return url.pathname + url.search + url.hash;
55350
55350
  }
@@ -56438,11 +56438,11 @@ Please change the parent <Route path="${parentPath}"> to <Route path="${parentPa
56438
56438
  return shouldBlock({
56439
56439
  currentLocation: {
56440
56440
  ...currentLocation,
56441
- pathname: stripBasename(currentLocation.pathname, basename) || currentLocation.pathname
56441
+ pathname: stripBasename$1(currentLocation.pathname, basename) || currentLocation.pathname
56442
56442
  },
56443
56443
  nextLocation: {
56444
56444
  ...nextLocation,
56445
- pathname: stripBasename(nextLocation.pathname, basename) || nextLocation.pathname
56445
+ pathname: stripBasename$1(nextLocation.pathname, basename) || nextLocation.pathname
56446
56446
  },
56447
56447
  historyAction
56448
56448
  });
@@ -56905,7 +56905,7 @@ Please change the parent <Route path="${parentPath}"> to <Route path="${parentPa
56905
56905
  key = "default"
56906
56906
  } = locationProp;
56907
56907
  let locationContext = React__namespace.useMemo(() => {
56908
- let trailingPathname = stripBasename(pathname, basename);
56908
+ let trailingPathname = stripBasename$1(pathname, basename);
56909
56909
  if (trailingPathname == null) {
56910
56910
  return null;
56911
56911
  }
@@ -57216,7 +57216,7 @@ Please change the parent <Route path="${parentPath}"> to <Route path="${parentPa
57216
57216
  let body;
57217
57217
  if (isFormElement(target)) {
57218
57218
  let attr2 = target.getAttribute("action");
57219
- action = attr2 ? stripBasename(attr2, basename) : null;
57219
+ action = attr2 ? stripBasename$1(attr2, basename) : null;
57220
57220
  method = target.getAttribute("method") || defaultMethod;
57221
57221
  encType = getFormEncType(target.getAttribute("enctype")) || defaultEncType;
57222
57222
  formData = new FormData(target);
@@ -57228,7 +57228,7 @@ Please change the parent <Route path="${parentPath}"> to <Route path="${parentPa
57228
57228
  );
57229
57229
  }
57230
57230
  let attr2 = target.getAttribute("formaction") || form.getAttribute("action");
57231
- action = attr2 ? stripBasename(attr2, basename) : null;
57231
+ action = attr2 ? stripBasename$1(attr2, basename) : null;
57232
57232
  method = target.getAttribute("formmethod") || form.getAttribute("method") || defaultMethod;
57233
57233
  encType = getFormEncType(target.getAttribute("formenctype")) || getFormEncType(form.getAttribute("enctype")) || defaultEncType;
57234
57234
  formData = new FormData(form, target);
@@ -58255,7 +58255,7 @@ Please change the parent <Route path="${parentPath}"> to <Route path="${parentPa
58255
58255
  ) : reqUrl;
58256
58256
  if (url.pathname === "/") {
58257
58257
  url.pathname = `_root.${extension}`;
58258
- } else if (basename && stripBasename(url.pathname, basename) === "/") {
58258
+ } else if (basename && stripBasename$1(url.pathname, basename) === "/") {
58259
58259
  url.pathname = `${basename.replace(/\/$/, "")}/_root.${extension}`;
58260
58260
  } else {
58261
58261
  url.pathname = `${url.pathname.replace(/\/$/, "")}.${extension}`;
@@ -60069,7 +60069,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
60069
60069
  try {
60070
60070
  let currentUrl = new URL(window.location.href);
60071
60071
  let targetUrl = to.startsWith("//") ? new URL(currentUrl.protocol + to) : new URL(to);
60072
- let path = stripBasename(targetUrl.pathname, basename);
60072
+ let path = stripBasename$1(targetUrl.pathname, basename);
60073
60073
  if (targetUrl.origin === currentUrl.origin && path != null) {
60074
60074
  to = path + targetUrl.search + targetUrl.hash;
60075
60075
  } else {
@@ -60149,7 +60149,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
60149
60149
  toPathname = toPathname.toLowerCase();
60150
60150
  }
60151
60151
  if (nextLocationPathname && basename) {
60152
- nextLocationPathname = stripBasename(nextLocationPathname, basename) || nextLocationPathname;
60152
+ nextLocationPathname = stripBasename$1(nextLocationPathname, basename) || nextLocationPathname;
60153
60153
  }
60154
60154
  const endSlashPosition = toPathname !== "/" && toPathname.endsWith("/") ? toPathname.length - 1 : toPathname.length;
60155
60155
  let isActive = locationPathname === toPathname || !end2 && locationPathname.startsWith(toPathname) && locationPathname.charAt(endSlashPosition) === "/";
@@ -60541,7 +60541,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
60541
60541
  key = getKey(
60542
60542
  {
60543
60543
  ...location2,
60544
- pathname: stripBasename(location2.pathname, basename) || location2.pathname
60544
+ pathname: stripBasename$1(location2.pathname, basename) || location2.pathname
60545
60545
  },
60546
60546
  matches2
60547
60547
  );
@@ -60704,8 +60704,8 @@ import(${JSON.stringify(manifest.entry.module)});`;
60704
60704
  if (!vtContext.isTransitioning) {
60705
60705
  return false;
60706
60706
  }
60707
- let currentPath = stripBasename(vtContext.currentLocation.pathname, basename) || vtContext.currentLocation.pathname;
60708
- let nextPath = stripBasename(vtContext.nextLocation.pathname, basename) || vtContext.nextLocation.pathname;
60707
+ let currentPath = stripBasename$1(vtContext.currentLocation.pathname, basename) || vtContext.currentLocation.pathname;
60708
+ let nextPath = stripBasename$1(vtContext.nextLocation.pathname, basename) || vtContext.nextLocation.pathname;
60709
60709
  return matchPath(path.pathname, nextPath) != null || matchPath(path.pathname, currentPath) != null;
60710
60710
  }
60711
60711
  function StaticRouter({
@@ -62164,7 +62164,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
62164
62164
  function getSingleFetchRedirect(status, headers, basename) {
62165
62165
  let redirect2 = headers.get("Location");
62166
62166
  if (basename) {
62167
- redirect2 = stripBasename(redirect2, basename) || redirect2;
62167
+ redirect2 = stripBasename$1(redirect2, basename) || redirect2;
62168
62168
  }
62169
62169
  return {
62170
62170
  redirect: redirect2,
@@ -62291,19 +62291,19 @@ import(${JSON.stringify(manifest.entry.module)});`;
62291
62291
  let url = new URL(request2.url);
62292
62292
  let normalizedBasename = _build.basename || "/";
62293
62293
  let normalizedPath = url.pathname;
62294
- if (stripBasename(normalizedPath, normalizedBasename) === "/_root.data") {
62294
+ if (stripBasename$1(normalizedPath, normalizedBasename) === "/_root.data") {
62295
62295
  normalizedPath = normalizedBasename;
62296
62296
  } else if (normalizedPath.endsWith(".data")) {
62297
62297
  normalizedPath = normalizedPath.replace(/\.data$/, "");
62298
62298
  }
62299
- if (stripBasename(normalizedPath, normalizedBasename) !== "/" && normalizedPath.endsWith("/")) {
62299
+ if (stripBasename$1(normalizedPath, normalizedBasename) !== "/" && normalizedPath.endsWith("/")) {
62300
62300
  normalizedPath = normalizedPath.slice(0, -1);
62301
62301
  }
62302
62302
  let isSpaMode = getBuildTimeHeader(request2, "X-React-Router-SPA-Mode") === "yes";
62303
62303
  if (!_build.ssr) {
62304
62304
  let decodedPath = decodeURI(normalizedPath);
62305
62305
  if (normalizedBasename !== "/") {
62306
- let strippedPath = stripBasename(decodedPath, normalizedBasename);
62306
+ let strippedPath = stripBasename$1(decodedPath, normalizedBasename);
62307
62307
  if (strippedPath == null) {
62308
62308
  errorHandler(
62309
62309
  new ErrorResponseImpl(
@@ -74769,30 +74769,54 @@ body,
74769
74769
  const root = clientExports.createRoot(container);
74770
74770
  root.render(/* @__PURE__ */ React.createElement(Root, { daraData, queryClient, importers }));
74771
74771
  }
74772
- function isValidHttpUrl(url) {
74773
- if (url.startsWith("/")) {
74774
- return true;
74772
+ function getBasename() {
74773
+ if (window.dara.base_url !== "") {
74774
+ return new URL(window.dara.base_url, window.origin).pathname;
74775
74775
  }
74776
- let newUrl;
74777
- try {
74778
- newUrl = new URL(url);
74779
- } catch {
74780
- return false;
74776
+ return "/";
74777
+ }
74778
+ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
74779
+ function stripBasename(pathname, basename) {
74780
+ if (basename === "/") {
74781
+ return pathname;
74782
+ }
74783
+ if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
74784
+ return null;
74785
+ }
74786
+ const startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;
74787
+ const nextChar = pathname.charAt(startIndex);
74788
+ if (nextChar && nextChar !== "/") {
74789
+ return null;
74781
74790
  }
74782
- return newUrl.protocol === "http:" || newUrl.protocol === "https:";
74791
+ return pathname.slice(startIndex) || "/";
74783
74792
  }
74784
74793
  const NavigateTo = (ctx, actionImpl) => {
74785
- const isValidUrl = isValidHttpUrl(actionImpl.url);
74786
- if (!isValidUrl) {
74787
- throw new Error(`Invalid URL: ${actionImpl.url}`);
74788
- }
74789
- if (actionImpl.new_tab) {
74790
- window.open(actionImpl.url, actionImpl.new_tab ? "_blank" : void 0);
74791
- } else if (actionImpl.url.startsWith("/")) {
74792
- ctx.navigate(actionImpl.url);
74793
- } else {
74794
- window.location.href = actionImpl.url;
74794
+ const basename = getBasename();
74795
+ let isExternal = false;
74796
+ let to = actionImpl.url;
74797
+ if (typeof actionImpl.url === "string" && ABSOLUTE_URL_REGEX.test(actionImpl.url)) {
74798
+ try {
74799
+ const currentUrl = new URL(window.location.href);
74800
+ const targetUrl = new URL(actionImpl.url);
74801
+ const path = stripBasename(targetUrl.pathname, basename);
74802
+ if (targetUrl.origin === currentUrl.origin && path != null) {
74803
+ to = path + targetUrl.search + targetUrl.hash;
74804
+ } else {
74805
+ isExternal = true;
74806
+ }
74807
+ } catch {
74808
+ throw new Error(`Invalid URL: ${actionImpl.url}`);
74809
+ }
74810
+ }
74811
+ if (isExternal && typeof to === "string") {
74812
+ if (actionImpl.new_tab) {
74813
+ window.open(to, "_blank");
74814
+ } else {
74815
+ window.location.href = to;
74816
+ }
74817
+ return;
74795
74818
  }
74819
+ ctx.navigate(to, actionImpl.options);
74796
74820
  };
74797
74821
  const ResetVariables = (ctx, actionImpl) => {
74798
74822
  actionImpl.variables.filter(isVariable).forEach((variable) => {
@@ -98334,7 +98358,11 @@ body,
98334
98358
  inactiveStyle,
98335
98359
  onMouseMove: props.prefetch ? handleMouseMove : void 0,
98336
98360
  onFocus: props.prefetch ? handleFocus : void 0,
98337
- onTouchStart: props.prefetch ? handleTouchStart : void 0
98361
+ onTouchStart: props.prefetch ? handleTouchStart : void 0,
98362
+ download: props.download,
98363
+ referrerPolicy: props.referrer_policy,
98364
+ target: props.target,
98365
+ rel: props.rel
98338
98366
  },
98339
98367
  props.children.map((child, idx) => /* @__PURE__ */ React__namespace.createElement(DynamicComponent$1, { component: child, key: idx }))
98340
98368
  ));
@@ -98758,6 +98786,7 @@ body,
98758
98786
  exports.fetchTabularServerVariable = fetchTabularServerVariable;
98759
98787
  exports.fetchTaskResult = fetchTaskResult;
98760
98788
  exports.findFirstPath = findFirstPath;
98789
+ exports.getBasename = getBasename;
98761
98790
  exports.getComponentRegistryKey = getComponentRegistryKey;
98762
98791
  exports.getDeps = getDeps;
98763
98792
  exports.getFromPreloadCache = getFromPreloadCache;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dara-core
3
- Version: 1.21.11
3
+ Version: 1.21.13
4
4
  Summary: Dara Framework Core
5
5
  Home-page: https://dara.causalens.com/
6
6
  License: Apache-2.0
@@ -21,10 +21,10 @@ Requires-Dist: cachetools (>=5.0.0,<6.0.0)
21
21
  Requires-Dist: certifi (>=2024.7.4)
22
22
  Requires-Dist: click (==8.1.3)
23
23
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
24
- Requires-Dist: create-dara-app (==1.21.11)
24
+ Requires-Dist: create-dara-app (==1.21.13)
25
25
  Requires-Dist: croniter (>=1.0.15,<3.0.0)
26
26
  Requires-Dist: cryptography (>=42.0.4)
27
- Requires-Dist: dara-components (==1.21.11) ; extra == "all"
27
+ Requires-Dist: dara-components (==1.21.13) ; extra == "all"
28
28
  Requires-Dist: exceptiongroup (>=1.1.3,<2.0.0)
29
29
  Requires-Dist: fastapi (>=0.115.0,<0.116.0)
30
30
  Requires-Dist: fastapi_vite_dara (==0.4.0)
@@ -55,7 +55,7 @@ Description-Content-Type: text/markdown
55
55
 
56
56
  # Dara Application Framework
57
57
 
58
- <img src="https://github.com/causalens/dara/blob/v1.21.11/img/dara_light.svg?raw=true">
58
+ <img src="https://github.com/causalens/dara/blob/v1.21.13/img/dara_light.svg?raw=true">
59
59
 
60
60
  ![Master tests](https://github.com/causalens/dara/actions/workflows/tests.yml/badge.svg?branch=master)
61
61
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -100,7 +100,7 @@ source .venv/bin/activate
100
100
  dara start
101
101
  ```
102
102
 
103
- ![Dara App](https://github.com/causalens/dara/blob/v1.21.11/img/components_gallery.png?raw=true)
103
+ ![Dara App](https://github.com/causalens/dara/blob/v1.21.13/img/components_gallery.png?raw=true)
104
104
 
105
105
  Note: `pip` installation uses [PEP 660](https://peps.python.org/pep-0660/) `pyproject.toml`-based editable installs which require `pip >= 21.3` and `setuptools >= 64.0.0`. You can upgrade both with:
106
106
 
@@ -117,9 +117,9 @@ Explore some of our favorite apps - a great way of getting started and getting t
117
117
 
118
118
  | Dara App | Description |
119
119
  | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | ![Large Language Model](https://github.com/causalens/dara/blob/v1.21.11/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
121
- | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.21.11/img/plot_interactivity.png?raw=true) | Demonstrates how to enable the user to interact with plots, trigger actions based on clicks, mouse movements and other interactions with `Bokeh` or `Plotly` plots |
122
- | ![Graph Editor](https://github.com/causalens/dara/blob/v1.21.11/img/graph_viewer.png?raw=true) | Demonstrates how to use the `CausalGraphViewer` component to display your graphs or networks, customising the displayed information through colors and tooltips, and updating the page based on user interaction. |
120
+ | ![Large Language Model](https://github.com/causalens/dara/blob/v1.21.13/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
121
+ | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.21.13/img/plot_interactivity.png?raw=true) | Demonstrates how to enable the user to interact with plots, trigger actions based on clicks, mouse movements and other interactions with `Bokeh` or `Plotly` plots |
122
+ | ![Graph Editor](https://github.com/causalens/dara/blob/v1.21.13/img/graph_viewer.png?raw=true) | Demonstrates how to use the `CausalGraphViewer` component to display your graphs or networks, customising the displayed information through colors and tooltips, and updating the page based on user interaction. |
123
123
 
124
124
  Check out our [App Gallery](https://dara.causalens.com/gallery) for more inspiration!
125
125
 
@@ -146,9 +146,9 @@ And the supporting UI packages and tools.
146
146
  - `ui-utils` - miscellaneous utility functions
147
147
  - `ui-widgets` - widget components
148
148
 
149
- More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.21.11/CONTRIBUTING.md) file.
149
+ More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.21.13/CONTRIBUTING.md) file.
150
150
 
151
151
  ## License
152
152
 
153
- Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.21.11/LICENSE).
153
+ Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.21.13/LICENSE).
154
154
 
@@ -6,7 +6,7 @@ dara/core/auth/basic.py,sha256=8ebWpHx53ObkuzTHIKSRdtsmKq0R5v8RuQXhZcnpTsA,4933
6
6
  dara/core/auth/definitions.py,sha256=wTsFWzX7bGHTU_vxGW50pqcmB_AmhETIIUdGB5UrMBU,3467
7
7
  dara/core/auth/routes.py,sha256=0o5KXApRbkL0F5qFsarQk_cq5lbQ3QfIHR_mwLRgBEY,7217
8
8
  dara/core/auth/utils.py,sha256=iEWP5qwfH17Mi-5t3sP-DNMUrWN0oSRk96NhieY2Zw4,7334
9
- dara/core/base_definitions.py,sha256=ptc6OSx-qElWWSuZB4-06kWmnj6hekpKR47eeB36igQ,16915
9
+ dara/core/base_definitions.py,sha256=wDeXoKro7WcMB8RXImaAZrwbBYdhnN_jgd2s7eDDO2M,18386
10
10
  dara/core/cli.py,sha256=V__LAK3ozWGsVTEQHqvJwqSfpC3o6R76YGABJ-YVoSE,8185
11
11
  dara/core/configuration.py,sha256=caPebHNUmYqh9czEBwQiIDjGrH8ltsphayZICj1CRhc,23489
12
12
  dara/core/css.py,sha256=UkNZ6n7RDBLsHmpZvn_a3K2nAwxVggQJbQMKNbgXONA,1753
@@ -15,7 +15,7 @@ dara/core/defaults.py,sha256=y-PU4DhF3uEEfrFmlJnenM8jhnQDTDaOU5kuAYZE2gw,4740
15
15
  dara/core/definitions.py,sha256=I4TUJ6zu968T4PNmeqbC-u-DfiKay-4XxurLNASkDz0,18358
16
16
  dara/core/http.py,sha256=-OzbCHlYSnfVnw2492C_UmaWOI5oSONfWk03eEGxSRI,4761
17
17
  dara/core/interactivity/__init__.py,sha256=ZppwTvxaCsaAdf0qX6j1qTcmbj6wxVcbiaMZfwTVKtU,2679
18
- dara/core/interactivity/actions.py,sha256=ADLVhffplOcIWy7l74SrpZk8oCe3q12BJ9060rVDpfU,48170
18
+ dara/core/interactivity/actions.py,sha256=_guOKc5VkXH1J91-pdTeIQumCFHkve6G0ES5rFBm1vY,48478
19
19
  dara/core/interactivity/any_data_variable.py,sha256=GhT6lzCDRlQtYG2gNNUHq4th7K3RacqDfkjaq7o4mCc,317
20
20
  dara/core/interactivity/any_variable.py,sha256=b6aMOZZc9Q4IFPPjuIbFgqYdw52-6QRsxNRj5ohZeL0,13648
21
21
  dara/core/interactivity/client_variable.py,sha256=bFc4gmKKVfAvZHmv5Y2ME1VMTxSM_v_0lcmyRNTLOKk,2320
@@ -55,7 +55,7 @@ dara/core/internal/pool/__init__.py,sha256=pBbXE5GR3abVC9Lg3i0QxfdmsrBDMJUYAYb0S
55
55
  dara/core/internal/pool/channel.py,sha256=TbyIE-PnfzzsQYhl3INOs5UIHHbF_h9bMFne5FjbWlQ,4948
56
56
  dara/core/internal/pool/definitions.py,sha256=27dtsyHsztc7zgr2axMWCn1O9pBNI_VKByGxjvKHBGc,4649
57
57
  dara/core/internal/pool/task_pool.py,sha256=MqzUtoGEjeXdpCdTmBXQmQt582u0suiL5vwrsj1slhY,17780
58
- dara/core/internal/pool/utils.py,sha256=HoNlHy2V0VmX5Xm2cqsFbPIOV74E2MslzTFgyNRmiB0,5411
58
+ dara/core/internal/pool/utils.py,sha256=7M8A3lYfp3TVB6yPf-6-nnSr5j6TCqHzdT9I639lUS0,5470
59
59
  dara/core/internal/pool/worker.py,sha256=eBi3FS652goZxu9chaRT89eHDvqJW0aHjt8I5UgyESA,6808
60
60
  dara/core/internal/port_utils.py,sha256=3NN94CubNrIYQKmPM4SEwstY-UMqsbbe1M_JhyPcncA,1654
61
61
  dara/core/internal/registries.py,sha256=1lY9xrggiyUlM32pPyzFEfflW3_37QGOxTDrfWQ4CN4,3659
@@ -65,7 +65,7 @@ dara/core/internal/routing.py,sha256=El2rRuJODoTuZvyay1LSbxCX8IvGDos3dwXYn1RIHZs
65
65
  dara/core/internal/scheduler.py,sha256=rmj-NnHHToWBPuteKlf0RmKcav2Hz0Z9-l4uyrAGKSI,13069
66
66
  dara/core/internal/settings.py,sha256=DxRvXcelawaKemTcX6JIVIilibn0XO5Bd98dCXau4zg,3937
67
67
  dara/core/internal/store.py,sha256=Z4EUMwHR0DR-TVCL9csSGYE3toxh15iRpBP2c7MUgr8,6442
68
- dara/core/internal/tasks.py,sha256=LVRrcK_cuJ7fujSUb8S6xSkPLYmxrAcnXKxEvo7G3l0,34814
68
+ dara/core/internal/tasks.py,sha256=84-87FtxsoCp-SrhE3w-rcMuj-yiaNs2itPYt0n-0dQ,36098
69
69
  dara/core/internal/utils.py,sha256=nUnwy1BLW9HUQro9ASAYb-AlNJkbaT_bQ1bE2e1q1wo,9088
70
70
  dara/core/internal/websocket.py,sha256=rcipt5XY48akMuLHFdvUUIgNLqzJx7ovLeiPsYVMELM,21953
71
71
  dara/core/jinja/index.html,sha256=5Sq_FwXn0F4dOyRFprfoh_tn0GUq6_aLYyYdpzQLdeM,3312
@@ -90,10 +90,10 @@ dara/core/metrics/utils.py,sha256=inR1Ab5hmWZY2lsgGwLCQOEdyhCD9PumU52X2JbwlKY,22
90
90
  dara/core/persistence.py,sha256=ZUmfAjcTwk_oYyaulDrtpiCwIg8vaOw8uTZd2qT4nNo,19410
91
91
  dara/core/router/__init__.py,sha256=yGI_MgLQU37ircCtYVNjnhqCjWQxKd5amoNqvMyrhOs,121
92
92
  dara/core/router/compat.py,sha256=WAVzDcJFJOVoIQ5inplIhXD58TWsWwTTebTCqpG4nGs,3137
93
- dara/core/router/components.py,sha256=K6bj04_ZPJcU9JQjaxPJkbUeaHtA_jeJNEvCqQbDf3A,5090
93
+ dara/core/router/components.py,sha256=lnh8dCin6BunI0KILdua-zJZOuwbDbniLGb6eF_V9hM,6183
94
94
  dara/core/router/dependency_graph.py,sha256=AyjSk3DuvCgACrgpID4oSpms1X6GQJUbt-scY5X_LN4,2305
95
95
  dara/core/router/router.py,sha256=1r3rFGiftOXC6GP66hKqTdcVNDRJsZWBWvL74wNG4dA,29719
96
- dara/core/umd/dara.core.umd.cjs,sha256=QxIkXaGT8Otvw65YHp4yDAjv3oTEeoMKH-wSJm50Pg0,5151009
96
+ dara/core/umd/dara.core.umd.cjs,sha256=KXoJu_06chU79sKcrZPHoEvtC7o20QLfE98puoxlvcA,5152119
97
97
  dara/core/umd/style.css,sha256=yT3PKpi2sKI2-kQIF8xtVbTPQqgpK7-Ua7tfzDPuSsI,4095881
98
98
  dara/core/visual/__init__.py,sha256=QN0wbG9HPQ_vXh8BO8DnBXeYLIENVTNtRmYzZf1lx7c,577
99
99
  dara/core/visual/components/__init__.py,sha256=nmCsnMLXeZAjkhMYz-mIFodpVY-69IO1fvwwXbFlMQ4,2447
@@ -120,8 +120,8 @@ dara/core/visual/themes/__init__.py,sha256=aM4mgoIYo2neBSw5FRzswsht7PUKjLthiHLmF
120
120
  dara/core/visual/themes/dark.py,sha256=UQGDooOc8ric73eHs9E0ltYP4UCrwqQ3QxqN_fb4PwY,1942
121
121
  dara/core/visual/themes/definitions.py,sha256=5g83t24w8Ar51Cl9REBJfCU7_DtlashBQeUTKDg3D1M,2862
122
122
  dara/core/visual/themes/light.py,sha256=-Tviq8oEwGbdFULoDOqPuHO0UpAZGsBy8qFi0kAGolQ,1944
123
- dara_core-1.21.11.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
124
- dara_core-1.21.11.dist-info/METADATA,sha256=Sy6DKIN0id1OrPQEyUk8Z0sqIyzJpBHjueqTGx7BySg,7544
125
- dara_core-1.21.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
126
- dara_core-1.21.11.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
127
- dara_core-1.21.11.dist-info/RECORD,,
123
+ dara_core-1.21.13.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
124
+ dara_core-1.21.13.dist-info/METADATA,sha256=TrgbUFRw_sb5zVZux8BKLvN_iDKIhlg-gx3hBBepjfA,7544
125
+ dara_core-1.21.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
126
+ dara_core-1.21.13.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
127
+ dara_core-1.21.13.dist-info/RECORD,,