dara-core 1.15.7__py3-none-any.whl → 1.16.0__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 (50) hide show
  1. dara/core/__init__.py +16 -27
  2. dara/core/auth/base.py +3 -2
  3. dara/core/auth/definitions.py +0 -3
  4. dara/core/auth/utils.py +1 -1
  5. dara/core/base_definitions.py +122 -65
  6. dara/core/configuration.py +5 -8
  7. dara/core/defaults.py +0 -3
  8. dara/core/definitions.py +95 -231
  9. dara/core/interactivity/__init__.py +12 -18
  10. dara/core/interactivity/actions.py +22 -19
  11. dara/core/interactivity/any_data_variable.py +2 -4
  12. dara/core/interactivity/any_variable.py +10 -2
  13. dara/core/interactivity/condition.py +7 -10
  14. dara/core/interactivity/data_variable.py +11 -12
  15. dara/core/interactivity/derived_data_variable.py +7 -7
  16. dara/core/interactivity/derived_variable.py +20 -17
  17. dara/core/interactivity/filtering.py +1 -1
  18. dara/core/interactivity/plain_variable.py +53 -6
  19. dara/core/interactivity/url_variable.py +7 -6
  20. dara/core/internal/download.py +1 -1
  21. dara/core/internal/encoder_registry.py +14 -0
  22. dara/core/internal/hashing.py +1 -1
  23. dara/core/internal/normalization.py +0 -24
  24. dara/core/internal/routing.py +10 -10
  25. dara/core/internal/scheduler.py +3 -2
  26. dara/core/internal/settings.py +2 -4
  27. dara/core/internal/store.py +0 -3
  28. dara/core/internal/tasks.py +2 -2
  29. dara/core/internal/websocket.py +29 -20
  30. dara/core/js_tooling/js_utils.py +1 -1
  31. dara/core/main.py +2 -2
  32. dara/core/persistence.py +12 -4
  33. dara/core/umd/dara.core.umd.js +13 -277
  34. dara/core/visual/components/__init__.py +0 -3
  35. dara/core/visual/components/fallback.py +3 -3
  36. dara/core/visual/components/invalid_component.py +3 -3
  37. dara/core/visual/components/menu.py +3 -3
  38. dara/core/visual/components/progress_tracker.py +3 -2
  39. dara/core/visual/components/raw_string.py +3 -3
  40. dara/core/visual/components/router_content.py +3 -3
  41. dara/core/visual/components/sidebar_frame.py +3 -3
  42. dara/core/visual/components/topbar_frame.py +3 -3
  43. dara/core/visual/css/__init__.py +2 -6
  44. dara/core/visual/dynamic_component.py +3 -6
  45. {dara_core-1.15.7.dist-info → dara_core-1.16.0.dist-info}/METADATA +13 -12
  46. {dara_core-1.15.7.dist-info → dara_core-1.16.0.dist-info}/RECORD +49 -50
  47. dara/core/visual/components/for_cmp.py +0 -150
  48. {dara_core-1.15.7.dist-info → dara_core-1.16.0.dist-info}/LICENSE +0 -0
  49. {dara_core-1.15.7.dist-info → dara_core-1.16.0.dist-info}/WHEEL +0 -0
  50. {dara_core-1.15.7.dist-info → dara_core-1.16.0.dist-info}/entry_points.txt +0 -0
dara/core/__init__.py CHANGED
@@ -14,35 +14,16 @@ 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
-
18
- from __future__ import annotations
19
-
20
17
  from importlib.metadata import version
21
18
 
22
- from dara.core.base_definitions import Cache, CacheType
19
+ from pydantic import BaseModel
20
+
21
+ from dara.core.base_definitions import *
23
22
  from dara.core.configuration import ConfigurationBuilder
24
23
  from dara.core.css import CSSProperties, get_icon
25
- from dara.core.definitions import ComponentInstance, ErrorHandlingConfig, template
26
- from dara.core.interactivity import (
27
- DataVariable,
28
- DerivedDataVariable,
29
- DerivedVariable,
30
- DownloadContent,
31
- DownloadContentImpl,
32
- DownloadVariable,
33
- NavigateTo,
34
- NavigateToImpl,
35
- Notify,
36
- ResetVariables,
37
- SideEffect,
38
- TriggerVariable,
39
- UpdateVariable,
40
- UpdateVariableImpl,
41
- UrlVariable,
42
- Variable,
43
- action,
44
- )
45
- from dara.core.visual.components import Fallback, For
24
+ from dara.core.definitions import *
25
+ from dara.core.interactivity import *
26
+ from dara.core.visual.components import Fallback
46
27
  from dara.core.visual.dynamic_component import py_component
47
28
  from dara.core.visual.progress_updater import ProgressUpdater, track_progress
48
29
 
@@ -75,11 +56,19 @@ __all__ = [
75
56
  'ProgressUpdater',
76
57
  'track_progress',
77
58
  'ComponentInstance',
59
+ 'StyledComponentInstance',
78
60
  'ErrorHandlingConfig',
79
- 'template',
80
- 'For',
81
61
  'Fallback',
82
62
  'UpdateVariableImpl',
83
63
  'DownloadContentImpl',
84
64
  'NavigateToImpl',
85
65
  ]
66
+
67
+ for symbol in list(globals().values()):
68
+ try:
69
+ if issubclass(symbol, BaseModel) and symbol is not BaseModel:
70
+ symbol.model_rebuild()
71
+ except Exception as e:
72
+ from dara.core.logging import dev_logger
73
+
74
+ dev_logger.warning(f'Error rebuilding model "{symbol}": {e}')
dara/core/auth/base.py CHANGED
@@ -19,7 +19,7 @@ import abc
19
19
  from typing import Any, ClassVar, Dict, Union
20
20
 
21
21
  from fastapi import HTTPException, Response
22
- from pydantic import BaseModel
22
+ from pydantic import BaseModel, model_serializer
23
23
  from typing_extensions import TypedDict
24
24
 
25
25
  from dara.core.auth.definitions import (
@@ -59,7 +59,8 @@ class AuthComponentConfig(BaseModel):
59
59
  extra: Dict[str, AuthComponent] = {}
60
60
  """Extra components, map of route -> component"""
61
61
 
62
- def dict(self, *args, **kwargs):
62
+ @model_serializer()
63
+ def ser_model(self) -> dict:
63
64
  return {'login': self.login, 'logout': self.logout, **self.extra}
64
65
 
65
66
 
@@ -44,9 +44,6 @@ class TokenData(BaseModel):
44
44
  id_token: Optional[str] = None
45
45
  groups: Optional[List[str]] = []
46
46
 
47
- class Config:
48
- smart_union = True
49
-
50
47
 
51
48
  class UserData(BaseModel):
52
49
  """
dara/core/auth/utils.py CHANGED
@@ -79,7 +79,7 @@ def sign_jwt(
79
79
  identity_email=identity_email,
80
80
  groups=groups,
81
81
  id_token=id_token,
82
- ).dict(),
82
+ ).model_dump(),
83
83
  settings.jwt_secret,
84
84
  algorithm=JWT_ALGO,
85
85
  )
@@ -24,58 +24,133 @@ import uuid
24
24
  from enum import Enum
25
25
  from typing import (
26
26
  TYPE_CHECKING,
27
+ Annotated,
27
28
  Any,
28
29
  Awaitable,
29
30
  Callable,
30
31
  ClassVar,
32
+ Dict,
31
33
  List,
32
34
  Mapping,
33
35
  Optional,
36
+ Tuple,
34
37
  Union,
38
+ get_args,
39
+ get_origin,
35
40
  )
36
41
 
37
42
  import anyio
38
43
  from anyio.streams.memory import MemoryObjectSendStream
39
- from pydantic import BaseModel, Field
44
+ from pydantic import (
45
+ BaseModel,
46
+ ConfigDict,
47
+ Field,
48
+ SerializeAsAny,
49
+ SerializerFunctionWrapHandler,
50
+ model_serializer,
51
+ )
52
+ from pydantic._internal._model_construction import ModelMetaclass
40
53
 
41
54
  if TYPE_CHECKING:
42
55
  from dara.core.interactivity.actions import ActionCtx
43
56
 
44
57
 
45
- class DaraBaseModel(BaseModel):
58
+ def annotation_has_base_model(typ: Any) -> bool:
59
+ """
60
+ Check whether the given value is of the given type.
61
+
62
+ Handles Union and List types.
46
63
  """
47
- Custom BaseModel which handles TemplateMarkers.
48
- If TemplateMarkers are present, validation is skipped.
64
+ try:
65
+ type_args = get_args(typ)
66
+ if len(type_args) > 0:
67
+ return any(annotation_has_base_model(arg) for arg in type_args)
68
+ except: # pylint: disable=bare-except
69
+ # canot get arguments, should be a simple type
70
+ pass
71
+
72
+ # Handle simple types
73
+ return typ is BaseModel or issubclass(typ, BaseModel)
74
+
75
+
76
+ # This reverts v1->v2 change to make any BaseModel field duck-type serializable.
77
+ # It works by adding SerializeAsAny to all fields of the model.
78
+ # See https://github.com/pydantic/pydantic/issues/6381
79
+ class SerializeAsAnyMeta(ModelMetaclass):
80
+ def __new__(
81
+ self, name: str, bases: Tuple[type], namespaces: Dict[str, Any], **kwargs
82
+ ): # pylint: disable=bad-mcs-classmethod-argument
83
+ annotations: dict = namespaces.get('__annotations__', {}).copy()
84
+
85
+ for base in bases:
86
+ for base_ in base.__mro__:
87
+ if base_ is BaseModel:
88
+ annotations.update(base_.__annotations__)
89
+
90
+ for field, annotation in annotations.items():
91
+ if not field.startswith('__'):
92
+ # Wrapping `ClassVar`s in `SerializeAsAny` breaks pydantic behaviour of making `ClassVar` fields
93
+ # accessible via the class itself
94
+ # NOTE: the annotation can be a string due to future annotations
95
+ if isinstance(annotation, str) or annotation is ClassVar:
96
+ continue
97
+ if annotation_has_base_model(annotation):
98
+ annotations[field] = SerializeAsAny[annotation] # type: ignore
99
+
100
+ namespaces['__annotations__'] = annotations
101
+
102
+ return super().__new__(self, name, bases, namespaces, **kwargs)
103
+
104
+
105
+ class DaraBaseModel(BaseModel, metaclass=SerializeAsAnyMeta):
106
+ """
107
+ Custom BaseModel for dara internals.
49
108
  """
50
109
 
51
- class Config:
52
- smart_union = True
53
- extra = 'forbid'
110
+ model_config = ConfigDict(extra='forbid')
54
111
 
55
- def __init__(self, *args, **kwargs):
56
- has_template_var_marker = any(isinstance(arg, TemplateMarker) for arg in args) or any(
57
- isinstance(value, TemplateMarker) for value in kwargs.values()
112
+ @classmethod
113
+ def model_rebuild(
114
+ cls, *, force: bool = False, raise_errors: bool = True, _parent_namespace_depth: int = 2, _types_namespace=None
115
+ ) -> bool | None:
116
+ """
117
+ Rebuild the model to re-apply the SerializeAsAny wrapper once we've resolved the string annotations
118
+ """
119
+ # rebuild once to get all types sorted
120
+ super().model_rebuild(
121
+ force=force,
122
+ raise_errors=raise_errors,
123
+ _parent_namespace_depth=_parent_namespace_depth + 1,
124
+ _types_namespace=_types_namespace,
58
125
  )
59
126
 
60
- if has_template_var_marker:
61
- # Imitate pydantic's BaseModel.__init__ but avoid validation
62
- values = {}
63
- fields_set = set()
64
- _missing = object()
65
-
66
- for name, field in self.__class__.__fields__.items():
67
- value = kwargs.get(field.alias, _missing)
68
- if value is _missing:
69
- value = field.get_default()
70
- else:
71
- fields_set.add(name)
72
- values[name] = value
73
-
74
- object.__setattr__(self, '__dict__', values)
75
- object.__setattr__(self, '__fields_set__', fields_set)
76
- self._init_private_attributes()
77
- else:
78
- super().__init__(*args, **kwargs)
127
+ # Re-apply the SerializeAsAny wrapper to any fields that were originally strings
128
+ for field_info in cls.__pydantic_fields__.values():
129
+ # if the original was a str, we've just resolved it so we can re-apply the SerializeAsAny wrapper
130
+ if (
131
+ field_info.annotation is not SerializeAsAny
132
+ and field_info.annotation is not ClassVar
133
+ and annotation_has_base_model(field_info.annotation)
134
+ ):
135
+ # Skip if it has metadata that is already annotated with SerializeAsAny
136
+ if any(isinstance(x, SerializeAsAny) for x in field_info.metadata): # type: ignore
137
+ continue
138
+ # Skip if the type is already annotated with SerializeAsAny
139
+ if get_origin(field_info.annotation) is Annotated and any(
140
+ isinstance(arg, SerializeAsAny) for arg in field_info.annotation.__metadata__ # type: ignore
141
+ ):
142
+ continue
143
+
144
+ field_info.annotation = SerializeAsAny[field_info.annotation] # type: ignore
145
+ field_info.metadata = list(field_info.annotation.__metadata__) # type: ignore
146
+
147
+ # Rebuild again with force to ensure we rebuild the schema with new annotations
148
+ return super().model_rebuild(
149
+ force=True,
150
+ raise_errors=raise_errors,
151
+ _parent_namespace_depth=_parent_namespace_depth + 1,
152
+ _types_namespace=_types_namespace,
153
+ )
79
154
 
80
155
 
81
156
  class CacheType(str, Enum):
@@ -115,7 +190,7 @@ class LruCachePolicy(BaseCachePolicy):
115
190
  depending on `cache_type` set in the policy
116
191
  """
117
192
 
118
- policy: str = Field(const=True, default='lru')
193
+ policy: str = Field(default='lru', frozen=True)
119
194
  max_size: int = 10
120
195
 
121
196
 
@@ -124,8 +199,8 @@ class MostRecentCachePolicy(LruCachePolicy):
124
199
  Most recent cache policy. Only keeps the most recent item in the cache.
125
200
  """
126
201
 
127
- policy: str = Field(const=True, default='most-recent')
128
- max_size: int = Field(const=True, default=1)
202
+ policy: str = Field(default='most-recent', frozen=True)
203
+ max_size: int = Field(default=1, frozen=True)
129
204
 
130
205
 
131
206
  class KeepAllCachePolicy(BaseCachePolicy):
@@ -135,7 +210,7 @@ class KeepAllCachePolicy(BaseCachePolicy):
135
210
  Should be used with caution as it can lead to memory leaks.
136
211
  """
137
212
 
138
- policy: str = Field(const=True, default='keep-all')
213
+ policy: str = Field(default='keep-all', frozen=True)
139
214
 
140
215
 
141
216
  class TTLCachePolicy(BaseCachePolicy):
@@ -146,7 +221,7 @@ class TTLCachePolicy(BaseCachePolicy):
146
221
  :param ttl: time-to-live in seconds
147
222
  """
148
223
 
149
- policy: str = Field(const=True, default='ttl')
224
+ policy: str = Field(default='ttl', frozen=True)
150
225
  ttl: int
151
226
 
152
227
 
@@ -218,7 +293,7 @@ class CachedRegistryEntry(BaseModel):
218
293
  via the cache policy.
219
294
  """
220
295
 
221
- cache: Optional[BaseCachePolicy]
296
+ cache: Optional[BaseCachePolicy] = None
222
297
  uid: str
223
298
 
224
299
  def to_store_key(self):
@@ -242,17 +317,15 @@ class TaskProgressUpdate(BaseTaskMessage):
242
317
 
243
318
  class TaskResult(BaseTaskMessage):
244
319
  result: Any
245
- cache_key: Optional[str]
246
- reg_entry: Optional[CachedRegistryEntry]
320
+ cache_key: Optional[str] = None
321
+ reg_entry: Optional[CachedRegistryEntry] = None
247
322
 
248
323
 
249
324
  class TaskError(BaseTaskMessage):
250
325
  error: BaseException
251
- cache_key: Optional[str]
252
- reg_entry: Optional[CachedRegistryEntry]
253
-
254
- class Config:
255
- arbitrary_types_allowed = True
326
+ cache_key: Optional[str] = None
327
+ reg_entry: Optional[CachedRegistryEntry] = None
328
+ model_config = ConfigDict(arbitrary_types_allowed=True)
256
329
 
257
330
 
258
331
  TaskMessage = Union[TaskProgressUpdate, TaskResult, TaskError]
@@ -288,9 +361,6 @@ class PendingTask(BaseTask):
288
361
  Is associated to an underlying task definition.
289
362
  """
290
363
 
291
- cache_key: None = None
292
- reg_entry: None = None
293
-
294
364
  def __init__(self, task_id: str, task_def: BaseTask, ws_channel: Optional[str] = None):
295
365
  self.task_id = task_id
296
366
  self.task_def = task_def
@@ -431,7 +501,7 @@ class AnnotatedAction(BaseModel):
431
501
  # pylint: disable-next=import-error, import-outside-toplevel
432
502
  from dara.core.interactivity.plain_variable import Variable
433
503
 
434
- self.update_forward_refs(Variable=Variable)
504
+ self.model_rebuild()
435
505
  super().__init__(**data, loading=Variable(False))
436
506
 
437
507
 
@@ -456,11 +526,12 @@ class ActionImpl(DaraBaseModel):
456
526
  """
457
527
  await ctx._push_action(self)
458
528
 
459
- def dict(self, *args, **kwargs):
529
+ @model_serializer(mode='wrap')
530
+ def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
460
531
  """
461
532
  This structure is expected by the frontend, must match the JS implementation
462
533
  """
463
- dict_form = super().dict(*args, **kwargs)
534
+ dict_form = nxt(self)
464
535
  dict_form['name'] = self.__class__.py_name or self.__class__.__name__
465
536
  dict_form['__typename'] = 'ActionImpl'
466
537
  return dict_form
@@ -497,14 +568,14 @@ class ActionDef(BaseModel):
497
568
 
498
569
  name: str
499
570
  py_module: str
500
- js_module: Optional[str]
571
+ js_module: Optional[str] = None
501
572
 
502
573
 
503
574
  class ActionResolverDef(BaseModel):
504
575
  uid: str
505
576
  """Unique id of the action definition"""
506
577
 
507
- resolver: Optional[Callable]
578
+ resolver: Optional[Callable] = None
508
579
  """Resolver function for the action"""
509
580
 
510
581
  execute_action: Callable[..., Awaitable[Any]]
@@ -512,7 +583,7 @@ class ActionResolverDef(BaseModel):
512
583
 
513
584
 
514
585
  class UploadResolverDef(BaseModel):
515
- resolver: Optional[Callable]
586
+ resolver: Optional[Callable] = None
516
587
  """Optional custom resolver function for the upload"""
517
588
  upload: Callable
518
589
  """Upload handling function, default dara.core.interactivity.any_data_variable.upload"""
@@ -523,17 +594,3 @@ class ComponentType(Enum):
523
594
 
524
595
  JS = 'js'
525
596
  PY = 'py'
526
-
527
-
528
- class TemplateMarker(BaseModel):
529
- """
530
- Template marker used to mark fields that should be replaced with a data field on the client before being rendered.
531
- See dara_core.definitions.template for details
532
- """
533
-
534
- field_name: str
535
-
536
- def dict(self, *args, **kwargs):
537
- dict_form = super().dict(*args, **kwargs)
538
- dict_form['__typename'] = 'TemplateMarker'
539
- return dict_form
@@ -33,7 +33,7 @@ from typing import (
33
33
  )
34
34
 
35
35
  from fastapi.middleware import Middleware
36
- from pydantic.generics import GenericModel
36
+ from pydantic import BaseModel, ConfigDict
37
37
  from starlette.middleware.base import BaseHTTPMiddleware
38
38
 
39
39
  from dara.core.auth.base import BaseAuthConfig
@@ -65,7 +65,7 @@ from dara.core.visual.components import RawString
65
65
  from dara.core.visual.themes import BaseTheme, ThemeDef
66
66
 
67
67
 
68
- class Configuration(GenericModel):
68
+ class Configuration(BaseModel):
69
69
  """Definition of the main framework configuration"""
70
70
 
71
71
  auth_config: BaseAuthConfig
@@ -85,7 +85,7 @@ class Configuration(GenericModel):
85
85
  static_files_dir: str
86
86
  package_tag_processors: List[Callable[[Dict[str, List[str]]], Dict[str, List[str]]]]
87
87
  template_extra_js: str
88
- task_module: Optional[str]
88
+ task_module: Optional[str] = None
89
89
  template: str
90
90
  template_renderers: Dict[str, Callable[..., Template]]
91
91
  theme: Union[BaseTheme, str]
@@ -93,10 +93,7 @@ class Configuration(GenericModel):
93
93
  ws_handlers: Dict[str, Callable[[str, Any], Any]]
94
94
  encoders: Dict[Type[Any], Encoder]
95
95
  middlewares: List[Middleware]
96
-
97
- class Config:
98
- extra = 'forbid'
99
- arbitrary_types_allowed = True
96
+ model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
100
97
 
101
98
  def get_package_map(self) -> Dict[str, str]:
102
99
  """
@@ -117,7 +114,7 @@ class Configuration(GenericModel):
117
114
  packages[act_def.py_module] = act_def.js_module
118
115
 
119
116
  # Handle auth components
120
- for comp in self.auth_config.component_config.dict().values():
117
+ for comp in self.auth_config.component_config.model_dump().values():
121
118
  packages[comp['py_module']] = comp['js_module']
122
119
 
123
120
  return packages
dara/core/defaults.py CHANGED
@@ -39,8 +39,6 @@ from dara.core.visual.components import (
39
39
  DynamicComponent,
40
40
  DynamicComponentDef,
41
41
  Fallback,
42
- For,
43
- ForDef,
44
42
  Menu,
45
43
  MenuDef,
46
44
  ProgressTracker,
@@ -79,7 +77,6 @@ CORE_COMPONENTS: Dict[str, ComponentTypeAnnotation] = {
79
77
  TopBarFrame.__name__: TopBarFrameDef,
80
78
  Fallback.Default.py_component: DefaultFallbackDef,
81
79
  Fallback.Row.py_component: RowFallbackDef,
82
- For.__name__: ForDef,
83
80
  }
84
81
 
85
82
  # These actions are provided by the core JS of this module