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.
Files changed (82) hide show
  1. dara/core/__init__.py +0 -3
  2. dara/core/actions.py +2 -1
  3. dara/core/auth/basic.py +16 -22
  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 +64 -22
  8. dara/core/cli.py +7 -8
  9. dara/core/configuration.py +2 -5
  10. dara/core/css.py +2 -1
  11. dara/core/data_utils.py +19 -18
  12. dara/core/defaults.py +7 -6
  13. dara/core/definitions.py +19 -50
  14. dara/core/http.py +3 -7
  15. dara/core/interactivity/__init__.py +0 -6
  16. dara/core/interactivity/actions.py +50 -52
  17. dara/core/interactivity/any_data_variable.py +134 -7
  18. dara/core/interactivity/any_variable.py +8 -5
  19. dara/core/interactivity/data_variable.py +266 -8
  20. dara/core/interactivity/derived_data_variable.py +290 -7
  21. dara/core/interactivity/derived_variable.py +176 -416
  22. dara/core/interactivity/filtering.py +27 -46
  23. dara/core/interactivity/loop_variable.py +2 -2
  24. dara/core/interactivity/non_data_variable.py +68 -5
  25. dara/core/interactivity/plain_variable.py +15 -89
  26. dara/core/interactivity/switch_variable.py +19 -19
  27. dara/core/interactivity/url_variable.py +90 -10
  28. dara/core/internal/cache_store/base_impl.py +1 -2
  29. dara/core/internal/cache_store/cache_store.py +25 -22
  30. dara/core/internal/cache_store/keep_all.py +1 -4
  31. dara/core/internal/cache_store/lru.py +1 -5
  32. dara/core/internal/cache_store/ttl.py +1 -4
  33. dara/core/internal/cgroup.py +1 -1
  34. dara/core/internal/dependency_resolution.py +66 -60
  35. dara/core/internal/devtools.py +5 -12
  36. dara/core/internal/download.py +4 -13
  37. dara/core/internal/encoder_registry.py +7 -7
  38. dara/core/internal/execute_action.py +13 -13
  39. dara/core/internal/hashing.py +3 -1
  40. dara/core/internal/import_discovery.py +4 -3
  41. dara/core/internal/normalization.py +18 -9
  42. dara/core/internal/pandas_utils.py +5 -107
  43. dara/core/internal/pool/definitions.py +1 -1
  44. dara/core/internal/pool/task_pool.py +16 -25
  45. dara/core/internal/pool/utils.py +18 -21
  46. dara/core/internal/pool/worker.py +2 -3
  47. dara/core/internal/port_utils.py +1 -1
  48. dara/core/internal/registries.py +6 -12
  49. dara/core/internal/registry.py +2 -4
  50. dara/core/internal/registry_lookup.py +5 -11
  51. dara/core/internal/routing.py +145 -109
  52. dara/core/internal/scheduler.py +8 -13
  53. dara/core/internal/settings.py +2 -2
  54. dara/core/internal/store.py +29 -2
  55. dara/core/internal/tasks.py +195 -379
  56. dara/core/internal/utils.py +13 -36
  57. dara/core/internal/websocket.py +20 -21
  58. dara/core/js_tooling/js_utils.py +26 -28
  59. dara/core/js_tooling/templates/vite.config.template.ts +3 -12
  60. dara/core/logging.py +12 -13
  61. dara/core/main.py +11 -14
  62. dara/core/metrics/cache.py +1 -1
  63. dara/core/metrics/utils.py +3 -3
  64. dara/core/persistence.py +5 -27
  65. dara/core/umd/dara.core.umd.js +55428 -59098
  66. dara/core/visual/components/__init__.py +2 -2
  67. dara/core/visual/components/fallback.py +4 -30
  68. dara/core/visual/components/for_cmp.py +1 -4
  69. dara/core/visual/css/__init__.py +31 -30
  70. dara/core/visual/dynamic_component.py +28 -31
  71. dara/core/visual/progress_updater.py +3 -4
  72. {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/METADATA +11 -12
  73. dara_core-1.20.1a1.dist-info/RECORD +114 -0
  74. dara/core/interactivity/client_variable.py +0 -71
  75. dara/core/interactivity/server_variable.py +0 -325
  76. dara/core/interactivity/state_variable.py +0 -69
  77. dara/core/interactivity/tabular_variable.py +0 -94
  78. dara/core/internal/multi_resource_lock.py +0 -70
  79. dara_core-1.20.0.dist-info/RECORD +0 -119
  80. {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/LICENSE +0 -0
  81. {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/WHEEL +0 -0
  82. {dara_core-1.20.0.dist-info → dara_core-1.20.1a1.dist-info}/entry_points.txt +0 -0
@@ -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, NonTabularDataError, UploadResolverDef
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 data_response_to_json, df_to_json, is_data_response
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)) from 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)) from 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') # explicitly unauthenticated
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
- 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)
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 (KeyError, ValueError) as e:
208
- raise ValueError('Invalid or expired download code') from e
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}') from err
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 TabularRequestBody(BaseModel):
296
+ class DataVariableRequestBody(BaseModel):
311
297
  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"""
298
+ cache_key: Optional[str] = None
299
+ ws_channel: Optional[str] = None
317
300
 
318
- @core_api_router.post('/tabular-variable/{uid}', dependencies=[Depends(verify_session)])
319
- async def get_tabular_variable(
301
+ @core_api_router.post('/data-variable/{uid}', dependencies=[Depends(verify_session)])
302
+ async def get_data_variable(
320
303
  uid: str,
321
- body: TabularRequestBody,
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
- pagination = Pagination(offset=offset, limit=limit, orderBy=order_by, index=index)
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
- # ServerVariable
338
- if body.dv_values is None:
339
- server_variable_entry = await registry_mgr.get(server_variable_registry, uid)
340
- data_response = await ServerVariable.get_tabular_data(server_variable_entry, body.filters, pagination)
341
- return Response(data_response_to_json(data_response), media_type='application/json')
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
- # DerivedVariable
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
- task_mgr: TaskManager = utils_registry.get('TaskManager')
346
- variable_def = await registry_mgr.get(derived_variable_registry, uid)
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
- result = await variable_def.get_tabular_data(
350
- variable_def, store, task_mgr, values, body.force_key, pagination, body.filters
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 isinstance(result, BaseTask):
354
- await task_mgr.run_task(result, body.ws_channel)
355
- return {'task_id': result.task_id}
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 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
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('/server-variable/{uid}/sequence', dependencies=[Depends(verify_session)])
362
- async def get_server_variable_sequence(
363
- uid: str,
364
- ):
365
- registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
366
- server_variable_entry = await registry_mgr.get(server_variable_registry, uid)
367
- seq_num = await ServerVariable.get_sequence_number(server_variable_entry)
368
- return {'sequence_number': seq_num}
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)) from e
443
+ raise HTTPException(status_code=400, detail=str(e))
401
444
 
402
445
  class DerivedStateRequestBody(BaseModel):
403
446
  values: NormalizedPayload[List[Any]]
404
- force_key: Optional[str] = None
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.force_key)
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, either direct or as a DataResponse
526
+ # Serialize dataframes correctly
486
527
  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')
528
+ return Response(df_to_json(res))
490
529
 
491
530
  return res
492
- except Exception as err:
493
- raise ValueError(f'The result for task id {task_id} could not be found') from err
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 as err:
513
- raise HTTPException(status_code=404, detail=f'Template: {template}, not found in registry') from err
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
 
@@ -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, cast
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 as err:
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
- ) from err
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: int
70
+ interval = self.interval
71
71
  # If there's more than one interval to wait, i.e. this is a weekday process
72
- if isinstance(self.interval, list):
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(cast(int, interval))
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:
@@ -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')) # type: ignore
86
+ return Settings(**dotenv_values('.env.test'))
87
87
 
88
88
  env_error = False
89
89
 
@@ -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]: