dara-core 1.20.3__py3-none-any.whl → 1.21.1__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 +8 -42
- dara/core/configuration.py +33 -4
- dara/core/defaults.py +7 -0
- dara/core/definitions.py +22 -35
- dara/core/interactivity/actions.py +29 -28
- dara/core/interactivity/plain_variable.py +6 -2
- dara/core/interactivity/switch_variable.py +2 -2
- dara/core/internal/execute_action.py +75 -6
- dara/core/internal/routing.py +526 -354
- dara/core/internal/tasks.py +1 -1
- dara/core/jinja/index.html +97 -1
- dara/core/jinja/index_autojs.html +116 -10
- dara/core/js_tooling/js_utils.py +35 -14
- dara/core/main.py +137 -89
- dara/core/persistence.py +6 -2
- dara/core/router/__init__.py +5 -0
- dara/core/router/compat.py +77 -0
- dara/core/router/components.py +143 -0
- dara/core/router/dependency_graph.py +62 -0
- dara/core/router/router.py +887 -0
- dara/core/umd/{dara.core.umd.js → dara.core.umd.cjs} +62588 -46966
- dara/core/umd/style.css +52 -9
- dara/core/visual/components/__init__.py +16 -11
- dara/core/visual/components/menu.py +4 -0
- dara/core/visual/components/menu_link.py +1 -0
- dara/core/visual/components/powered_by_causalens.py +9 -0
- dara/core/visual/components/sidebar_frame.py +1 -0
- dara/core/visual/dynamic_component.py +1 -1
- {dara_core-1.20.3.dist-info → dara_core-1.21.1.dist-info}/METADATA +10 -10
- {dara_core-1.20.3.dist-info → dara_core-1.21.1.dist-info}/RECORD +33 -26
- {dara_core-1.20.3.dist-info → dara_core-1.21.1.dist-info}/LICENSE +0 -0
- {dara_core-1.20.3.dist-info → dara_core-1.21.1.dist-info}/WHEEL +0 -0
- {dara_core-1.20.3.dist-info → dara_core-1.21.1.dist-info}/entry_points.txt +0 -0
dara/core/internal/routing.py
CHANGED
|
@@ -16,27 +16,34 @@ limitations under the License.
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import inspect
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
19
21
|
import os
|
|
22
|
+
import traceback
|
|
20
23
|
from collections.abc import Mapping
|
|
21
24
|
from functools import wraps
|
|
22
25
|
from importlib.metadata import version
|
|
23
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
26
|
+
from typing import Annotated, Any, Callable, Dict, List, Literal, Optional, Union
|
|
27
|
+
from urllib.parse import unquote
|
|
24
28
|
|
|
25
29
|
import anyio
|
|
30
|
+
from anyio.streams.memory import MemoryObjectSendStream
|
|
26
31
|
from fastapi import (
|
|
27
32
|
APIRouter,
|
|
28
33
|
Body,
|
|
29
34
|
Depends,
|
|
35
|
+
FastAPI,
|
|
30
36
|
File,
|
|
31
37
|
Form,
|
|
32
38
|
HTTPException,
|
|
39
|
+
Path,
|
|
33
40
|
Response,
|
|
34
41
|
UploadFile,
|
|
35
42
|
)
|
|
36
43
|
from fastapi.encoders import jsonable_encoder
|
|
37
44
|
from fastapi.responses import StreamingResponse
|
|
38
45
|
from pandas import DataFrame
|
|
39
|
-
from pydantic import BaseModel
|
|
46
|
+
from pydantic import BaseModel, Field
|
|
40
47
|
from starlette.background import BackgroundTask
|
|
41
48
|
from starlette.status import HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
42
49
|
|
|
@@ -47,8 +54,9 @@ from dara.core.interactivity.any_data_variable import upload
|
|
|
47
54
|
from dara.core.interactivity.filtering import FilterQuery, Pagination
|
|
48
55
|
from dara.core.interactivity.server_variable import ServerVariable
|
|
49
56
|
from dara.core.internal.cache_store import CacheStore
|
|
57
|
+
from dara.core.internal.devtools import print_stacktrace
|
|
50
58
|
from dara.core.internal.download import DownloadRegistryEntry
|
|
51
|
-
from dara.core.internal.execute_action import CURRENT_ACTION_ID
|
|
59
|
+
from dara.core.internal.execute_action import CURRENT_ACTION_ID, execute_action_sync
|
|
52
60
|
from dara.core.internal.normalization import NormalizedPayload, denormalize, normalize
|
|
53
61
|
from dara.core.internal.pandas_utils import data_response_to_json, df_to_json, is_data_response
|
|
54
62
|
from dara.core.internal.registries import (
|
|
@@ -61,12 +69,10 @@ from dara.core.internal.registries import (
|
|
|
61
69
|
latest_value_registry,
|
|
62
70
|
server_variable_registry,
|
|
63
71
|
static_kwargs_registry,
|
|
64
|
-
template_registry,
|
|
65
72
|
upload_resolver_registry,
|
|
66
73
|
utils_registry,
|
|
67
74
|
)
|
|
68
75
|
from dara.core.internal.registry_lookup import RegistryLookup
|
|
69
|
-
from dara.core.internal.settings import get_settings
|
|
70
76
|
from dara.core.internal.tasks import TaskManager, TaskManagerError
|
|
71
77
|
from dara.core.internal.utils import get_cache_scope
|
|
72
78
|
from dara.core.internal.websocket import WS_CHANNEL, ws_handler
|
|
@@ -106,419 +112,585 @@ def error_decorator(handler: Callable[..., Any]):
|
|
|
106
112
|
return _inner_func
|
|
107
113
|
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
"""
|
|
111
|
-
Create the main Dara core API router
|
|
115
|
+
core_api_router = APIRouter()
|
|
112
116
|
|
|
113
|
-
:param config: Dara app configuration
|
|
114
|
-
"""
|
|
115
|
-
core_api_router = APIRouter()
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
@core_api_router.get('/actions', dependencies=[Depends(verify_session)])
|
|
119
|
+
async def get_actions():
|
|
120
|
+
return action_def_registry.get_all().items()
|
|
120
121
|
|
|
121
|
-
class ActionRequestBody(BaseModel):
|
|
122
|
-
values: NormalizedPayload[Mapping[str, Any]]
|
|
123
|
-
"""Dynamic kwarg values"""
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
class ActionRequestBody(BaseModel):
|
|
124
|
+
values: NormalizedPayload[Mapping[str, Any]]
|
|
125
|
+
"""Dynamic kwarg values"""
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
input: Any = None
|
|
128
|
+
"""Input from the component"""
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
ws_channel: str
|
|
131
|
+
"""Websocket channel assigned to the client"""
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
uid: str
|
|
134
|
+
"""Instance uid"""
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
store: CacheStore = utils_registry.get('Store')
|
|
140
|
-
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
141
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
142
|
-
action_def: ActionResolverDef = await registry_mgr.get(action_registry, uid)
|
|
136
|
+
execution_id: str
|
|
137
|
+
"""Execution id, unique to this request"""
|
|
143
138
|
|
|
144
|
-
CURRENT_ACTION_ID.set(body.uid)
|
|
145
|
-
WS_CHANNEL.set(body.ws_channel)
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
@core_api_router.post('/action/{uid}', dependencies=[Depends(verify_session)])
|
|
141
|
+
async def get_action(uid: str, body: ActionRequestBody):
|
|
142
|
+
store: CacheStore = utils_registry.get('Store')
|
|
143
|
+
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
144
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
145
|
+
action_def: ActionResolverDef = await registry_mgr.get(action_registry, uid)
|
|
146
|
+
|
|
147
|
+
CURRENT_ACTION_ID.set(body.uid)
|
|
148
|
+
WS_CHANNEL.set(body.ws_channel)
|
|
149
|
+
|
|
150
|
+
# Denormalize the values
|
|
151
|
+
values = denormalize(body.values.data, body.values.lookup)
|
|
152
|
+
|
|
153
|
+
# Fetch static kwargs
|
|
154
|
+
static_kwargs = await registry_mgr.get(static_kwargs_registry, body.uid)
|
|
155
|
+
|
|
156
|
+
# Execute the action - kick off a background task to run the handler
|
|
157
|
+
response = await action_def.execute_action(
|
|
158
|
+
action_def,
|
|
159
|
+
body.input,
|
|
160
|
+
values,
|
|
161
|
+
static_kwargs,
|
|
162
|
+
body.execution_id,
|
|
163
|
+
body.ws_channel,
|
|
164
|
+
store,
|
|
165
|
+
task_mgr,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if isinstance(response, BaseTask):
|
|
169
|
+
await task_mgr.run_task(response, body.ws_channel)
|
|
170
|
+
return {'task_id': response.task_id}
|
|
171
|
+
|
|
172
|
+
return {'execution_id': response}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@core_api_router.get('/download') # explicitly unauthenticated
|
|
176
|
+
async def get_download(code: str):
|
|
177
|
+
store: CacheStore = utils_registry.get('Store')
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
data_entry = await store.get(DownloadRegistryEntry, key=code)
|
|
149
181
|
|
|
150
|
-
#
|
|
182
|
+
# If not found directly in the store, use the override registry
|
|
183
|
+
# to check if we can get the download entry from there
|
|
184
|
+
if data_entry is None:
|
|
185
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
186
|
+
# NOTE: This will throw a Value/KeyError if the code is not found so no need to rethrow
|
|
187
|
+
data_entry = await registry_mgr.get(download_code_registry, code)
|
|
188
|
+
# We managed to find one from the lookup,
|
|
189
|
+
# remove it from the registry immediately because it's one time use
|
|
190
|
+
download_code_registry.remove(code)
|
|
191
|
+
|
|
192
|
+
async_file, cleanup = await data_entry.download(data_entry)
|
|
193
|
+
|
|
194
|
+
file_name = os.path.basename(data_entry.file_path)
|
|
195
|
+
|
|
196
|
+
# This mirrors builtin's FastAPI FileResponse implementation
|
|
197
|
+
async def stream_file():
|
|
198
|
+
has_content = True
|
|
199
|
+
chunk_size = 64 * 1024
|
|
200
|
+
while has_content:
|
|
201
|
+
chunk = await async_file.read(chunk_size)
|
|
202
|
+
has_content = chunk_size == len(chunk)
|
|
203
|
+
yield chunk
|
|
204
|
+
|
|
205
|
+
return StreamingResponse(
|
|
206
|
+
content=stream_file(),
|
|
207
|
+
headers={'Content-Disposition': f'attachment; filename={file_name}'},
|
|
208
|
+
background=BackgroundTask(cleanup),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
except (KeyError, ValueError) as e:
|
|
212
|
+
raise ValueError('Invalid or expired download code') from e
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@core_api_router.get('/components/{name}/definition', dependencies=[Depends(verify_session)])
|
|
216
|
+
async def get_component_definition(name: str):
|
|
217
|
+
"""
|
|
218
|
+
Attempt to refetch a component definition from the backend.
|
|
219
|
+
This is used when a component isn't immediately available in the initial registry,
|
|
220
|
+
e.g. when it was added by a py_component.
|
|
221
|
+
|
|
222
|
+
:param name: the name of component
|
|
223
|
+
"""
|
|
224
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
225
|
+
component = await registry_mgr.get(component_registry, name)
|
|
226
|
+
return component.model_dump(exclude={'func'})
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ComponentRequestBody(BaseModel):
|
|
230
|
+
# Dynamic kwarg values
|
|
231
|
+
values: NormalizedPayload[Mapping[str, Any]]
|
|
232
|
+
# Instance uid
|
|
233
|
+
uid: str
|
|
234
|
+
# Websocket channel assigned to the client
|
|
235
|
+
ws_channel: str
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@core_api_router.post('/components/{component}', dependencies=[Depends(verify_session)])
|
|
239
|
+
async def get_component(component: str, body: ComponentRequestBody):
|
|
240
|
+
CURRENT_COMPONENT_ID.set(body.uid)
|
|
241
|
+
WS_CHANNEL.set(body.ws_channel)
|
|
242
|
+
store: CacheStore = utils_registry.get('Store')
|
|
243
|
+
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
244
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
245
|
+
comp_def = await registry_mgr.get(component_registry, component)
|
|
246
|
+
|
|
247
|
+
if isinstance(comp_def, PyComponentDef):
|
|
151
248
|
static_kwargs = await registry_mgr.get(static_kwargs_registry, body.uid)
|
|
249
|
+
values = denormalize(body.values.data, body.values.lookup)
|
|
152
250
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
body.execution_id,
|
|
160
|
-
body.ws_channel,
|
|
161
|
-
store,
|
|
162
|
-
task_mgr,
|
|
251
|
+
response = await comp_def.render_component(comp_def, store, task_mgr, values, static_kwargs)
|
|
252
|
+
|
|
253
|
+
dev_logger.debug(
|
|
254
|
+
f'PyComponent {comp_def.func.__name__ if comp_def.func else "anonymous"}',
|
|
255
|
+
'return value',
|
|
256
|
+
{'value': response},
|
|
163
257
|
)
|
|
164
258
|
|
|
165
259
|
if isinstance(response, BaseTask):
|
|
166
260
|
await task_mgr.run_task(response, body.ws_channel)
|
|
167
261
|
return {'task_id': response.task_id}
|
|
168
262
|
|
|
169
|
-
return
|
|
263
|
+
return response
|
|
264
|
+
|
|
265
|
+
raise HTTPException(status_code=400, detail='Requesting this type of component is not supported')
|
|
170
266
|
|
|
171
|
-
|
|
172
|
-
|
|
267
|
+
|
|
268
|
+
@core_api_router.get('/derived-variable/{uid}/latest', dependencies=[Depends(verify_session)])
|
|
269
|
+
async def get_latest_derived_variable(uid: str):
|
|
270
|
+
try:
|
|
173
271
|
store: CacheStore = utils_registry.get('Store')
|
|
272
|
+
latest_value_entry = latest_value_registry.get(uid)
|
|
273
|
+
variable_entry = derived_variable_registry.get(uid)
|
|
174
274
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# If not found directly in the store, use the override registry
|
|
179
|
-
# to check if we can get the download entry from there
|
|
180
|
-
if data_entry is None:
|
|
181
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
182
|
-
# NOTE: This will throw a Value/KeyError if the code is not found so no need to rethrow
|
|
183
|
-
data_entry = await registry_mgr.get(download_code_registry, code)
|
|
184
|
-
# We managed to find one from the lookup,
|
|
185
|
-
# remove it from the registry immediately because it's one time use
|
|
186
|
-
download_code_registry.remove(code)
|
|
187
|
-
|
|
188
|
-
async_file, cleanup = await data_entry.download(data_entry)
|
|
189
|
-
|
|
190
|
-
file_name = os.path.basename(data_entry.file_path)
|
|
191
|
-
|
|
192
|
-
# This mirrors builtin's FastAPI FileResponse implementation
|
|
193
|
-
async def stream_file():
|
|
194
|
-
has_content = True
|
|
195
|
-
chunk_size = 64 * 1024
|
|
196
|
-
while has_content:
|
|
197
|
-
chunk = await async_file.read(chunk_size)
|
|
198
|
-
has_content = chunk_size == len(chunk)
|
|
199
|
-
yield chunk
|
|
200
|
-
|
|
201
|
-
return StreamingResponse(
|
|
202
|
-
content=stream_file(),
|
|
203
|
-
headers={'Content-Disposition': f'attachment; filename={file_name}'},
|
|
204
|
-
background=BackgroundTask(cleanup),
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
except (KeyError, ValueError) as e:
|
|
208
|
-
raise ValueError('Invalid or expired download code') from e
|
|
209
|
-
|
|
210
|
-
@core_api_router.get('/config', dependencies=[Depends(verify_session)])
|
|
211
|
-
async def get_config():
|
|
212
|
-
return {
|
|
213
|
-
**config.model_dump(
|
|
214
|
-
include={
|
|
215
|
-
'enable_devtools',
|
|
216
|
-
'live_reload',
|
|
217
|
-
'template',
|
|
218
|
-
'theme',
|
|
219
|
-
'title',
|
|
220
|
-
'context_components',
|
|
221
|
-
'powered_by_causalens',
|
|
222
|
-
}
|
|
223
|
-
),
|
|
224
|
-
'application_name': get_settings().project_name,
|
|
225
|
-
}
|
|
275
|
+
# Lookup the latest key in the cache
|
|
276
|
+
scope = get_cache_scope(variable_entry.cache.cache_type if variable_entry.cache else None)
|
|
277
|
+
latest_key = await store.get(latest_value_entry, key=scope)
|
|
226
278
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
'auth_components': config.auth_config.component_config.model_dump(),
|
|
231
|
-
}
|
|
279
|
+
if latest_key is None:
|
|
280
|
+
return None
|
|
232
281
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"""
|
|
236
|
-
If name is passed, will try to register the component
|
|
282
|
+
# Lookup latest value for that key
|
|
283
|
+
latest_value = await store.get_or_wait(variable_entry, key=latest_key)
|
|
237
284
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
285
|
+
dev_logger.debug(
|
|
286
|
+
f'DerivedVariable {variable_entry.uid[:3]}..{variable_entry.uid[-3:]}',
|
|
287
|
+
'latest value',
|
|
288
|
+
{'value': latest_value, 'uid': uid},
|
|
289
|
+
)
|
|
290
|
+
return latest_value
|
|
291
|
+
|
|
292
|
+
except KeyError as err:
|
|
293
|
+
raise ValueError(f'Could not find latest value for derived variable with uid: {uid}') from err
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TabularRequestBody(BaseModel):
|
|
297
|
+
filters: Optional[FilterQuery] = None
|
|
298
|
+
ws_channel: str
|
|
299
|
+
dv_values: Optional[NormalizedPayload[List[Any]]] = None
|
|
300
|
+
"""DerivedVariable values if variable is a DerivedVariable"""
|
|
301
|
+
force_key: Optional[str] = None
|
|
302
|
+
"""Optional force key if variable is a DerivedVariable and a recalculation is forced"""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@core_api_router.post('/tabular-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
306
|
+
async def get_tabular_variable(
|
|
307
|
+
uid: str,
|
|
308
|
+
body: TabularRequestBody,
|
|
309
|
+
offset: Optional[int] = None,
|
|
310
|
+
limit: Optional[int] = None,
|
|
311
|
+
order_by: Optional[str] = None,
|
|
312
|
+
index: Optional[str] = None,
|
|
313
|
+
):
|
|
314
|
+
"""
|
|
315
|
+
Generic endpoint for getting tabular data from a variable.
|
|
316
|
+
Supports ServerVariables and DerivedVariables.
|
|
317
|
+
"""
|
|
318
|
+
WS_CHANNEL.set(body.ws_channel)
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
pagination = Pagination(offset=offset, limit=limit, orderBy=order_by, index=index)
|
|
322
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
323
|
+
|
|
324
|
+
# ServerVariable
|
|
325
|
+
if body.dv_values is None:
|
|
326
|
+
server_variable_entry = await registry_mgr.get(server_variable_registry, uid)
|
|
327
|
+
data_response = await ServerVariable.get_tabular_data(server_variable_entry, body.filters, pagination)
|
|
328
|
+
return Response(data_response_to_json(data_response), media_type='application/json')
|
|
329
|
+
|
|
330
|
+
# DerivedVariable
|
|
258
331
|
store: CacheStore = utils_registry.get('Store')
|
|
259
332
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
260
|
-
|
|
261
|
-
|
|
333
|
+
variable_def = await registry_mgr.get(derived_variable_registry, uid)
|
|
334
|
+
values = denormalize(body.dv_values.data, body.dv_values.lookup)
|
|
262
335
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
336
|
+
result = await variable_def.get_tabular_data(
|
|
337
|
+
variable_def, store, task_mgr, values, body.force_key, pagination, body.filters
|
|
338
|
+
)
|
|
266
339
|
|
|
267
|
-
|
|
340
|
+
if isinstance(result, BaseTask):
|
|
341
|
+
await task_mgr.run_task(result, body.ws_channel)
|
|
342
|
+
return {'task_id': result.task_id}
|
|
268
343
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{'value': response},
|
|
273
|
-
)
|
|
344
|
+
return Response(data_response_to_json(result), media_type='application/json')
|
|
345
|
+
except NonTabularDataError as e:
|
|
346
|
+
raise HTTPException(status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail=str(e)) from e
|
|
274
347
|
|
|
275
|
-
if isinstance(response, BaseTask):
|
|
276
|
-
await task_mgr.run_task(response, body.ws_channel)
|
|
277
|
-
return {'task_id': response.task_id}
|
|
278
348
|
|
|
279
|
-
|
|
349
|
+
@core_api_router.get('/server-variable/{uid}/sequence', dependencies=[Depends(verify_session)])
|
|
350
|
+
async def get_server_variable_sequence(
|
|
351
|
+
uid: str,
|
|
352
|
+
):
|
|
353
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
354
|
+
server_variable_entry = await registry_mgr.get(server_variable_registry, uid)
|
|
355
|
+
seq_num = await ServerVariable.get_sequence_number(server_variable_entry)
|
|
356
|
+
return {'sequence_number': seq_num}
|
|
280
357
|
|
|
281
|
-
raise HTTPException(status_code=400, detail='Requesting this type of component is not supported')
|
|
282
358
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return None
|
|
296
|
-
|
|
297
|
-
# Lookup latest value for that key
|
|
298
|
-
latest_value = await store.get_or_wait(variable_entry, key=latest_key)
|
|
299
|
-
|
|
300
|
-
dev_logger.debug(
|
|
301
|
-
f'DerivedVariable {variable_entry.uid[:3]}..{variable_entry.uid[-3:]}',
|
|
302
|
-
'latest value',
|
|
303
|
-
{'value': latest_value, 'uid': uid},
|
|
304
|
-
)
|
|
305
|
-
return latest_value
|
|
306
|
-
|
|
307
|
-
except KeyError as err:
|
|
308
|
-
raise ValueError(f'Could not find latest value for derived variable with uid: {uid}') from err
|
|
309
|
-
|
|
310
|
-
class TabularRequestBody(BaseModel):
|
|
311
|
-
filters: Optional[FilterQuery] = None
|
|
312
|
-
ws_channel: str
|
|
313
|
-
dv_values: Optional[NormalizedPayload[List[Any]]] = None
|
|
314
|
-
"""DerivedVariable values if variable is a DerivedVariable"""
|
|
315
|
-
force_key: Optional[str] = None
|
|
316
|
-
"""Optional force key if variable is a DerivedVariable and a recalculation is forced"""
|
|
317
|
-
|
|
318
|
-
@core_api_router.post('/tabular-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
319
|
-
async def get_tabular_variable(
|
|
320
|
-
uid: str,
|
|
321
|
-
body: TabularRequestBody,
|
|
322
|
-
offset: Optional[int] = None,
|
|
323
|
-
limit: Optional[int] = None,
|
|
324
|
-
order_by: Optional[str] = None,
|
|
325
|
-
index: Optional[str] = None,
|
|
326
|
-
):
|
|
327
|
-
"""
|
|
328
|
-
Generic endpoint for getting tabular data from a variable.
|
|
329
|
-
Supports ServerVariables and DerivedVariables.
|
|
330
|
-
"""
|
|
331
|
-
WS_CHANNEL.set(body.ws_channel)
|
|
359
|
+
@core_api_router.post('/data/upload', dependencies=[Depends(verify_session)])
|
|
360
|
+
async def upload_data(
|
|
361
|
+
data_uid: Optional[str] = None,
|
|
362
|
+
data: UploadFile = File(),
|
|
363
|
+
resolver_id: Optional[str] = Form(default=None),
|
|
364
|
+
):
|
|
365
|
+
"""
|
|
366
|
+
Upload endpoint.
|
|
367
|
+
Can run a custom resolver_id (if previously registered, otherwise runs a default one)
|
|
368
|
+
and update a data variable with its return value (if target is specified).
|
|
369
|
+
"""
|
|
370
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
332
371
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
372
|
+
if data_uid is None and resolver_id is None:
|
|
373
|
+
raise HTTPException(
|
|
374
|
+
400,
|
|
375
|
+
'Neither resolver_id or data_uid specified, at least one of them is required',
|
|
376
|
+
)
|
|
336
377
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
378
|
+
try:
|
|
379
|
+
# If resolver id is provided, run the custom
|
|
380
|
+
if resolver_id:
|
|
381
|
+
upload_resolver_def: UploadResolverDef = await registry_mgr.get(upload_resolver_registry, resolver_id)
|
|
382
|
+
await upload_resolver_def.upload(data, data_uid, resolver_id)
|
|
383
|
+
else:
|
|
384
|
+
# Run the default logic as a fallback, e.g. programmatic upload
|
|
385
|
+
await upload(data, data_uid, resolver_id)
|
|
342
386
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
variable_def = await registry_mgr.get(derived_variable_registry, uid)
|
|
347
|
-
values = denormalize(body.dv_values.data, body.dv_values.lookup)
|
|
387
|
+
return {'status': 'SUCCESS'}
|
|
388
|
+
except Exception as e:
|
|
389
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
348
390
|
|
|
349
|
-
result = await variable_def.get_tabular_data(
|
|
350
|
-
variable_def, store, task_mgr, values, body.force_key, pagination, body.filters
|
|
351
|
-
)
|
|
352
391
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
392
|
+
class DerivedStateRequestBody(BaseModel):
|
|
393
|
+
values: NormalizedPayload[List[Any]]
|
|
394
|
+
force_key: Optional[str] = None
|
|
395
|
+
ws_channel: str
|
|
356
396
|
|
|
357
|
-
return Response(data_response_to_json(result), media_type='application/json')
|
|
358
|
-
except NonTabularDataError as e:
|
|
359
|
-
raise HTTPException(status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail=str(e)) from e
|
|
360
397
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
seq_num = await ServerVariable.get_sequence_number(server_variable_entry)
|
|
368
|
-
return {'sequence_number': seq_num}
|
|
369
|
-
|
|
370
|
-
@core_api_router.post('/data/upload', dependencies=[Depends(verify_session)])
|
|
371
|
-
async def upload_data(
|
|
372
|
-
data_uid: Optional[str] = None,
|
|
373
|
-
data: UploadFile = File(),
|
|
374
|
-
resolver_id: Optional[str] = Form(default=None),
|
|
375
|
-
):
|
|
376
|
-
"""
|
|
377
|
-
Upload endpoint.
|
|
378
|
-
Can run a custom resolver_id (if previously registered, otherwise runs a default one)
|
|
379
|
-
and update a data variable with its return value (if target is specified).
|
|
380
|
-
"""
|
|
381
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
398
|
+
@core_api_router.post('/derived-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
399
|
+
async def get_derived_variable(uid: str, body: DerivedStateRequestBody):
|
|
400
|
+
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
401
|
+
store: CacheStore = utils_registry.get('Store')
|
|
402
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
403
|
+
variable_def = await registry_mgr.get(derived_variable_registry, uid)
|
|
382
404
|
|
|
383
|
-
|
|
384
|
-
raise HTTPException(
|
|
385
|
-
400,
|
|
386
|
-
'Neither resolver_id or data_uid specified, at least one of them is required',
|
|
387
|
-
)
|
|
405
|
+
values = denormalize(body.values.data, body.values.lookup)
|
|
388
406
|
|
|
389
|
-
|
|
390
|
-
# If resolver id is provided, run the custom
|
|
391
|
-
if resolver_id:
|
|
392
|
-
upload_resolver_def: UploadResolverDef = await registry_mgr.get(upload_resolver_registry, resolver_id)
|
|
393
|
-
await upload_resolver_def.upload(data, data_uid, resolver_id)
|
|
394
|
-
else:
|
|
395
|
-
# Run the default logic as a fallback, e.g. programmatic upload
|
|
396
|
-
await upload(data, data_uid, resolver_id)
|
|
397
|
-
|
|
398
|
-
return {'status': 'SUCCESS'}
|
|
399
|
-
except Exception as e:
|
|
400
|
-
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
401
|
-
|
|
402
|
-
class DerivedStateRequestBody(BaseModel):
|
|
403
|
-
values: NormalizedPayload[List[Any]]
|
|
404
|
-
force_key: Optional[str] = None
|
|
405
|
-
ws_channel: str
|
|
406
|
-
|
|
407
|
-
@core_api_router.post('/derived-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
408
|
-
async def get_derived_variable(uid: str, body: DerivedStateRequestBody):
|
|
409
|
-
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
410
|
-
store: CacheStore = utils_registry.get('Store')
|
|
411
|
-
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
412
|
-
variable_def = await registry_mgr.get(derived_variable_registry, uid)
|
|
407
|
+
result = await variable_def.get_value(variable_def, store, task_mgr, values, body.force_key)
|
|
413
408
|
|
|
414
|
-
|
|
409
|
+
response: Any = result
|
|
415
410
|
|
|
416
|
-
|
|
411
|
+
WS_CHANNEL.set(body.ws_channel)
|
|
417
412
|
|
|
418
|
-
|
|
413
|
+
if isinstance(result['value'], BaseTask):
|
|
414
|
+
# Kick off the task
|
|
415
|
+
await task_mgr.run_task(result['value'], body.ws_channel)
|
|
416
|
+
response = {
|
|
417
|
+
'task_id': result['value'].task_id,
|
|
418
|
+
'cache_key': result['cache_key'],
|
|
419
|
+
}
|
|
419
420
|
|
|
420
|
-
|
|
421
|
+
dev_logger.debug(
|
|
422
|
+
f'DerivedVariable {variable_def.uid[:3]}..{variable_def.uid[-3:]}',
|
|
423
|
+
'return value',
|
|
424
|
+
{'value': response, 'uid': uid},
|
|
425
|
+
)
|
|
421
426
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
await task_mgr.run_task(result['value'], body.ws_channel)
|
|
425
|
-
response = {
|
|
426
|
-
'task_id': result['value'].task_id,
|
|
427
|
-
'cache_key': result['cache_key'],
|
|
428
|
-
}
|
|
427
|
+
# Return {cache_key: <cache_key>, value: <value>}
|
|
428
|
+
return response
|
|
429
429
|
|
|
430
|
-
dev_logger.debug(
|
|
431
|
-
f'DerivedVariable {variable_def.uid[:3]}..{variable_def.uid[-3:]}',
|
|
432
|
-
'return value',
|
|
433
|
-
{'value': response, 'uid': uid},
|
|
434
|
-
)
|
|
435
430
|
|
|
436
|
-
|
|
437
|
-
|
|
431
|
+
@core_api_router.get('/store/{store_uid}', dependencies=[Depends(verify_session)])
|
|
432
|
+
async def read_backend_store(store_uid: str):
|
|
433
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
434
|
+
store_entry: BackendStoreEntry = await registry_mgr.get(backend_store_registry, store_uid)
|
|
435
|
+
result = store_entry.store.read()
|
|
438
436
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
437
|
+
# Backend implementation could return a coroutine
|
|
438
|
+
if inspect.iscoroutine(result):
|
|
439
|
+
result = await result
|
|
440
|
+
|
|
441
|
+
# Get the current key and sequence number for this store
|
|
442
|
+
store = store_entry.store
|
|
443
|
+
key = await store._get_key()
|
|
444
|
+
sequence_number = store.sequence_number.get(key, 0)
|
|
445
|
+
|
|
446
|
+
return {'value': result, 'sequence_number': sequence_number}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@core_api_router.post('/store', dependencies=[Depends(verify_session)])
|
|
450
|
+
async def sync_backend_store(ws_channel: str = Body(), values: Dict[str, Any] = Body()):
|
|
451
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
452
|
+
|
|
453
|
+
async def _write(store_uid: str, value: Any):
|
|
454
|
+
WS_CHANNEL.set(ws_channel)
|
|
442
455
|
store_entry: BackendStoreEntry = await registry_mgr.get(backend_store_registry, store_uid)
|
|
443
|
-
result = store_entry.store.
|
|
456
|
+
result = store_entry.store.write(value, ignore_channel=ws_channel)
|
|
444
457
|
|
|
445
458
|
# Backend implementation could return a coroutine
|
|
446
459
|
if inspect.iscoroutine(result):
|
|
447
|
-
|
|
460
|
+
await result
|
|
448
461
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
sequence_number = store.sequence_number.get(key, 0)
|
|
462
|
+
async with anyio.create_task_group() as tg:
|
|
463
|
+
for store_uid, value in values.items():
|
|
464
|
+
tg.start_soon(_write, store_uid, value)
|
|
453
465
|
|
|
454
|
-
return {'value': result, 'sequence_number': sequence_number}
|
|
455
466
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
467
|
+
@core_api_router.get('/tasks/{task_id}', dependencies=[Depends(verify_session)])
|
|
468
|
+
async def get_task_result(task_id: str):
|
|
469
|
+
try:
|
|
470
|
+
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
471
|
+
res = await task_mgr.get_result(task_id)
|
|
459
472
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
473
|
+
dev_logger.debug(
|
|
474
|
+
f'Retrieving result for Task {task_id}',
|
|
475
|
+
'return value',
|
|
476
|
+
{'value': res},
|
|
477
|
+
)
|
|
464
478
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
479
|
+
# Serialize dataframes correctly, either direct or as a DataResponse
|
|
480
|
+
if isinstance(res, DataFrame):
|
|
481
|
+
return Response(df_to_json(res), media_type='application/json')
|
|
482
|
+
elif is_data_response(res):
|
|
483
|
+
return Response(data_response_to_json(res), media_type='application/json')
|
|
468
484
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
485
|
+
return res
|
|
486
|
+
except KeyError as err:
|
|
487
|
+
raise HTTPException(status_code=404, detail=str(err)) from err
|
|
488
|
+
except Exception as err:
|
|
489
|
+
raise ValueError(f'The result for task id {task_id} could not be found') from err
|
|
472
490
|
|
|
473
|
-
@core_api_router.get('/tasks/{task_id}', dependencies=[Depends(verify_session)])
|
|
474
|
-
async def get_task_result(task_id: str):
|
|
475
|
-
try:
|
|
476
|
-
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
477
|
-
res = await task_mgr.get_result(task_id)
|
|
478
491
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
492
|
+
@core_api_router.delete('/tasks/{task_id}', dependencies=[Depends(verify_session)])
|
|
493
|
+
async def cancel_task(task_id: str):
|
|
494
|
+
try:
|
|
495
|
+
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
496
|
+
return await task_mgr.cancel_task(task_id)
|
|
497
|
+
except TaskManagerError as e:
|
|
498
|
+
dev_logger.error(
|
|
499
|
+
f'The task id {task_id} could not be found, it may have already been cancelled',
|
|
500
|
+
e,
|
|
501
|
+
)
|
|
484
502
|
|
|
485
|
-
# Serialize dataframes correctly, either direct or as a DataResponse
|
|
486
|
-
if isinstance(res, DataFrame):
|
|
487
|
-
return Response(df_to_json(res), media_type='application/json')
|
|
488
|
-
elif is_data_response(res):
|
|
489
|
-
return Response(data_response_to_json(res), media_type='application/json')
|
|
490
503
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
504
|
+
@core_api_router.get('/version', dependencies=[Depends(verify_session)])
|
|
505
|
+
async def get_version():
|
|
506
|
+
return {'version': version('dara_core')}
|
|
494
507
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
508
|
+
|
|
509
|
+
# Add the main websocket connection
|
|
510
|
+
core_api_router.add_api_websocket_route('/ws', ws_handler)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class ActionPayload(BaseModel):
|
|
514
|
+
uid: str
|
|
515
|
+
definition_uid: str
|
|
516
|
+
values: NormalizedPayload[Mapping[str, Any]]
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class DerivedVariablePayload(BaseModel):
|
|
520
|
+
uid: str
|
|
521
|
+
values: NormalizedPayload[List[Any]]
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class PyComponentPayload(BaseModel):
|
|
525
|
+
uid: str
|
|
526
|
+
name: str
|
|
527
|
+
values: NormalizedPayload[Mapping[str, Any]]
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class RouteDataRequestBody(BaseModel):
|
|
531
|
+
action_payloads: List[ActionPayload] = Field(default_factory=list)
|
|
532
|
+
derived_variable_payloads: List[DerivedVariablePayload] = Field(default_factory=list)
|
|
533
|
+
py_component_payloads: List[PyComponentPayload] = Field(default_factory=list)
|
|
534
|
+
ws_channel: str
|
|
535
|
+
params: Dict[str, str] = Field(default_factory=dict)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class Result(BaseModel):
|
|
539
|
+
ok: bool
|
|
540
|
+
value: Any
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def success(value: Any) -> 'Result':
|
|
544
|
+
return Result(ok=True, value=value)
|
|
545
|
+
|
|
546
|
+
@staticmethod
|
|
547
|
+
def error(error: str) -> 'Result':
|
|
548
|
+
return Result(ok=False, value=error)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class DerivedVariableChunk(BaseModel):
|
|
552
|
+
type: Literal['derived_variable'] = 'derived_variable'
|
|
553
|
+
uid: str
|
|
554
|
+
result: Result
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class PyComponentChunk(BaseModel):
|
|
558
|
+
type: Literal['py_component'] = 'py_component'
|
|
559
|
+
uid: str
|
|
560
|
+
result: Result
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
Chunk = Union[DerivedVariableChunk, PyComponentChunk]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def create_loader_route(config: Configuration, app: FastAPI):
|
|
567
|
+
route_map = config.router.to_route_map()
|
|
568
|
+
|
|
569
|
+
@app.post('/api/core/route/{route_id}', dependencies=[Depends(verify_session)])
|
|
570
|
+
async def get_route_data(route_id: Annotated[str, Path()], body: Annotated[RouteDataRequestBody, Body()]):
|
|
571
|
+
# unquote route_id since it can be url-encoded
|
|
572
|
+
route_id = unquote(route_id)
|
|
573
|
+
|
|
574
|
+
route_data = route_map.get(route_id)
|
|
575
|
+
|
|
576
|
+
if route_data is None:
|
|
577
|
+
raise HTTPException(status_code=404, detail=f'Route {route_id} not found')
|
|
578
|
+
|
|
579
|
+
action_results: Dict[str, Any] = {}
|
|
580
|
+
|
|
581
|
+
if len(body.action_payloads) > 0:
|
|
582
|
+
store: CacheStore = utils_registry.get('Store')
|
|
498
583
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
584
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
585
|
+
|
|
586
|
+
WS_CHANNEL.set(body.ws_channel)
|
|
587
|
+
|
|
588
|
+
# Run actions in order to guarantee execution order
|
|
589
|
+
for action_payload in body.action_payloads:
|
|
590
|
+
action_def = await registry_mgr.get(action_registry, action_payload.definition_uid)
|
|
591
|
+
static_kwargs = await registry_mgr.get(static_kwargs_registry, action_payload.uid)
|
|
592
|
+
|
|
593
|
+
CURRENT_ACTION_ID.set(action_payload.uid)
|
|
594
|
+
values = denormalize(action_payload.values.data, action_payload.values.lookup)
|
|
595
|
+
try:
|
|
596
|
+
action_results[action_payload.uid] = await execute_action_sync(
|
|
597
|
+
action_def,
|
|
598
|
+
inp={'params': body.params, 'route': route_data.definition},
|
|
599
|
+
values=values,
|
|
600
|
+
static_kwargs=static_kwargs,
|
|
601
|
+
store=store,
|
|
602
|
+
task_mgr=task_mgr,
|
|
603
|
+
)
|
|
604
|
+
except BaseException as e:
|
|
605
|
+
assert route_data.definition is not None
|
|
606
|
+
route_path = route_data.definition.full_path
|
|
607
|
+
action_name = str(action_def.resolver)
|
|
608
|
+
raise HTTPException(
|
|
609
|
+
status_code=500,
|
|
610
|
+
detail={
|
|
611
|
+
'error': str(e),
|
|
612
|
+
'stacktrace': print_stacktrace(e),
|
|
613
|
+
'path': route_path,
|
|
614
|
+
'action_name': action_name,
|
|
615
|
+
},
|
|
616
|
+
) from e
|
|
617
|
+
|
|
618
|
+
async def process_variables(send_stream: MemoryObjectSendStream[Chunk]):
|
|
619
|
+
for payload in body.derived_variable_payloads:
|
|
620
|
+
try:
|
|
621
|
+
# Run the usual DV endpoint logic
|
|
622
|
+
result = await get_derived_variable(
|
|
623
|
+
uid=payload.uid,
|
|
624
|
+
body=DerivedStateRequestBody(
|
|
625
|
+
values=payload.values,
|
|
626
|
+
ws_channel=body.ws_channel,
|
|
627
|
+
force_key=None,
|
|
628
|
+
),
|
|
629
|
+
)
|
|
630
|
+
await send_stream.send(DerivedVariableChunk(uid=payload.uid, result=Result.success(result)))
|
|
631
|
+
except BaseException as e:
|
|
632
|
+
dev_logger.error(f'Error streaming derived_variable {payload.uid}', error=e)
|
|
633
|
+
await send_stream.send(
|
|
634
|
+
DerivedVariableChunk(uid=payload.uid, result=Result.error(str(e))),
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
async def process_py_components(send_stream: MemoryObjectSendStream[Chunk]):
|
|
638
|
+
for payload in body.py_component_payloads:
|
|
639
|
+
try:
|
|
640
|
+
result = await get_component(
|
|
641
|
+
component=payload.name,
|
|
642
|
+
body=ComponentRequestBody(
|
|
643
|
+
uid=payload.uid,
|
|
644
|
+
values=payload.values,
|
|
645
|
+
ws_channel=body.ws_channel,
|
|
646
|
+
),
|
|
647
|
+
)
|
|
648
|
+
await send_stream.send(PyComponentChunk(uid=payload.uid, result=Result.success(result)))
|
|
649
|
+
except BaseException as e:
|
|
650
|
+
dev_logger.error(f'Error streaming py_component {payload.name}', error=e)
|
|
651
|
+
await send_stream.send(
|
|
652
|
+
PyComponentChunk(uid=payload.uid, result=Result(ok=False, value=str(e))),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
normalized_template, lookup = normalize(jsonable_encoder(route_data.content))
|
|
656
|
+
|
|
657
|
+
# Setup the stream response
|
|
658
|
+
async def stream():
|
|
659
|
+
try:
|
|
660
|
+
|
|
661
|
+
def create_chunk(x):
|
|
662
|
+
return json.dumps(x) + '\r\n'
|
|
663
|
+
|
|
664
|
+
# 1. Send the template and actions
|
|
665
|
+
yield create_chunk(
|
|
666
|
+
{
|
|
667
|
+
'type': 'template',
|
|
668
|
+
'template': {
|
|
669
|
+
'data': normalized_template,
|
|
670
|
+
'lookup': lookup,
|
|
671
|
+
},
|
|
672
|
+
}
|
|
673
|
+
)
|
|
674
|
+
yield create_chunk({'type': 'actions', 'actions': jsonable_encoder(action_results)})
|
|
675
|
+
|
|
676
|
+
# 2. Optionally, if there are DVs or py_components to preload, process them in the background and stream them back as they arrive
|
|
677
|
+
if len(body.derived_variable_payloads) > 0 or len(body.py_component_payloads) > 0:
|
|
678
|
+
send_stream, receive_stream = anyio.create_memory_object_stream[Chunk](max_buffer_size=math.inf)
|
|
679
|
+
|
|
680
|
+
async def process_derived_state():
|
|
681
|
+
async with send_stream, anyio.create_task_group() as tg:
|
|
682
|
+
if len(body.derived_variable_payloads) > 0:
|
|
683
|
+
tg.start_soon(process_variables, send_stream)
|
|
684
|
+
if len(body.py_component_payloads) > 0:
|
|
685
|
+
tg.start_soon(process_py_components, send_stream)
|
|
686
|
+
|
|
687
|
+
async with anyio.create_task_group() as tg:
|
|
688
|
+
tg.start_soon(process_derived_state)
|
|
689
|
+
|
|
690
|
+
async for item in receive_stream:
|
|
691
|
+
yield create_chunk(jsonable_encoder(item))
|
|
692
|
+
except Exception as e:
|
|
693
|
+
traceback.print_exc()
|
|
694
|
+
dev_logger.error(f'Error streaming loader data for route {route_id}', error=e)
|
|
695
|
+
|
|
696
|
+
return StreamingResponse(content=stream(), media_type='application/x-ndjson')
|