dara-core 1.20.1a1__py3-none-any.whl → 1.20.1a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. dara/core/__init__.py +3 -0
  2. dara/core/actions.py +1 -2
  3. dara/core/auth/basic.py +22 -16
  4. dara/core/auth/definitions.py +2 -2
  5. dara/core/auth/routes.py +5 -5
  6. dara/core/auth/utils.py +5 -5
  7. dara/core/base_definitions.py +22 -64
  8. dara/core/cli.py +8 -7
  9. dara/core/configuration.py +5 -2
  10. dara/core/css.py +1 -2
  11. dara/core/data_utils.py +18 -19
  12. dara/core/defaults.py +6 -7
  13. dara/core/definitions.py +50 -19
  14. dara/core/http.py +7 -3
  15. dara/core/interactivity/__init__.py +6 -0
  16. dara/core/interactivity/actions.py +52 -50
  17. dara/core/interactivity/any_data_variable.py +7 -134
  18. dara/core/interactivity/any_variable.py +5 -8
  19. dara/core/interactivity/client_variable.py +71 -0
  20. dara/core/interactivity/data_variable.py +8 -266
  21. dara/core/interactivity/derived_data_variable.py +7 -290
  22. dara/core/interactivity/derived_variable.py +416 -176
  23. dara/core/interactivity/filtering.py +46 -27
  24. dara/core/interactivity/loop_variable.py +2 -2
  25. dara/core/interactivity/non_data_variable.py +5 -68
  26. dara/core/interactivity/plain_variable.py +89 -15
  27. dara/core/interactivity/server_variable.py +325 -0
  28. dara/core/interactivity/state_variable.py +69 -0
  29. dara/core/interactivity/switch_variable.py +19 -19
  30. dara/core/interactivity/tabular_variable.py +94 -0
  31. dara/core/interactivity/url_variable.py +10 -90
  32. dara/core/internal/cache_store/base_impl.py +2 -1
  33. dara/core/internal/cache_store/cache_store.py +22 -25
  34. dara/core/internal/cache_store/keep_all.py +4 -1
  35. dara/core/internal/cache_store/lru.py +5 -1
  36. dara/core/internal/cache_store/ttl.py +4 -1
  37. dara/core/internal/cgroup.py +1 -1
  38. dara/core/internal/dependency_resolution.py +60 -66
  39. dara/core/internal/devtools.py +12 -5
  40. dara/core/internal/download.py +13 -4
  41. dara/core/internal/encoder_registry.py +7 -7
  42. dara/core/internal/execute_action.py +13 -13
  43. dara/core/internal/hashing.py +1 -3
  44. dara/core/internal/import_discovery.py +3 -4
  45. dara/core/internal/multi_resource_lock.py +70 -0
  46. dara/core/internal/normalization.py +9 -18
  47. dara/core/internal/pandas_utils.py +107 -5
  48. dara/core/internal/pool/definitions.py +1 -1
  49. dara/core/internal/pool/task_pool.py +25 -16
  50. dara/core/internal/pool/utils.py +21 -18
  51. dara/core/internal/pool/worker.py +3 -2
  52. dara/core/internal/port_utils.py +1 -1
  53. dara/core/internal/registries.py +12 -6
  54. dara/core/internal/registry.py +4 -2
  55. dara/core/internal/registry_lookup.py +11 -5
  56. dara/core/internal/routing.py +109 -145
  57. dara/core/internal/scheduler.py +13 -8
  58. dara/core/internal/settings.py +2 -2
  59. dara/core/internal/store.py +2 -29
  60. dara/core/internal/tasks.py +379 -195
  61. dara/core/internal/utils.py +36 -13
  62. dara/core/internal/websocket.py +21 -20
  63. dara/core/js_tooling/js_utils.py +28 -26
  64. dara/core/js_tooling/templates/vite.config.template.ts +12 -3
  65. dara/core/logging.py +13 -12
  66. dara/core/main.py +14 -11
  67. dara/core/metrics/cache.py +1 -1
  68. dara/core/metrics/utils.py +3 -3
  69. dara/core/persistence.py +27 -5
  70. dara/core/umd/dara.core.umd.js +68291 -64718
  71. dara/core/visual/components/__init__.py +2 -2
  72. dara/core/visual/components/fallback.py +30 -4
  73. dara/core/visual/components/for_cmp.py +4 -1
  74. dara/core/visual/css/__init__.py +30 -31
  75. dara/core/visual/dynamic_component.py +31 -28
  76. dara/core/visual/progress_updater.py +4 -3
  77. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/METADATA +12 -11
  78. dara_core-1.20.1a3.dist-info/RECORD +119 -0
  79. dara_core-1.20.1a1.dist-info/RECORD +0 -114
  80. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/LICENSE +0 -0
  81. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/WHEEL +0 -0
  82. {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/entry_points.txt +0 -0
@@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  """
17
17
 
18
+ from collections.abc import Mapping
18
19
  from datetime import datetime
19
- from typing import Any, Callable, Mapping, Set
20
+ from typing import Any, Callable, Set
20
21
 
21
22
  from dara.core.auth import BaseAuthConfig
22
23
  from dara.core.base_definitions import ActionDef, ActionResolverDef, UploadResolverDef
@@ -26,23 +27,25 @@ from dara.core.definitions import (
26
27
  EndpointConfiguration,
27
28
  Template,
28
29
  )
29
- from dara.core.interactivity.data_variable import DataVariableRegistryEntry
30
30
  from dara.core.interactivity.derived_variable import (
31
31
  DerivedVariableRegistryEntry,
32
32
  LatestValueRegistryEntry,
33
33
  )
34
+ from dara.core.interactivity.server_variable import ServerVariableRegistryEntry
35
+ from dara.core.internal.download import DownloadDataEntry
34
36
  from dara.core.internal.registry import Registry, RegistryType
35
37
  from dara.core.internal.websocket import CustomClientMessagePayload
36
38
  from dara.core.persistence import BackendStoreEntry
37
39
 
38
- action_def_registry = Registry[ActionDef](RegistryType.ACTION_DEF, CORE_ACTIONS) # all registered actions
39
- action_registry = Registry[ActionResolverDef](RegistryType.ACTION) # functions for actions requiring backend calls
40
+ action_def_registry = Registry[ActionDef](RegistryType.ACTION_DEF, CORE_ACTIONS) # all registered actions
41
+ action_registry = Registry[ActionResolverDef](RegistryType.ACTION) # functions for actions requiring backend calls
40
42
  upload_resolver_registry = Registry[UploadResolverDef](
41
43
  RegistryType.UPLOAD_RESOLVER
42
- ) # functions for upload resolvers requiring backend calls
44
+ ) # functions for upload resolvers requiring backend calls
43
45
  component_registry = Registry[ComponentTypeAnnotation](RegistryType.COMPONENTS, CORE_COMPONENTS)
44
46
  config_registry = Registry[EndpointConfiguration](RegistryType.ENDPOINT_CONFIG)
45
- data_variable_registry = Registry[DataVariableRegistryEntry](RegistryType.DATA_VARIABLE, allow_duplicates=False)
47
+ server_variable_registry = Registry[ServerVariableRegistryEntry](RegistryType.SERVER_VARIABLE, allow_duplicates=False)
48
+ """map of server variable uid -> server variable entry"""
46
49
  derived_variable_registry = Registry[DerivedVariableRegistryEntry](
47
50
  RegistryType.DERIVED_VARIABLE, allow_duplicates=False
48
51
  )
@@ -69,3 +72,6 @@ custom_ws_handlers_registry = Registry[Callable[[str, CustomClientMessagePayload
69
72
 
70
73
  backend_store_registry = Registry[BackendStoreEntry](RegistryType.BACKEND_STORE, allow_duplicates=False)
71
74
  """map of store uid -> store instance"""
75
+
76
+ download_code_registry = Registry[DownloadDataEntry](RegistryType.DOWNLOAD_CODE, allow_duplicates=False)
77
+ """map of download codes -> download data entry, used only to allow overriding download code behaviour via RegistryLookup"""
@@ -16,8 +16,9 @@ limitations under the License.
16
16
  """
17
17
 
18
18
  import copy
19
+ from collections.abc import MutableMapping
19
20
  from enum import Enum
20
- from typing import Generic, MutableMapping, Optional, TypeVar
21
+ from typing import Generic, Optional, TypeVar
21
22
 
22
23
  from dara.core.metrics import CACHE_METRICS_TRACKER, total_size
23
24
 
@@ -31,7 +32,7 @@ class RegistryType(str, Enum):
31
32
  DOWNLOAD = 'Download'
32
33
  COMPONENTS = 'Components'
33
34
  ENDPOINT_CONFIG = 'Endpoint Configuration'
34
- DATA_VARIABLE = 'DataVariable'
35
+ SERVER_VARIABLE = 'ServerVariable'
35
36
  DERIVED_VARIABLE = 'DerivedVariable'
36
37
  LAST_VALUE = 'LatestValue'
37
38
  TEMPLATE = 'Template'
@@ -43,6 +44,7 @@ class RegistryType(str, Enum):
43
44
  PENDING_TOKENS = 'Pending tokens'
44
45
  CUSTOM_WS_HANDLERS = 'Custom WS handlers'
45
46
  BACKEND_STORE = 'Backend Store'
47
+ DOWNLOAD_CODE = 'Download Code'
46
48
 
47
49
 
48
50
  class Registry(Generic[T]):
@@ -15,7 +15,8 @@ See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  """
17
17
 
18
- from typing import Callable, Coroutine, Dict, Literal
18
+ from collections.abc import Coroutine
19
+ from typing import Callable, Dict, Literal, TypeVar, Union
19
20
 
20
21
  from dara.core.internal.registry import Registry, RegistryType
21
22
  from dara.core.internal.utils import async_dedupe
@@ -23,25 +24,30 @@ from dara.core.internal.utils import async_dedupe
23
24
  RegistryLookupKey = Literal[
24
25
  RegistryType.ACTION,
25
26
  RegistryType.COMPONENTS,
26
- RegistryType.DATA_VARIABLE,
27
27
  RegistryType.DERIVED_VARIABLE,
28
+ RegistryType.SERVER_VARIABLE,
28
29
  RegistryType.STATIC_KWARGS,
29
30
  RegistryType.UPLOAD_RESOLVER,
30
31
  RegistryType.BACKEND_STORE,
32
+ RegistryType.DOWNLOAD_CODE,
31
33
  ]
32
34
  CustomRegistryLookup = Dict[RegistryLookupKey, Callable[[str], Coroutine]]
33
35
 
36
+ RegistryType = TypeVar('RegistryType')
37
+
34
38
 
35
39
  class RegistryLookup:
36
40
  """
37
41
  Manages registry Lookup.
38
42
  """
39
43
 
40
- def __init__(self, handlers: CustomRegistryLookup = {}):
44
+ def __init__(self, handlers: Union[CustomRegistryLookup, None] = None):
45
+ if handlers is None:
46
+ handlers = {}
41
47
  self.handlers = handlers
42
48
 
43
49
  @async_dedupe
44
- async def get(self, registry: Registry, uid: str):
50
+ async def get(self, registry: Registry[RegistryType], uid: str) -> RegistryType:
45
51
  """
46
52
  Get the entry from registry by uid.
47
53
  If uid is not in registry and it has a external handler that defined, will execute the handler
@@ -62,4 +68,4 @@ class RegistryLookup:
62
68
  return entry
63
69
  raise ValueError(
64
70
  f'Could not find uid {uid} in {registry.name} registry, did you register it before the app was initialized?'
65
- ).with_traceback(e.__traceback__)
71
+ ) from e
@@ -16,14 +16,13 @@ limitations under the License.
16
16
  """
17
17
 
18
18
  import inspect
19
- import json
20
19
  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, Mapping, Optional
23
+ from typing import Any, Callable, Dict, List, Optional
24
24
 
25
25
  import anyio
26
- import pandas
27
26
  from fastapi import (
28
27
  APIRouter,
29
28
  Body,
@@ -39,25 +38,28 @@ from fastapi.responses import StreamingResponse
39
38
  from pandas import DataFrame
40
39
  from pydantic import BaseModel
41
40
  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, UploadResolverDef
44
+ from dara.core.base_definitions import ActionResolverDef, BaseTask, NonTabularDataError, UploadResolverDef
45
45
  from dara.core.configuration import Configuration
46
- from dara.core.interactivity.any_data_variable import DataVariableRegistryEntry, upload
46
+ from dara.core.interactivity.any_data_variable import upload
47
47
  from dara.core.interactivity.filtering import FilterQuery, Pagination
48
+ from dara.core.interactivity.server_variable import ServerVariable
48
49
  from dara.core.internal.cache_store import CacheStore
49
50
  from dara.core.internal.download import DownloadRegistryEntry
50
51
  from dara.core.internal.execute_action import CURRENT_ACTION_ID
51
52
  from dara.core.internal.normalization import NormalizedPayload, denormalize, normalize
52
- from dara.core.internal.pandas_utils import df_to_json
53
+ from dara.core.internal.pandas_utils import data_response_to_json, df_to_json, is_data_response
53
54
  from dara.core.internal.registries import (
54
55
  action_def_registry,
55
56
  action_registry,
56
57
  backend_store_registry,
57
58
  component_registry,
58
- data_variable_registry,
59
59
  derived_variable_registry,
60
+ download_code_registry,
60
61
  latest_value_registry,
62
+ server_variable_registry,
61
63
  static_kwargs_registry,
62
64
  template_registry,
63
65
  upload_resolver_registry,
@@ -86,7 +88,7 @@ def error_decorator(handler: Callable[..., Any]):
86
88
  if isinstance(err, HTTPException):
87
89
  raise err
88
90
  dev_logger.error('Unhandled error', error=err)
89
- raise HTTPException(status_code=500, detail=str(err))
91
+ raise HTTPException(status_code=500, detail=str(err)) from err
90
92
 
91
93
  return _async_inner_func
92
94
 
@@ -99,7 +101,7 @@ def error_decorator(handler: Callable[..., Any]):
99
101
  if isinstance(err, HTTPException):
100
102
  raise err
101
103
  dev_logger.error('Unhandled error', error=err)
102
- raise HTTPException(status_code=500, detail=str(err))
104
+ raise HTTPException(status_code=500, detail=str(err)) from err
103
105
 
104
106
  return _inner_func
105
107
 
@@ -113,7 +115,7 @@ def create_router(config: Configuration):
113
115
  core_api_router = APIRouter()
114
116
 
115
117
  @core_api_router.get('/actions', dependencies=[Depends(verify_session)])
116
- async def get_actions(): # pylint: disable=unused-variable
118
+ async def get_actions():
117
119
  return action_def_registry.get_all().items()
118
120
 
119
121
  class ActionRequestBody(BaseModel):
@@ -133,7 +135,7 @@ def create_router(config: Configuration):
133
135
  """Execution id, unique to this request"""
134
136
 
135
137
  @core_api_router.post('/action/{uid}', dependencies=[Depends(verify_session)])
136
- async def get_action(uid: str, body: ActionRequestBody): # pylint: disable=unused-variable
138
+ async def get_action(uid: str, body: ActionRequestBody):
137
139
  store: CacheStore = utils_registry.get('Store')
138
140
  task_mgr: TaskManager = utils_registry.get('TaskManager')
139
141
  registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
@@ -150,7 +152,14 @@ def create_router(config: Configuration):
150
152
 
151
153
  # Execute the action - kick off a background task to run the handler
152
154
  response = await action_def.execute_action(
153
- action_def, body.input, values, static_kwargs, body.execution_id, body.ws_channel, store, task_mgr
155
+ action_def,
156
+ body.input,
157
+ values,
158
+ static_kwargs,
159
+ body.execution_id,
160
+ body.ws_channel,
161
+ store,
162
+ task_mgr,
154
163
  )
155
164
 
156
165
  if isinstance(response, BaseTask):
@@ -159,15 +168,22 @@ def create_router(config: Configuration):
159
168
 
160
169
  return {'execution_id': response}
161
170
 
162
- @core_api_router.get('/download')
163
- async def get_download(code: str): # pylint: disable=unused-variable
171
+ @core_api_router.get('/download') # explicitly unauthenticated
172
+ async def get_download(code: str):
164
173
  store: CacheStore = utils_registry.get('Store')
165
174
 
166
175
  try:
167
176
  data_entry = await store.get(DownloadRegistryEntry, key=code)
168
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
169
180
  if data_entry is None:
170
- raise ValueError('Invalid or expired download code')
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)
171
187
 
172
188
  async_file, cleanup = await data_entry.download(data_entry)
173
189
 
@@ -188,11 +204,11 @@ def create_router(config: Configuration):
188
204
  background=BackgroundTask(cleanup),
189
205
  )
190
206
 
191
- except KeyError:
192
- raise ValueError('Invalid or expired download code')
207
+ except (KeyError, ValueError) as e:
208
+ raise ValueError('Invalid or expired download code') from e
193
209
 
194
210
  @core_api_router.get('/config', dependencies=[Depends(verify_session)])
195
- async def get_config(): # pylint: disable=unused-variable
211
+ async def get_config():
196
212
  return {
197
213
  **config.model_dump(
198
214
  include={
@@ -209,13 +225,13 @@ def create_router(config: Configuration):
209
225
  }
210
226
 
211
227
  @core_api_router.get('/auth-config')
212
- async def get_auth_config(): # pylint: disable=unused-variable
228
+ async def get_auth_config():
213
229
  return {
214
230
  'auth_components': config.auth_config.component_config.model_dump(),
215
231
  }
216
232
 
217
233
  @core_api_router.get('/components', dependencies=[Depends(verify_session)])
218
- async def get_components(name: Optional[str] = None): # pylint: disable=unused-variable
234
+ async def get_components(name: Optional[str] = None):
219
235
  """
220
236
  If name is passed, will try to register the component
221
237
 
@@ -236,7 +252,7 @@ def create_router(config: Configuration):
236
252
  ws_channel: str
237
253
 
238
254
  @core_api_router.post('/components/{component}', dependencies=[Depends(verify_session)])
239
- async def get_component(component: str, body: ComponentRequestBody): # pylint: disable=unused-variable
255
+ async def get_component(component: str, body: ComponentRequestBody):
240
256
  CURRENT_COMPONENT_ID.set(body.uid)
241
257
  WS_CHANNEL.set(body.ws_channel)
242
258
  store: CacheStore = utils_registry.get('Store')
@@ -265,7 +281,7 @@ def create_router(config: Configuration):
265
281
  raise HTTPException(status_code=400, detail='Requesting this type of component is not supported')
266
282
 
267
283
  @core_api_router.get('/derived-variable/{uid}/latest', dependencies=[Depends(verify_session)])
268
- async def get_latest_derived_variable(uid: str): # pylint: disable=unused-variable
284
+ async def get_latest_derived_variable(uid: str):
269
285
  try:
270
286
  store: CacheStore = utils_registry.get('Store')
271
287
  latest_value_entry = latest_value_registry.get(uid)
@@ -289,129 +305,67 @@ def create_router(config: Configuration):
289
305
  return latest_value
290
306
 
291
307
  except KeyError as err:
292
- raise ValueError(f'Could not find latest value for derived variable with uid: {uid}').with_traceback(
293
- err.__traceback__
294
- )
308
+ raise ValueError(f'Could not find latest value for derived variable with uid: {uid}') from err
295
309
 
296
- class DataVariableRequestBody(BaseModel):
310
+ class TabularRequestBody(BaseModel):
297
311
  filters: Optional[FilterQuery] = None
298
- cache_key: Optional[str] = None
299
- ws_channel: Optional[str] = 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"""
300
317
 
301
- @core_api_router.post('/data-variable/{uid}', dependencies=[Depends(verify_session)])
302
- async def get_data_variable(
318
+ @core_api_router.post('/tabular-variable/{uid}', dependencies=[Depends(verify_session)])
319
+ async def get_tabular_variable(
303
320
  uid: str,
304
- body: DataVariableRequestBody,
321
+ body: TabularRequestBody,
305
322
  offset: Optional[int] = None,
306
323
  limit: Optional[int] = None,
307
324
  order_by: Optional[str] = None,
308
325
  index: Optional[str] = None,
309
- ): # pylint: disable=unused-variable
310
- try:
311
- store: CacheStore = utils_registry.get('Store')
312
- task_mgr: TaskManager = utils_registry.get('TaskManager')
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
- )
350
-
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
359
-
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
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)
371
332
 
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
333
  try:
375
- store: CacheStore = utils_registry.get('Store')
334
+ pagination = Pagination(offset=offset, limit=limit, orderBy=order_by, index=index)
376
335
  registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
377
- variable_def = await registry_mgr.get(data_variable_registry, uid)
378
-
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
- )
383
336
 
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
- )
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')
388
342
 
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))
392
-
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:
343
+ # DerivedVariable
396
344
  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)
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)
399
348
 
400
- if data_def.type == 'plain':
401
- return await data_def.get_schema(data_def, store)
349
+ result = await variable_def.get_tabular_data(
350
+ variable_def, store, task_mgr, values, body.force_key, pagination, body.filters
351
+ )
402
352
 
403
- if cache_key is None:
404
- raise HTTPException(
405
- status_code=400, detail='Cache key is required when requesting DerivedDataVariable schema'
406
- )
353
+ if isinstance(result, BaseTask):
354
+ await task_mgr.run_task(result, body.ws_channel)
355
+ return {'task_id': result.task_id}
407
356
 
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))
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
+
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}
415
369
 
416
370
  @core_api_router.post('/data/upload', dependencies=[Depends(verify_session)])
417
371
  async def upload_data(
@@ -427,7 +381,10 @@ def create_router(config: Configuration):
427
381
  registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
428
382
 
429
383
  if data_uid is None and resolver_id is None:
430
- raise HTTPException(400, 'Neither resolver_id or data_uid specified, at least one of them is required')
384
+ raise HTTPException(
385
+ 400,
386
+ 'Neither resolver_id or data_uid specified, at least one of them is required',
387
+ )
431
388
 
432
389
  try:
433
390
  # If resolver id is provided, run the custom
@@ -440,16 +397,15 @@ def create_router(config: Configuration):
440
397
 
441
398
  return {'status': 'SUCCESS'}
442
399
  except Exception as e:
443
- raise HTTPException(status_code=400, detail=str(e))
400
+ raise HTTPException(status_code=400, detail=str(e)) from e
444
401
 
445
402
  class DerivedStateRequestBody(BaseModel):
446
403
  values: NormalizedPayload[List[Any]]
447
- force: bool
404
+ force_key: Optional[str] = None
448
405
  ws_channel: str
449
- is_data_variable: Optional[bool] = False
450
406
 
451
407
  @core_api_router.post('/derived-variable/{uid}', dependencies=[Depends(verify_session)])
452
- async def get_derived_variable(uid: str, body: DerivedStateRequestBody): # pylint: disable=unused-variable
408
+ async def get_derived_variable(uid: str, body: DerivedStateRequestBody):
453
409
  task_mgr: TaskManager = utils_registry.get('TaskManager')
454
410
  store: CacheStore = utils_registry.get('Store')
455
411
  registry_mgr: RegistryLookup = utils_registry.get('RegistryLookup')
@@ -457,7 +413,7 @@ def create_router(config: Configuration):
457
413
 
458
414
  values = denormalize(body.values.data, body.values.lookup)
459
415
 
460
- result = await variable_def.get_value(variable_def, store, task_mgr, values, body.force)
416
+ result = await variable_def.get_value(variable_def, store, task_mgr, values, body.force_key)
461
417
 
462
418
  response: Any = result
463
419
 
@@ -466,7 +422,10 @@ def create_router(config: Configuration):
466
422
  if isinstance(result['value'], BaseTask):
467
423
  # Kick off the task
468
424
  await task_mgr.run_task(result['value'], body.ws_channel)
469
- response = {'task_id': result['value'].task_id, 'cache_key': result['cache_key']}
425
+ response = {
426
+ 'task_id': result['value'].task_id,
427
+ 'cache_key': result['cache_key'],
428
+ }
470
429
 
471
430
  dev_logger.debug(
472
431
  f'DerivedVariable {variable_def.uid[:3]}..{variable_def.uid[-3:]}',
@@ -512,7 +471,7 @@ def create_router(config: Configuration):
512
471
  tg.start_soon(_write, store_uid, value)
513
472
 
514
473
  @core_api_router.get('/tasks/{task_id}', dependencies=[Depends(verify_session)])
515
- async def get_task_result(task_id: str): # pylint: disable=unused-variable
474
+ async def get_task_result(task_id: str):
516
475
  try:
517
476
  task_mgr: TaskManager = utils_registry.get('TaskManager')
518
477
  res = await task_mgr.get_result(task_id)
@@ -523,30 +482,35 @@ def create_router(config: Configuration):
523
482
  {'value': res},
524
483
  )
525
484
 
526
- # Serialize dataframes correctly
485
+ # Serialize dataframes correctly, either direct or as a DataResponse
527
486
  if isinstance(res, DataFrame):
528
- return Response(df_to_json(res))
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')
529
490
 
530
491
  return res
531
- except Exception as e:
532
- raise ValueError(f'The result for task id {task_id} could not be found').with_traceback(e.__traceback__)
492
+ except Exception as err:
493
+ raise ValueError(f'The result for task id {task_id} could not be found') from err
533
494
 
534
495
  @core_api_router.delete('/tasks/{task_id}', dependencies=[Depends(verify_session)])
535
- async def cancel_task(task_id: str): # pylint: disable=unused-variable
496
+ async def cancel_task(task_id: str):
536
497
  try:
537
498
  task_mgr: TaskManager = utils_registry.get('TaskManager')
538
499
  return await task_mgr.cancel_task(task_id)
539
500
  except TaskManagerError as e:
540
- dev_logger.error(f'The task id {task_id} could not be found, it may have already been cancelled', 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
+ )
541
505
 
542
506
  @core_api_router.get('/template/{template}', dependencies=[Depends(verify_session)])
543
- async def get_template(template: str): # pylint: disable=unused-variable
507
+ async def get_template(template: str):
544
508
  try:
545
509
  selected_template = template_registry.get(template)
546
510
  normalized_template, lookup = normalize(jsonable_encoder(selected_template))
547
511
  return {'data': normalized_template, 'lookup': lookup}
548
- except KeyError:
549
- raise HTTPException(status_code=404, detail=f'Template: {template}, not found in registry')
512
+ except KeyError as err:
513
+ raise HTTPException(status_code=404, detail=f'Template: {template}, not found in registry') from err
550
514
  except Exception as e:
551
515
  dev_logger.error('Something went wrong while trying to get the template', e)
552
516
 
@@ -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, cast
24
24
 
25
25
  from croniter import croniter
26
26
  from pydantic import BaseModel, field_validator
@@ -56,25 +56,28 @@ 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 as err:
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
+ ) from err
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 = self.interval
70
+ interval: int
71
71
  # If there's more than one interval to wait, i.e. this is a weekday process
72
- if type(interval) == list:
72
+ if isinstance(self.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
+
75
78
  self.first_execution = False
76
79
  # Wait the interval and then run the job
77
- time.sleep(interval)
80
+ time.sleep(cast(int, interval))
78
81
  func(*args)
79
82
 
80
83
 
@@ -176,7 +179,7 @@ class ScheduledJobFactory(BaseModel):
176
179
 
177
180
  @field_validator('weekday', mode='before')
178
181
  @classmethod
179
- def validate_weekday(cls, weekday: Any) -> datetime: # pylint: disable=E0213
182
+ def validate_weekday(cls, weekday: Any) -> datetime:
180
183
  if isinstance(weekday, datetime):
181
184
  return weekday
182
185
  if isinstance(weekday, str):
@@ -283,7 +286,9 @@ class Scheduler:
283
286
  def _weekday(self, weekday: int):
284
287
  # The job must execute on a weekly interval
285
288
  return ScheduledJobFactory(
286
- interval=self.interval * 604800, run_once=self._run_once, weekday=str(weekday) # type: ignore
289
+ interval=self.interval * 604800,
290
+ run_once=self._run_once,
291
+ weekday=str(weekday), # type: ignore
287
292
  )
288
293
 
289
294
  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'))
86
+ return Settings(**dotenv_values('.env.test')) # type: ignore
87
87
 
88
88
  env_error = False
89
89