dara-core 1.16.12__py3-none-any.whl → 1.16.14__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/persistence.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import abc
2
2
  import json
3
3
  import os
4
- from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Set
4
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Literal, Optional, Set
5
5
  from uuid import uuid4
6
6
 
7
7
  import aiorwlock
@@ -18,16 +18,16 @@ from pydantic import (
18
18
  from dara.core.auth.definitions import USER
19
19
  from dara.core.internal.utils import run_user_handler
20
20
  from dara.core.internal.websocket import WS_CHANNEL
21
+ from dara.core.logging import dev_logger
21
22
 
22
23
  if TYPE_CHECKING:
23
24
  from dara.core.interactivity.plain_variable import Variable
25
+ from dara.core.internal.websocket import WebsocketManager
24
26
 
25
27
 
26
28
  class PersistenceBackend(BaseModel, abc.ABC):
27
29
  """
28
- Abstract base class for a BackendStore backend
29
-
30
- Warning: the API is not stable yet and may change in the future
30
+ Abstract base class for a BackendStore backend.
31
31
  """
32
32
 
33
33
  @abc.abstractmethod
@@ -60,6 +60,12 @@ class PersistenceBackend(BaseModel, abc.ABC):
60
60
  Get all the values as a dictionary of key-value pairs
61
61
  """
62
62
 
63
+ async def subscribe(self, on_value: Callable[[str, Any], Awaitable[None]]):
64
+ """
65
+ Subscribe to changes in the backend. Called with a callback that should be invoked whenever a value is updated.
66
+ """
67
+ # Default implementation does nothing, not all backends need to support this
68
+
63
69
 
64
70
  class InMemoryBackend(PersistenceBackend):
65
71
  """
@@ -173,17 +179,23 @@ class BackendStore(PersistenceStore):
173
179
  :param backend: the backend to use for storing data; defaults to an in-memory backend
174
180
  :param scope: the scope for the store; if 'global' a single value is stored for all users,
175
181
  if 'user' a value is stored per user
182
+ :param readonly: whether to use the backend in read-only mode, i.e. skip syncing values from client to backend and raise if write()/delete() is called
176
183
  """
177
184
 
178
185
  uid: str = Field(default_factory=lambda: str(uuid4()))
179
186
  backend: PersistenceBackend = Field(default_factory=InMemoryBackend, exclude=True)
180
187
  scope: Literal['global', 'user'] = 'global'
188
+ readonly: bool = False
181
189
 
182
190
  default_value: Any = Field(default=None, exclude=True)
183
191
  initialized_scopes: Set[str] = Field(default_factory=set, exclude=True)
184
192
 
185
193
  def __init__(
186
- self, backend: Optional[PersistenceBackend] = None, uid: Optional[str] = None, scope: Optional[str] = None
194
+ self,
195
+ backend: Optional[PersistenceBackend] = None,
196
+ uid: Optional[str] = None,
197
+ scope: Optional[str] = None,
198
+ readonly: bool = False,
187
199
  ):
188
200
  """
189
201
  Persistence store implementation that uses a backend implementation to store data server-side
@@ -192,6 +204,7 @@ class BackendStore(PersistenceStore):
192
204
  :param backend: the backend to use for storing data; defaults to an in-memory backend
193
205
  :param scope: the scope for the store; if 'global' a single value is stored for all users,
194
206
  if 'user' a value is stored per user
207
+ :param readonly: whether to use the backend in read-only mode, i.e. skip syncing values from client to backend and raise if write()/delete() is called
195
208
  """
196
209
  kwargs: Dict[str, Any] = {}
197
210
  if backend:
@@ -203,6 +216,9 @@ class BackendStore(PersistenceStore):
203
216
  if scope:
204
217
  kwargs['scope'] = scope
205
218
 
219
+ if readonly:
220
+ kwargs['readonly'] = readonly
221
+
206
222
  super().__init__(**kwargs)
207
223
 
208
224
  async def _get_key(self):
@@ -235,10 +251,23 @@ class BackendStore(PersistenceStore):
235
251
 
236
252
  raise ValueError('User not found when trying to compute the key for a user-scoped store')
237
253
 
254
+ def _get_user(self, key: str) -> Optional[str]:
255
+ """
256
+ Get the user for a given key. Returns None if the key is global.
257
+ Reverts the `_get_key` method to get the user for a given key.
258
+ """
259
+ if key == 'global':
260
+ return None
261
+
262
+ # otherwise key is a user identity_id or identity_name
263
+ return key
264
+
238
265
  def _register(self):
239
266
  """
240
267
  Register this store in the backend store registry.
241
- Raises ValueError if the uid is not unique, i.e. another store with the same uid already exists
268
+ Warns if the uid is not unique, i.e. another store with the same uid already exists.
269
+
270
+ :return: True if the store was registered, False if it was already registered previously
242
271
  """
243
272
  from dara.core.internal.registries import backend_store_registry
244
273
 
@@ -250,23 +279,59 @@ class BackendStore(PersistenceStore):
250
279
  store=self,
251
280
  ),
252
281
  )
253
- except ValueError as e:
254
- raise ValueError('BackendStore uid must be unique') from e
282
+ return True
283
+ except ValueError:
284
+ dev_logger.info(f'BackendStore with uid "{self.uid}" already exists, reusing the same instance')
285
+ return False
286
+
287
+ @property
288
+ def ws_mgr(self) -> 'WebsocketManager':
289
+ from dara.core.internal.registries import utils_registry
290
+
291
+ return utils_registry.get('WebsocketManager')
292
+
293
+ def _create_msg(self, value: Any) -> Dict[str, Any]:
294
+ """
295
+ Create a message to send to the frontend.
296
+ :param value: value to send
297
+ """
298
+ return {'store_uid': self.uid, 'value': value}
255
299
 
256
- async def _notify(self, value: Any):
300
+ async def _notify_user(self, user_identifier: str, value: Any, ignore_current_channel: bool = True):
257
301
  """
258
- Notify all clients about the new value for this store
302
+ Notify a given user about the new value for this store.
259
303
 
304
+ :param user_identifier: user to notify
260
305
  :param value: value to notify about
306
+ :param ignore_current_channel: if True, ignore the current websocket channel
261
307
  """
262
- from dara.core.internal.registries import utils_registry
263
- from dara.core.internal.websocket import WebsocketManager
308
+ return await self.ws_mgr.send_message_to_user(
309
+ user_identifier,
310
+ self._create_msg(value),
311
+ ignore_channel=WS_CHANNEL.get() if ignore_current_channel else None,
312
+ )
313
+
314
+ async def _notify_global(self, value: Any, ignore_current_channel: bool = True):
315
+ """
316
+ Notify all users about the new value for this store.
317
+
318
+ :param value: value to notify about
319
+ :param ignore_current_channel: if True, ignore the current websocket channel
320
+ """
321
+ return await self.ws_mgr.broadcast(
322
+ self._create_msg(value),
323
+ ignore_channel=WS_CHANNEL.get() if ignore_current_channel else None,
324
+ )
264
325
 
265
- ws_mgr: WebsocketManager = utils_registry.get('WebsocketManager')
266
- msg = {'store_uid': self.uid, 'value': value}
326
+ async def _notify_value(self, value: Any):
327
+ """
328
+ Notify all clients about the new value for this store.
329
+ Broadcasts to all users if scope is global or sends to the current user if scope is user.
267
330
 
331
+ :param value: value to notify about
332
+ """
268
333
  if self.scope == 'global':
269
- return await ws_mgr.broadcast(msg, ignore_channel=WS_CHANNEL.get())
334
+ return await self._notify_global(value)
270
335
 
271
336
  # For user scope, we need to find channels for the user and notify them
272
337
  user = USER.get()
@@ -275,7 +340,7 @@ class BackendStore(PersistenceStore):
275
340
  return
276
341
 
277
342
  user_identifier = user.identity_id or user.identity_name
278
- return await ws_mgr.send_message_to_user(user_identifier, msg, ignore_channel=WS_CHANNEL.get())
343
+ return await self._notify_user(user_identifier, value)
279
344
 
280
345
  async def init(self, variable: 'Variable'):
281
346
  """
@@ -283,9 +348,19 @@ class BackendStore(PersistenceStore):
283
348
 
284
349
  :param variable: the variable to initialize the store for
285
350
  """
286
- self._register()
287
351
  self.default_value = variable.default
288
352
 
353
+ # only if successfully registered, subscribe to the backend - this makes sure we do it once
354
+ if self._register():
355
+
356
+ async def _on_value(key: str, value: Any):
357
+ # here we explicitly DON'T ignore the current channel, in case we created this variable inside e.g. a py_component we want to notify its creator as well
358
+ if user := self._get_user(key):
359
+ return await self._notify_user(user, value, ignore_current_channel=False)
360
+ return await self._notify_global(value, ignore_current_channel=False)
361
+
362
+ await self.backend.subscribe(_on_value)
363
+
289
364
  async def write(self, value: Any, notify=True):
290
365
  """
291
366
  Persist a value to the store.
@@ -296,10 +371,13 @@ class BackendStore(PersistenceStore):
296
371
  :param value: value to write
297
372
  :param notify: whether to broadcast the new value to clients
298
373
  """
374
+ if self.readonly:
375
+ raise ValueError('Cannot write to a read-only store')
376
+
299
377
  key = await self._get_key()
300
378
 
301
379
  if notify:
302
- await self._notify(value)
380
+ await self._notify_value(value)
303
381
 
304
382
  return await run_user_handler(self.backend.write, (key, value))
305
383
 
@@ -325,10 +403,13 @@ class BackendStore(PersistenceStore):
325
403
 
326
404
  :param notify: whether to broadcast that the value was deleted to clients
327
405
  """
406
+ if self.readonly:
407
+ raise ValueError('Cannot delete from a read-only store')
408
+
328
409
  key = await self._get_key()
329
410
  if notify:
330
411
  # Schedule notification on delete
331
- await self._notify(None)
412
+ await self._notify_value(None)
332
413
  return await run_user_handler(self.backend.delete, (key,))
333
414
 
334
415
  async def get_all(self) -> Dict[str, Any]:
@@ -7527,6 +7527,7 @@ var __privateWrapper = (obj, member, setter, getter) => ({
7527
7527
  useRetain: Recoil_useRetain,
7528
7528
  retentionZone: retentionZone$1
7529
7529
  };
7530
+ var Recoil_index_1 = Recoil_index.DefaultValue;
7530
7531
  var Recoil_index_5 = Recoil_index.RecoilRoot;
7531
7532
  var Recoil_index_8 = Recoil_index.atom;
7532
7533
  var Recoil_index_10 = Recoil_index.atomFamily;
@@ -57943,7 +57944,16 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
57943
57944
  return RecoilSync_index_2({
57944
57945
  itemKey: variable.store.uid,
57945
57946
  refine: Refine_index_6(),
57946
- storeKey: "BackendStore"
57947
+ storeKey: "BackendStore",
57948
+ write({ write: write2 }, newValue) {
57949
+ if (variable.store.readonly) {
57950
+ return;
57951
+ }
57952
+ if (newValue instanceof Recoil_index_1) {
57953
+ return;
57954
+ }
57955
+ write2(variable.store.uid, newValue);
57956
+ }
57947
57957
  });
57948
57958
  }
57949
57959
  function getSessionKey(uid2) {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dara-core
3
- Version: 1.16.12
3
+ Version: 1.16.14
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.16.12)
23
+ Requires-Dist: create-dara-app (==1.16.14)
24
24
  Requires-Dist: croniter (>=1.0.15,<3.0.0)
25
25
  Requires-Dist: cryptography (>=42.0.4)
26
- Requires-Dist: dara-components (==1.16.12) ; extra == "all"
26
+ Requires-Dist: dara-components (==1.16.14) ; extra == "all"
27
27
  Requires-Dist: exceptiongroup (>=1.1.3,<2.0.0)
28
28
  Requires-Dist: fastapi (>=0.115.0,<0.116.0)
29
29
  Requires-Dist: fastapi_vite_dara (==0.4.0)
@@ -53,7 +53,7 @@ Description-Content-Type: text/markdown
53
53
 
54
54
  # Dara Application Framework
55
55
 
56
- <img src="https://github.com/causalens/dara/blob/v1.16.12/img/dara_light.svg?raw=true">
56
+ <img src="https://github.com/causalens/dara/blob/v1.16.14/img/dara_light.svg?raw=true">
57
57
 
58
58
  ![Master tests](https://github.com/causalens/dara/actions/workflows/tests.yml/badge.svg?branch=master)
59
59
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
@@ -98,7 +98,7 @@ source .venv/bin/activate
98
98
  dara start
99
99
  ```
100
100
 
101
- ![Dara App](https://github.com/causalens/dara/blob/v1.16.12/img/components_gallery.png?raw=true)
101
+ ![Dara App](https://github.com/causalens/dara/blob/v1.16.14/img/components_gallery.png?raw=true)
102
102
 
103
103
  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:
104
104
 
@@ -115,9 +115,9 @@ Explore some of our favorite apps - a great way of getting started and getting t
115
115
 
116
116
  | Dara App | Description |
117
117
  | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
- | ![Large Language Model](https://github.com/causalens/dara/blob/v1.16.12/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
119
- | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.16.12/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 |
120
- | ![Graph Editor](https://github.com/causalens/dara/blob/v1.16.12/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. |
118
+ | ![Large Language Model](https://github.com/causalens/dara/blob/v1.16.14/img/llm.png?raw=true) | Demonstrates how to use incorporate a LLM chat box into your decision app to understand model insights |
119
+ | ![Plot Interactivity](https://github.com/causalens/dara/blob/v1.16.14/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 |
120
+ | ![Graph Editor](https://github.com/causalens/dara/blob/v1.16.14/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. |
121
121
 
122
122
  Check out our [App Gallery](https://dara.causalens.com/gallery) for more inspiration!
123
123
 
@@ -144,9 +144,9 @@ And the supporting UI packages and tools.
144
144
  - `ui-utils` - miscellaneous utility functions
145
145
  - `ui-widgets` - widget components
146
146
 
147
- More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.16.12/CONTRIBUTING.md) file.
147
+ More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.16.14/CONTRIBUTING.md) file.
148
148
 
149
149
  ## License
150
150
 
151
- Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.16.12/LICENSE).
151
+ Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.16.14/LICENSE).
152
152
 
@@ -80,8 +80,8 @@ dara/core/metrics/__init__.py,sha256=2UqpWHv-Ie58QLJIHJ9Szfjq8xifAuwy5FYGUIFwWtI
80
80
  dara/core/metrics/cache.py,sha256=bGXwjO_rSc-FkS3PnPi1mvIZf1x-hvmG113dAUk1g-Y,2616
81
81
  dara/core/metrics/runtime.py,sha256=YP-6Dz0GeI9_Yr7bUk_-OqShyFySGH_AKpDO126l6es,1833
82
82
  dara/core/metrics/utils.py,sha256=aKaa_hskV3M3h4xOGZYvegDLq_OWOEUlslkQKrrPQiI,2281
83
- dara/core/persistence.py,sha256=RaGiGQE3oIzEH7kHC1ZXji2VJa0aP0ewMjt1wRWYWjI,10634
84
- dara/core/umd/dara.core.umd.js,sha256=rmSffasPPrCogBEk8EVuHvCdFtfBfUL9OpP4HMEyH-A,4869727
83
+ dara/core/persistence.py,sha256=lEyrOfcTDQ2CvlvZZVAuz9fBGwyzjRa3yMw-BBWpOoM,14032
84
+ dara/core/umd/dara.core.umd.js,sha256=zv9FZRX2NtIjgKhiqjzyeFmwuGnkRtxgxFNVfzePAOI,4870020
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=4km21GLMLM2cr42SwehLUD7DJOlzvhwHl5hdG1OqmIM,2274
@@ -104,8 +104,8 @@ dara/core/visual/themes/__init__.py,sha256=aM4mgoIYo2neBSw5FRzswsht7PUKjLthiHLmF
104
104
  dara/core/visual/themes/dark.py,sha256=UQGDooOc8ric73eHs9E0ltYP4UCrwqQ3QxqN_fb4PwY,1942
105
105
  dara/core/visual/themes/definitions.py,sha256=nS_gQvOzCt5hTmj74d0_siq_9QWuj6wNuir4VCHy0Dk,2779
106
106
  dara/core/visual/themes/light.py,sha256=-Tviq8oEwGbdFULoDOqPuHO0UpAZGsBy8qFi0kAGolQ,1944
107
- dara_core-1.16.12.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
108
- dara_core-1.16.12.dist-info/METADATA,sha256=W0YuhRucRe-8fD3TOj0xDEvrYY7d-AIiHyhknUDrogE,7473
109
- dara_core-1.16.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
110
- dara_core-1.16.12.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
111
- dara_core-1.16.12.dist-info/RECORD,,
107
+ dara_core-1.16.14.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
108
+ dara_core-1.16.14.dist-info/METADATA,sha256=FZ4vUFZrIFf0k_WCWoLd-m4P8T7BpBVI91dtu-zZMbA,7473
109
+ dara_core-1.16.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
110
+ dara_core-1.16.14.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
111
+ dara_core-1.16.14.dist-info/RECORD,,