dara-core 1.20.1a1__py3-none-any.whl → 1.20.1a3__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.
Files changed (82) hide show
  1. dara/core/__init__.py +3 -0
  2. dara/core/actions.py +1 -2
  3. dara/core/auth/basic.py +22 -16
  4. dara/core/auth/definitions.py +2 -2
  5. dara/core/auth/routes.py +5 -5
  6. dara/core/auth/utils.py +5 -5
  7. dara/core/base_definitions.py +22 -64
  8. dara/core/cli.py +8 -7
  9. dara/core/configuration.py +5 -2
  10. dara/core/css.py +1 -2
  11. dara/core/data_utils.py +18 -19
  12. dara/core/defaults.py +6 -7
  13. dara/core/definitions.py +50 -19
  14. dara/core/http.py +7 -3
  15. dara/core/interactivity/__init__.py +6 -0
  16. dara/core/interactivity/actions.py +52 -50
  17. dara/core/interactivity/any_data_variable.py +7 -134
  18. dara/core/interactivity/any_variable.py +5 -8
  19. dara/core/interactivity/client_variable.py +71 -0
  20. dara/core/interactivity/data_variable.py +8 -266
  21. dara/core/interactivity/derived_data_variable.py +7 -290
  22. dara/core/interactivity/derived_variable.py +416 -176
  23. dara/core/interactivity/filtering.py +46 -27
  24. dara/core/interactivity/loop_variable.py +2 -2
  25. dara/core/interactivity/non_data_variable.py +5 -68
  26. dara/core/interactivity/plain_variable.py +89 -15
  27. dara/core/interactivity/server_variable.py +325 -0
  28. dara/core/interactivity/state_variable.py +69 -0
  29. dara/core/interactivity/switch_variable.py +19 -19
  30. dara/core/interactivity/tabular_variable.py +94 -0
  31. dara/core/interactivity/url_variable.py +10 -90
  32. dara/core/internal/cache_store/base_impl.py +2 -1
  33. dara/core/internal/cache_store/cache_store.py +22 -25
  34. dara/core/internal/cache_store/keep_all.py +4 -1
  35. dara/core/internal/cache_store/lru.py +5 -1
  36. dara/core/internal/cache_store/ttl.py +4 -1
  37. dara/core/internal/cgroup.py +1 -1
  38. dara/core/internal/dependency_resolution.py +60 -66
  39. dara/core/internal/devtools.py +12 -5
  40. dara/core/internal/download.py +13 -4
  41. dara/core/internal/encoder_registry.py +7 -7
  42. dara/core/internal/execute_action.py +13 -13
  43. dara/core/internal/hashing.py +1 -3
  44. dara/core/internal/import_discovery.py +3 -4
  45. dara/core/internal/multi_resource_lock.py +70 -0
  46. dara/core/internal/normalization.py +9 -18
  47. dara/core/internal/pandas_utils.py +107 -5
  48. dara/core/internal/pool/definitions.py +1 -1
  49. dara/core/internal/pool/task_pool.py +25 -16
  50. dara/core/internal/pool/utils.py +21 -18
  51. dara/core/internal/pool/worker.py +3 -2
  52. dara/core/internal/port_utils.py +1 -1
  53. dara/core/internal/registries.py +12 -6
  54. dara/core/internal/registry.py +4 -2
  55. dara/core/internal/registry_lookup.py +11 -5
  56. dara/core/internal/routing.py +109 -145
  57. dara/core/internal/scheduler.py +13 -8
  58. dara/core/internal/settings.py +2 -2
  59. dara/core/internal/store.py +2 -29
  60. dara/core/internal/tasks.py +379 -195
  61. dara/core/internal/utils.py +36 -13
  62. dara/core/internal/websocket.py +21 -20
  63. dara/core/js_tooling/js_utils.py +28 -26
  64. dara/core/js_tooling/templates/vite.config.template.ts +12 -3
  65. dara/core/logging.py +13 -12
  66. dara/core/main.py +14 -11
  67. dara/core/metrics/cache.py +1 -1
  68. dara/core/metrics/utils.py +3 -3
  69. dara/core/persistence.py +27 -5
  70. dara/core/umd/dara.core.umd.js +68291 -64718
  71. dara/core/visual/components/__init__.py +2 -2
  72. dara/core/visual/components/fallback.py +30 -4
  73. dara/core/visual/components/for_cmp.py +4 -1
  74. dara/core/visual/css/__init__.py +30 -31
  75. dara/core/visual/dynamic_component.py +31 -28
  76. dara/core/visual/progress_updater.py +4 -3
  77. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/METADATA +12 -11
  78. dara_core-1.20.1a3.dist-info/RECORD +119 -0
  79. dara_core-1.20.1a1.dist-info/RECORD +0 -114
  80. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/LICENSE +0 -0
  81. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/WHEEL +0 -0
  82. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,325 @@
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)
@@ -0,0 +1,69 @@
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
+ }
@@ -21,11 +21,11 @@ from typing import Any, Dict, Optional, Union
21
21
 
22
22
  from pydantic import SerializerFunctionWrapHandler, field_validator, model_serializer
23
23
 
24
+ from dara.core.interactivity.client_variable import ClientVariable
24
25
  from dara.core.interactivity.condition import Condition
25
- from dara.core.interactivity.non_data_variable import NonDataVariable
26
26
 
27
27
 
28
- class SwitchVariable(NonDataVariable):
28
+ class SwitchVariable(ClientVariable):
29
29
  """
30
30
  A SwitchVariable represents a conditional value that switches between
31
31
  different values based on a condition or variable value.
@@ -216,21 +216,21 @@ class SwitchVariable(NonDataVariable):
216
216
  Key Serialization:
217
217
  When using mappings with SwitchVariable, be aware that JavaScript object keys
218
218
  are always strings. The system automatically converts lookup keys to strings:
219
- - Python: {True: 'admin', False: 'user'}
220
- - JavaScript: {"true": "admin", "false": "user"}
219
+ - Python: `{True: 'admin', False: 'user'}`
220
+ - JavaScript: `{"true": "admin", "false": "user"}`
221
221
  - Boolean values are converted to lowercase strings ("true"/"false")
222
222
  - Other values use standard string conversion to match JavaScript's String() behavior
223
223
  """
224
224
 
225
- value: Optional[Union[Condition, NonDataVariable, Any]] = None
225
+ value: Optional[Union[Condition, ClientVariable, Any]] = None
226
226
  # must be typed as any, otherwise pydantic is trying to instantiate the variables incorrectly
227
227
  value_map: Optional[Any] = None
228
228
  default: Optional[Any] = None
229
229
 
230
230
  def __init__(
231
231
  self,
232
- value: Union[Condition, NonDataVariable, Any],
233
- value_map: Dict[Any, Any] | NonDataVariable,
232
+ value: Union[Condition, ClientVariable, Any],
233
+ value_map: Dict[Any, Any] | ClientVariable,
234
234
  default: Optional[Any] = None,
235
235
  uid: Optional[str] = None,
236
236
  ):
@@ -253,28 +253,28 @@ class SwitchVariable(NonDataVariable):
253
253
  @classmethod
254
254
  def validate_value_map(cls, v):
255
255
  """
256
- Validate that value_map is either a dict or a NonDataVariable.
256
+ Validate that value_map is either a dict or a ClientVariable.
257
257
 
258
258
  :param v: The value to validate
259
259
  :return: The validated value
260
- :raises ValueError: If value_map is not a dict or NonDataVariable
260
+ :raises ValueError: If value_map is not a dict or ClientVariable
261
261
  """
262
262
  if v is None:
263
263
  return v
264
264
  if isinstance(v, dict):
265
265
  return v
266
- if isinstance(v, NonDataVariable):
266
+ if isinstance(v, ClientVariable):
267
267
  return v
268
- raise ValueError(f'value_map must be a dict or NonDataVariable, got {type(v)}')
268
+ raise ValueError(f'value_map must be a dict or ClientVariable, got {type(v)}')
269
269
 
270
270
  @classmethod
271
271
  def when(
272
272
  cls,
273
- condition: Union[Condition, NonDataVariable, Any],
274
- true_value: Union[Any, NonDataVariable],
275
- false_value: Union[Any, NonDataVariable],
273
+ condition: Union[Condition, ClientVariable, Any],
274
+ true_value: Union[Any, ClientVariable],
275
+ false_value: Union[Any, ClientVariable],
276
276
  uid: Optional[str] = None,
277
- ) -> 'SwitchVariable':
277
+ ) -> SwitchVariable:
278
278
  """
279
279
  Create a SwitchVariable for boolean conditions.
280
280
 
@@ -346,11 +346,11 @@ class SwitchVariable(NonDataVariable):
346
346
  @classmethod
347
347
  def match(
348
348
  cls,
349
- value: Union[NonDataVariable, Any],
350
- mapping: Union[Dict[Any, Any], NonDataVariable],
351
- default: Optional[Union[Any, NonDataVariable]] = None,
349
+ value: Union[ClientVariable, Any],
350
+ mapping: Union[Dict[Any, Any], ClientVariable],
351
+ default: Optional[Union[Any, ClientVariable]] = None,
352
352
  uid: Optional[str] = None,
353
- ) -> 'SwitchVariable':
353
+ ) -> SwitchVariable:
354
354
  """
355
355
  Create a SwitchVariable with a custom mapping.
356
356
 
@@ -0,0 +1,94 @@
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)