dara-core 1.14.0a2__py3-none-any.whl → 1.14.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/auth/routes.py CHANGED
@@ -40,7 +40,7 @@ from dara.core.auth.definitions import (
40
40
  AuthError,
41
41
  SessionRequestBody,
42
42
  )
43
- from dara.core.auth.utils import decode_token
43
+ from dara.core.auth.utils import cached_refresh_token, decode_token
44
44
  from dara.core.logging import dev_logger
45
45
 
46
46
  auth_router = APIRouter()
@@ -139,33 +139,18 @@ async def handle_refresh_token(
139
139
  ),
140
140
  )
141
141
 
142
- from dara.core.internal.registries import (
143
- auth_registry,
144
- utils_registry,
145
- websocket_registry,
146
- )
147
- from dara.core.internal.websocket import WebsocketManager
142
+ from dara.core.internal.registries import auth_registry
148
143
 
149
144
  auth_config: BaseAuthConfig = auth_registry.get('auth_config')
150
- ws_manager: WebsocketManager = utils_registry.get('WebsocketManager')
151
145
 
152
146
  try:
153
147
  # decode the old token ignoring expiry date
154
148
  old_token_data = decode_token(credentials.credentials, options={'verify_exp': False})
155
149
 
156
150
  # Refresh logic up to implementation - passing in old token data so session_id can be preserved
157
- session_token, refresh_token = auth_config.refresh_token(old_token_data, dara_refresh_token)
158
-
159
- # Notify the active websocket handlers (i.e. active connections, per each tab open)
160
- # so they can update the data in ContextVars
161
- async def notify_ws_connections():
162
- session_token_data = decode_token(session_token)
163
- channels = websocket_registry.get(old_token_data.session_id)
164
- for channel in channels:
165
- if handler := ws_manager.handlers.get(channel):
166
- await handler.update_token(session_token_data)
167
-
168
- background_tasks.add_task(notify_ws_connections)
151
+ session_token, refresh_token = await cached_refresh_token(
152
+ auth_config.refresh_token, old_token_data, dara_refresh_token
153
+ )
169
154
 
170
155
  # Using 'Strict' as it is only used for the refresh-token endpoint so cross-site requests are not expected
171
156
  response.set_cookie(
dara/core/auth/utils.py CHANGED
@@ -15,11 +15,13 @@ See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  """
17
17
 
18
+ import asyncio
18
19
  import uuid
19
20
  from datetime import datetime, timedelta, timezone
20
- from typing import List, Optional, Union
21
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
21
22
 
22
23
  import jwt
24
+ from anyio import to_thread
23
25
 
24
26
  from dara.core.auth.definitions import (
25
27
  EXPIRED_TOKEN_ERROR,
@@ -100,3 +102,120 @@ def get_user_data():
100
102
  )
101
103
 
102
104
  return user_data
105
+
106
+
107
+ class AsyncTokenRefreshCache:
108
+ """
109
+ An asynchronous cache for token refresh operations that handles concurrent requests
110
+ and provides time-based cache invalidation.
111
+
112
+ This cache is designed to prevent multiple simultaneous refresh attempts with the
113
+ same refresh token, while also providing a short-term cache to reduce unnecessary
114
+ token refreshes from multiple tabs/windows.
115
+ """
116
+
117
+ def __init__(self, ttl_seconds: int = 5):
118
+ self.cache: Dict[str, Tuple[Any, datetime]] = {}
119
+ self.locks: Dict[str, asyncio.Lock] = {}
120
+ self.locks_lock = asyncio.Lock()
121
+ self.ttl = timedelta(seconds=ttl_seconds)
122
+
123
+ async def _get_or_create_lock(self, key: str) -> asyncio.Lock:
124
+ """
125
+ Get an existing lock for the given key or create a new one if it doesn't exist.
126
+
127
+ This method is thread-safe and ensures that only one lock exists per key.
128
+
129
+ :param key: The key to get or create a lock for.
130
+ """
131
+
132
+ async with self.locks_lock:
133
+ if key not in self.locks:
134
+ self.locks[key] = asyncio.Lock()
135
+ return self.locks[key]
136
+
137
+ def _cleanup_old_entries(self):
138
+ """
139
+ Remove expired entries from both the cache and locks dictionaries.
140
+
141
+ This method is called before each cache access to prevent memory leaks
142
+ from accumulated expired entries.
143
+ """
144
+ current_time = datetime.now()
145
+ expired_keys = [key for key, (_, timestamp) in self.cache.items() if current_time - timestamp > self.ttl]
146
+ for key in expired_keys:
147
+ self.cache.pop(key, None)
148
+ # We can modify self.locks here because we're always under an async lock when calling this
149
+ self.locks.pop(key, None)
150
+
151
+ def get_cached_value(self, key: str) -> Tuple[Any, bool]:
152
+ """
153
+ Retrieve a value from the cache if it exists and hasn't expired.
154
+
155
+ :param key: The key to retrieve from the cache.
156
+ :return: A tuple containing the value and a boolean indicating whether the value was found.
157
+ """
158
+ self._cleanup_old_entries()
159
+ if key in self.cache:
160
+ value, timestamp = self.cache[key]
161
+ if datetime.now() - timestamp <= self.ttl:
162
+ return value, True
163
+ return None, False
164
+
165
+ def set_cached_value(self, key: str, value: Any):
166
+ """
167
+ Set a value in the cache with the current timestamp.
168
+
169
+ :param key: The key to set in the cache.
170
+ :param value: The value to set in the cache.
171
+ """
172
+ self.cache[key] = (value, datetime.now())
173
+
174
+ def clear(self):
175
+ """
176
+ Clear the cache and locks dictionaries.
177
+ """
178
+ self.cache.clear()
179
+ self.locks.clear()
180
+
181
+
182
+ token_refresh_cache = AsyncTokenRefreshCache(ttl_seconds=5)
183
+ """
184
+ Shared token refresh cache instance
185
+ """
186
+
187
+
188
+ async def cached_refresh_token(
189
+ do_refresh_token: Callable[[TokenData, str], Tuple[str, str]], old_token_data: TokenData, refresh_token: str
190
+ ):
191
+ """
192
+ A utility to run a token refresh method with caching to prevent multiple concurrent refreshes
193
+ and short-term caching to reduce unnecessary refreshes from multiple tabs/windows.
194
+
195
+ :param do_refresh_token: The function to perform the token refresh
196
+ :param old_token_data: The old token data
197
+ :param refresh_token: The refresh token to use
198
+ """
199
+ cache_key = refresh_token
200
+
201
+ # check for cache hit
202
+ cached_result, found = token_refresh_cache.get_cached_value(cache_key)
203
+ if found:
204
+ return cached_result
205
+
206
+ # cache miss, acquire lock so only one call for given refresh_token is allowed
207
+ lock = await token_refresh_cache._get_or_create_lock(cache_key)
208
+
209
+ async with lock:
210
+ # check cache again in case another call already refreshed the token while we were waiting
211
+ cached_result, found = token_refresh_cache.get_cached_value(cache_key)
212
+ if found:
213
+ return cached_result
214
+
215
+ # Run the refresh function
216
+ result = await to_thread.run_sync(do_refresh_token, old_token_data, refresh_token)
217
+
218
+ # update cache
219
+ token_refresh_cache.set_cached_value(cache_key, result)
220
+
221
+ return result
@@ -156,16 +156,6 @@ class WebSocketHandler:
156
156
  Stream containing messages to send to the client.
157
157
  """
158
158
 
159
- token_send_stream: MemoryObjectSendStream[TokenData]
160
- """
161
- Stream for sending token updates to the WS connection.
162
- """
163
-
164
- token_receive_stream: MemoryObjectReceiveStream[TokenData]
165
- """
166
- Stream for receiving token updates in the WS connection.
167
- """
168
-
169
159
  pending_responses: Dict[str, Tuple[Event, Optional[Any]]]
170
160
  """
171
161
  A map of pending responses from the client. The key is the message ID and the value is a tuple of the event to
@@ -180,35 +170,9 @@ class WebSocketHandler:
180
170
  self.receive_stream = receive_stream
181
171
  self.send_stream = send_stream
182
172
 
183
- token_send_stream, token_receive_stream = anyio.create_memory_object_stream[TokenData](math.inf)
184
- self.token_send_stream = token_send_stream
185
- self.token_receive_stream = token_receive_stream
186
-
187
173
  self.channel_id = channel_id
188
174
  self.pending_responses = {}
189
175
 
190
- async def update_token(self, token_data: TokenData):
191
- """
192
- Update the token for the client.
193
- Should be used if the token is refreshed or changed in some way
194
- so the live WS connection can update it's ContextVars accordingly
195
- and they're up to date in custom message handlers.
196
-
197
- :param token_data: The new token data
198
- """
199
- await self.token_send_stream.send(token_data)
200
-
201
- def get_token_update(self) -> Optional[TokenData]:
202
- """
203
- Get the latest token update for the client.
204
-
205
- :return: The latest token update
206
- """
207
- try:
208
- return self.token_receive_stream.receive_nowait()
209
- except Exception:
210
- return None
211
-
212
176
  async def send_message(self, message: ServerMessage):
213
177
  """
214
178
  Send a message to the client.
@@ -528,13 +492,15 @@ async def ws_handler(websocket: WebSocket, token: Optional[str] = Query(default=
528
492
  # as the latter does not properly handle disconnections e.g. when relaoading the server
529
493
  data = await websocket.receive_json()
530
494
 
531
- # update Auth context vars for the WS connection
532
- while new_token_data := handler.get_token_update():
533
- update_context(new_token_data)
534
-
535
495
  # Heartbeat to keep connection alive
536
496
  if data['type'] == 'ping':
537
497
  await websocket.send_json({'type': 'pong', 'message': None})
498
+ elif data['type'] == 'token_update':
499
+ try:
500
+ # update Auth context vars for the WS connection
501
+ update_context(decode_token(data['message']))
502
+ except Exception as e:
503
+ eng_logger.error('Error updating token data', error=e)
538
504
  else:
539
505
  try:
540
506
  parsed_data = parse_obj_as(ClientMessage, data)
@@ -30,14 +30,10 @@ var __privateWrapper = (obj, member, setter, getter) => ({
30
30
  return __privateGet(obj, member, getter);
31
31
  }
32
32
  });
33
- var __privateMethod = (obj, member, method) => {
34
- __accessCheck(obj, member, "access private method");
35
- return method;
36
- };
37
33
  (function(global2, factory) {
38
34
  typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("react"), require("@tanstack/react-query"), require("react-dom"), require("styled-components")) : typeof define === "function" && define.amd ? define(["exports", "react", "@tanstack/react-query", "react-dom", "styled-components"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory((global2.dara = global2.dara || {}, global2.dara.core = {}), global2.React, global2.ReactQuery, global2.ReactDOM, global2.styled));
39
35
  })(this, function(exports, React, reactQuery, ReactDOM, styled) {
40
- var _state, _locks, _subscribers, _notify, notify_fn, _pingInterval, _socketUrl, _reconnectCount, _events$, _parentBus, _instance, _observers;
36
+ var _locks, _subscribers, _pingInterval, _socketUrl, _reconnectCount, _events$, _parentBus, _instance, _observers;
41
37
  "use strict";
42
38
  const _interopDefaultLegacy = (e3) => e3 && typeof e3 === "object" && "default" in e3 ? e3 : { default: e3 };
43
39
  function _interopNamespace(e3) {
@@ -5095,7 +5091,7 @@ var __privateMethod = (obj, member, method) => {
5095
5091
  subscriptions.current.delete(key);
5096
5092
  }
5097
5093
  }, [subscriptions]);
5098
- const updateState = useCallback$1$1((_state2, key) => {
5094
+ const updateState = useCallback$1$1((_state, key) => {
5099
5095
  if (subscriptions.current.has(key)) {
5100
5096
  forceUpdate([]);
5101
5097
  }
@@ -5265,7 +5261,7 @@ var __privateMethod = (obj, member, method) => {
5265
5261
  return prevState.loadable.is(nextState.loadable) && prevState.key === nextState.key ? prevState : nextState;
5266
5262
  }, [getState2]);
5267
5263
  useEffect$3$1(() => {
5268
- const subscription = subscribeToRecoilValue$1(storeRef.current, recoilValue, (_state2) => {
5264
+ const subscription = subscribeToRecoilValue$1(storeRef.current, recoilValue, (_state) => {
5269
5265
  setState(updateState);
5270
5266
  }, componentName);
5271
5267
  setState(updateState);
@@ -5293,7 +5289,7 @@ var __privateMethod = (obj, member, method) => {
5293
5289
  useEffect$3$1(() => {
5294
5290
  const store2 = storeRef.current;
5295
5291
  const storeState = store2.getState();
5296
- const subscription = subscribeToRecoilValue$1(store2, recoilValue, (_state2) => {
5292
+ const subscription = subscribeToRecoilValue$1(store2, recoilValue, (_state) => {
5297
5293
  var _prevLoadableRef$curr;
5298
5294
  if (!Recoil_gkx("recoil_suppress_rerender_in_callback")) {
5299
5295
  return forceUpdate([]);
@@ -49400,7 +49396,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
49400
49396
  key: "render",
49401
49397
  value: function render2() {
49402
49398
  var _props = this.props, children = _props.children, className = _props.className, disableHeight = _props.disableHeight, disableWidth = _props.disableWidth, style = _props.style;
49403
- var _state2 = this.state, height = _state2.height, width = _state2.width;
49399
+ var _state = this.state, height = _state.height, width = _state.width;
49404
49400
  var outerStyle = { overflow: "visible" };
49405
49401
  var childParams = {};
49406
49402
  var bailoutOnChildren = false;
@@ -54958,16 +54954,12 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
54958
54954
  var cloneDeep_1 = cloneDeep;
54959
54955
  class GlobalStore {
54960
54956
  constructor() {
54961
- __privateAdd(this, _notify);
54962
- __privateAdd(this, _state, void 0);
54963
54957
  __privateAdd(this, _locks, void 0);
54964
54958
  __privateAdd(this, _subscribers, void 0);
54965
- __privateSet(this, _state, {});
54966
54959
  __privateSet(this, _locks, {});
54967
54960
  __privateSet(this, _subscribers, {});
54968
54961
  }
54969
54962
  clear() {
54970
- __privateSet(this, _state, {});
54971
54963
  __privateSet(this, _locks, {});
54972
54964
  __privateSet(this, _subscribers, {});
54973
54965
  }
@@ -54975,14 +54967,17 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
54975
54967
  if (__privateGet(this, _locks)[key]) {
54976
54968
  return __privateGet(this, _locks)[key];
54977
54969
  }
54978
- return __privateGet(this, _state)[key];
54970
+ return localStorage.getItem(key);
54979
54971
  }
54980
54972
  getValueSync(key) {
54981
- return __privateGet(this, _state)[key];
54973
+ return localStorage.getItem(key);
54982
54974
  }
54983
54975
  setValue(key, value) {
54984
- __privateGet(this, _state)[key] = value;
54985
- __privateMethod(this, _notify, notify_fn).call(this, key, value);
54976
+ if (value === null) {
54977
+ localStorage.removeItem(key);
54978
+ } else {
54979
+ localStorage.setItem(key, value);
54980
+ }
54986
54981
  }
54987
54982
  async replaceValue(key, fn) {
54988
54983
  if (__privateGet(this, _locks)[key]) {
@@ -55011,21 +55006,19 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
55011
55006
  if (!__privateGet(this, _subscribers)[key]) {
55012
55007
  __privateGet(this, _subscribers)[key] = [];
55013
55008
  }
55014
- __privateGet(this, _subscribers)[key].push(callback);
55009
+ const subFunc = (e3) => {
55010
+ if (e3.storageArea === localStorage && e3.key === key) {
55011
+ callback(e3.newValue);
55012
+ }
55013
+ };
55014
+ window.addEventListener("storage", subFunc);
55015
55015
  return () => {
55016
- __privateGet(this, _subscribers)[key] = __privateGet(this, _subscribers)[key].filter((cb) => cb !== callback);
55016
+ window.removeEventListener("storage", subFunc);
55017
55017
  };
55018
55018
  }
55019
55019
  }
55020
- _state = new WeakMap();
55021
55020
  _locks = new WeakMap();
55022
55021
  _subscribers = new WeakMap();
55023
- _notify = new WeakSet();
55024
- notify_fn = function(key, value) {
55025
- if (__privateGet(this, _subscribers)[key]) {
55026
- __privateGet(this, _subscribers)[key].forEach((cb) => cb(value));
55027
- }
55028
- };
55029
55022
  const store = new GlobalStore();
55030
55023
  class RequestExtrasSerializable {
55031
55024
  constructor(extras) {
@@ -55054,10 +55047,10 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
55054
55047
  return JSON.stringify(serializable);
55055
55048
  }
55056
55049
  }
55057
- async function request(url, options, extras) {
55050
+ async function request(url, ...options) {
55058
55051
  var _a, _b;
55059
- const sessionToken = await store.getValue("sessionToken");
55060
- const mergedOptions = extras ? { ...options, ...extras } : options;
55052
+ const sessionToken = await store.getValue(getTokenKey());
55053
+ const mergedOptions = options.reduce((acc, opt) => ({ ...acc, ...opt }), {});
55061
55054
  const { headers, ...other } = mergedOptions;
55062
55055
  const headersInterface = new Headers(headers);
55063
55056
  if (!headersInterface.has("Accept")) {
@@ -55077,7 +55070,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
55077
55070
  });
55078
55071
  if (response.status === 401 || response.status === 403) {
55079
55072
  try {
55080
- const refreshedToken = await store.replaceValue("sessionToken", async () => {
55073
+ const refreshedToken = await store.replaceValue(getTokenKey(), async () => {
55081
55074
  const refreshResponse = await fetch(`${baseUrl}/api/auth/refresh-token`, {
55082
55075
  headers: headersInterface,
55083
55076
  ...other,
@@ -56573,6 +56566,16 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
56573
56566
  );
56574
56567
  }
56575
56568
  }
56569
+ updateToken(newToken) {
56570
+ if (this.socket.readyState === WebSocket.OPEN) {
56571
+ this.socket.send(
56572
+ JSON.stringify({
56573
+ message: newToken,
56574
+ type: "token_update"
56575
+ })
56576
+ );
56577
+ }
56578
+ }
56576
56579
  sendCustomMessage(kind, data, awaitResponse = false) {
56577
56580
  if (this.socket.readyState === WebSocket.OPEN) {
56578
56581
  if (awaitResponse) {
@@ -58092,17 +58095,17 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
58092
58095
  }
58093
58096
  return resolver(getOrRegisterPlainVariable(variable, client, taskContext, extras));
58094
58097
  }
58095
- function tokenSubscribe(cb) {
58096
- return store.subscribe("sessionToken", cb);
58098
+ function onTokenChange(cb) {
58099
+ return store.subscribe(getTokenKey(), cb);
58097
58100
  }
58098
58101
  function getSessionToken() {
58099
- return store.getValueSync("sessionToken");
58102
+ return store.getValueSync(getTokenKey());
58100
58103
  }
58101
58104
  function setSessionToken(token) {
58102
- store.setValue("sessionToken", token);
58105
+ store.setValue(getTokenKey(), token);
58103
58106
  }
58104
58107
  function useSessionToken() {
58105
- return React__namespace.useSyncExternalStore(tokenSubscribe, getSessionToken);
58108
+ return React__namespace.useSyncExternalStore(onTokenChange, getSessionToken);
58106
58109
  }
58107
58110
  const STORE_EXTRAS_MAP = /* @__PURE__ */ new Map();
58108
58111
  function BackendStoreSync({ children }) {
@@ -59122,18 +59125,8 @@ Inferred class string: "${iconClasses}."`
59122
59125
  const isMounted = React.useRef(false);
59123
59126
  if (!isMounted.current) {
59124
59127
  isMounted.current = true;
59125
- store.setValue("sessionToken", getToken());
59128
+ setSessionToken(getToken());
59126
59129
  }
59127
- React.useEffect(() => {
59128
- return store.subscribe("sessionToken", (newToken) => {
59129
- const key = getTokenKey();
59130
- if (newToken) {
59131
- localStorage.setItem(key, newToken);
59132
- } else {
59133
- localStorage.removeItem(key);
59134
- }
59135
- });
59136
- }, []);
59137
59130
  if (isLoading) {
59138
59131
  return /* @__PURE__ */ React__default.default.createElement(Center, null, /* @__PURE__ */ React__default.default.createElement(DefaultFallback, null));
59139
59132
  }
@@ -60406,6 +60399,14 @@ Inferred class string: "${iconClasses}."`
60406
60399
  React.useEffect(() => {
60407
60400
  cleanSessionCache(token);
60408
60401
  }, [token]);
60402
+ React.useEffect(() => {
60403
+ if (!wsClient) {
60404
+ return;
60405
+ }
60406
+ return onTokenChange((newToken) => {
60407
+ wsClient.updateToken(newToken);
60408
+ });
60409
+ }, [wsClient]);
60409
60410
  React.useEffect(() => {
60410
60411
  if (config2 == null ? void 0 : config2.title) {
60411
60412
  document.title = config2.title;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dara-core
3
- Version: 1.14.0a2
3
+ Version: 1.14.1
4
4
  Summary: Dara Framework Core
5
5
  Home-page: https://dara.causalens.com/
6
6
  License: Apache-2.0
@@ -20,10 +20,10 @@ Requires-Dist: async-asgi-testclient (>=1.4.11,<2.0.0)
20
20
  Requires-Dist: certifi (>=2024.7.4)
21
21
  Requires-Dist: click (==8.1.3)
22
22
  Requires-Dist: colorama (>=0.4.6,<0.5.0)
23
- Requires-Dist: create-dara-app (==1.14.0-alpha.2)
23
+ Requires-Dist: create-dara-app (==1.14.1)
24
24
  Requires-Dist: croniter (>=1.0.15,<2.0.0)
25
25
  Requires-Dist: cryptography (>=42.0.4)
26
- Requires-Dist: dara-components (==1.14.0-alpha.2) ; extra == "all"
26
+ Requires-Dist: dara-components (==1.14.1) ; extra == "all"
27
27
  Requires-Dist: exceptiongroup (>=1.1.3,<2.0.0)
28
28
  Requires-Dist: fastapi (==0.109.0)
29
29
  Requires-Dist: fastapi-vite (==0.3.1)
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  # Dara Application Framework
53
53
 
54
- <img src="https://github.com/causalens/dara/blob/v1.14.0-alpha.2/img/dara_light.svg?raw=true">
54
+ <img src="https://github.com/causalens/dara/blob/v1.14.1/img/dara_light.svg?raw=true">
55
55
 
56
56
  ![Master tests](https://github.com/causalens/dara/actions/workflows/tests.yml/badge.svg?branch=master)
57
57
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -96,7 +96,7 @@ source .venv/bin/activate
96
96
  dara start
97
97
  ```
98
98
 
99
- ![Dara App](https://github.com/causalens/dara/blob/v1.14.0-alpha.2/img/components_gallery.png?raw=true)
99
+ ![Dara App](https://github.com/causalens/dara/blob/v1.14.1/img/components_gallery.png?raw=true)
100
100
 
101
101
  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:
102
102
 
@@ -113,9 +113,9 @@ Explore some of our favorite apps - a great way of getting started and getting t
113
113
 
114
114
  | Dara App | Description |
115
115
  | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
- | ![Large Language Model](https://github.com/causalens/dara/blob/v1.14.0-alpha.2/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
117
- | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.14.0-alpha.2/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 |
118
- | ![Graph Editor](https://github.com/causalens/dara/blob/v1.14.0-alpha.2/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. |
116
+ | ![Large Language Model](https://github.com/causalens/dara/blob/v1.14.1/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
117
+ | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.14.1/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 |
118
+ | ![Graph Editor](https://github.com/causalens/dara/blob/v1.14.1/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. |
119
119
 
120
120
  Check out our [App Gallery](https://dara.causalens.com/gallery) for more inspiration!
121
121
 
@@ -142,9 +142,9 @@ And the supporting UI packages and tools.
142
142
  - `ui-utils` - miscellaneous utility functions
143
143
  - `ui-widgets` - widget components
144
144
 
145
- More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.14.0-alpha.2/CONTRIBUTING.md) file.
145
+ More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.14.1/CONTRIBUTING.md) file.
146
146
 
147
147
  ## License
148
148
 
149
- Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.14.0-alpha.2/LICENSE).
149
+ Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.14.1/LICENSE).
150
150
 
@@ -4,8 +4,8 @@ dara/core/auth/__init__.py,sha256=H0bJoXff5wIRZmHvvQ3y9p5SXA9lM8OuLCGceYGqfb0,85
4
4
  dara/core/auth/base.py,sha256=jZNuCMoBHQcxWeLpTUzcxdbkbWUJ42jbtKgnnrwvNVA,3201
5
5
  dara/core/auth/basic.py,sha256=IMkoC1OeeRmnmjIqPHpybs8zSdbLlNKYLRvj08ajirg,4692
6
6
  dara/core/auth/definitions.py,sha256=fx-VCsElP9X97gM0Eql-4lFpLa0UryokmGZhQQat2NU,3511
7
- dara/core/auth/routes.py,sha256=x3oFLOkgi7-r0mqFD0GgM3hdS1JkmLHWQ6mNnEKRDDk,7951
8
- dara/core/auth/utils.py,sha256=ngOi5j71Xu-G59yWxGoejBEmMnVyGS67aF7czt_0i7A,3062
7
+ dara/core/auth/routes.py,sha256=k5y9G-mBDNM3sYX3rk1p1khZPKBD97rPdrs8PV2LSJs,7269
8
+ dara/core/auth/utils.py,sha256=_iyS_FExxlYZVDvnHJO5uhFpOW-g8YlhBH9zz8oPsPE,7314
9
9
  dara/core/base_definitions.py,sha256=r_W_qk6_VvvskbPEPjTF6xUh3o_lkNBWMFhN1Pic8Ks,14868
10
10
  dara/core/cli.py,sha256=ycTB7QHCB-74OnKnjXqkXq-GBqyjBqo7u4v1kTgv2jE,7656
11
11
  dara/core/configuration.py,sha256=8VynDds7a_uKXSpeNvjOUK3qfclg0WPniFEspL-6fi8,21153
@@ -60,7 +60,7 @@ dara/core/internal/settings.py,sha256=wAWxl-HXjq7PW3twe_CrR-UuMRw9VBudC3eRmevZAh
60
60
  dara/core/internal/store.py,sha256=qVyU7JfC3zE2vYC2mfjmvECWMlFS9b-nMF1k-alg4Y8,7756
61
61
  dara/core/internal/tasks.py,sha256=XK-GTIyge8RBYAfzNs3rmLYVNSKIarCzPdqRSVGg-4M,24728
62
62
  dara/core/internal/utils.py,sha256=b1YYkn8qHl6-GY6cCm2MS1NXRS9j_rElYCKMZOxJgrY,8232
63
- dara/core/internal/websocket.py,sha256=i6QXWHfcWbQDT3dk9F_7yw5CNhkC-lnqMoH7EKHBqC4,21676
63
+ dara/core/internal/websocket.py,sha256=KlEzIofyXWX8Axe4-mUILH1PO6hnK7webmYfDeCkvxc,20642
64
64
  dara/core/jinja/index.html,sha256=iykqiRh3H_HkcjHJeeSRXRu45nZ2y1sZX5FLdPRhlQY,726
65
65
  dara/core/jinja/index_autojs.html,sha256=MRF5J0vNfzZQm9kPEeLl23sbr08fVSRd_PAUD6Fkc_0,1253
66
66
  dara/core/js_tooling/custom_js_scaffold/index.tsx,sha256=FEzSV5o5Nyzxw6eXvGLi7BkEBkXf3brV34_7ATLnY7o,68
@@ -81,7 +81,7 @@ dara/core/metrics/cache.py,sha256=ybofUhZO0TCHeyhB_AtldWk1QTmTKh7GucTXpOkeTFA,25
81
81
  dara/core/metrics/runtime.py,sha256=YP-6Dz0GeI9_Yr7bUk_-OqShyFySGH_AKpDO126l6es,1833
82
82
  dara/core/metrics/utils.py,sha256=rYlBinxFc7VehFT5cTNXLk8gC74UEj7ZGq6vLgIDpSg,2247
83
83
  dara/core/persistence.py,sha256=TO94rPAN7jxZKVCC5YA4eE7GGDoNlCPe-BkkItV2VUE,10379
84
- dara/core/umd/dara.core.umd.js,sha256=yJvT54abv74o8fFuSoeH1NucuJVl90UFfeM3ZsCp3c8,4877860
84
+ dara/core/umd/dara.core.umd.js,sha256=h7NVytgGpX9sRWUhRa44Vai12T0VqF7t1nAjV_HAQGE,4877529
85
85
  dara/core/umd/style.css,sha256=YQtQ4veiSktnyONl0CU1iU1kKfcQhreH4iASi1MP7Ak,4095007
86
86
  dara/core/visual/__init__.py,sha256=QN0wbG9HPQ_vXh8BO8DnBXeYLIENVTNtRmYzZf1lx7c,577
87
87
  dara/core/visual/components/__init__.py,sha256=O-Em_glGdZNO0LLl2RWmJSrQiXKxliXg_PuhVXGT81I,1811
@@ -105,8 +105,8 @@ dara/core/visual/themes/__init__.py,sha256=aM4mgoIYo2neBSw5FRzswsht7PUKjLthiHLmF
105
105
  dara/core/visual/themes/dark.py,sha256=UQGDooOc8ric73eHs9E0ltYP4UCrwqQ3QxqN_fb4PwY,1942
106
106
  dara/core/visual/themes/definitions.py,sha256=m3oN0txs65MZepqjj7AKMMxybf2aq5fTjcTwJmHqEbk,2744
107
107
  dara/core/visual/themes/light.py,sha256=-Tviq8oEwGbdFULoDOqPuHO0UpAZGsBy8qFi0kAGolQ,1944
108
- dara_core-1.14.0a2.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
109
- dara_core-1.14.0a2.dist-info/METADATA,sha256=RLDwxR63x52m9wlBKPA8P5Eg8s8bKifiPIybYPcpTT0,7457
110
- dara_core-1.14.0a2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
111
- dara_core-1.14.0a2.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
112
- dara_core-1.14.0a2.dist-info/RECORD,,
108
+ dara_core-1.14.1.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
109
+ dara_core-1.14.1.dist-info/METADATA,sha256=9TBuOfX5YHGR7BfIrIulINEVpUmno2GoOzrV3upD0RM,7383
110
+ dara_core-1.14.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
111
+ dara_core-1.14.1.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
112
+ dara_core-1.14.1.dist-info/RECORD,,