dara-core 1.14.8__py3-none-any.whl → 1.15.0__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.
@@ -490,10 +490,11 @@ def create_router(config: Configuration):
490
490
  return result
491
491
 
492
492
  @core_api_router.post('/store', dependencies=[Depends(verify_session)])
493
- async def sync_backend_store(values: Dict[str, Any] = Body()):
493
+ async def sync_backend_store(ws_channel: str = Body(), values: Dict[str, Any] = Body()):
494
494
  registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
495
495
 
496
496
  async def _write(store_uid: str, value: Any):
497
+ WS_CHANNEL.set(ws_channel)
497
498
  store_entry: BackendStoreEntry = await registry_mgr.get(backend_store_registry, store_uid)
498
499
  result = store_entry.store.write(value)
499
500
 
@@ -14,6 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  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 inspect
19
20
  import math
@@ -26,7 +27,7 @@ import anyio
26
27
  from anyio import Event, create_memory_object_stream, create_task_group
27
28
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
28
29
  from exceptiongroup import catch
29
- from fastapi import HTTPException, Query
30
+ from fastapi import Query, WebSocketException
30
31
  from fastapi.encoders import jsonable_encoder
31
32
  from jwt import DecodeError
32
33
  from pydantic import BaseModel, Field, parse_obj_as
@@ -80,6 +81,7 @@ class CustomClientMessage(BaseModel):
80
81
 
81
82
  ClientMessage = Union[DaraClientMessage, CustomClientMessage]
82
83
 
84
+
83
85
  # Server message types
84
86
  class ServerMessagePayload(BaseModel):
85
87
  rchan: Optional[str] = Field(default=None, alias='__rchan')
@@ -117,7 +119,7 @@ class DaraServerMessage(BaseModel):
117
119
  """
118
120
 
119
121
  type: Literal['message'] = Field(default='message', const=True)
120
- message: ServerMessagePayload # exact messages expected by frontend are defined in js/api/websocket.tsx
122
+ message: ServerMessagePayload # exact messages expected by frontend are defined in js/api/websocket.tsx
121
123
 
122
124
 
123
125
  class CustomServerMessage(BaseModel):
@@ -204,7 +206,10 @@ class WebSocketHandler:
204
206
  existing_messages.append(message.message)
205
207
  else:
206
208
  existing_messages = [message.message]
207
- self.pending_responses[message_id] = (event, existing_messages)
209
+ self.pending_responses[message_id] = (
210
+ event,
211
+ existing_messages,
212
+ )
208
213
 
209
214
  # If all chunks have been received, set the event to notify the waiting coroutine
210
215
  if len(existing_messages) == message.chunk_count:
@@ -235,7 +240,9 @@ class WebSocketHandler:
235
240
  await self.send_message(
236
241
  CustomServerMessage(
237
242
  message=CustomServerMessagePayload(
238
- kind=kind, data=response, __response_for=message.message.rchan
243
+ kind=kind,
244
+ data=response,
245
+ __response_for=message.message.rchan,
239
246
  )
240
247
  )
241
248
  )
@@ -249,7 +256,9 @@ class WebSocketHandler:
249
256
  return self.send_message(
250
257
  CustomServerMessage(
251
258
  message=CustomServerMessagePayload(
252
- kind=kind, data=response, __response_for=message.message.rchan
259
+ kind=kind,
260
+ data=response,
261
+ __response_for=message.message.rchan,
253
262
  )
254
263
  )
255
264
  )
@@ -312,6 +321,9 @@ class WebsocketManager:
312
321
 
313
322
  def __init__(self):
314
323
  self.handlers: Dict[str, WebSocketHandler] = {}
324
+ """
325
+ A mapping of channel IDs to WebSocketHandler instances.
326
+ """
315
327
 
316
328
  def _construct_message(self, payload: LoosePayload, custom: bool) -> ServerMessage:
317
329
  """
@@ -335,24 +347,30 @@ class WebsocketManager:
335
347
  self.handlers[channel_id] = handler
336
348
  return handler
337
349
 
338
- async def broadcast(self, message: LoosePayload, custom=False):
350
+ async def broadcast(self, message: LoosePayload, custom=False, ignore_channel: Optional[str] = None):
339
351
  """
340
352
  Send a message to all connected clients.
341
353
 
342
354
  :param message: The message to send
343
355
  :param custom: Whether the message is a custom message
356
+ :param ignore_channel: A channel ID to ignore when broadcasting
344
357
  """
345
358
  async with anyio.create_task_group() as tg:
346
359
  for handler in self.handlers.values():
360
+ if ignore_channel is not None and handler.channel_id == ignore_channel:
361
+ continue
347
362
  tg.start_soon(handler.send_message, self._construct_message(message, custom))
348
363
 
349
- async def send_message_to_user(self, user_id: str, message: LoosePayload, custom=False):
364
+ async def send_message_to_user(
365
+ self, user_id: str, message: LoosePayload, custom=False, ignore_channel: Optional[str] = None
366
+ ):
350
367
  """
351
368
  Send a message to all connected channels associated with the given user.
352
369
 
353
370
  :param user_id: The user ID to send the message to
354
371
  :param message: The message payload to send
355
372
  :param custom: Whether the message is a custom message
373
+ :param ignore_channel: A channel ID to ignore when sending
356
374
  """
357
375
  channels = get_user_channels(user_id)
358
376
 
@@ -361,6 +379,8 @@ class WebsocketManager:
361
379
 
362
380
  async with anyio.create_task_group() as tg:
363
381
  for channel in channels:
382
+ if ignore_channel is not None and channel == ignore_channel:
383
+ continue
364
384
  tg.start_soon(self.send_message, channel, message, custom)
365
385
 
366
386
  async def send_message(self, channel_id: str, message: LoosePayload, custom=False):
@@ -405,7 +425,7 @@ async def ws_handler(websocket: WebSocket, token: Optional[str] = Query(default=
405
425
  :param token: The authentication token
406
426
  """
407
427
  if token is None:
408
- raise HTTPException(403, 'Token missing from websocket connection query parameter')
428
+ raise WebSocketException(code=403, reason='Token missing from websocket connection query parameter')
409
429
 
410
430
  from dara.core.auth.definitions import ID_TOKEN, SESSION_ID, USER, UserData
411
431
  from dara.core.internal.registries import (
@@ -420,10 +440,10 @@ async def ws_handler(websocket: WebSocket, token: Optional[str] = Query(default=
420
440
 
421
441
  try:
422
442
  token_content: TokenData = decode_token(token)
423
- except DecodeError:
424
- raise HTTPException(status_code=403, detail='Invalid or expired token')
443
+ except DecodeError as err:
444
+ raise WebSocketException(code=403, reason='Invalid or expired token') from err
425
445
  except AuthError as err:
426
- raise HTTPException(status_code=403, detail=err.detail)
446
+ raise WebSocketException(code=403, reason=str(err.detail)) from err
427
447
 
428
448
  # Register once accepted - map session id to the channel so subsequent requests can identify the client
429
449
  if websocket_registry.has(token_content.session_id):
dara/core/persistence.py CHANGED
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field, PrivateAttr, validator
10
10
 
11
11
  from dara.core.auth.definitions import USER
12
12
  from dara.core.internal.utils import run_user_handler
13
+ from dara.core.internal.websocket import WS_CHANNEL
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from dara.core.interactivity.plain_variable import Variable
@@ -257,7 +258,7 @@ class BackendStore(PersistenceStore):
257
258
  msg = {'store_uid': self.uid, 'value': value}
258
259
 
259
260
  if self.scope == 'global':
260
- return await ws_mgr.broadcast(msg)
261
+ return await ws_mgr.broadcast(msg, ignore_channel=WS_CHANNEL.get())
261
262
 
262
263
  # For user scope, we need to find channels for the user and notify them
263
264
  user = USER.get()
@@ -266,7 +267,7 @@ class BackendStore(PersistenceStore):
266
267
  return
267
268
 
268
269
  user_identifier = user.identity_id or user.identity_name
269
- return await ws_mgr.send_message_to_user(user_identifier, msg)
270
+ return await ws_mgr.send_message_to_user(user_identifier, msg, ignore_channel=WS_CHANNEL.get())
270
271
 
271
272
  async def init(self, variable: 'Variable'):
272
273
  """
@@ -56448,6 +56448,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
56448
56448
  }
56449
56449
  initialize(isReconnect = false) {
56450
56450
  const url = new URL(__privateGet(this, _socketUrl));
56451
+ this.token = store.getValueSync(getTokenKey());
56451
56452
  url.searchParams.set("token", this.token);
56452
56453
  const socket = new WebSocket(url);
56453
56454
  __privateSet(this, _pingInterval, setInterval(() => {
@@ -56591,6 +56592,7 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
56591
56592
  }
56592
56593
  }
56593
56594
  updateToken(newToken) {
56595
+ this.token = newToken;
56594
56596
  if (this.socket.readyState === WebSocket.OPEN) {
56595
56597
  this.socket.send(
56596
56598
  JSON.stringify({
@@ -56598,7 +56600,6 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
56598
56600
  type: "token_update"
56599
56601
  })
56600
56602
  );
56601
- this.token = newToken;
56602
56603
  }
56603
56604
  }
56604
56605
  sendCustomMessage(kind, data, awaitResponse = false) {
@@ -58143,30 +58144,39 @@ You must set sticky: 'left' | 'right' for the '${bugWithUnderColumnsSticky.Heade
58143
58144
  const val = await response.json();
58144
58145
  return val;
58145
58146
  }, []);
58146
- const syncStoreValues = React__namespace.useCallback(async ({ diff }) => {
58147
- const extrasMap = /* @__PURE__ */ new Map();
58148
- for (const [itemKey, value] of diff.entries()) {
58149
- const extras = STORE_EXTRAS_MAP.get(itemKey);
58150
- if (!extrasMap.has(extras)) {
58151
- extrasMap.set(extras, {});
58147
+ const syncStoreValues = React__namespace.useCallback(
58148
+ async ({ diff }) => {
58149
+ const extrasMap = /* @__PURE__ */ new Map();
58150
+ for (const [itemKey, value] of diff.entries()) {
58151
+ const extras = STORE_EXTRAS_MAP.get(itemKey);
58152
+ if (!extrasMap.has(extras)) {
58153
+ extrasMap.set(extras, {});
58154
+ }
58155
+ extrasMap.get(extras)[itemKey] = value;
58156
+ }
58157
+ async function sendRequest(serializableExtras, storeDiff) {
58158
+ const response = await request(
58159
+ `/api/core/store`,
58160
+ {
58161
+ body: JSON.stringify({
58162
+ values: storeDiff,
58163
+ ws_channel: await client.getChannel()
58164
+ }),
58165
+ method: "POST"
58166
+ },
58167
+ serializableExtras.extras
58168
+ );
58169
+ await handleAuthErrors(response, true);
58170
+ await validateResponse(response, `Failed to sync the store values`);
58152
58171
  }
58153
- extrasMap.get(extras)[itemKey] = value;
58154
- }
58155
- async function sendRequest(serializableExtras, storeDiff) {
58156
- const response = await request(
58157
- `/api/core/store`,
58158
- { body: JSON.stringify(storeDiff), method: "POST" },
58159
- serializableExtras.extras
58172
+ await Promise.allSettled(
58173
+ Array.from(extrasMap.entries()).map(
58174
+ ([serializableExtras, storeDiff]) => sendRequest(serializableExtras, storeDiff)
58175
+ )
58160
58176
  );
58161
- await handleAuthErrors(response, true);
58162
- await validateResponse(response, `Failed to sync the store values`);
58163
- }
58164
- await Promise.allSettled(
58165
- Array.from(extrasMap.entries()).map(
58166
- ([serializableExtras, storeDiff]) => sendRequest(serializableExtras, storeDiff)
58167
- )
58168
- );
58169
- }, []);
58177
+ },
58178
+ [client]
58179
+ );
58170
58180
  const listenToStoreChanges = React__namespace.useCallback(
58171
58181
  ({ updateItem }) => {
58172
58182
  if (!client) {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dara-core
3
- Version: 1.14.8
3
+ Version: 1.15.0
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.8)
23
+ Requires-Dist: create-dara-app (==1.15.0)
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.14.8) ; extra == "all"
26
+ Requires-Dist: dara-components (==1.15.0) ; 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_dara (==0.3.1.0)
@@ -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.8/img/dara_light.svg?raw=true">
54
+ <img src="https://github.com/causalens/dara/blob/v1.15.0/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.8/img/components_gallery.png?raw=true)
99
+ ![Dara App](https://github.com/causalens/dara/blob/v1.15.0/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.8/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.8/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.8/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.15.0/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.15.0/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.15.0/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.8/CONTRIBUTING.md) file.
145
+ More information on the repository structure can be found in the [CONTRIBUTING.md](https://github.com/causalens/dara/blob/v1.15.0/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.8/LICENSE).
149
+ Dara is open-source and licensed under the [Apache 2.0 License](https://github.com/causalens/dara/blob/v1.15.0/LICENSE).
150
150
 
@@ -54,13 +54,13 @@ dara/core/internal/port_utils.py,sha256=AQOUNiFNBYKVUwQ7i9UlY1NQ3sWb5xh5GkO6P1Bm
54
54
  dara/core/internal/registries.py,sha256=9WDczIsNeSmzi6aViIq_b14lmmYGGkdsUGHpv0Sg9zo,3278
55
55
  dara/core/internal/registry.py,sha256=ONCDusqaL0q59Py_r8-fFVN3vbkkDf5TXzNvbB9SrGQ,4305
56
56
  dara/core/internal/registry_lookup.py,sha256=8snHu2wUUsngXjHyHh6eZqL_WwonTTQB6-WBX-R_WZg,2238
57
- dara/core/internal/routing.py,sha256=Wdy11iWDeAf2PyHXZv5mPeJ_BEcoR0XftOl3M8vUhJU,22782
57
+ dara/core/internal/routing.py,sha256=XViBysQSchhX9yoFyDoGe4r5OQ8xMfn6rvA2fi8Yd70,22847
58
58
  dara/core/internal/scheduler.py,sha256=z6OYwazBf3GYo8CzMC9IuGC2P96gI7JwxquT8GaoTMk,12944
59
59
  dara/core/internal/settings.py,sha256=wAWxl-HXjq7PW3twe_CrR-UuMRw9VBudC3eRmevZAhM,3869
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=KlEzIofyXWX8Axe4-mUILH1PO6hnK7webmYfDeCkvxc,20642
63
+ dara/core/internal/websocket.py,sha256=yj_IVDfwyjPmljY-9NWktaM0QGG50KrWQERWrpfNn00,21480
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
@@ -80,8 +80,8 @@ dara/core/metrics/__init__.py,sha256=2UqpWHv-Ie58QLJIHJ9Szfjq8xifAuwy5FYGUIFwWtI
80
80
  dara/core/metrics/cache.py,sha256=ybofUhZO0TCHeyhB_AtldWk1QTmTKh7GucTXpOkeTFA,2580
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
- dara/core/persistence.py,sha256=TO94rPAN7jxZKVCC5YA4eE7GGDoNlCPe-BkkItV2VUE,10379
84
- dara/core/umd/dara.core.umd.js,sha256=W3h6YomUxrDAGy-mTKuZHYwnHobad-y6F9BGk6j21Z0,4878770
83
+ dara/core/persistence.py,sha256=9GBLhK_so-T-DnNOVT3zo7mHXNjwIU2TzgcIOr7XEro,10497
84
+ dara/core/umd/dara.core.umd.js,sha256=AjBfi7G00OO78EvnWerCpfruED4xAI4PiKgApSQ2HIM,4879029
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.8.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
109
- dara_core-1.14.8.dist-info/METADATA,sha256=IoGUbYGqrQv4nGac61-oSeG8yo9qDrbj8dx8P-Vx27k,7390
110
- dara_core-1.14.8.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
111
- dara_core-1.14.8.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
112
- dara_core-1.14.8.dist-info/RECORD,,
108
+ dara_core-1.15.0.dist-info/LICENSE,sha256=r9u1w2RvpLMV6YjuXHIKXRBKzia3fx_roPwboGcLqCc,10944
109
+ dara_core-1.15.0.dist-info/METADATA,sha256=IhMXPa_0o-eECRfArK7m9tXmrGTPYB8k8elFdHI86ME,7390
110
+ dara_core-1.15.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
111
+ dara_core-1.15.0.dist-info/entry_points.txt,sha256=H__D5sNIGuPIhVam0DChNL-To5k8Y7nY7TAFz9Mz6cc,139
112
+ dara_core-1.15.0.dist-info/RECORD,,