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/utils.py
CHANGED
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import asyncio
|
|
21
21
|
import inspect
|
|
22
22
|
import os
|
|
23
|
+
from collections.abc import Awaitable, Coroutine, Sequence
|
|
23
24
|
from functools import wraps
|
|
24
25
|
from importlib import import_module
|
|
25
26
|
from importlib.util import find_spec
|
|
@@ -27,21 +28,21 @@ from types import ModuleType
|
|
|
27
28
|
from typing import (
|
|
28
29
|
TYPE_CHECKING,
|
|
29
30
|
Any,
|
|
30
|
-
Awaitable,
|
|
31
31
|
Callable,
|
|
32
|
-
Coroutine,
|
|
33
32
|
Dict,
|
|
34
33
|
Literal,
|
|
35
34
|
Optional,
|
|
36
|
-
Sequence,
|
|
37
35
|
Tuple,
|
|
36
|
+
Type,
|
|
37
|
+
TypeVar,
|
|
38
38
|
Union,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
import anyio
|
|
42
42
|
from anyio import from_thread
|
|
43
|
-
from exceptiongroup import ExceptionGroup
|
|
43
|
+
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
|
|
44
44
|
from starlette.concurrency import run_in_threadpool
|
|
45
|
+
from typing_extensions import ParamSpec
|
|
45
46
|
|
|
46
47
|
from dara.core.auth.definitions import SESSION_ID, USER
|
|
47
48
|
from dara.core.base_definitions import CacheType
|
|
@@ -79,7 +80,7 @@ def get_cache_scope(cache_type: Optional[CacheType]) -> CacheScope:
|
|
|
79
80
|
return 'global'
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
async def run_user_handler(handler: Callable, args: Sequence =
|
|
83
|
+
async def run_user_handler(handler: Callable, args: Union[Sequence, None] = None, kwargs: Union[dict, None] = None):
|
|
83
84
|
"""
|
|
84
85
|
Run a user-defined handler function. Runs sync functions in a threadpool.
|
|
85
86
|
Handles SystemExits cleanly.
|
|
@@ -88,6 +89,10 @@ async def run_user_handler(handler: Callable, args: Sequence = [], kwargs: dict
|
|
|
88
89
|
:param args: list of arguments to pass to the function
|
|
89
90
|
:param kwargs: dict of kwargs to past to the function
|
|
90
91
|
"""
|
|
92
|
+
if args is None:
|
|
93
|
+
args = []
|
|
94
|
+
if kwargs is None:
|
|
95
|
+
kwargs = {}
|
|
91
96
|
with handle_system_exit('User defined function quit unexpectedly'):
|
|
92
97
|
if inspect.iscoroutinefunction(handler):
|
|
93
98
|
return await handler(*args, **kwargs)
|
|
@@ -164,17 +169,21 @@ def enforce_sso(conf: ConfigurationBuilder):
|
|
|
164
169
|
Raises if SSO is not used
|
|
165
170
|
"""
|
|
166
171
|
try:
|
|
167
|
-
from dara.enterprise import SSOAuthConfig
|
|
172
|
+
from dara.enterprise import SSOAuthConfig # pyright: ignore[reportMissingImports]
|
|
168
173
|
|
|
169
174
|
if conf.auth_config is None or not isinstance(conf.auth_config, SSOAuthConfig):
|
|
170
175
|
raise ValueError('Config does not have SSO auth enabled. Please update your application to configure SSO.')
|
|
171
|
-
except ImportError:
|
|
176
|
+
except ImportError as err:
|
|
172
177
|
raise ValueError(
|
|
173
178
|
'SSO is not enabled. Please install the dara_enterprise package and configure SSO to use this feature.'
|
|
174
|
-
)
|
|
179
|
+
) from err
|
|
175
180
|
|
|
176
181
|
|
|
177
|
-
|
|
182
|
+
P = ParamSpec('P')
|
|
183
|
+
T = TypeVar('T')
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def async_dedupe(fn: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
178
187
|
"""
|
|
179
188
|
Decorator to deduplicate concurrent calls to asynchronous functions based on their arguments.
|
|
180
189
|
|
|
@@ -192,7 +201,7 @@ def async_dedupe(fn: Callable[..., Awaitable]):
|
|
|
192
201
|
is_method = 'self' in inspect.signature(fn).parameters
|
|
193
202
|
|
|
194
203
|
@wraps(fn)
|
|
195
|
-
async def wrapped(*args, **kwargs):
|
|
204
|
+
async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
196
205
|
non_self_args = args[1:] if is_method else args
|
|
197
206
|
key = (non_self_args, frozenset(kwargs.items()))
|
|
198
207
|
lock = locks.get(key)
|
|
@@ -228,8 +237,22 @@ def resolve_exception_group(error: Any):
|
|
|
228
237
|
|
|
229
238
|
:param error: The error to resolve
|
|
230
239
|
"""
|
|
231
|
-
if isinstance(error, ExceptionGroup):
|
|
232
|
-
|
|
233
|
-
return resolve_exception_group(error.exceptions[0])
|
|
240
|
+
if isinstance(error, ExceptionGroup) and len(error.exceptions) == 1:
|
|
241
|
+
return resolve_exception_group(error.exceptions[0])
|
|
234
242
|
|
|
235
243
|
return error
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def exception_group_contains(err_type: Type[BaseException], group: BaseExceptionGroup) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Check if an ExceptionGroup contains an error of a given type, recursively
|
|
249
|
+
|
|
250
|
+
:param err_type: The type of error to check for
|
|
251
|
+
:param group: The ExceptionGroup to check
|
|
252
|
+
"""
|
|
253
|
+
for exc in group.exceptions:
|
|
254
|
+
if isinstance(exc, err_type):
|
|
255
|
+
return True
|
|
256
|
+
if isinstance(exc, BaseExceptionGroup):
|
|
257
|
+
return exception_group_contains(err_type, exc)
|
|
258
|
+
return False
|
dara/core/internal/websocket.py
CHANGED
|
@@ -204,28 +204,27 @@ class WebSocketHandler:
|
|
|
204
204
|
message_id = message.channel
|
|
205
205
|
|
|
206
206
|
# If the message has a channel ID, it's a response to a previous message
|
|
207
|
-
if message_id:
|
|
208
|
-
|
|
209
|
-
event, existing_messages = self.pending_responses[message_id]
|
|
210
|
-
|
|
211
|
-
# If the response is chunked then collect the messages in pending responses
|
|
212
|
-
if message.chunk_count is not None:
|
|
213
|
-
if existing_messages is not None and isinstance(existing_messages, list):
|
|
214
|
-
existing_messages.append(message.message)
|
|
215
|
-
else:
|
|
216
|
-
existing_messages = [message.message]
|
|
217
|
-
self.pending_responses[message_id] = (
|
|
218
|
-
event,
|
|
219
|
-
existing_messages,
|
|
220
|
-
)
|
|
207
|
+
if message_id and message_id in self.pending_responses:
|
|
208
|
+
event, existing_messages = self.pending_responses[message_id]
|
|
221
209
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
210
|
+
# If the response is chunked then collect the messages in pending responses
|
|
211
|
+
if message.chunk_count is not None:
|
|
212
|
+
if existing_messages is not None and isinstance(existing_messages, list):
|
|
213
|
+
existing_messages.append(message.message)
|
|
225
214
|
else:
|
|
226
|
-
|
|
227
|
-
self.pending_responses[message_id] = (
|
|
215
|
+
existing_messages = [message.message]
|
|
216
|
+
self.pending_responses[message_id] = (
|
|
217
|
+
event,
|
|
218
|
+
existing_messages,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# If all chunks have been received, set the event to notify the waiting coroutine
|
|
222
|
+
if len(existing_messages) == message.chunk_count:
|
|
228
223
|
event.set()
|
|
224
|
+
else:
|
|
225
|
+
# Store the response and set the event to notify the waiting coroutine
|
|
226
|
+
self.pending_responses[message_id] = (event, message.message)
|
|
227
|
+
event.set()
|
|
229
228
|
|
|
230
229
|
return None
|
|
231
230
|
|
|
@@ -475,7 +474,7 @@ async def ws_handler(websocket: WebSocket, token: Optional[str] = Query(default=
|
|
|
475
474
|
if pending_tokens_registry.has(token):
|
|
476
475
|
pending_tokens_registry.remove(token)
|
|
477
476
|
|
|
478
|
-
user_identifier = token_content.identity_id
|
|
477
|
+
user_identifier = token_content.identity_id
|
|
479
478
|
|
|
480
479
|
# Add the new session ID to known sessions for that user
|
|
481
480
|
if sessions_registry.has(user_identifier):
|
|
@@ -497,6 +496,8 @@ async def ws_handler(websocket: WebSocket, token: Optional[str] = Query(default=
|
|
|
497
496
|
SESSION_ID.set(token_data.session_id)
|
|
498
497
|
ID_TOKEN.set(token_data.id_token)
|
|
499
498
|
|
|
499
|
+
WS_CHANNEL.set(channel)
|
|
500
|
+
|
|
500
501
|
# Set initial Auth context vars for the WS connection
|
|
501
502
|
update_context(token_content)
|
|
502
503
|
|
dara/core/js_tooling/js_utils.py
CHANGED
|
@@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
|
|
15
15
|
limitations under the License.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
import contextlib
|
|
18
19
|
import importlib
|
|
19
20
|
import json
|
|
20
21
|
import os
|
|
@@ -236,10 +237,8 @@ class BuildCache(BaseModel):
|
|
|
236
237
|
|
|
237
238
|
# Create a symlink from the custom js folder into the static files directory
|
|
238
239
|
new_path = os.path.abspath(os.path.join(self.static_files_dir, self.build_config.js_config.local_entry))
|
|
239
|
-
|
|
240
|
+
with contextlib.suppress(FileNotFoundError):
|
|
240
241
|
os.unlink(new_path)
|
|
241
|
-
except FileNotFoundError:
|
|
242
|
-
pass
|
|
243
242
|
os.symlink(absolute_path, new_path)
|
|
244
243
|
|
|
245
244
|
# Create a symlink for the node modules in the custom_js folder
|
|
@@ -247,10 +246,8 @@ class BuildCache(BaseModel):
|
|
|
247
246
|
new_node_modules_path = os.path.abspath(
|
|
248
247
|
os.path.join(os.getcwd(), self.build_config.js_config.local_entry, 'node_modules')
|
|
249
248
|
)
|
|
250
|
-
|
|
249
|
+
with contextlib.suppress(FileNotFoundError):
|
|
251
250
|
os.unlink(new_node_modules_path)
|
|
252
|
-
except FileNotFoundError:
|
|
253
|
-
pass
|
|
254
251
|
os.symlink(node_modules_path, new_node_modules_path)
|
|
255
252
|
|
|
256
253
|
def get_importers(self) -> Dict[str, str]:
|
|
@@ -274,7 +271,7 @@ class BuildCache(BaseModel):
|
|
|
274
271
|
"""
|
|
275
272
|
py_modules = set()
|
|
276
273
|
|
|
277
|
-
for module in self.package_map
|
|
274
|
+
for module in self.package_map:
|
|
278
275
|
py_modules.add(module)
|
|
279
276
|
|
|
280
277
|
if 'dara.core' in py_modules:
|
|
@@ -316,8 +313,9 @@ class BuildCache(BaseModel):
|
|
|
316
313
|
# Append core deps required for building/dev mode
|
|
317
314
|
pkg_json['dependencies'] = {
|
|
318
315
|
**deps,
|
|
319
|
-
'@vitejs/plugin-react': '
|
|
320
|
-
'vite': '
|
|
316
|
+
'@vitejs/plugin-react': '4.6.0',
|
|
317
|
+
'vite': '7.0.4',
|
|
318
|
+
'vite-plugin-node-polyfills': '0.24.0',
|
|
321
319
|
}
|
|
322
320
|
|
|
323
321
|
return pkg_json
|
|
@@ -434,13 +432,16 @@ def _get_module_file(module: str) -> str:
|
|
|
434
432
|
return cast(str, imported_module.__file__)
|
|
435
433
|
|
|
436
434
|
|
|
437
|
-
def rebuild_js(build_cache: BuildCache, build_diff: BuildCacheDiff =
|
|
435
|
+
def rebuild_js(build_cache: BuildCache, build_diff: Union[BuildCacheDiff, None] = None):
|
|
438
436
|
"""
|
|
439
437
|
Generic 'rebuild' function which bundles/prepares assets depending on the build mode chosen
|
|
440
438
|
|
|
441
439
|
:param build_cache: current build configuration cache
|
|
442
440
|
:param build_diff: the difference between the current build cache and the previous build cache
|
|
443
441
|
"""
|
|
442
|
+
if build_diff is None:
|
|
443
|
+
build_diff = BuildCacheDiff.full_diff()
|
|
444
|
+
|
|
444
445
|
# If we are in docker mode, skip the JS build
|
|
445
446
|
if os.environ.get('DARA_DOCKER_MODE', 'FALSE') == 'TRUE':
|
|
446
447
|
dev_logger.debug('Docker mode, skipping JS build')
|
|
@@ -485,17 +486,16 @@ def bundle_js(build_cache: BuildCache, copy_js: bool = False):
|
|
|
485
486
|
:param copy_js: whether to copy JS instead of symlinking it
|
|
486
487
|
"""
|
|
487
488
|
# If custom JS is present, symlink it
|
|
488
|
-
if build_cache.build_config.js_config is not None:
|
|
489
|
-
if
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
build_cache.symlink_js()
|
|
489
|
+
if build_cache.build_config.js_config is not None and os.path.isdir(build_cache.build_config.js_config.local_entry):
|
|
490
|
+
if copy_js:
|
|
491
|
+
# Just move the directory to output
|
|
492
|
+
js_folder_name = os.path.basename(build_cache.build_config.js_config.local_entry)
|
|
493
|
+
shutil.copytree(
|
|
494
|
+
build_cache.build_config.js_config.local_entry,
|
|
495
|
+
os.path.join(build_cache.static_files_dir, js_folder_name),
|
|
496
|
+
)
|
|
497
|
+
else:
|
|
498
|
+
build_cache.symlink_js()
|
|
499
499
|
|
|
500
500
|
# Determine template paths
|
|
501
501
|
entry_template = os.path.join(pathlib.Path(__file__).parent.absolute(), 'templates/_entry.template.tsx')
|
|
@@ -544,16 +544,18 @@ def bundle_js(build_cache: BuildCache, copy_js: bool = False):
|
|
|
544
544
|
|
|
545
545
|
cwd = os.getcwd()
|
|
546
546
|
os.chdir(build_cache.static_files_dir)
|
|
547
|
-
|
|
547
|
+
dev_logger.info('Installing JS dependencies...')
|
|
548
|
+
exit_code = os.system(f'{package_manager} install') # nosec B605 # package_manager is validated
|
|
548
549
|
if exit_code > 0:
|
|
549
550
|
raise SystemError(
|
|
550
551
|
"Failed to install the JS dependencies - there's likely a connection issue or a broken package"
|
|
551
552
|
)
|
|
553
|
+
dev_logger.info('JS dependencies installed successfully')
|
|
552
554
|
|
|
553
555
|
# Load entry template as a string
|
|
554
|
-
with open(entry_template,
|
|
556
|
+
with open(entry_template, encoding='utf-8') as f:
|
|
555
557
|
entry_template_str = f.read()
|
|
556
|
-
with open(vite_template,
|
|
558
|
+
with open(vite_template, encoding='utf-8') as f:
|
|
557
559
|
vite_template_str = f.read()
|
|
558
560
|
|
|
559
561
|
# Convert importers dict to a string for injection into the template
|
|
@@ -574,7 +576,7 @@ def bundle_js(build_cache: BuildCache, copy_js: bool = False):
|
|
|
574
576
|
dev_logger.warning('App is in DEV mode, running `dara dev` CLI command alongside this process is required')
|
|
575
577
|
else:
|
|
576
578
|
# Run build pointed at the generated entry file
|
|
577
|
-
exit_code = os.system(f'{package_manager} run build')
|
|
579
|
+
exit_code = os.system(f'{package_manager} run build') # nosec B605 # package_manager is validated
|
|
578
580
|
if exit_code > 0:
|
|
579
581
|
raise SystemError('Failed to build the JS part of the project')
|
|
580
582
|
|
|
@@ -640,7 +642,7 @@ def build_autojs_template(html_template: str, build_cache: BuildCache, config: C
|
|
|
640
642
|
"""
|
|
641
643
|
settings = get_settings()
|
|
642
644
|
entry_template = os.path.join(pathlib.Path(__file__).parent.absolute(), 'templates/_entry_autojs.template.tsx')
|
|
643
|
-
with open(entry_template,
|
|
645
|
+
with open(entry_template, encoding='utf-8') as f:
|
|
644
646
|
entry_template_str = f.read()
|
|
645
647
|
|
|
646
648
|
importers_dict = build_cache.get_importers()
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import react from '@vitejs/plugin-react';
|
|
2
2
|
import { defineConfig } from 'vite';
|
|
3
|
+
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
|
3
4
|
|
|
4
5
|
export default defineConfig({
|
|
5
6
|
base: '',
|
|
6
7
|
plugins: [
|
|
7
8
|
react({
|
|
8
9
|
jsxRuntime: 'classic',
|
|
9
|
-
})
|
|
10
|
+
}),
|
|
11
|
+
// Some package we're pulling requires node polyfills for stream
|
|
12
|
+
nodePolyfills({
|
|
13
|
+
globals: {
|
|
14
|
+
process: true,
|
|
15
|
+
Buffer: true,
|
|
16
|
+
global: true,
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
10
19
|
],
|
|
11
20
|
publicDir: false,
|
|
12
21
|
build: {
|
|
13
22
|
outDir: '$$output$$',
|
|
14
23
|
assetsDir: '',
|
|
15
|
-
manifest:
|
|
24
|
+
manifest: 'manifest.json',
|
|
16
25
|
rollupOptions: {
|
|
17
26
|
input: './_entry.tsx',
|
|
18
27
|
},
|
|
@@ -35,5 +44,5 @@ export default defineConfig({
|
|
|
35
44
|
},
|
|
36
45
|
worker: {
|
|
37
46
|
format: 'es',
|
|
38
|
-
}
|
|
47
|
+
},
|
|
39
48
|
});
|
dara/core/logging.py
CHANGED
|
@@ -74,7 +74,7 @@ class Logger:
|
|
|
74
74
|
|
|
75
75
|
self._logger.warning(payload, extra={'content': extra})
|
|
76
76
|
|
|
77
|
-
def error(self, title: str, error:
|
|
77
|
+
def error(self, title: str, error: BaseException, extra: Optional[Dict[str, Any]] = None):
|
|
78
78
|
"""
|
|
79
79
|
Log a message at the ERROR level
|
|
80
80
|
|
|
@@ -135,6 +135,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|
|
135
135
|
# This is required so that requesting the body content doesn't hang the request
|
|
136
136
|
if request.headers.get('Content-Type') == 'application/json' and content_length < one_mb:
|
|
137
137
|
old_recieve = request._receive
|
|
138
|
+
|
|
138
139
|
# Add the debug logging into a new receive call that wraps the old one. This is required to make
|
|
139
140
|
# streaming requests and responses work as streaming sends further messages to trigger
|
|
140
141
|
# sending/receiving further data
|
|
@@ -170,11 +171,14 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|
|
170
171
|
return response
|
|
171
172
|
|
|
172
173
|
|
|
173
|
-
def _print_stacktrace():
|
|
174
|
+
def _print_stacktrace(err: Optional[BaseException] = None) -> str:
|
|
174
175
|
"""
|
|
175
176
|
Prints out the current stack trace whilst unwinding all the logging calls on the way so it just shows the relevant
|
|
176
177
|
parts. Will also extract any exceptions and print them at the end.
|
|
177
178
|
"""
|
|
179
|
+
if err is not None:
|
|
180
|
+
return ''.join(traceback.format_exception(type(err), err, err.__traceback__))
|
|
181
|
+
|
|
178
182
|
exc = sys.exc_info()[0]
|
|
179
183
|
stack = traceback.extract_stack()[:-1]
|
|
180
184
|
if exc is not None:
|
|
@@ -192,7 +196,7 @@ def _print_stacktrace():
|
|
|
192
196
|
trc = 'Traceback (most recent call last):\n'
|
|
193
197
|
stackstr = trc + ''.join(traceback.format_list(stack))
|
|
194
198
|
if exc is not None:
|
|
195
|
-
stackstr += ' ' + traceback.format_exc().lstrip(trc)
|
|
199
|
+
stackstr += ' ' + traceback.format_exc().lstrip(trc)
|
|
196
200
|
return stackstr
|
|
197
201
|
|
|
198
202
|
|
|
@@ -204,11 +208,7 @@ class DaraProdFormatter(logging.Formatter):
|
|
|
204
208
|
|
|
205
209
|
@staticmethod
|
|
206
210
|
def _get_payload(record: logging.LogRecord) -> Dict[str, JsonSerializable]:
|
|
207
|
-
timestamp = time.strftime(
|
|
208
|
-
'%Y-%m-%dT%H:%M:%S', time.localtime(record.created)
|
|
209
|
-
) + '.%s' % int( # pylint:disable=consider-using-f-string
|
|
210
|
-
record.msecs
|
|
211
|
-
)
|
|
211
|
+
timestamp = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime(record.created)) + '.%s' % int(record.msecs)
|
|
212
212
|
if isinstance(record.msg, dict):
|
|
213
213
|
payload: Dict[str, JsonSerializable] = {
|
|
214
214
|
'timestamp': timestamp,
|
|
@@ -218,8 +218,9 @@ class DaraProdFormatter(logging.Formatter):
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
if record.levelname == 'ERROR':
|
|
221
|
-
|
|
222
|
-
payload['
|
|
221
|
+
err = payload.pop('error')
|
|
222
|
+
payload['error'] = str(err)
|
|
223
|
+
payload['stacktrace'] = _print_stacktrace(err if isinstance(err, BaseException) else None)
|
|
223
224
|
|
|
224
225
|
return payload
|
|
225
226
|
|
|
@@ -284,8 +285,8 @@ class DaraDevFormatter(logging.Formatter):
|
|
|
284
285
|
|
|
285
286
|
if record.levelname == 'ERROR':
|
|
286
287
|
error = ''
|
|
287
|
-
if payload.get('error'):
|
|
288
|
-
error = _print_stacktrace()
|
|
288
|
+
if err := payload.get('error'):
|
|
289
|
+
error = _print_stacktrace(err if isinstance(err, BaseException) else None)
|
|
289
290
|
content = base_msg
|
|
290
291
|
if record.__dict__.get('content'):
|
|
291
292
|
content = content + '\r\n' + self.extra_template % (spacer, record.__dict__['content'])
|
dara/core/main.py
CHANGED
|
@@ -14,6 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
14
14
|
See the License for the specific language governing permissions and
|
|
15
15
|
limitations under the License.
|
|
16
16
|
"""
|
|
17
|
+
|
|
17
18
|
import asyncio
|
|
18
19
|
import os
|
|
19
20
|
import sys
|
|
@@ -91,10 +92,11 @@ def _start_application(config: Configuration):
|
|
|
91
92
|
os.environ['VITE_MANIFEST_PATH'] = f'{config.static_files_dir}/manifest.json'
|
|
92
93
|
os.environ['VITE_STATIC_PATH'] = config.static_files_dir
|
|
93
94
|
import fastapi_vite_dara
|
|
95
|
+
import fastapi_vite_dara.config
|
|
94
96
|
|
|
95
97
|
if len(config.pages) > 0:
|
|
96
98
|
BASE_DIR = Path(__file__).parent
|
|
97
|
-
jinja_templates = Jinja2Templates(directory=str(
|
|
99
|
+
jinja_templates = Jinja2Templates(directory=str(Path(BASE_DIR, 'jinja')))
|
|
98
100
|
jinja_templates.env.globals['vite_hmr_client'] = fastapi_vite_dara.vite_hmr_client
|
|
99
101
|
jinja_templates.env.globals['vite_asset'] = fastapi_vite_dara.vite_asset
|
|
100
102
|
jinja_templates.env.globals['static_url'] = fastapi_vite_dara.config.settings.static_url
|
|
@@ -107,7 +109,7 @@ def _start_application(config: Configuration):
|
|
|
107
109
|
|
|
108
110
|
# Configure the default executor for threads run via the async loop
|
|
109
111
|
loop = asyncio.get_event_loop()
|
|
110
|
-
loop.set_default_executor(ThreadPoolExecutor(max_workers=int(os.environ.get('DARA_NUM_COMPONENT_THREADS', 8))))
|
|
112
|
+
loop.set_default_executor(ThreadPoolExecutor(max_workers=int(os.environ.get('DARA_NUM_COMPONENT_THREADS', '8'))))
|
|
111
113
|
|
|
112
114
|
is_production = os.environ.get('DARA_DOCKER_MODE') == 'TRUE'
|
|
113
115
|
|
|
@@ -169,7 +171,7 @@ def _start_application(config: Configuration):
|
|
|
169
171
|
worker_parameters={'task_module': config.task_module},
|
|
170
172
|
max_workers=max_workers,
|
|
171
173
|
)
|
|
172
|
-
await task_pool.start(60)
|
|
174
|
+
await task_pool.start(60) # timeout after 60s
|
|
173
175
|
utils_registry.set('TaskPool', task_pool)
|
|
174
176
|
dev_logger.info('Task pool initialized')
|
|
175
177
|
|
|
@@ -337,15 +339,15 @@ def _start_application(config: Configuration):
|
|
|
337
339
|
|
|
338
340
|
# Start metrics server in a daemon thread
|
|
339
341
|
if os.environ.get('DARA_DISABLE_METRICS') != 'TRUE' and os.environ.get('DARA_TEST_FLAG', None) is None:
|
|
340
|
-
port = int(os.environ.get('DARA_METRICS_PORT', 10000))
|
|
342
|
+
port = int(os.environ.get('DARA_METRICS_PORT', '10000'))
|
|
341
343
|
start_http_server(port)
|
|
342
344
|
|
|
343
345
|
# Start profiling server in a daemon thread if explicitly enabled (only works on linux)
|
|
344
346
|
if os.environ.get('DARA_PYPPROF_PORT', None) is not None:
|
|
345
|
-
profiling_port = int(os.environ.get('DARA_PYPPROF_PORT', 10001))
|
|
347
|
+
profiling_port = int(os.environ.get('DARA_PYPPROF_PORT', '10001'))
|
|
346
348
|
dev_logger.warning('Starting cpu/memory profiling server', extra={'port': profiling_port})
|
|
347
349
|
|
|
348
|
-
from pypprof.net_http import start_pprof_server
|
|
350
|
+
from pypprof.net_http import start_pprof_server # pyright: ignore[reportMissingImports]
|
|
349
351
|
|
|
350
352
|
start_pprof_server(port=profiling_port)
|
|
351
353
|
|
|
@@ -358,7 +360,7 @@ def _start_application(config: Configuration):
|
|
|
358
360
|
app.include_router(core_api_router, prefix='/api/core')
|
|
359
361
|
|
|
360
362
|
@app.get('/api/{rest_of_path:path}')
|
|
361
|
-
async def not_found():
|
|
363
|
+
async def not_found():
|
|
362
364
|
raise HTTPException(status_code=404, detail='API endpoint not found')
|
|
363
365
|
|
|
364
366
|
if len(config.pages) > 0:
|
|
@@ -369,22 +371,23 @@ def _start_application(config: Configuration):
|
|
|
369
371
|
# Auto-js mode - serve the built template with UMDs
|
|
370
372
|
if build_cache.build_config.mode == BuildMode.AUTO_JS:
|
|
371
373
|
# Load template
|
|
372
|
-
|
|
374
|
+
template_path = os.path.join(Path(BASE_DIR, 'jinja'), 'index_autojs.html') # type: ignore
|
|
375
|
+
with open(template_path, encoding='utf-8') as fp:
|
|
373
376
|
template = fp.read()
|
|
374
377
|
|
|
375
378
|
# Generate tags for the template
|
|
376
379
|
template = build_autojs_template(template, build_cache, config)
|
|
377
380
|
|
|
378
381
|
@app.get('/{full_path:path}', include_in_schema=False, response_class=HTMLResponse)
|
|
379
|
-
async def serve_app(request: Request): #
|
|
382
|
+
async def serve_app(request: Request): # pyright: ignore[reportRedeclaration]
|
|
380
383
|
return HTMLResponse(template)
|
|
381
384
|
|
|
382
385
|
else:
|
|
383
386
|
# Otherwise serve the Vite template
|
|
384
387
|
|
|
385
388
|
@app.get('/{full_path:path}', include_in_schema=False, response_class=_TemplateResponse)
|
|
386
|
-
async def serve_app(request: Request): #
|
|
387
|
-
return jinja_templates.TemplateResponse(request, 'index.html')
|
|
389
|
+
async def serve_app(request: Request): # pyright: ignore[reportRedeclaration]
|
|
390
|
+
return jinja_templates.TemplateResponse(request, 'index.html') # type: ignore
|
|
388
391
|
|
|
389
392
|
return app
|
|
390
393
|
|
dara/core/metrics/cache.py
CHANGED
|
@@ -47,7 +47,7 @@ def format_bytes(num: Union[int, float]) -> str:
|
|
|
47
47
|
# We only shrink the number if we HAVEN'T reached the last unit.
|
|
48
48
|
num /= unit_step
|
|
49
49
|
|
|
50
|
-
return f'{num:.2f} {unit}'
|
|
50
|
+
return f'{num:.2f} {unit}' # type: ignore
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class CacheMetricsTracker(BaseModel):
|
dara/core/metrics/utils.py
CHANGED
|
@@ -51,11 +51,11 @@ def total_size(o: object):
|
|
|
51
51
|
|
|
52
52
|
try:
|
|
53
53
|
all_handlers = {tuple: iter, list: iter, dict: dict_handler, set: iter, BaseModel: pydantic_handler}
|
|
54
|
-
seen = set()
|
|
55
|
-
default_size = getsizeof(0)
|
|
54
|
+
seen = set() # track which object id's have already been seen
|
|
55
|
+
default_size = getsizeof(0) # estimate sizeof object without __sizeof__
|
|
56
56
|
|
|
57
57
|
def sizeof(o):
|
|
58
|
-
if id(o) in seen:
|
|
58
|
+
if id(o) in seen: # do not double count the same object
|
|
59
59
|
return 0
|
|
60
60
|
seen.add(id(o))
|
|
61
61
|
s = getsizeof(o, default_size)
|
dara/core/persistence.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Awaitable
|
|
4
5
|
from typing import (
|
|
5
6
|
TYPE_CHECKING,
|
|
6
7
|
Any,
|
|
7
|
-
Awaitable,
|
|
8
8
|
Callable,
|
|
9
9
|
Dict,
|
|
10
10
|
List,
|
|
@@ -255,7 +255,7 @@ class BackendStore(PersistenceStore):
|
|
|
255
255
|
user = USER.get()
|
|
256
256
|
|
|
257
257
|
if user:
|
|
258
|
-
user_key = user.identity_id
|
|
258
|
+
user_key = user.identity_id
|
|
259
259
|
|
|
260
260
|
# Make sure the store is initialized
|
|
261
261
|
if user_key not in self.initialized_scopes:
|
|
@@ -277,7 +277,7 @@ class BackendStore(PersistenceStore):
|
|
|
277
277
|
if key == 'global':
|
|
278
278
|
return None
|
|
279
279
|
|
|
280
|
-
# otherwise key is a user identity_id
|
|
280
|
+
# otherwise key is a user identity_id
|
|
281
281
|
return key
|
|
282
282
|
|
|
283
283
|
def _register(self):
|
|
@@ -374,7 +374,7 @@ class BackendStore(PersistenceStore):
|
|
|
374
374
|
if not user:
|
|
375
375
|
return
|
|
376
376
|
|
|
377
|
-
user_identifier = user.identity_id
|
|
377
|
+
user_identifier = user.identity_id
|
|
378
378
|
return await self._notify_user(user_identifier, value=value, ignore_channel=ignore_channel)
|
|
379
379
|
|
|
380
380
|
async def _notify_patches(self, patches: List[Dict[str, Any]]):
|
|
@@ -393,7 +393,7 @@ class BackendStore(PersistenceStore):
|
|
|
393
393
|
if not user:
|
|
394
394
|
return
|
|
395
395
|
|
|
396
|
-
user_identifier = user.identity_id
|
|
396
|
+
user_identifier = user.identity_id
|
|
397
397
|
return await self._notify_user(user_identifier, patches=patches)
|
|
398
398
|
|
|
399
399
|
async def init(self, variable: 'Variable'):
|
|
@@ -540,3 +540,25 @@ class BackendStoreEntry(BaseModel):
|
|
|
540
540
|
uid: str
|
|
541
541
|
store: BackendStore
|
|
542
542
|
"""Store instance"""
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class BrowserStore(PersistenceStore):
|
|
546
|
+
"""
|
|
547
|
+
Persistence store implementation that uses browser local storage
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
async def init(self, variable: 'Variable'):
|
|
551
|
+
# noop
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class QueryParamStore(PersistenceStore):
|
|
556
|
+
"""
|
|
557
|
+
Persistence store implementation that uses a URL query parameter
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
query: str
|
|
561
|
+
|
|
562
|
+
async def init(self, variable: 'Variable'):
|
|
563
|
+
# noop
|
|
564
|
+
pass
|