dara-core 1.16.22__py3-none-any.whl → 1.17.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.
@@ -428,6 +428,15 @@ class PendingTask(BaseTask):
428
428
  """
429
429
  self.subscribers -= 1
430
430
 
431
+ async def value(self):
432
+ """
433
+ Wait for the task to complete and return the result
434
+ """
435
+ await self.event.wait()
436
+ if self.error:
437
+ raise self.error
438
+ return self.result
439
+
431
440
 
432
441
  class PendingValue:
433
442
  """
dara/core/definitions.py CHANGED
@@ -70,6 +70,11 @@ DEFAULT_ERROR_TITLE = 'Unexpected error occurred'
70
70
  DEFAULT_ERROR_DESCRIPTION = 'Try again or contact the application owner'
71
71
 
72
72
 
73
+ def _kebab_to_camel(string: str):
74
+ chunks = string.split('-')
75
+ return chunks[0] + ''.join([chunk[0].upper() + chunk[1:].lower() for chunk in chunks[1:]])
76
+
77
+
73
78
  class ErrorHandlingConfig(BaseModel):
74
79
  title: str = DEFAULT_ERROR_TITLE
75
80
  """Title to display in the error boundary"""
@@ -77,8 +82,27 @@ class ErrorHandlingConfig(BaseModel):
77
82
  description: str = DEFAULT_ERROR_DESCRIPTION
78
83
  """Description to display in the error boundary"""
79
84
 
80
- raw_css: Optional[Union[CSSProperties, dict, str]] = None
81
- """Raw styling to apply to the displayed error boundary"""
85
+ raw_css: Optional[Any] = None
86
+ """
87
+ Raw styling to apply to the displayed error boundary.
88
+ Accepts a CSSProperties, dict, str, or NonDataVariable.
89
+ """
90
+
91
+ @field_validator('raw_css', mode='before')
92
+ @classmethod
93
+ def validate_raw_css(cls, value):
94
+ from dara.core.interactivity.non_data_variable import NonDataVariable
95
+
96
+ if isinstance(value, str):
97
+ return str
98
+ if isinstance(value, dict):
99
+ return {_kebab_to_camel(k): v for k, v in value.items()}
100
+ if isinstance(value, NonDataVariable):
101
+ return value
102
+ if isinstance(value, CSSProperties):
103
+ return value
104
+
105
+ raise ValueError(f'raw_css must be a CSSProperties, dict, str, or NonDataVariable, got {type(value)}')
82
106
 
83
107
  def model_dump(self, *args, **kwargs):
84
108
  result = super().model_dump(*args, **kwargs)
@@ -120,14 +144,15 @@ class ComponentInstance(BaseModel):
120
144
  required_routes: ClassVar[List[ApiRoute]] = []
121
145
  """List of routes the component depends on. Will be implicitly added to the app if this component is used"""
122
146
 
123
- raw_css: Optional[Union[CSSProperties, dict, str]] = None
147
+ raw_css: Optional[Any] = None
124
148
  """
125
149
  Raw styling to apply to the component.
126
- Can be an dict/CSSProperties instance representing the `styles` tag, or a string injected directly into the CSS of the wrapping component.
150
+ Can be an dict/CSSProperties instance representing the `styles` tag, a string injected directly into the CSS of the wrapping component,
151
+ or a NonDataVariable resoling to either of the above.
127
152
 
128
153
  ```python
129
154
 
130
- from dara.core import CSSProperties
155
+ from dara.core import CSSProperties, Variable
131
156
 
132
157
  # `style` - use the class for autocompletion/typesense
133
158
  Stack(..., raw_css=CSSProperties(maxWidth='100px'))
@@ -140,6 +165,9 @@ class ComponentInstance(BaseModel):
140
165
  max-width: 100px;
141
166
  \"\"\")
142
167
 
168
+ css_var = Variable('color: red;')
169
+ Stack(..., raw_css=css_var)
170
+
143
171
  ```
144
172
  """
145
173
 
@@ -173,17 +201,23 @@ class ComponentInstance(BaseModel):
173
201
 
174
202
  @field_validator('raw_css', mode='before')
175
203
  @classmethod
176
- def parse_css(cls, css: Optional[Union[CSSProperties, dict, str]]):
204
+ def parse_css(cls, css: Optional[Any]):
205
+ from dara.core.interactivity.non_data_variable import NonDataVariable
206
+
177
207
  # If it's a plain dict, change kebab case to camel case
178
208
  if isinstance(css, dict):
209
+ return {_kebab_to_camel(k): v for k, v in css.items()}
210
+
211
+ if isinstance(css, NonDataVariable):
212
+ return css
179
213
 
180
- def kebab_to_camel(string: str):
181
- chunks = string.split('-')
182
- return chunks[0] + ''.join([chunk[0].upper() + chunk[1:].lower() for chunk in chunks[1:]])
214
+ if isinstance(css, CSSProperties):
215
+ return css
183
216
 
184
- return {kebab_to_camel(k): v for k, v in css.items()}
217
+ if isinstance(css, str):
218
+ return css
185
219
 
186
- return css
220
+ raise ValueError(f'raw_css must be a CSSProperties, dict, str, or NonDataVariable, got {type(css)}')
187
221
 
188
222
  @classmethod
189
223
  def isinstance(cls, obj: Any) -> bool:
@@ -399,7 +433,6 @@ class BaseFallback(StyledComponentInstance):
399
433
 
400
434
  ComponentInstance.model_rebuild()
401
435
 
402
-
403
436
  ComponentInstanceType = Union[ComponentInstance, Callable[..., ComponentInstance]]
404
437
 
405
438
 
@@ -42,6 +42,7 @@ from dara.core.interactivity.derived_data_variable import DerivedDataVariable
42
42
  from dara.core.interactivity.derived_variable import DerivedVariable
43
43
  from dara.core.interactivity.non_data_variable import NonDataVariable
44
44
  from dara.core.interactivity.plain_variable import Variable
45
+ from dara.core.interactivity.switch_variable import SwitchVariable
45
46
  from dara.core.interactivity.url_variable import UrlVariable
46
47
 
47
48
  __all__ = [
@@ -52,6 +53,7 @@ __all__ = [
52
53
  'DataVariable',
53
54
  'NonDataVariable',
54
55
  'Variable',
56
+ 'SwitchVariable',
55
57
  'DerivedVariable',
56
58
  'DerivedDataVariable',
57
59
  'UrlVariable',
@@ -1247,15 +1247,7 @@ class ActionCtx:
1247
1247
 
1248
1248
  task = Task(func=func, args=args, kwargs=kwargs, on_progress=on_progress)
1249
1249
  pending_task = await task_mgr.run_task(task)
1250
-
1251
- # Run until completion
1252
- await pending_task.event.wait()
1253
-
1254
- # Raise exception if there was one
1255
- if pending_task.error:
1256
- raise pending_task.error
1257
-
1258
- return pending_task.result
1250
+ return await pending_task.value()
1259
1251
 
1260
1252
  async def execute_action(self, action: ActionImpl):
1261
1253
  """
@@ -20,6 +20,8 @@ from __future__ import annotations
20
20
  from enum import Enum
21
21
  from typing import TYPE_CHECKING, ClassVar, Union
22
22
 
23
+ from pydantic import SerializerFunctionWrapHandler, model_serializer
24
+
23
25
  from dara.core.base_definitions import DaraBaseModel as BaseModel
24
26
 
25
27
  # Type-only imports
@@ -46,3 +48,9 @@ class Condition(BaseModel):
46
48
  variable: AnyVariable
47
49
 
48
50
  Operator: ClassVar[OperatorType] = Operator
51
+
52
+ @model_serializer(mode='wrap')
53
+ def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
54
+ parent_dict = nxt(self)
55
+
56
+ return {**parent_dict, '__typename': 'Condition'}
@@ -37,20 +37,6 @@ from dara.core.internal.utils import call_async
37
37
  from dara.core.logging import dev_logger
38
38
  from dara.core.persistence import PersistenceStore
39
39
 
40
-
41
- def _is_subclass_safe(value: type, base: type) -> bool:
42
- """
43
- Check if a class is a subclass of another class. Returns False if the value is not a class.
44
-
45
- :param value: the class to check
46
- :param base: the class to check against
47
- """
48
- try:
49
- return issubclass(value, base)
50
- except TypeError:
51
- return False
52
-
53
-
54
40
  VARIABLE_INIT_OVERRIDE = ContextVar[Optional[Callable[[dict], dict]]]('VARIABLE_INIT_OVERRIDE', default=None)
55
41
 
56
42
  VariableType = TypeVar('VariableType')
@@ -0,0 +1,416 @@
1
+ """
2
+ Copyright 2023 Impulse Innovations Limited
3
+
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Dict, Optional, Union
21
+
22
+ from pydantic import SerializerFunctionWrapHandler, field_validator, model_serializer
23
+
24
+ from dara.core.interactivity.condition import Condition
25
+ from dara.core.interactivity.non_data_variable import NonDataVariable
26
+
27
+
28
+ class SwitchVariable(NonDataVariable):
29
+ """
30
+ A SwitchVariable represents a conditional value that switches between
31
+ different values based on a condition or variable value.
32
+
33
+ SwitchVariable provides a clean way to create conditional logic in your
34
+ application by mapping input values or conditions to output values. It
35
+ supports boolean conditions, value mappings, and default fallbacks.
36
+
37
+ There are three main patterns for creating SwitchVariables:
38
+
39
+ 1. **Boolean Conditions**: Switch between two values based on a true/false condition
40
+ 2. **Value Mapping**: Map specific input values to corresponding output values
41
+ 3. **Mixed Usage**: Combine conditions and mappings with default fallbacks
42
+
43
+ Examples:
44
+ Basic boolean switching:
45
+
46
+ ```python
47
+ from dara.core import ConfigurationBuilder, Variable, SwitchVariable
48
+ from dara.components import Stack, Text, Button, Card
49
+
50
+ config = ConfigurationBuilder()
51
+ is_admin = Variable(default=False)
52
+
53
+ # Show different UI based on admin status
54
+ ui_mode = SwitchVariable.when(
55
+ condition=is_admin,
56
+ true_value='Admin Panel',
57
+ false_value='User Panel'
58
+ )
59
+
60
+ # Complete component usage
61
+ page_content = Card(
62
+ Stack(
63
+ Text('Current Mode:'),
64
+ Text(text=ui_mode),
65
+ Button('Toggle Admin', onclick=is_admin.toggle())
66
+ ),
67
+ title='Admin Panel Demo'
68
+ )
69
+
70
+ config.add_page('Admin Demo', content=page_content)
71
+ ```
72
+
73
+ Value mapping with defaults:
74
+
75
+ ```python
76
+ from dara.core import ConfigurationBuilder, Variable, SwitchVariable
77
+ from dara.components import Stack, Text, Select, Item, Card
78
+
79
+ config = ConfigurationBuilder()
80
+ user_role = Variable(default='guest')
81
+
82
+ # Map user roles to permission levels
83
+ permissions = SwitchVariable.match(
84
+ value=user_role,
85
+ mapping={
86
+ 'admin': 'Full Access',
87
+ 'editor': 'Write Access',
88
+ 'viewer': 'Read Access'
89
+ },
90
+ default='No Access' # for unknown roles
91
+ )
92
+
93
+ # Complete component usage
94
+ page_content = Card(
95
+ Stack(
96
+ Text('Select Role:'),
97
+ Select(
98
+ value=user_role,
99
+ items=[
100
+ Item(label='Guest', value='guest'),
101
+ Item(label='Viewer', value='viewer'),
102
+ Item(label='Editor', value='editor'),
103
+ Item(label='Admin', value='admin')
104
+ ]
105
+ ),
106
+ Text('Permissions:'),
107
+ Text(text=permissions)
108
+ ),
109
+ title='Role Permissions'
110
+ )
111
+
112
+ config.add_page('Permissions Demo', content=page_content)
113
+ ```
114
+
115
+ Complex conditions:
116
+
117
+ ```python
118
+ from dara.core import Variable, SwitchVariable
119
+ from dara.components import Stack, Text, Input, Card
120
+
121
+ score = Variable(default=85)
122
+
123
+ # Grade based on score ranges
124
+ grade = SwitchVariable.when(
125
+ condition=score >= 90,
126
+ true_value='A Grade',
127
+ false_value='B Grade'
128
+ )
129
+
130
+ # Complete component usage
131
+ grade_component = Card(
132
+ Stack(
133
+ Text('Enter Score:'),
134
+ Input(value=score, type='number'),
135
+ Text('Grade (>=90 = A, <90 = B):'),
136
+ Text(text=grade)
137
+ ),
138
+ title='Grade Calculator'
139
+ )
140
+ ```
141
+
142
+ Switching on computed conditions:
143
+
144
+ ```python
145
+ from dara.core import Variable, SwitchVariable
146
+ from dara.components import Stack, Text, Input, Card
147
+
148
+ temperature = Variable(default=20)
149
+
150
+ # Weather advice based on temperature
151
+ advice = SwitchVariable.when(
152
+ condition=temperature > 25,
153
+ true_value='Wear light clothes - it\'s warm!',
154
+ false_value='Wear warm clothes - it\'s cool!'
155
+ )
156
+
157
+ # Complete component usage
158
+ weather_app = Card(
159
+ Stack(
160
+ Text('Temperature (°C):'),
161
+ Input(value=temperature, type='number'),
162
+ Text('Advice:'),
163
+ Text(text=advice)
164
+ ),
165
+ title='Weather Advisor'
166
+ )
167
+ ```
168
+
169
+ Using with other variables:
170
+
171
+ ```python
172
+ from dara.core import Variable, SwitchVariable
173
+ from dara.components import Stack, Text, Select, Item, Card
174
+
175
+ user_preference = Variable(default='auto')
176
+ mapping_variable = Variable({
177
+ 'auto': 'System Theme',
178
+ 'light': 'Light Theme',
179
+ 'dark': 'Dark Theme'
180
+ })
181
+
182
+ # Theme selection with user preference override
183
+ active_theme = SwitchVariable.match(
184
+ value=user_preference,
185
+ mapping=mapping_variable,
186
+ default='Default Theme'
187
+ )
188
+
189
+ # Complete component usage
190
+ theme_selector = Card(
191
+ Stack(
192
+ Text('Theme Preference:'),
193
+ Select(
194
+ value=user_preference,
195
+ items=[
196
+ Item(label='Auto', value='auto'),
197
+ Item(label='Light', value='light'),
198
+ Item(label='Dark', value='dark')
199
+ ]
200
+ ),
201
+ Text('Active Theme:'),
202
+ Text(text=active_theme)
203
+ ),
204
+ title='Theme Selector'
205
+ )
206
+ ```
207
+
208
+ Note:
209
+ - Value can be a condition, raw value, or variable
210
+ - Value map can be a variable or a dict
211
+ - Default can be a variable or a raw value
212
+ - The switch evaluation happens reactively when underlying variables change
213
+ - Default values are only used in mapping scenarios when the switch value
214
+ doesn't match any key in the mapping
215
+
216
+ Key Serialization:
217
+ When using mappings with SwitchVariable, be aware that JavaScript object keys
218
+ are always strings. The system automatically converts lookup keys to strings:
219
+ - Python: {True: 'admin', False: 'user'}
220
+ - JavaScript: {"true": "admin", "false": "user"}
221
+ - Boolean values are converted to lowercase strings ("true"/"false")
222
+ - Other values use standard string conversion to match JavaScript's String() behavior
223
+ """
224
+
225
+ value: Optional[Union[Condition, NonDataVariable, Any]] = None
226
+ # must be typed as any, otherwise pydantic is trying to instantiate the variables incorrectly
227
+ value_map: Optional[Any] = None
228
+ default: Optional[Any] = None
229
+
230
+ def __init__(
231
+ self,
232
+ value: Union[Condition, NonDataVariable, Any],
233
+ value_map: Dict[Any, Any] | NonDataVariable,
234
+ default: Optional[Any] = None,
235
+ uid: Optional[str] = None,
236
+ ):
237
+ """
238
+ Create a SwitchVariable with a mapping of values.
239
+
240
+ :param value: Variable, condition, or value to switch on
241
+ :param value_map: Dict mapping switch values to return values
242
+ :param default: Default value when switch value not in mapping
243
+ :param uid: Unique identifier for this variable
244
+ """
245
+ super().__init__(
246
+ uid=uid,
247
+ value=value,
248
+ value_map=value_map,
249
+ default=default,
250
+ )
251
+
252
+ @field_validator('value_map')
253
+ @classmethod
254
+ def validate_value_map(cls, v):
255
+ """
256
+ Validate that value_map is either a dict or a NonDataVariable.
257
+
258
+ :param v: The value to validate
259
+ :return: The validated value
260
+ :raises ValueError: If value_map is not a dict or NonDataVariable
261
+ """
262
+ if v is None:
263
+ return v
264
+ if isinstance(v, dict):
265
+ return v
266
+ if isinstance(v, NonDataVariable):
267
+ return v
268
+ raise ValueError(f'value_map must be a dict or NonDataVariable, got {type(v)}')
269
+
270
+ @classmethod
271
+ def when(
272
+ cls,
273
+ condition: Union[Condition, NonDataVariable, Any],
274
+ true_value: Union[Any, NonDataVariable],
275
+ false_value: Union[Any, NonDataVariable],
276
+ uid: Optional[str] = None,
277
+ ) -> 'SwitchVariable':
278
+ """
279
+ Create a SwitchVariable for boolean conditions.
280
+
281
+ This is the most common pattern for simple if/else logic. The condition
282
+ is evaluated and returns true_value if truthy, false_value otherwise.
283
+
284
+ :param condition: Condition to evaluate (Variable, Condition object, or any value)
285
+ :param true_value: Value to return when condition evaluates to True
286
+ :param false_value: Value to return when condition evaluates to False
287
+ :param uid: Unique identifier for this variable
288
+ :return: SwitchVariable configured for boolean switching
289
+
290
+ Example with variable condition:
291
+ ```python
292
+ from dara.core import Variable, SwitchVariable
293
+ from dara.components import Stack, Text, Button
294
+
295
+ is_loading = Variable(default=True)
296
+
297
+ # Show spinner while loading, content when done
298
+ display = SwitchVariable.when(
299
+ condition=is_loading,
300
+ true_value='Loading...',
301
+ false_value='Content loaded!'
302
+ )
303
+
304
+ # Use in a complete component
305
+ page_content = Stack(
306
+ Text(text=display),
307
+ Button(
308
+ text='Toggle Loading',
309
+ onclick=is_loading.toggle()
310
+ )
311
+ )
312
+ ```
313
+
314
+ Example with a condition object:
315
+
316
+ ```python
317
+ from dara.core import Variable, SwitchVariable
318
+ from dara.components import Stack, Text, Input, Card
319
+
320
+ # Temperature-based clothing advice
321
+ temperature = Variable(default=20)
322
+ advice = SwitchVariable.when(
323
+ condition=temperature > 25,
324
+ true_value='Wear light clothes - it\'s warm!',
325
+ false_value='Wear warm clothes - it\'s cool!'
326
+ )
327
+
328
+ # Complete weather advice component
329
+ weather_component = Card(
330
+ Stack(
331
+ Text('Enter temperature (°C):'),
332
+ Input(value=temperature, type='number'),
333
+ Text('Advice:'),
334
+ Text(text=advice)
335
+ ),
336
+ title='Weather Advice'
337
+ )
338
+ ```
339
+ """
340
+ return cls(
341
+ value=condition,
342
+ value_map={True: true_value, False: false_value},
343
+ uid=uid,
344
+ )
345
+
346
+ @classmethod
347
+ def match(
348
+ cls,
349
+ value: Union[NonDataVariable, Any],
350
+ mapping: Union[Dict[Any, Any], NonDataVariable],
351
+ default: Optional[Union[Any, NonDataVariable]] = None,
352
+ uid: Optional[str] = None,
353
+ ) -> 'SwitchVariable':
354
+ """
355
+ Create a SwitchVariable with a custom mapping.
356
+
357
+ This pattern is useful when you have multiple specific values to map to
358
+ different outputs, similar to a switch statement in other languages.
359
+
360
+ :param value: Variable or value to switch on
361
+ :param mapping: Dict mapping switch values to return values
362
+ :param default: Default value when switch value not found in mapping
363
+ :param uid: Unique identifier for this variable
364
+ :return: SwitchVariable configured with the provided mapping
365
+
366
+ Example:
367
+ ```python
368
+ from dara.core import Variable, SwitchVariable
369
+ from dara.components import Stack, Text, Select, Item
370
+
371
+ status = Variable(default='pending')
372
+
373
+ # Map status codes to user-friendly messages
374
+ message = SwitchVariable.match(
375
+ value=status,
376
+ mapping={
377
+ 'pending': 'Please wait...',
378
+ 'success': 'Operation completed!',
379
+ 'error': 'Something went wrong',
380
+ 'cancelled': 'Operation was cancelled'
381
+ },
382
+ default='Unknown status'
383
+ )
384
+
385
+ # Use in a complete component
386
+ page_content = Stack(
387
+ Select(
388
+ value=status,
389
+ items=[
390
+ Item(label='Pending', value='pending'),
391
+ Item(label='Success', value='success'),
392
+ Item(label='Error', value='error'),
393
+ Item(label='Cancelled', value='cancelled')
394
+ ]
395
+ ),
396
+ Text(text=message)
397
+ )
398
+ ```
399
+ """
400
+ return cls(
401
+ value=value,
402
+ value_map=mapping,
403
+ default=default,
404
+ uid=uid,
405
+ )
406
+
407
+ @model_serializer(mode='wrap')
408
+ def ser_model(self, nxt: SerializerFunctionWrapHandler) -> dict:
409
+ """
410
+ Serialize the SwitchVariable model with additional metadata.
411
+
412
+ :param nxt: The next serializer function in the chain
413
+ :return: Serialized dictionary with __typename and uid fields
414
+ """
415
+ parent_dict = nxt(self)
416
+ return {**parent_dict, '__typename': 'SwitchVariable', 'uid': str(parent_dict['uid'])}