dara-core 1.20.0a1__py3-none-any.whl → 1.20.1a1__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/__init__.py +0 -3
- dara/core/actions.py +2 -1
- dara/core/auth/basic.py +16 -22
- dara/core/auth/definitions.py +2 -2
- dara/core/auth/routes.py +5 -5
- dara/core/auth/utils.py +5 -5
- dara/core/base_definitions.py +64 -22
- dara/core/cli.py +7 -8
- dara/core/configuration.py +2 -5
- dara/core/css.py +2 -1
- dara/core/data_utils.py +19 -18
- dara/core/defaults.py +7 -6
- dara/core/definitions.py +19 -50
- dara/core/http.py +3 -7
- dara/core/interactivity/__init__.py +0 -6
- dara/core/interactivity/actions.py +50 -52
- dara/core/interactivity/any_data_variable.py +134 -7
- dara/core/interactivity/any_variable.py +8 -5
- dara/core/interactivity/data_variable.py +266 -8
- dara/core/interactivity/derived_data_variable.py +290 -7
- dara/core/interactivity/derived_variable.py +174 -414
- dara/core/interactivity/filtering.py +27 -46
- dara/core/interactivity/loop_variable.py +2 -2
- dara/core/interactivity/non_data_variable.py +68 -5
- dara/core/interactivity/plain_variable.py +15 -89
- dara/core/interactivity/switch_variable.py +19 -19
- dara/core/interactivity/url_variable.py +90 -10
- dara/core/internal/cache_store/base_impl.py +1 -2
- dara/core/internal/cache_store/cache_store.py +25 -22
- dara/core/internal/cache_store/keep_all.py +1 -4
- dara/core/internal/cache_store/lru.py +1 -5
- dara/core/internal/cache_store/ttl.py +1 -4
- dara/core/internal/cgroup.py +1 -1
- dara/core/internal/dependency_resolution.py +66 -60
- dara/core/internal/devtools.py +5 -12
- dara/core/internal/download.py +4 -13
- dara/core/internal/encoder_registry.py +7 -7
- dara/core/internal/execute_action.py +13 -13
- dara/core/internal/hashing.py +3 -1
- dara/core/internal/import_discovery.py +4 -3
- dara/core/internal/normalization.py +18 -9
- dara/core/internal/pandas_utils.py +5 -107
- dara/core/internal/pool/definitions.py +1 -1
- dara/core/internal/pool/task_pool.py +16 -25
- dara/core/internal/pool/utils.py +18 -21
- dara/core/internal/pool/worker.py +2 -3
- dara/core/internal/port_utils.py +1 -1
- dara/core/internal/registries.py +6 -12
- dara/core/internal/registry.py +2 -4
- dara/core/internal/registry_lookup.py +5 -11
- dara/core/internal/routing.py +145 -109
- dara/core/internal/scheduler.py +8 -13
- dara/core/internal/settings.py +2 -2
- dara/core/internal/store.py +29 -2
- dara/core/internal/tasks.py +195 -379
- dara/core/internal/utils.py +13 -36
- dara/core/internal/websocket.py +20 -21
- dara/core/js_tooling/js_utils.py +26 -28
- dara/core/js_tooling/templates/vite.config.template.ts +3 -12
- dara/core/logging.py +12 -13
- dara/core/main.py +11 -14
- dara/core/metrics/cache.py +1 -1
- dara/core/metrics/utils.py +3 -3
- dara/core/persistence.py +5 -27
- dara/core/umd/dara.core.umd.js +55425 -59091
- dara/core/visual/components/__init__.py +2 -2
- dara/core/visual/components/fallback.py +4 -30
- dara/core/visual/components/for_cmp.py +1 -4
- dara/core/visual/css/__init__.py +31 -30
- dara/core/visual/dynamic_component.py +28 -31
- dara/core/visual/progress_updater.py +3 -4
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/METADATA +11 -12
- dara_core-1.20.1a1.dist-info/RECORD +114 -0
- dara/core/interactivity/client_variable.py +0 -71
- dara/core/interactivity/server_variable.py +0 -325
- dara/core/interactivity/state_variable.py +0 -69
- dara/core/interactivity/tabular_variable.py +0 -94
- dara/core/internal/multi_resource_lock.py +0 -70
- dara_core-1.20.0a1.dist-info/RECORD +0 -119
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/LICENSE +0 -0
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/WHEEL +0 -0
- {dara_core-1.20.0a1.dist-info → dara_core-1.20.1a1.dist-info}/entry_points.txt +0 -0
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Copyright 2023 Impulse Innovations Limited
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
you may not use this file except in compliance with the License.
|
|
7
|
-
You may obtain a copy of the License at
|
|
8
|
-
|
|
9
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
|
|
11
|
-
Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
See the License for the specific language governing permissions and
|
|
15
|
-
limitations under the License.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import abc
|
|
21
|
-
from typing import Optional
|
|
22
|
-
|
|
23
|
-
from dara.core.interactivity.any_variable import AnyVariable
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class ClientVariable(AnyVariable, abc.ABC):
|
|
27
|
-
"""
|
|
28
|
-
Client represents any variable which can be ordinarily serialized to the client
|
|
29
|
-
|
|
30
|
-
:param uid: the unique identifier for this variable; if not provided a random one is generated
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
uid: str
|
|
34
|
-
|
|
35
|
-
def __init__(self, uid: Optional[str] = None, **kwargs) -> None:
|
|
36
|
-
super().__init__(uid=uid, **kwargs)
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def list_item(self):
|
|
40
|
-
"""
|
|
41
|
-
Get a LoopVariable that represents the current item in the list.
|
|
42
|
-
Should only be used in conjunction with the `For` component.
|
|
43
|
-
|
|
44
|
-
Note that it is a type of a Variable so it can be used in places where a regular Variable is expected.
|
|
45
|
-
|
|
46
|
-
By default, the entire list item is used as the item.
|
|
47
|
-
|
|
48
|
-
`LoopVariable` supports nested property access using `get` or index access i.e. `[]`.
|
|
49
|
-
You can mix and match those two methods to access nested properties as they are equivalent.
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
my_list = Variable(['foo', 'bar', 'baz'])
|
|
53
|
-
|
|
54
|
-
# Represents the entire item in the list
|
|
55
|
-
my_list.list_item
|
|
56
|
-
|
|
57
|
-
my_list_of_objects = Variable([
|
|
58
|
-
{'id': 1, 'name': 'John', 'data': {'city': 'London', 'country': 'UK'}},
|
|
59
|
-
{'id': 2, 'name': 'Jane', 'data': {'city': 'Paris', 'country': 'France'}},
|
|
60
|
-
])
|
|
61
|
-
|
|
62
|
-
# Represents the item 'name' property
|
|
63
|
-
my_list_of_objects.list_item['name']
|
|
64
|
-
|
|
65
|
-
# Represents the item 'data.country' property
|
|
66
|
-
my_list_of_objects.list_item.get('data')['country']
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
from .loop_variable import LoopVariable
|
|
70
|
-
|
|
71
|
-
return LoopVariable()
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
from collections import defaultdict
|
|
3
|
-
from typing import Any, DefaultDict, Dict, Literal, Optional, Tuple, Union
|
|
4
|
-
|
|
5
|
-
from pandas import DataFrame
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field, SerializerFunctionWrapHandler, model_serializer
|
|
7
|
-
|
|
8
|
-
from dara.core.auth.definitions import USER
|
|
9
|
-
from dara.core.base_definitions import CachedRegistryEntry, NonTabularDataError
|
|
10
|
-
from dara.core.interactivity.filtering import FilterQuery, Pagination, apply_filters, coerce_to_filter_query
|
|
11
|
-
from dara.core.internal.pandas_utils import DataResponse, append_index, build_data_response
|
|
12
|
-
from dara.core.internal.utils import call_async
|
|
13
|
-
from dara.core.internal.websocket import ServerMessagePayload, WebsocketManager
|
|
14
|
-
|
|
15
|
-
from .any_variable import AnyVariable
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ServerVariableMessage(ServerMessagePayload):
|
|
19
|
-
typ: Literal['ServerVariable'] = Field(alias='__type', default='ServerVariable')
|
|
20
|
-
uid: str
|
|
21
|
-
sequence_number: int
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ServerBackend(BaseModel, abc.ABC):
|
|
25
|
-
scope: Literal['global', 'user']
|
|
26
|
-
|
|
27
|
-
@abc.abstractmethod
|
|
28
|
-
async def write(self, key: str, value: Any):
|
|
29
|
-
"""
|
|
30
|
-
Persist a value
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
@abc.abstractmethod
|
|
34
|
-
async def read(self, key: str) -> Any:
|
|
35
|
-
"""
|
|
36
|
-
Read a value
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
@abc.abstractmethod
|
|
40
|
-
async def read_filtered(
|
|
41
|
-
self, key: str, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
42
|
-
) -> Tuple[Optional[DataFrame], int]:
|
|
43
|
-
"""
|
|
44
|
-
Read a value
|
|
45
|
-
:param filters: filters to apply
|
|
46
|
-
:param pagination: pagination to apply
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
@abc.abstractmethod
|
|
50
|
-
async def get_sequence_number(self, key: str) -> int:
|
|
51
|
-
"""
|
|
52
|
-
Get the sequence number for a given key
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class MemoryBackend(ServerBackend):
|
|
57
|
-
data: Dict[str, Any] = Field(default_factory=dict)
|
|
58
|
-
sequence_number: DefaultDict[str, int] = Field(default_factory=lambda: defaultdict(int))
|
|
59
|
-
|
|
60
|
-
def __init__(self, scope: Literal['user', 'global'] = 'user'):
|
|
61
|
-
super().__init__(scope=scope)
|
|
62
|
-
|
|
63
|
-
async def write(self, key: str, value: Any):
|
|
64
|
-
self.data[key] = value
|
|
65
|
-
self.sequence_number[key] += 1
|
|
66
|
-
return value
|
|
67
|
-
|
|
68
|
-
async def read(self, key: str) -> Any:
|
|
69
|
-
return self.data.get(key)
|
|
70
|
-
|
|
71
|
-
async def read_filtered(
|
|
72
|
-
self, key: str, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
73
|
-
) -> Tuple[Optional[DataFrame], int]:
|
|
74
|
-
dataset = self.data.get(key)
|
|
75
|
-
|
|
76
|
-
# print user-friendly error message if the data is not a DataFrame
|
|
77
|
-
# most likely due to user passing a non-tabular server variable to e.g. a Table
|
|
78
|
-
if dataset is not None and not isinstance(dataset, DataFrame):
|
|
79
|
-
raise NonTabularDataError(
|
|
80
|
-
f'Failed to retrieve ServerVariable tabular data, expected pandas.DataFrame, got {type(dataset)}'
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
dataset = append_index(dataset)
|
|
84
|
-
return apply_filters(dataset, coerce_to_filter_query(filters), pagination)
|
|
85
|
-
|
|
86
|
-
async def get_sequence_number(self, key: str) -> int:
|
|
87
|
-
return self.sequence_number[key]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class ServerVariable(AnyVariable):
|
|
91
|
-
"""
|
|
92
|
-
A ServerVariable represents server-side data that is synchronized with the client.
|
|
93
|
-
|
|
94
|
-
Unlike Variables with BackendStore (which are client state persisted on server),
|
|
95
|
-
ServerVariable holds data that originates and is managed on the server, with
|
|
96
|
-
clients receiving reactive updates when the data changes.
|
|
97
|
-
|
|
98
|
-
ServerVariable can store any Python object, including non-serializable data like
|
|
99
|
-
database connections, ML models, or complex objects.
|
|
100
|
-
|
|
101
|
-
However, when used with components expecting tabular data (like Table), the data must be
|
|
102
|
-
serializable or a NonTabularDataError will be raised. The default backend implementation
|
|
103
|
-
expects the tabular data to be a pandas DataFrame. To support other data types, you can
|
|
104
|
-
implement a custom backend that translates the data into a filtered DataFrame in the
|
|
105
|
-
`read_filtered` method.
|
|
106
|
-
|
|
107
|
-
```python
|
|
108
|
-
import pandas as pd
|
|
109
|
-
from dara.core import ServerVariable, action
|
|
110
|
-
from dara.core.interactivity.server_variable import ServerBackend
|
|
111
|
-
from sklearn.ensemble import RandomForestClassifier
|
|
112
|
-
|
|
113
|
-
# Basic usage with DataFrame
|
|
114
|
-
data = ServerVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
115
|
-
|
|
116
|
-
# Non-serializable data (ML model)
|
|
117
|
-
model = ServerVariable(trained_sklearn_model, scope='global')
|
|
118
|
-
|
|
119
|
-
# Custom backend
|
|
120
|
-
class DatabaseBackend(ServerBackend):
|
|
121
|
-
# ... implements all the methods as DB operations
|
|
122
|
-
|
|
123
|
-
data = ServerVariable(backend=DatabaseBackend(...))
|
|
124
|
-
|
|
125
|
-
# User-specific data
|
|
126
|
-
user_prefs = ServerVariable(scope='user')
|
|
127
|
-
|
|
128
|
-
@action
|
|
129
|
-
async def on_click(ctx):
|
|
130
|
-
# write to the data for the user who initiated the action
|
|
131
|
-
await user_prefs.write('dark')
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
:param default: Initial value for the variable (global scope only)
|
|
135
|
-
:param backend: Custom backend for data storage and retrieval
|
|
136
|
-
:param scope: 'global' (shared across all users) or 'user' (per-user data)
|
|
137
|
-
"""
|
|
138
|
-
|
|
139
|
-
backend: ServerBackend = Field(exclude=True)
|
|
140
|
-
scope: Literal['user', 'global']
|
|
141
|
-
|
|
142
|
-
def __init__(
|
|
143
|
-
self,
|
|
144
|
-
default: Optional[Any] = None,
|
|
145
|
-
backend: Optional[ServerBackend] = None,
|
|
146
|
-
scope: Literal['user', 'global'] = 'global',
|
|
147
|
-
uid: Optional[str] = None,
|
|
148
|
-
**kwargs,
|
|
149
|
-
) -> None:
|
|
150
|
-
from dara.core.internal.registries import server_variable_registry
|
|
151
|
-
|
|
152
|
-
if backend is None:
|
|
153
|
-
backend = MemoryBackend(scope=scope)
|
|
154
|
-
|
|
155
|
-
if default is not None:
|
|
156
|
-
assert scope == 'global', (
|
|
157
|
-
'ServerVariable can only be used with global scope, cannot initialize user-specific values'
|
|
158
|
-
)
|
|
159
|
-
call_async(backend.write, 'global', default)
|
|
160
|
-
|
|
161
|
-
super().__init__(uid=uid, backend=backend, scope=scope, **kwargs)
|
|
162
|
-
|
|
163
|
-
var_entry = ServerVariableRegistryEntry(uid=str(self.uid), backend=backend)
|
|
164
|
-
server_variable_registry.register(str(self.uid), var_entry)
|
|
165
|
-
|
|
166
|
-
@classmethod
|
|
167
|
-
async def get_value(cls, entry: 'ServerVariableRegistryEntry'):
|
|
168
|
-
"""
|
|
169
|
-
Internal method to get the value of a server variable based in its registry entry.
|
|
170
|
-
"""
|
|
171
|
-
key = cls.get_key(entry.backend.scope)
|
|
172
|
-
return await entry.backend.read(key)
|
|
173
|
-
|
|
174
|
-
@classmethod
|
|
175
|
-
async def write_value(cls, entry: 'ServerVariableRegistryEntry', value: Any):
|
|
176
|
-
"""
|
|
177
|
-
Internal method to write the value of a server variable based in its registry entry.
|
|
178
|
-
"""
|
|
179
|
-
key = cls.get_key(entry.backend.scope)
|
|
180
|
-
await entry.backend.write(key, value)
|
|
181
|
-
await cls._notify(entry.uid, key, entry.backend)
|
|
182
|
-
|
|
183
|
-
@classmethod
|
|
184
|
-
async def get_sequence_number(cls, entry: 'ServerVariableRegistryEntry'):
|
|
185
|
-
"""
|
|
186
|
-
Internal method to get the sequence number of a server variable based in its registry entry.
|
|
187
|
-
"""
|
|
188
|
-
key = cls.get_key(entry.backend.scope)
|
|
189
|
-
return await entry.backend.get_sequence_number(key)
|
|
190
|
-
|
|
191
|
-
@classmethod
|
|
192
|
-
async def get_tabular_data(
|
|
193
|
-
cls,
|
|
194
|
-
entry: 'ServerVariableRegistryEntry',
|
|
195
|
-
filters: Optional[FilterQuery] = None,
|
|
196
|
-
pagination: Optional[Pagination] = None,
|
|
197
|
-
) -> DataResponse:
|
|
198
|
-
"""
|
|
199
|
-
Internal method to get tabular data from the backend
|
|
200
|
-
"""
|
|
201
|
-
key = cls.get_key(entry.backend.scope)
|
|
202
|
-
data, count = await entry.backend.read_filtered(key, filters, pagination)
|
|
203
|
-
if data is None:
|
|
204
|
-
return DataResponse(data=None, count=0, schema=None)
|
|
205
|
-
return build_data_response(data, count)
|
|
206
|
-
|
|
207
|
-
@classmethod
|
|
208
|
-
def get_key(cls, scope: Literal['global', 'user']):
|
|
209
|
-
"""
|
|
210
|
-
Resolve the key for the given scope
|
|
211
|
-
|
|
212
|
-
:param scope: the scope to resolve the key for
|
|
213
|
-
"""
|
|
214
|
-
if scope == 'global':
|
|
215
|
-
return 'global'
|
|
216
|
-
|
|
217
|
-
user = USER.get()
|
|
218
|
-
|
|
219
|
-
if user:
|
|
220
|
-
return user.identity_id
|
|
221
|
-
|
|
222
|
-
raise ValueError('User not found when trying to compute the key for a user-scoped store')
|
|
223
|
-
|
|
224
|
-
@property
|
|
225
|
-
def key(self):
|
|
226
|
-
"""
|
|
227
|
-
Current key for the backend
|
|
228
|
-
"""
|
|
229
|
-
return self.get_key(self.scope)
|
|
230
|
-
|
|
231
|
-
@classmethod
|
|
232
|
-
async def _notify(cls, uid: str, key: str, backend: ServerBackend):
|
|
233
|
-
"""
|
|
234
|
-
Internal method to notify clients of a change in the value
|
|
235
|
-
|
|
236
|
-
:param uid: the uid of the variable
|
|
237
|
-
:param key: the key for the backend
|
|
238
|
-
:param backend: the backend instance
|
|
239
|
-
"""
|
|
240
|
-
from dara.core.internal.registries import utils_registry
|
|
241
|
-
|
|
242
|
-
ws_mgr: WebsocketManager = utils_registry.get('WebsocketManager')
|
|
243
|
-
|
|
244
|
-
message = ServerVariableMessage(uid=uid, sequence_number=await backend.get_sequence_number(key))
|
|
245
|
-
|
|
246
|
-
if backend.scope == 'global':
|
|
247
|
-
return await ws_mgr.broadcast(message)
|
|
248
|
-
|
|
249
|
-
user = USER.get()
|
|
250
|
-
assert user is not None, 'User not found when trying to send notification'
|
|
251
|
-
user_id = user.identity_id
|
|
252
|
-
return await ws_mgr.send_message_to_user(user_id, message)
|
|
253
|
-
|
|
254
|
-
def update(self, value: Any):
|
|
255
|
-
"""
|
|
256
|
-
Create an action to update the value of this Variable to a provided value.
|
|
257
|
-
|
|
258
|
-
```python
|
|
259
|
-
import pandas as pd
|
|
260
|
-
from dara.core import ServerVariable
|
|
261
|
-
from dara.components import Button
|
|
262
|
-
|
|
263
|
-
data = ServerVariable(pd.DataFrame({'a': [1, 2, 3]}))
|
|
264
|
-
|
|
265
|
-
Button(
|
|
266
|
-
'Empty Data',
|
|
267
|
-
onclick=data.update(None),
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
"""
|
|
272
|
-
from dara.core.interactivity.actions import UpdateVariableImpl
|
|
273
|
-
|
|
274
|
-
return UpdateVariableImpl(variable=self, value=value)
|
|
275
|
-
|
|
276
|
-
def reset(self):
|
|
277
|
-
raise NotImplementedError('ServerVariable does not support reset')
|
|
278
|
-
|
|
279
|
-
async def read(self):
|
|
280
|
-
"""
|
|
281
|
-
Read the current value from the backend.
|
|
282
|
-
Depending on the scope, the value will be global or user-specific.
|
|
283
|
-
"""
|
|
284
|
-
return await self.backend.read(self.key)
|
|
285
|
-
|
|
286
|
-
async def write(self, value: Any):
|
|
287
|
-
"""
|
|
288
|
-
Write a new value to the backend.
|
|
289
|
-
Depending on the scope, the value will be global or user-specific.
|
|
290
|
-
|
|
291
|
-
:param value: the new value to write
|
|
292
|
-
"""
|
|
293
|
-
value = await self.backend.write(self.key, value)
|
|
294
|
-
await self._notify(self.uid, self.key, self.backend)
|
|
295
|
-
return value
|
|
296
|
-
|
|
297
|
-
async def read_filtered(
|
|
298
|
-
self, filters: Optional[Union[FilterQuery, dict]] = None, pagination: Optional[Pagination] = None
|
|
299
|
-
):
|
|
300
|
-
"""
|
|
301
|
-
Read a filtered value from the backend.
|
|
302
|
-
Depending on the scope, the value will be global or user-specific.
|
|
303
|
-
|
|
304
|
-
:param filters: the filters to apply
|
|
305
|
-
:param pagination: the pagination to apply
|
|
306
|
-
"""
|
|
307
|
-
return await self.backend.read_filtered(self.key, filters, pagination)
|
|
308
|
-
|
|
309
|
-
@model_serializer(mode='wrap')
|
|
310
|
-
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
311
|
-
parent_dict = nxt(self)
|
|
312
|
-
return {**parent_dict, '__typename': 'ServerVariable', 'uid': str(parent_dict['uid'])}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
class ServerVariableRegistryEntry(CachedRegistryEntry):
|
|
316
|
-
"""
|
|
317
|
-
Registry entry for ServerVariable.
|
|
318
|
-
"""
|
|
319
|
-
|
|
320
|
-
backend: ServerBackend
|
|
321
|
-
"""
|
|
322
|
-
Backend instance
|
|
323
|
-
"""
|
|
324
|
-
|
|
325
|
-
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Copyright 2023 Impulse Innovations Limited
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
you may not use this file except in compliance with the License.
|
|
7
|
-
You may obtain a copy of the License at
|
|
8
|
-
|
|
9
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
|
|
11
|
-
Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
See the License for the specific language governing permissions and
|
|
15
|
-
limitations under the License.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
from typing import TYPE_CHECKING, Optional
|
|
21
|
-
|
|
22
|
-
from pydantic import SerializerFunctionWrapHandler, model_serializer
|
|
23
|
-
from typing_extensions import Literal
|
|
24
|
-
|
|
25
|
-
from dara.core.interactivity.client_variable import ClientVariable
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from dara.core.interactivity.derived_variable import DerivedVariable
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class StateVariable(ClientVariable):
|
|
32
|
-
"""
|
|
33
|
-
A StateVariable is an internal variable type used to track client-side state of other variables.
|
|
34
|
-
It is not meant to be created directly by users, but rather returned by properties like
|
|
35
|
-
DerivedVariable.is_loading, DerivedVariable.has_error, etc.
|
|
36
|
-
|
|
37
|
-
This variable tracks the state of a parent DerivedVariable and maps to specific properties
|
|
38
|
-
like loading state, error state, etc.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
parent_variable: 'DerivedVariable'
|
|
42
|
-
property_name: Literal['loading', 'error', 'hasValue']
|
|
43
|
-
|
|
44
|
-
def __init__(
|
|
45
|
-
self,
|
|
46
|
-
parent_variable: 'DerivedVariable',
|
|
47
|
-
property_name: Literal['loading', 'error', 'hasValue'],
|
|
48
|
-
uid: Optional[str] = None,
|
|
49
|
-
**kwargs,
|
|
50
|
-
):
|
|
51
|
-
"""
|
|
52
|
-
Initialize a StateVariable.
|
|
53
|
-
|
|
54
|
-
:param parent_variable: The DerivedVariable this StateVariable tracks
|
|
55
|
-
:param property_name: The property name this StateVariable represents ('loading', 'error', etc.)
|
|
56
|
-
:param uid: Optional unique identifier; if not provided, one is generated
|
|
57
|
-
"""
|
|
58
|
-
super().__init__(uid=uid, parent_variable=parent_variable, property_name=property_name, **kwargs)
|
|
59
|
-
|
|
60
|
-
@model_serializer(mode='wrap')
|
|
61
|
-
def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
|
|
62
|
-
parent_dict = nxt(self)
|
|
63
|
-
return {
|
|
64
|
-
**parent_dict,
|
|
65
|
-
'__typename': 'StateVariable',
|
|
66
|
-
'uid': str(parent_dict['uid']),
|
|
67
|
-
'parent_variable': self.parent_variable,
|
|
68
|
-
'property_name': self.property_name,
|
|
69
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Copyright 2023 Impulse Innovations Limited
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
you may not use this file except in compliance with the License.
|
|
7
|
-
You may obtain a copy of the License at
|
|
8
|
-
|
|
9
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
|
|
11
|
-
Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
See the License for the specific language governing permissions and
|
|
15
|
-
limitations under the License.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import io
|
|
19
|
-
import os
|
|
20
|
-
from typing import Literal, Optional, TypedDict, Union, cast
|
|
21
|
-
|
|
22
|
-
import pandas
|
|
23
|
-
from fastapi import UploadFile
|
|
24
|
-
|
|
25
|
-
from dara.core.base_definitions import UploadResolverDef
|
|
26
|
-
from dara.core.internal.registry_lookup import RegistryLookup
|
|
27
|
-
from dara.core.internal.utils import run_user_handler
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class FieldType(TypedDict):
|
|
31
|
-
name: Union[str, tuple[str, ...]]
|
|
32
|
-
type: Literal['integer', 'number', 'boolean', 'datetime', 'duration', 'any', 'str']
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class DataFrameSchema(TypedDict):
|
|
36
|
-
fields: list[FieldType]
|
|
37
|
-
primaryKey: list[str]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
async def upload(data: UploadFile, data_uid: Optional[str] = None, resolver_id: Optional[str] = None):
|
|
41
|
-
"""
|
|
42
|
-
Handler for uploading data.
|
|
43
|
-
|
|
44
|
-
:param data: the file to upload
|
|
45
|
-
:param data_uid: optional uid of the data variable to upload to
|
|
46
|
-
:param resolver_id: optional id of the upload resolver to use, falls back to default handlers for csv/xlsx
|
|
47
|
-
"""
|
|
48
|
-
from dara.core.interactivity.server_variable import ServerVariable
|
|
49
|
-
from dara.core.internal.registries import (
|
|
50
|
-
server_variable_registry,
|
|
51
|
-
upload_resolver_registry,
|
|
52
|
-
utils_registry,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
56
|
-
|
|
57
|
-
if data.filename is None:
|
|
58
|
-
raise ValueError('Filename not provided')
|
|
59
|
-
|
|
60
|
-
variable_entry = None
|
|
61
|
-
|
|
62
|
-
_name, file_type = os.path.splitext(data.filename)
|
|
63
|
-
|
|
64
|
-
if data_uid is not None:
|
|
65
|
-
try:
|
|
66
|
-
variable_entry = await registry_mgr.get(server_variable_registry, data_uid)
|
|
67
|
-
except KeyError as e:
|
|
68
|
-
raise ValueError(f'Data Variable {data_uid} does not exist') from e
|
|
69
|
-
|
|
70
|
-
content = cast(bytes, await data.read())
|
|
71
|
-
|
|
72
|
-
resolver = None
|
|
73
|
-
|
|
74
|
-
# If Id is provided, lookup the definition from registry
|
|
75
|
-
if resolver_id is not None:
|
|
76
|
-
resolver_def: UploadResolverDef = await registry_mgr.get(upload_resolver_registry, resolver_id)
|
|
77
|
-
resolver = resolver_def.resolver
|
|
78
|
-
|
|
79
|
-
if resolver:
|
|
80
|
-
content = await run_user_handler(handler=resolver, args=(content, data.filename))
|
|
81
|
-
# If resolver is not provided, follow roughly the cl_dataset_parser logic
|
|
82
|
-
elif file_type == '.xlsx':
|
|
83
|
-
file_object_xlsx = io.BytesIO(content)
|
|
84
|
-
content = pandas.read_excel(file_object_xlsx, index_col=None)
|
|
85
|
-
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
86
|
-
else:
|
|
87
|
-
# default to csv
|
|
88
|
-
file_object_csv = io.StringIO(content.decode('utf-8'))
|
|
89
|
-
content = pandas.read_csv(file_object_csv, index_col=0)
|
|
90
|
-
content.columns = content.columns.str.replace('Unnamed: *', 'column_', regex=True) # type: ignore
|
|
91
|
-
|
|
92
|
-
# If a server variable is provided, update it with the new content
|
|
93
|
-
if variable_entry:
|
|
94
|
-
await ServerVariable.write_value(variable_entry, content)
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
from collections import Counter
|
|
2
|
-
from contextlib import asynccontextmanager
|
|
3
|
-
|
|
4
|
-
import anyio
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class MultiResourceLock:
|
|
8
|
-
"""
|
|
9
|
-
A class that manages multiple named locks for concurrent access to shared resources.
|
|
10
|
-
|
|
11
|
-
This class allows for acquiring and releasing locks on named resources, ensuring
|
|
12
|
-
that only one task can access a specific resource at a time. It automatically
|
|
13
|
-
creates locks for new resources and cleans them up when they're no longer in use.
|
|
14
|
-
|
|
15
|
-
:reentrant:
|
|
16
|
-
If True a task can acquire the same resource more than once; every
|
|
17
|
-
subsequent acquire of an already-held lock is a no-op. If False the
|
|
18
|
-
second attempt raises ``RuntimeError``.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self):
|
|
22
|
-
self._locks: dict[str, anyio.Lock] = {}
|
|
23
|
-
self._waiters = Counter[str]()
|
|
24
|
-
self._cleanup_lock = anyio.Lock()
|
|
25
|
-
|
|
26
|
-
def is_locked(self, resource_name: str) -> bool:
|
|
27
|
-
"""
|
|
28
|
-
Check if a lock for the specified resource is currently held.
|
|
29
|
-
|
|
30
|
-
:param resource_name (str): The name of the resource to check.
|
|
31
|
-
:return: True if the lock is held, False otherwise.
|
|
32
|
-
"""
|
|
33
|
-
return resource_name in self._locks and self._locks[resource_name].locked()
|
|
34
|
-
|
|
35
|
-
@asynccontextmanager
|
|
36
|
-
async def acquire(self, resource_name: str):
|
|
37
|
-
"""
|
|
38
|
-
Acquire a lock for the specified resource.
|
|
39
|
-
|
|
40
|
-
This method is an async context manager that acquires a lock for the given
|
|
41
|
-
resource name. If the lock doesn't exist, it creates one. It also keeps
|
|
42
|
-
track of waiters to ensure proper cleanup when the resource is no longer in use.
|
|
43
|
-
|
|
44
|
-
:param resource_name (str): The name of the resource to lock.
|
|
45
|
-
|
|
46
|
-
Usage:
|
|
47
|
-
```python
|
|
48
|
-
async with multi_lock.acquire_lock("resource_a"):
|
|
49
|
-
# Critical section for "resource_a"
|
|
50
|
-
...
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Note:
|
|
54
|
-
The lock is automatically released when exiting the context manager.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
async with self._cleanup_lock:
|
|
58
|
-
if resource_name not in self._locks:
|
|
59
|
-
self._locks[resource_name] = anyio.Lock()
|
|
60
|
-
self._waiters[resource_name] += 1
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
async with self._locks[resource_name]:
|
|
64
|
-
yield
|
|
65
|
-
finally:
|
|
66
|
-
async with self._cleanup_lock:
|
|
67
|
-
self._waiters[resource_name] -= 1
|
|
68
|
-
if self._waiters[resource_name] <= 0:
|
|
69
|
-
del self._waiters[resource_name]
|
|
70
|
-
del self._locks[resource_name]
|