dara-core 1.20.0__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 +176 -416
- 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 +55428 -59098
- 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.0.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.0.dist-info/RECORD +0 -119
- {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/LICENSE +0 -0
- {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/WHEEL +0 -0
- {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/entry_points.txt +0 -0
dara/core/internal/routing.py
CHANGED
|
@@ -16,13 +16,14 @@ limitations under the License.
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import inspect
|
|
19
|
+
import json
|
|
19
20
|
import os
|
|
20
|
-
from collections.abc import Mapping
|
|
21
21
|
from functools import wraps
|
|
22
22
|
from importlib.metadata import version
|
|
23
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
23
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional
|
|
24
24
|
|
|
25
25
|
import anyio
|
|
26
|
+
import pandas
|
|
26
27
|
from fastapi import (
|
|
27
28
|
APIRouter,
|
|
28
29
|
Body,
|
|
@@ -38,28 +39,25 @@ from fastapi.responses import StreamingResponse
|
|
|
38
39
|
from pandas import DataFrame
|
|
39
40
|
from pydantic import BaseModel
|
|
40
41
|
from starlette.background import BackgroundTask
|
|
41
|
-
from starlette.status import HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
42
42
|
|
|
43
43
|
from dara.core.auth.routes import verify_session
|
|
44
|
-
from dara.core.base_definitions import ActionResolverDef, BaseTask,
|
|
44
|
+
from dara.core.base_definitions import ActionResolverDef, BaseTask, UploadResolverDef
|
|
45
45
|
from dara.core.configuration import Configuration
|
|
46
|
-
from dara.core.interactivity.any_data_variable import upload
|
|
46
|
+
from dara.core.interactivity.any_data_variable import DataVariableRegistryEntry, upload
|
|
47
47
|
from dara.core.interactivity.filtering import FilterQuery, Pagination
|
|
48
|
-
from dara.core.interactivity.server_variable import ServerVariable
|
|
49
48
|
from dara.core.internal.cache_store import CacheStore
|
|
50
49
|
from dara.core.internal.download import DownloadRegistryEntry
|
|
51
50
|
from dara.core.internal.execute_action import CURRENT_ACTION_ID
|
|
52
51
|
from dara.core.internal.normalization import NormalizedPayload, denormalize, normalize
|
|
53
|
-
from dara.core.internal.pandas_utils import
|
|
52
|
+
from dara.core.internal.pandas_utils import df_to_json
|
|
54
53
|
from dara.core.internal.registries import (
|
|
55
54
|
action_def_registry,
|
|
56
55
|
action_registry,
|
|
57
56
|
backend_store_registry,
|
|
58
57
|
component_registry,
|
|
58
|
+
data_variable_registry,
|
|
59
59
|
derived_variable_registry,
|
|
60
|
-
download_code_registry,
|
|
61
60
|
latest_value_registry,
|
|
62
|
-
server_variable_registry,
|
|
63
61
|
static_kwargs_registry,
|
|
64
62
|
template_registry,
|
|
65
63
|
upload_resolver_registry,
|
|
@@ -88,7 +86,7 @@ def error_decorator(handler: Callable[..., Any]):
|
|
|
88
86
|
if isinstance(err, HTTPException):
|
|
89
87
|
raise err
|
|
90
88
|
dev_logger.error('Unhandled error', error=err)
|
|
91
|
-
raise HTTPException(status_code=500, detail=str(err))
|
|
89
|
+
raise HTTPException(status_code=500, detail=str(err))
|
|
92
90
|
|
|
93
91
|
return _async_inner_func
|
|
94
92
|
|
|
@@ -101,7 +99,7 @@ def error_decorator(handler: Callable[..., Any]):
|
|
|
101
99
|
if isinstance(err, HTTPException):
|
|
102
100
|
raise err
|
|
103
101
|
dev_logger.error('Unhandled error', error=err)
|
|
104
|
-
raise HTTPException(status_code=500, detail=str(err))
|
|
102
|
+
raise HTTPException(status_code=500, detail=str(err))
|
|
105
103
|
|
|
106
104
|
return _inner_func
|
|
107
105
|
|
|
@@ -115,7 +113,7 @@ def create_router(config: Configuration):
|
|
|
115
113
|
core_api_router = APIRouter()
|
|
116
114
|
|
|
117
115
|
@core_api_router.get('/actions', dependencies=[Depends(verify_session)])
|
|
118
|
-
async def get_actions():
|
|
116
|
+
async def get_actions(): # pylint: disable=unused-variable
|
|
119
117
|
return action_def_registry.get_all().items()
|
|
120
118
|
|
|
121
119
|
class ActionRequestBody(BaseModel):
|
|
@@ -135,7 +133,7 @@ def create_router(config: Configuration):
|
|
|
135
133
|
"""Execution id, unique to this request"""
|
|
136
134
|
|
|
137
135
|
@core_api_router.post('/action/{uid}', dependencies=[Depends(verify_session)])
|
|
138
|
-
async def get_action(uid: str, body: ActionRequestBody):
|
|
136
|
+
async def get_action(uid: str, body: ActionRequestBody): # pylint: disable=unused-variable
|
|
139
137
|
store: CacheStore = utils_registry.get('Store')
|
|
140
138
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
141
139
|
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
@@ -152,14 +150,7 @@ def create_router(config: Configuration):
|
|
|
152
150
|
|
|
153
151
|
# Execute the action - kick off a background task to run the handler
|
|
154
152
|
response = await action_def.execute_action(
|
|
155
|
-
action_def,
|
|
156
|
-
body.input,
|
|
157
|
-
values,
|
|
158
|
-
static_kwargs,
|
|
159
|
-
body.execution_id,
|
|
160
|
-
body.ws_channel,
|
|
161
|
-
store,
|
|
162
|
-
task_mgr,
|
|
153
|
+
action_def, body.input, values, static_kwargs, body.execution_id, body.ws_channel, store, task_mgr
|
|
163
154
|
)
|
|
164
155
|
|
|
165
156
|
if isinstance(response, BaseTask):
|
|
@@ -168,22 +159,15 @@ def create_router(config: Configuration):
|
|
|
168
159
|
|
|
169
160
|
return {'execution_id': response}
|
|
170
161
|
|
|
171
|
-
@core_api_router.get('/download')
|
|
172
|
-
async def get_download(code: str):
|
|
162
|
+
@core_api_router.get('/download')
|
|
163
|
+
async def get_download(code: str): # pylint: disable=unused-variable
|
|
173
164
|
store: CacheStore = utils_registry.get('Store')
|
|
174
165
|
|
|
175
166
|
try:
|
|
176
167
|
data_entry = await store.get(DownloadRegistryEntry, key=code)
|
|
177
168
|
|
|
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
169
|
if data_entry is None:
|
|
181
|
-
|
|
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)
|
|
170
|
+
raise ValueError('Invalid or expired download code')
|
|
187
171
|
|
|
188
172
|
async_file, cleanup = await data_entry.download(data_entry)
|
|
189
173
|
|
|
@@ -204,11 +188,11 @@ def create_router(config: Configuration):
|
|
|
204
188
|
background=BackgroundTask(cleanup),
|
|
205
189
|
)
|
|
206
190
|
|
|
207
|
-
except
|
|
208
|
-
raise ValueError('Invalid or expired download code')
|
|
191
|
+
except KeyError:
|
|
192
|
+
raise ValueError('Invalid or expired download code')
|
|
209
193
|
|
|
210
194
|
@core_api_router.get('/config', dependencies=[Depends(verify_session)])
|
|
211
|
-
async def get_config():
|
|
195
|
+
async def get_config(): # pylint: disable=unused-variable
|
|
212
196
|
return {
|
|
213
197
|
**config.model_dump(
|
|
214
198
|
include={
|
|
@@ -225,13 +209,13 @@ def create_router(config: Configuration):
|
|
|
225
209
|
}
|
|
226
210
|
|
|
227
211
|
@core_api_router.get('/auth-config')
|
|
228
|
-
async def get_auth_config():
|
|
212
|
+
async def get_auth_config(): # pylint: disable=unused-variable
|
|
229
213
|
return {
|
|
230
214
|
'auth_components': config.auth_config.component_config.model_dump(),
|
|
231
215
|
}
|
|
232
216
|
|
|
233
217
|
@core_api_router.get('/components', dependencies=[Depends(verify_session)])
|
|
234
|
-
async def get_components(name: Optional[str] = None):
|
|
218
|
+
async def get_components(name: Optional[str] = None): # pylint: disable=unused-variable
|
|
235
219
|
"""
|
|
236
220
|
If name is passed, will try to register the component
|
|
237
221
|
|
|
@@ -252,7 +236,7 @@ def create_router(config: Configuration):
|
|
|
252
236
|
ws_channel: str
|
|
253
237
|
|
|
254
238
|
@core_api_router.post('/components/{component}', dependencies=[Depends(verify_session)])
|
|
255
|
-
async def get_component(component: str, body: ComponentRequestBody):
|
|
239
|
+
async def get_component(component: str, body: ComponentRequestBody): # pylint: disable=unused-variable
|
|
256
240
|
CURRENT_COMPONENT_ID.set(body.uid)
|
|
257
241
|
WS_CHANNEL.set(body.ws_channel)
|
|
258
242
|
store: CacheStore = utils_registry.get('Store')
|
|
@@ -281,7 +265,7 @@ def create_router(config: Configuration):
|
|
|
281
265
|
raise HTTPException(status_code=400, detail='Requesting this type of component is not supported')
|
|
282
266
|
|
|
283
267
|
@core_api_router.get('/derived-variable/{uid}/latest', dependencies=[Depends(verify_session)])
|
|
284
|
-
async def get_latest_derived_variable(uid: str):
|
|
268
|
+
async def get_latest_derived_variable(uid: str): # pylint: disable=unused-variable
|
|
285
269
|
try:
|
|
286
270
|
store: CacheStore = utils_registry.get('Store')
|
|
287
271
|
latest_value_entry = latest_value_registry.get(uid)
|
|
@@ -305,67 +289,129 @@ def create_router(config: Configuration):
|
|
|
305
289
|
return latest_value
|
|
306
290
|
|
|
307
291
|
except KeyError as err:
|
|
308
|
-
raise ValueError(f'Could not find latest value for derived variable with uid: {uid}')
|
|
292
|
+
raise ValueError(f'Could not find latest value for derived variable with uid: {uid}').with_traceback(
|
|
293
|
+
err.__traceback__
|
|
294
|
+
)
|
|
309
295
|
|
|
310
|
-
class
|
|
296
|
+
class DataVariableRequestBody(BaseModel):
|
|
311
297
|
filters: Optional[FilterQuery] = None
|
|
312
|
-
|
|
313
|
-
|
|
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"""
|
|
298
|
+
cache_key: Optional[str] = None
|
|
299
|
+
ws_channel: Optional[str] = None
|
|
317
300
|
|
|
318
|
-
@core_api_router.post('/
|
|
319
|
-
async def
|
|
301
|
+
@core_api_router.post('/data-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
302
|
+
async def get_data_variable(
|
|
320
303
|
uid: str,
|
|
321
|
-
body:
|
|
304
|
+
body: DataVariableRequestBody,
|
|
322
305
|
offset: Optional[int] = None,
|
|
323
306
|
limit: Optional[int] = None,
|
|
324
307
|
order_by: Optional[str] = None,
|
|
325
308
|
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)
|
|
332
|
-
|
|
309
|
+
): # pylint: disable=unused-variable
|
|
333
310
|
try:
|
|
334
|
-
|
|
311
|
+
store: CacheStore = utils_registry.get('Store')
|
|
312
|
+
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
335
313
|
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
314
|
+
data_variable_entry: DataVariableRegistryEntry = await registry_mgr.get(data_variable_registry, uid)
|
|
315
|
+
|
|
316
|
+
data = None
|
|
317
|
+
WS_CHANNEL.set(body.ws_channel)
|
|
318
|
+
|
|
319
|
+
if data_variable_entry.type == 'derived':
|
|
320
|
+
if body.cache_key is None:
|
|
321
|
+
raise HTTPException(status_code=400, detail='Cache key is required for derived data variables')
|
|
322
|
+
|
|
323
|
+
if body.ws_channel is None:
|
|
324
|
+
raise HTTPException(
|
|
325
|
+
status_code=400, detail='Websocket channel is required for derived data variables'
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
derived_variable_entry = await registry_mgr.get(derived_variable_registry, uid)
|
|
329
|
+
|
|
330
|
+
data = await data_variable_entry.get_data(
|
|
331
|
+
derived_variable_entry,
|
|
332
|
+
data_variable_entry,
|
|
333
|
+
body.cache_key,
|
|
334
|
+
store,
|
|
335
|
+
body.filters,
|
|
336
|
+
Pagination(offset=offset, limit=limit, orderBy=order_by, index=index),
|
|
337
|
+
format_for_display=True,
|
|
338
|
+
)
|
|
339
|
+
if isinstance(data, BaseTask):
|
|
340
|
+
await task_mgr.run_task(data, body.ws_channel)
|
|
341
|
+
return {'task_id': data.task_id}
|
|
342
|
+
elif data_variable_entry.type == 'plain':
|
|
343
|
+
data = await data_variable_entry.get_data(
|
|
344
|
+
data_variable_entry,
|
|
345
|
+
store,
|
|
346
|
+
body.filters,
|
|
347
|
+
Pagination(offset=offset, limit=limit, orderBy=order_by, index=index),
|
|
348
|
+
format_for_display=True,
|
|
349
|
+
)
|
|
336
350
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
dev_logger.debug(
|
|
352
|
+
f'DataVariable {data_variable_entry.uid[:3]}..{data_variable_entry.uid[-3:]}',
|
|
353
|
+
'return value',
|
|
354
|
+
{'value': data.describe() if isinstance(data, pandas.DataFrame) else None, 'uid': uid}, # type: ignore
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if data is None:
|
|
358
|
+
return None
|
|
342
359
|
|
|
343
|
-
#
|
|
360
|
+
# Explicitly convert to JSON to avoid implicit serialization;
|
|
361
|
+
# return as records as that makes more sense in a JSON structure
|
|
362
|
+
return Response(
|
|
363
|
+
content=df_to_json(data) if isinstance(data, pandas.DataFrame) else data, media_type='application/json'
|
|
364
|
+
) # type: ignore
|
|
365
|
+
except ValueError as e:
|
|
366
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
367
|
+
|
|
368
|
+
class DataVariableCountRequestBody(BaseModel):
|
|
369
|
+
cache_key: Optional[str] = None
|
|
370
|
+
filters: Optional[FilterQuery] = None
|
|
371
|
+
|
|
372
|
+
@core_api_router.post('/data-variable/{uid}/count', dependencies=[Depends(verify_session)])
|
|
373
|
+
async def get_data_variable_count(uid: str, body: Optional[DataVariableCountRequestBody] = None):
|
|
374
|
+
try:
|
|
344
375
|
store: CacheStore = utils_registry.get('Store')
|
|
345
|
-
|
|
346
|
-
variable_def = await registry_mgr.get(
|
|
347
|
-
values = denormalize(body.dv_values.data, body.dv_values.lookup)
|
|
376
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
377
|
+
variable_def = await registry_mgr.get(data_variable_registry, uid)
|
|
348
378
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
if variable_def.type == 'plain':
|
|
380
|
+
return await variable_def.get_total_count(
|
|
381
|
+
variable_def, store, body.filters if body is not None else None
|
|
382
|
+
)
|
|
352
383
|
|
|
353
|
-
if
|
|
354
|
-
|
|
355
|
-
|
|
384
|
+
if body is None or body.cache_key is None:
|
|
385
|
+
raise HTTPException(
|
|
386
|
+
status_code=400, detail="Cache key is required when requesting DerivedDataVariable's count"
|
|
387
|
+
)
|
|
356
388
|
|
|
357
|
-
return
|
|
358
|
-
except
|
|
359
|
-
raise HTTPException(status_code=
|
|
389
|
+
return await variable_def.get_total_count(variable_def, store, body.cache_key, body.filters)
|
|
390
|
+
except ValueError as e:
|
|
391
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
360
392
|
|
|
361
|
-
@core_api_router.get('/
|
|
362
|
-
async def
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
393
|
+
@core_api_router.get('/data-variable/{uid}/schema', dependencies=[Depends(verify_session)])
|
|
394
|
+
async def get_data_variable_schema(uid: str, cache_key: Optional[str] = None):
|
|
395
|
+
try:
|
|
396
|
+
store: CacheStore = utils_registry.get('Store')
|
|
397
|
+
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
398
|
+
data_def = await registry_mgr.get(data_variable_registry, uid)
|
|
399
|
+
|
|
400
|
+
if data_def.type == 'plain':
|
|
401
|
+
return await data_def.get_schema(data_def, store)
|
|
402
|
+
|
|
403
|
+
if cache_key is None:
|
|
404
|
+
raise HTTPException(
|
|
405
|
+
status_code=400, detail='Cache key is required when requesting DerivedDataVariable schema'
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Use the other registry for derived variables
|
|
409
|
+
derived_ref = await registry_mgr.get(derived_variable_registry, uid)
|
|
410
|
+
data = await data_def.get_schema(derived_ref, store, cache_key)
|
|
411
|
+
content = json.dumps(jsonable_encoder(data)) if isinstance(data, dict) else data
|
|
412
|
+
return Response(content=content, media_type='application/json')
|
|
413
|
+
except ValueError as e:
|
|
414
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
369
415
|
|
|
370
416
|
@core_api_router.post('/data/upload', dependencies=[Depends(verify_session)])
|
|
371
417
|
async def upload_data(
|
|
@@ -381,10 +427,7 @@ def create_router(config: Configuration):
|
|
|
381
427
|
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
382
428
|
|
|
383
429
|
if data_uid is None and resolver_id is None:
|
|
384
|
-
raise HTTPException(
|
|
385
|
-
400,
|
|
386
|
-
'Neither resolver_id or data_uid specified, at least one of them is required',
|
|
387
|
-
)
|
|
430
|
+
raise HTTPException(400, 'Neither resolver_id or data_uid specified, at least one of them is required')
|
|
388
431
|
|
|
389
432
|
try:
|
|
390
433
|
# If resolver id is provided, run the custom
|
|
@@ -397,15 +440,16 @@ def create_router(config: Configuration):
|
|
|
397
440
|
|
|
398
441
|
return {'status': 'SUCCESS'}
|
|
399
442
|
except Exception as e:
|
|
400
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
443
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
401
444
|
|
|
402
445
|
class DerivedStateRequestBody(BaseModel):
|
|
403
446
|
values: NormalizedPayload[List[Any]]
|
|
404
|
-
|
|
447
|
+
force: bool
|
|
405
448
|
ws_channel: str
|
|
449
|
+
is_data_variable: Optional[bool] = False
|
|
406
450
|
|
|
407
451
|
@core_api_router.post('/derived-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
408
|
-
async def get_derived_variable(uid: str, body: DerivedStateRequestBody):
|
|
452
|
+
async def get_derived_variable(uid: str, body: DerivedStateRequestBody): # pylint: disable=unused-variable
|
|
409
453
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
410
454
|
store: CacheStore = utils_registry.get('Store')
|
|
411
455
|
registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
|
|
@@ -413,7 +457,7 @@ def create_router(config: Configuration):
|
|
|
413
457
|
|
|
414
458
|
values = denormalize(body.values.data, body.values.lookup)
|
|
415
459
|
|
|
416
|
-
result = await variable_def.get_value(variable_def, store, task_mgr, values, body.
|
|
460
|
+
result = await variable_def.get_value(variable_def, store, task_mgr, values, body.force)
|
|
417
461
|
|
|
418
462
|
response: Any = result
|
|
419
463
|
|
|
@@ -422,10 +466,7 @@ def create_router(config: Configuration):
|
|
|
422
466
|
if isinstance(result['value'], BaseTask):
|
|
423
467
|
# Kick off the task
|
|
424
468
|
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
|
-
}
|
|
469
|
+
response = {'task_id': result['value'].task_id, 'cache_key': result['cache_key']}
|
|
429
470
|
|
|
430
471
|
dev_logger.debug(
|
|
431
472
|
f'DerivedVariable {variable_def.uid[:3]}..{variable_def.uid[-3:]}',
|
|
@@ -471,7 +512,7 @@ def create_router(config: Configuration):
|
|
|
471
512
|
tg.start_soon(_write, store_uid, value)
|
|
472
513
|
|
|
473
514
|
@core_api_router.get('/tasks/{task_id}', dependencies=[Depends(verify_session)])
|
|
474
|
-
async def get_task_result(task_id: str):
|
|
515
|
+
async def get_task_result(task_id: str): # pylint: disable=unused-variable
|
|
475
516
|
try:
|
|
476
517
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
477
518
|
res = await task_mgr.get_result(task_id)
|
|
@@ -482,35 +523,30 @@ def create_router(config: Configuration):
|
|
|
482
523
|
{'value': res},
|
|
483
524
|
)
|
|
484
525
|
|
|
485
|
-
# Serialize dataframes correctly
|
|
526
|
+
# Serialize dataframes correctly
|
|
486
527
|
if isinstance(res, DataFrame):
|
|
487
|
-
return Response(df_to_json(res)
|
|
488
|
-
elif is_data_response(res):
|
|
489
|
-
return Response(data_response_to_json(res), media_type='application/json')
|
|
528
|
+
return Response(df_to_json(res))
|
|
490
529
|
|
|
491
530
|
return res
|
|
492
|
-
except Exception as
|
|
493
|
-
raise ValueError(f'The result for task id {task_id} could not be found')
|
|
531
|
+
except Exception as e:
|
|
532
|
+
raise ValueError(f'The result for task id {task_id} could not be found').with_traceback(e.__traceback__)
|
|
494
533
|
|
|
495
534
|
@core_api_router.delete('/tasks/{task_id}', dependencies=[Depends(verify_session)])
|
|
496
|
-
async def cancel_task(task_id: str):
|
|
535
|
+
async def cancel_task(task_id: str): # pylint: disable=unused-variable
|
|
497
536
|
try:
|
|
498
537
|
task_mgr: TaskManager = utils_registry.get('TaskManager')
|
|
499
538
|
return await task_mgr.cancel_task(task_id)
|
|
500
539
|
except TaskManagerError as e:
|
|
501
|
-
dev_logger.error(
|
|
502
|
-
f'The task id {task_id} could not be found, it may have already been cancelled',
|
|
503
|
-
e,
|
|
504
|
-
)
|
|
540
|
+
dev_logger.error(f'The task id {task_id} could not be found, it may have already been cancelled', e)
|
|
505
541
|
|
|
506
542
|
@core_api_router.get('/template/{template}', dependencies=[Depends(verify_session)])
|
|
507
|
-
async def get_template(template: str):
|
|
543
|
+
async def get_template(template: str): # pylint: disable=unused-variable
|
|
508
544
|
try:
|
|
509
545
|
selected_template = template_registry.get(template)
|
|
510
546
|
normalized_template, lookup = normalize(jsonable_encoder(selected_template))
|
|
511
547
|
return {'data': normalized_template, 'lookup': lookup}
|
|
512
|
-
except KeyError
|
|
513
|
-
raise HTTPException(status_code=404, detail=f'Template: {template}, not found in registry')
|
|
548
|
+
except KeyError:
|
|
549
|
+
raise HTTPException(status_code=404, detail=f'Template: {template}, not found in registry')
|
|
514
550
|
except Exception as e:
|
|
515
551
|
dev_logger.error('Something went wrong while trying to get the template', e)
|
|
516
552
|
|
dara/core/internal/scheduler.py
CHANGED
|
@@ -20,7 +20,7 @@ from datetime import datetime
|
|
|
20
20
|
from multiprocessing import get_context
|
|
21
21
|
from multiprocessing.process import BaseProcess
|
|
22
22
|
from pickle import PicklingError
|
|
23
|
-
from typing import Any, List, Optional, Union
|
|
23
|
+
from typing import Any, List, Optional, Union
|
|
24
24
|
|
|
25
25
|
from croniter import croniter
|
|
26
26
|
from pydantic import BaseModel, field_validator
|
|
@@ -56,28 +56,25 @@ class ScheduledJob(BaseModel):
|
|
|
56
56
|
job_process = ctx.Process(target=self._refresh_timer, args=(func, args), daemon=True)
|
|
57
57
|
job_process.start()
|
|
58
58
|
return job_process
|
|
59
|
-
except PicklingError
|
|
59
|
+
except PicklingError:
|
|
60
60
|
raise PicklingError(
|
|
61
61
|
"""
|
|
62
62
|
Unable to pickle scheduled function. Please ensure that the function you are trying
|
|
63
63
|
to schedule is not in the same file as the ConfigurationBuilder is defined and that
|
|
64
64
|
the function is not a lambda.
|
|
65
65
|
"""
|
|
66
|
-
)
|
|
66
|
+
)
|
|
67
67
|
|
|
68
68
|
def _refresh_timer(self, func, args):
|
|
69
69
|
while self.continue_running and not (self.run_once and not self.first_execution):
|
|
70
|
-
interval
|
|
70
|
+
interval = self.interval
|
|
71
71
|
# If there's more than one interval to wait, i.e. this is a weekday process
|
|
72
|
-
if
|
|
72
|
+
if type(interval) == list:
|
|
73
73
|
# Wait the first interval if this is the first execution of the job
|
|
74
74
|
interval = self.interval[0] if self.first_execution else self.interval[1]
|
|
75
|
-
else:
|
|
76
|
-
interval = self.interval
|
|
77
|
-
|
|
78
75
|
self.first_execution = False
|
|
79
76
|
# Wait the interval and then run the job
|
|
80
|
-
time.sleep(
|
|
77
|
+
time.sleep(interval)
|
|
81
78
|
func(*args)
|
|
82
79
|
|
|
83
80
|
|
|
@@ -179,7 +176,7 @@ class ScheduledJobFactory(BaseModel):
|
|
|
179
176
|
|
|
180
177
|
@field_validator('weekday', mode='before')
|
|
181
178
|
@classmethod
|
|
182
|
-
def validate_weekday(cls, weekday: Any) -> datetime:
|
|
179
|
+
def validate_weekday(cls, weekday: Any) -> datetime: # pylint: disable=E0213
|
|
183
180
|
if isinstance(weekday, datetime):
|
|
184
181
|
return weekday
|
|
185
182
|
if isinstance(weekday, str):
|
|
@@ -286,9 +283,7 @@ class Scheduler:
|
|
|
286
283
|
def _weekday(self, weekday: int):
|
|
287
284
|
# The job must execute on a weekly interval
|
|
288
285
|
return ScheduledJobFactory(
|
|
289
|
-
interval=self.interval * 604800,
|
|
290
|
-
run_once=self._run_once,
|
|
291
|
-
weekday=str(weekday), # type: ignore
|
|
286
|
+
interval=self.interval * 604800, run_once=self._run_once, weekday=str(weekday) # type: ignore
|
|
292
287
|
)
|
|
293
288
|
|
|
294
289
|
def monday(self) -> ScheduledJobFactory:
|
dara/core/internal/settings.py
CHANGED
|
@@ -74,7 +74,7 @@ def generate_env_file(filename='.env'):
|
|
|
74
74
|
f.write(env_content)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
@lru_cache
|
|
77
|
+
@lru_cache()
|
|
78
78
|
def get_settings():
|
|
79
79
|
"""
|
|
80
80
|
Get a cached instance of the settings, loading values from the .env if present.
|
|
@@ -83,7 +83,7 @@ def get_settings():
|
|
|
83
83
|
|
|
84
84
|
# Test purposes - if DARA_TEST_FLAG is set then override env with .env.test
|
|
85
85
|
if os.environ.get('DARA_TEST_FLAG', None) is not None:
|
|
86
|
-
return Settings(**dotenv_values('.env.test'))
|
|
86
|
+
return Settings(**dotenv_values('.env.test'))
|
|
87
87
|
|
|
88
88
|
env_error = False
|
|
89
89
|
|
dara/core/internal/store.py
CHANGED
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|
|
17
17
|
|
|
18
18
|
from typing import Any, Dict, List, Optional
|
|
19
19
|
|
|
20
|
-
from dara.core.base_definitions import CacheType, PendingTask
|
|
20
|
+
from dara.core.base_definitions import CacheType, PendingTask, PendingValue
|
|
21
21
|
from dara.core.internal.utils import get_cache_scope
|
|
22
22
|
|
|
23
23
|
|
|
@@ -59,6 +59,8 @@ class Store:
|
|
|
59
59
|
cache_key = get_cache_scope(cache_type)
|
|
60
60
|
value = self._store.get(cache_key, {}).get(key)
|
|
61
61
|
|
|
62
|
+
if isinstance(value, PendingValue):
|
|
63
|
+
return await value.wait()
|
|
62
64
|
if isinstance(value, PendingTask):
|
|
63
65
|
return await value.run()
|
|
64
66
|
return value
|
|
@@ -84,8 +86,33 @@ class Store:
|
|
|
84
86
|
if self._store.get(cache_key) is None:
|
|
85
87
|
self._store[cache_key] = {}
|
|
86
88
|
|
|
89
|
+
# If there is a PendingValue set for this key then trigger its resolution
|
|
90
|
+
if isinstance(self._store[cache_key].get(key), PendingValue):
|
|
91
|
+
if error is not None:
|
|
92
|
+
self._store[cache_key][key].error(error)
|
|
93
|
+
else:
|
|
94
|
+
self._store[cache_key][key].resolve(value)
|
|
95
|
+
|
|
87
96
|
self._store[cache_key][key] = value
|
|
88
97
|
|
|
98
|
+
def set_pending_value(self, key: str, cache_type: Optional[CacheType] = CacheType.GLOBAL):
|
|
99
|
+
"""
|
|
100
|
+
Set a pending state for a value in the store. This will trigger the async behavior of the get call if subsequent
|
|
101
|
+
requests ask for the same key. A future is created in the store, which all requests then listen for the
|
|
102
|
+
resolution of before returning.
|
|
103
|
+
|
|
104
|
+
:param key: the key to set as pending
|
|
105
|
+
:param cache_type: whether to pull the value from the specified cache specific store or the global one, defaults to
|
|
106
|
+
the global one
|
|
107
|
+
"""
|
|
108
|
+
cache_key = get_cache_scope(cache_type)
|
|
109
|
+
if self._store.get(cache_key) is None:
|
|
110
|
+
self._store[cache_key] = {}
|
|
111
|
+
|
|
112
|
+
pending_val = PendingValue()
|
|
113
|
+
|
|
114
|
+
self._store[cache_key][key] = pending_val
|
|
115
|
+
|
|
89
116
|
def set_pending_task(self, key: str, pending_task: PendingTask, cache_type: Optional[CacheType] = CacheType.GLOBAL):
|
|
90
117
|
"""
|
|
91
118
|
Store a pending task state for a given key in the store. This will trigger the async behavior of the get call if subsequent
|
|
@@ -131,7 +158,7 @@ class Store:
|
|
|
131
158
|
# Otherwise go through and remove any non-pending values
|
|
132
159
|
keys = list(cache_type_store.keys())
|
|
133
160
|
for key in keys:
|
|
134
|
-
if not isinstance(cache_type_store[key], PendingTask):
|
|
161
|
+
if not isinstance(cache_type_store[key], (PendingValue, PendingTask)):
|
|
135
162
|
cache_type_store.pop(key)
|
|
136
163
|
|
|
137
164
|
def list(self, cache_type: Optional[CacheType] = CacheType.GLOBAL) -> List[str]:
|