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
@@ -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 = [], kwargs: dict = {}):
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
- def async_dedupe(fn: Callable[..., Awaitable]):
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
- if len(error.exceptions) == 1:
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
@@ -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
- if message_id in self.pending_responses:
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
- # If all chunks have been received, set the event to notify the waiting coroutine
223
- if len(existing_messages) == message.chunk_count:
224
- event.set()
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
- # Store the response and set the event to notify the waiting coroutine
227
- self.pending_responses[message_id] = (event, message.message)
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 or token_content.identity_name
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
 
@@ -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
- try:
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
- try:
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.keys():
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': '2.1.0',
320
- 'vite': '3.1.8',
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 = BuildCacheDiff.full_diff()):
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 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()
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
- exit_code = os.system(f'{package_manager} install') # nosec B605 # package_manager is validated
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, 'r', encoding='utf-8') as f:
556
+ with open(entry_template, encoding='utf-8') as f:
555
557
  entry_template_str = f.read()
556
- with open(vite_template, 'r', encoding='utf-8') as f:
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') # nosec B605 # package_manager is validated
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, 'r', encoding='utf-8') as f:
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: true,
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: Exception, extra: Optional[Dict[str, Any]] = None):
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) # pylint:disable=bad-str-strip-call
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
- payload['error'] = str(payload.pop('error'))
222
- payload['stacktrace'] = _print_stacktrace()
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((Path(BASE_DIR, 'jinja'))))
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) # timeout after 60s
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(): # pylint: disable=unused-variable
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
- with open(os.path.join(Path(BASE_DIR, 'jinja'), 'index_autojs.html'), 'r', encoding='utf-8') as fp:
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): # pylint: disable=unused-variable
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): # pylint: disable=unused-variable
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
 
@@ -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):
@@ -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() # track which object id's have already been seen
55
- default_size = getsizeof(0) # estimate sizeof object without __sizeof__
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: # do not double count the same object
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 or user.identity_name
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 or identity_name
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 or user.identity_name
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 or user.identity_name
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