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.
- dara/core/__init__.py +3 -0
- dara/core/actions.py +1 -2
- dara/core/auth/basic.py +22 -16
- 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 +22 -64
- dara/core/cli.py +8 -7
- dara/core/configuration.py +5 -2
- dara/core/css.py +1 -2
- dara/core/data_utils.py +18 -19
- dara/core/defaults.py +6 -7
- dara/core/definitions.py +50 -19
- dara/core/http.py +7 -3
- dara/core/interactivity/__init__.py +6 -0
- dara/core/interactivity/actions.py +52 -50
- dara/core/interactivity/any_data_variable.py +7 -134
- dara/core/interactivity/any_variable.py +5 -8
- dara/core/interactivity/client_variable.py +71 -0
- dara/core/interactivity/data_variable.py +8 -266
- dara/core/interactivity/derived_data_variable.py +7 -290
- dara/core/interactivity/derived_variable.py +416 -176
- dara/core/interactivity/filtering.py +46 -27
- dara/core/interactivity/loop_variable.py +2 -2
- dara/core/interactivity/non_data_variable.py +5 -68
- dara/core/interactivity/plain_variable.py +89 -15
- dara/core/interactivity/server_variable.py +325 -0
- dara/core/interactivity/state_variable.py +69 -0
- dara/core/interactivity/switch_variable.py +19 -19
- dara/core/interactivity/tabular_variable.py +94 -0
- dara/core/interactivity/url_variable.py +10 -90
- dara/core/internal/cache_store/base_impl.py +2 -1
- dara/core/internal/cache_store/cache_store.py +22 -25
- dara/core/internal/cache_store/keep_all.py +4 -1
- dara/core/internal/cache_store/lru.py +5 -1
- dara/core/internal/cache_store/ttl.py +4 -1
- dara/core/internal/cgroup.py +1 -1
- dara/core/internal/dependency_resolution.py +60 -66
- dara/core/internal/devtools.py +12 -5
- dara/core/internal/download.py +13 -4
- dara/core/internal/encoder_registry.py +7 -7
- dara/core/internal/execute_action.py +13 -13
- dara/core/internal/hashing.py +1 -3
- dara/core/internal/import_discovery.py +3 -4
- dara/core/internal/multi_resource_lock.py +70 -0
- dara/core/internal/normalization.py +9 -18
- dara/core/internal/pandas_utils.py +107 -5
- dara/core/internal/pool/definitions.py +1 -1
- dara/core/internal/pool/task_pool.py +25 -16
- dara/core/internal/pool/utils.py +21 -18
- dara/core/internal/pool/worker.py +3 -2
- dara/core/internal/port_utils.py +1 -1
- dara/core/internal/registries.py +12 -6
- dara/core/internal/registry.py +4 -2
- dara/core/internal/registry_lookup.py +11 -5
- dara/core/internal/routing.py +109 -145
- dara/core/internal/scheduler.py +13 -8
- dara/core/internal/settings.py +2 -2
- dara/core/internal/store.py +2 -29
- dara/core/internal/tasks.py +379 -195
- dara/core/internal/utils.py +36 -13
- dara/core/internal/websocket.py +21 -20
- dara/core/js_tooling/js_utils.py +28 -26
- dara/core/js_tooling/templates/vite.config.template.ts +12 -3
- dara/core/logging.py +13 -12
- dara/core/main.py +14 -11
- dara/core/metrics/cache.py +1 -1
- dara/core/metrics/utils.py +3 -3
- dara/core/persistence.py +27 -5
- dara/core/umd/dara.core.umd.js +68291 -64718
- dara/core/visual/components/__init__.py +2 -2
- dara/core/visual/components/fallback.py +30 -4
- dara/core/visual/components/for_cmp.py +4 -1
- dara/core/visual/css/__init__.py +30 -31
- dara/core/visual/dynamic_component.py +31 -28
- dara/core/visual/progress_updater.py +4 -3
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/METADATA +12 -11
- dara_core-1.20.1a3.dist-info/RECORD +119 -0
- dara_core-1.20.1a1.dist-info/RECORD +0 -114
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/LICENSE +0 -0
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/WHEEL +0 -0
- {dara_core-1.20.1a1.dist-info → dara_core-1.20.1a3.dist-info}/entry_points.txt +0 -0
dara/core/internal/registries.py
CHANGED
|
@@ -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,
|
|
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)
|
|
39
|
-
action_registry = Registry[ActionResolverDef](RegistryType.ACTION)
|
|
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
|
-
)
|
|
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
|
-
|
|
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"""
|
dara/core/internal/registry.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
)
|
|
71
|
+
) from e
|
dara/core/internal/routing.py
CHANGED
|
@@ -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,
|
|
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
|
|
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():
|
|
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):
|
|
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,
|
|
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):
|
|
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
|
-
|
|
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():
|
|
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():
|
|
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):
|
|
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):
|
|
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):
|
|
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}')
|
|
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
|
|
310
|
+
class TabularRequestBody(BaseModel):
|
|
297
311
|
filters: Optional[FilterQuery] = None
|
|
298
|
-
|
|
299
|
-
|
|
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('/
|
|
302
|
-
async def
|
|
318
|
+
@core_api_router.post('/tabular-variable/{uid}', dependencies=[Depends(verify_session)])
|
|
319
|
+
async def get_tabular_variable(
|
|
303
320
|
uid: str,
|
|
304
|
-
body:
|
|
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
|
-
):
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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(
|
|
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
|
-
|
|
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):
|
|
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.
|
|
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 = {
|
|
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):
|
|
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
|
|
532
|
-
raise ValueError(f'The result for task id {task_id} could not be found')
|
|
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):
|
|
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(
|
|
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):
|
|
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
|
|
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, 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
|
|
70
|
+
interval: int
|
|
71
71
|
# If there's more than one interval to wait, i.e. this is a weekday process
|
|
72
|
-
if
|
|
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:
|
|
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,
|
|
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:
|
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')) # type: ignore
|
|
87
87
|
|
|
88
88
|
env_error = False
|
|
89
89
|
|