dara-core 1.16.12__py3-none-any.whl → 1.16.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.
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
@@ -21,13 +21,12 @@ from dara.core.internal.websocket import WS_CHANNEL
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from dara.core.interactivity.plain_variable import Variable
24
+ from dara.core.internal.websocket import WebsocketManager
24
25
 
25
26
 
26
27
  class PersistenceBackend(BaseModel, abc.ABC):
27
28
  """
28
- Abstract base class for a BackendStore backend
29
-
30
- Warning: the API is not stable yet and may change in the future
29
+ Abstract base class for a BackendStore backend.
31
30
  """
32
31
 
33
32
  @abc.abstractmethod
@@ -60,6 +59,12 @@ class PersistenceBackend(BaseModel, abc.ABC):
60
59
  Get all the values as a dictionary of key-value pairs
61
60
  """
62
61
 
62
+ async def subscribe(self, on_value: Callable[[str, Any], Awaitable[None]]):
63
+ """
64
+ Subscribe to changes in the backend. Called with a callback that should be invoked whenever a value is updated.
65
+ """
66
+ # Default implementation does nothing, not all backends need to support this
67
+
63
68
 
64
69
  class InMemoryBackend(PersistenceBackend):
65
70
  """
@@ -173,17 +178,23 @@ class BackendStore(PersistenceStore):
173
178
  :param backend: the backend to use for storing data; defaults to an in-memory backend
174
179
  :param scope: the scope for the store; if 'global' a single value is stored for all users,
175
180
  if 'user' a value is stored per user
181
+ :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
182
  """
177
183
 
178
184
  uid: str = Field(default_factory=lambda: str(uuid4()))
179
185
  backend: PersistenceBackend = Field(default_factory=InMemoryBackend, exclude=True)
180
186
  scope: Literal['global', 'user'] = 'global'
187
+ readonly: bool = False
181
188
 
182
189
  default_value: Any = Field(default=None, exclude=True)
183
190
  initialized_scopes: Set[str] = Field(default_factory=set, exclude=True)
184
191
 
185
192
  def __init__(
186
- self, backend: Optional[PersistenceBackend] = None, uid: Optional[str] = None, scope: Optional[str] = None
193
+ self,
194
+ backend: Optional[PersistenceBackend] = None,
195
+ uid: Optional[str] = None,
196
+ scope: Optional[str] = None,
197
+ readonly: bool = False,
187
198
  ):
188
199
  """
189
200
  Persistence store implementation that uses a backend implementation to store data server-side
@@ -192,6 +203,7 @@ class BackendStore(PersistenceStore):
192
203
  :param backend: the backend to use for storing data; defaults to an in-memory backend
193
204
  :param scope: the scope for the store; if 'global' a single value is stored for all users,
194
205
  if 'user' a value is stored per user
206
+ :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
207
  """
196
208
  kwargs: Dict[str, Any] = {}
197
209
  if backend:
@@ -203,6 +215,9 @@ class BackendStore(PersistenceStore):
203
215
  if scope:
204
216
  kwargs['scope'] = scope
205
217
 
218
+ if readonly:
219
+ kwargs['readonly'] = readonly
220
+
206
221
  super().__init__(**kwargs)
207
222
 
208
223
  async def _get_key(self):
@@ -235,6 +250,17 @@ class BackendStore(PersistenceStore):
235
250
 
236
251
  raise ValueError('User not found when trying to compute the key for a user-scoped store')
237
252
 
253
+ def _get_user(self, key: str) -> Optional[str]:
254
+ """
255
+ Get the user for a given key. Returns None if the key is global.
256
+ Reverts the `_get_key` method to get the user for a given key.
257
+ """
258
+ if key == 'global':
259
+ return None
260
+
261
+ # otherwise key is a user identity_id or identity_name
262
+ return key
263
+
238
264
  def _register(self):
239
265
  """
240
266
  Register this store in the backend store registry.
@@ -253,20 +279,50 @@ class BackendStore(PersistenceStore):
253
279
  except ValueError as e:
254
280
  raise ValueError('BackendStore uid must be unique') from e
255
281
 
256
- async def _notify(self, value: Any):
282
+ @property
283
+ def ws_mgr(self) -> 'WebsocketManager':
284
+ from dara.core.internal.registries import utils_registry
285
+
286
+ return utils_registry.get('WebsocketManager')
287
+
288
+ def _create_msg(self, value: Any) -> Dict[str, Any]:
289
+ """
290
+ Create a message to send to the frontend.
291
+ :param value: value to send
257
292
  """
258
- Notify all clients about the new value for this store
293
+ return {'store_uid': self.uid, 'value': value}
259
294
 
295
+ async def _notify_user(self, user_identifier: str, value: Any):
296
+ """
297
+ Notify a given user about the new value for this store.
298
+ :param user_identifier: user to notify
260
299
  :param value: value to notify about
261
300
  """
262
- from dara.core.internal.registries import utils_registry
263
- from dara.core.internal.websocket import WebsocketManager
301
+ return await self.ws_mgr.send_message_to_user(
302
+ user_identifier,
303
+ self._create_msg(value),
304
+ ignore_channel=WS_CHANNEL.get(),
305
+ )
264
306
 
265
- ws_mgr: WebsocketManager = utils_registry.get('WebsocketManager')
266
- msg = {'store_uid': self.uid, 'value': value}
307
+ async def _notify_global(self, value: Any):
308
+ """
309
+ Notify all users about the new value for this store.
310
+ :param value: value to notify about
311
+ """
312
+ return await self.ws_mgr.broadcast(
313
+ self._create_msg(value),
314
+ ignore_channel=WS_CHANNEL.get(),
315
+ )
316
+
317
+ async def _notify_value(self, value: Any):
318
+ """
319
+ Notify all clients about the new value for this store.
320
+ Broadcasts to all users if scope is global or sends to the current user if scope is user.
267
321
 
322
+ :param value: value to notify about
323
+ """
268
324
  if self.scope == 'global':
269
- return await ws_mgr.broadcast(msg, ignore_channel=WS_CHANNEL.get())
325
+ return await self._notify_global(value)
270
326
 
271
327
  # For user scope, we need to find channels for the user and notify them
272
328
  user = USER.get()
@@ -275,7 +331,7 @@ class BackendStore(PersistenceStore):
275
331
  return
276
332
 
277
333
  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())
334
+ return await self._notify_user(user_identifier, value)
279
335
 
280
336
  async def init(self, variable: 'Variable'):
281
337
  """
@@ -286,6 +342,13 @@ class BackendStore(PersistenceStore):
286
342
  self._register()
287
343
  self.default_value = variable.default
288
344
 
345
+ async def _on_value(key: str, value: Any):
346
+ if user := self._get_user(key):
347
+ return await self._notify_user(user, value)
348
+ return await self._notify_global(value)
349
+
350
+ await self.backend.subscribe(_on_value)
351
+
289
352
  async def write(self, value: Any, notify=True):
290
353
  """
291
354
  Persist a value to the store.
@@ -296,10 +359,13 @@ class BackendStore(PersistenceStore):
296
359
  :param value: value to write
297
360
  :param notify: whether to broadcast the new value to clients
298
361
  """
362
+ if self.readonly:
363
+ raise ValueError('Cannot write to a read-only store')
364
+
299
365
  key = await self._get_key()
300
366
 
301
367
  if notify:
302
- await self._notify(value)
368
+ await self._notify_value(value)
303
369
 
304
370
  return await run_user_handler(self.backend.write, (key, value))
305
371
 
@@ -325,10 +391,13 @@ class BackendStore(PersistenceStore):
325
391
 
326
392
  :param notify: whether to broadcast that the value was deleted to clients
327
393
  """
394
+ if self.readonly:
395
+ raise ValueError('Cannot delete from a read-only store')
396
+
328
397
  key = await self._get_key()
329
398
  if notify:
330
399
  # Schedule notification on delete
331
- await self._notify(None)
400
+ await self._notify_value(None)
332
401
  return await run_user_handler(self.backend.delete, (key,))
333
402
 
334
403
  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.13
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.13)
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.13) ; 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.13/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.13/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.13/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.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 |
120
+ | ![Graph Editor](https://github.com/causalens/dara/blob/v1.16.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. |
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.13/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.13/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=SsWGyBO1Wn9rlkaUqf0pqNVatRN8Aw-pK_CVV7v5fJc,13150
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.13.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
108
+ dara_core-1.16.13.dist-info/METADATA,sha256=uUJ6n6ebwTD7eQUw8Gn_NwAMdrKGmb8E1IJZmZQT8Do,7473
109
+ dara_core-1.16.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
110
+ dara_core-1.16.13.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
111
+ dara_core-1.16.13.dist-info/RECORD,,