violit 0.0.4.post1__py3-none-any.whl → 0.0.6__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.
- violit/app.py +2229 -1988
- violit/component.py +38 -38
- violit/context.py +1 -0
- violit/state.py +234 -0
- violit/widgets/__init__.py +30 -30
- violit/widgets/card_widgets.py +595 -595
- violit/widgets/chart_widgets.py +253 -253
- violit/widgets/data_widgets.py +547 -529
- violit/widgets/input_widgets.py +745 -745
- violit/widgets/layout_widgets.py +419 -419
- violit/widgets/status_widgets.py +308 -255
- violit/widgets/text_widgets.py +458 -413
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/METADATA +1 -1
- violit-0.0.6.dist-info/RECORD +26 -0
- violit-0.0.4.post1.dist-info/RECORD +0 -26
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/WHEEL +0 -0
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/top_level.txt +0 -0
violit/widgets/status_widgets.py
CHANGED
|
@@ -1,255 +1,308 @@
|
|
|
1
|
-
"""Status Widgets Mixin for Violit"""
|
|
2
|
-
|
|
3
|
-
from typing import Union, Callable, Optional
|
|
4
|
-
from ..component import Component
|
|
5
|
-
from ..context import rendering_ctx
|
|
6
|
-
from ..state import get_session_store, State
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class StatusWidgetsMixin:
|
|
10
|
-
"""Status display widgets (success, info, warning, error, toast, progress, spinner, status, balloons, snow, exception)"""
|
|
11
|
-
|
|
12
|
-
def success(self,
|
|
13
|
-
"""Display success alert"""
|
|
14
|
-
self.alert(
|
|
15
|
-
|
|
16
|
-
def warning(self,
|
|
17
|
-
"""Display warning alert"""
|
|
18
|
-
self.alert(
|
|
19
|
-
|
|
20
|
-
def error(self,
|
|
21
|
-
"""Display error alert"""
|
|
22
|
-
self.alert(
|
|
23
|
-
|
|
24
|
-
def info(self,
|
|
25
|
-
"""Display info alert"""
|
|
26
|
-
self.alert(
|
|
27
|
-
|
|
28
|
-
def alert(self,
|
|
29
|
-
"""Display alert message with Signal support"""
|
|
30
|
-
import html as html_lib
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
self.
|
|
244
|
-
|
|
245
|
-
self.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1
|
+
"""Status Widgets Mixin for Violit"""
|
|
2
|
+
|
|
3
|
+
from typing import Union, Callable, Optional
|
|
4
|
+
from ..component import Component
|
|
5
|
+
from ..context import rendering_ctx
|
|
6
|
+
from ..state import get_session_store, State
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StatusWidgetsMixin:
|
|
10
|
+
"""Status display widgets (success, info, warning, error, toast, progress, spinner, status, balloons, snow, exception)"""
|
|
11
|
+
|
|
12
|
+
def success(self, *args):
|
|
13
|
+
"""Display success alert"""
|
|
14
|
+
self.alert(*args, variant="success", icon="check-circle")
|
|
15
|
+
|
|
16
|
+
def warning(self, *args):
|
|
17
|
+
"""Display warning alert"""
|
|
18
|
+
self.alert(*args, variant="warning", icon="exclamation-triangle")
|
|
19
|
+
|
|
20
|
+
def error(self, *args):
|
|
21
|
+
"""Display error alert"""
|
|
22
|
+
self.alert(*args, variant="danger", icon="x-circle")
|
|
23
|
+
|
|
24
|
+
def info(self, *args):
|
|
25
|
+
"""Display info alert"""
|
|
26
|
+
self.alert(*args, variant="primary", icon="info-circle")
|
|
27
|
+
|
|
28
|
+
def alert(self, *args, variant="primary", icon=None):
|
|
29
|
+
"""Display alert message with Signal support (multiple arguments supported)"""
|
|
30
|
+
import html as html_lib
|
|
31
|
+
from ..state import State, ComputedState
|
|
32
|
+
|
|
33
|
+
cid = self._get_next_cid("alert")
|
|
34
|
+
def builder():
|
|
35
|
+
# Signal handling for multiple arguments
|
|
36
|
+
parts = []
|
|
37
|
+
token = rendering_ctx.set(cid)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
for arg in args:
|
|
41
|
+
if isinstance(arg, (State, ComputedState)):
|
|
42
|
+
parts.append(str(arg.value))
|
|
43
|
+
elif callable(arg):
|
|
44
|
+
parts.append(str(arg()))
|
|
45
|
+
else:
|
|
46
|
+
parts.append(str(arg))
|
|
47
|
+
finally:
|
|
48
|
+
rendering_ctx.reset(token)
|
|
49
|
+
|
|
50
|
+
val = " ".join(parts)
|
|
51
|
+
|
|
52
|
+
# XSS protection: escape content
|
|
53
|
+
escaped_val = html_lib.escape(str(val))
|
|
54
|
+
|
|
55
|
+
icon_html = f'<sl-icon slot="icon" name="{icon}"></sl-icon>' if icon else ""
|
|
56
|
+
html_output = f'<sl-alert variant="{variant}" open>{icon_html}{escaped_val}</sl-alert>'
|
|
57
|
+
return Component("div", id=cid, content=html_output)
|
|
58
|
+
self._register_component(cid, builder)
|
|
59
|
+
|
|
60
|
+
def toast(self, *args, icon="info-circle", variant="primary"):
|
|
61
|
+
"""Display toast notification (Signal support via evaluation)"""
|
|
62
|
+
import json
|
|
63
|
+
from ..state import State, ComputedState
|
|
64
|
+
|
|
65
|
+
# Check if any argument requires dynamic binding
|
|
66
|
+
is_dynamic = any(isinstance(a, (State, ComputedState, Callable)) for a in args)
|
|
67
|
+
|
|
68
|
+
if is_dynamic:
|
|
69
|
+
cid = self._get_next_cid("toast_trigger")
|
|
70
|
+
def builder():
|
|
71
|
+
token = rendering_ctx.set(cid)
|
|
72
|
+
parts = []
|
|
73
|
+
for arg in args:
|
|
74
|
+
if isinstance(arg, (State, ComputedState)):
|
|
75
|
+
parts.append(str(arg.value))
|
|
76
|
+
elif callable(arg):
|
|
77
|
+
parts.append(str(arg()))
|
|
78
|
+
else:
|
|
79
|
+
parts.append(str(arg))
|
|
80
|
+
val = " ".join(parts)
|
|
81
|
+
rendering_ctx.reset(token)
|
|
82
|
+
|
|
83
|
+
# XSS protection: safely escape with JSON.stringify
|
|
84
|
+
safe_val = json.dumps(str(val))
|
|
85
|
+
safe_variant = json.dumps(str(variant))
|
|
86
|
+
safe_icon = json.dumps(str(icon))
|
|
87
|
+
code = f"createToast({safe_val}, {safe_variant}, {safe_icon})"
|
|
88
|
+
return Component("script", id=cid, content=code)
|
|
89
|
+
self._register_component(cid, builder)
|
|
90
|
+
else:
|
|
91
|
+
message = " ".join(str(a) for a in args)
|
|
92
|
+
# XSS protection: safely escape with JSON.stringify
|
|
93
|
+
safe_message = json.dumps(str(message))
|
|
94
|
+
safe_variant = json.dumps(str(variant))
|
|
95
|
+
safe_icon = json.dumps(str(icon))
|
|
96
|
+
code = f"createToast({safe_message}, {safe_variant}, {safe_icon})"
|
|
97
|
+
self._enqueue_eval(code, toast_data={"message": str(message), "icon": str(icon), "variant": str(variant)})
|
|
98
|
+
|
|
99
|
+
def balloons(self):
|
|
100
|
+
"""Display balloons animation"""
|
|
101
|
+
code = "createBalloons()"
|
|
102
|
+
self._enqueue_eval(code, effect="balloons")
|
|
103
|
+
|
|
104
|
+
def snow(self):
|
|
105
|
+
"""Display snow animation"""
|
|
106
|
+
code = "createSnow()"
|
|
107
|
+
self._enqueue_eval(code, effect="snow")
|
|
108
|
+
|
|
109
|
+
def exception(self, exception: Exception):
|
|
110
|
+
"""Display exception with traceback"""
|
|
111
|
+
import traceback
|
|
112
|
+
import html as html_lib
|
|
113
|
+
|
|
114
|
+
cid = self._get_next_cid("exception")
|
|
115
|
+
tb = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
|
|
116
|
+
|
|
117
|
+
def builder():
|
|
118
|
+
# XSS protection: escape exception message and traceback
|
|
119
|
+
escaped_name = html_lib.escape(type(exception).__name__)
|
|
120
|
+
escaped_msg = html_lib.escape(str(exception))
|
|
121
|
+
escaped_tb = html_lib.escape(tb)
|
|
122
|
+
|
|
123
|
+
html_output = f'''
|
|
124
|
+
<sl-alert variant="danger" open style="margin-bottom:1rem;">
|
|
125
|
+
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
|
|
126
|
+
<strong>{escaped_name}:</strong> {escaped_msg}
|
|
127
|
+
<pre style="margin-top:0.5rem;padding:0.5rem;background:rgba(0,0,0,0.1);border-radius:0.25rem;overflow-x:auto;font-size:0.85rem;">{escaped_tb}</pre>
|
|
128
|
+
</sl-alert>
|
|
129
|
+
'''
|
|
130
|
+
return Component("div", id=cid, content=html_output)
|
|
131
|
+
self._register_component(cid, builder)
|
|
132
|
+
|
|
133
|
+
def _enqueue_eval(self, code, **lite_data):
|
|
134
|
+
"""Internal helper to enqueue JS evaluation or store for lite mode"""
|
|
135
|
+
if self.mode == 'ws':
|
|
136
|
+
store = get_session_store()
|
|
137
|
+
if 'eval_queue' not in store: store['eval_queue'] = []
|
|
138
|
+
store['eval_queue'].append(code)
|
|
139
|
+
else:
|
|
140
|
+
store = get_session_store()
|
|
141
|
+
if 'toasts' not in store: store['toasts'] = []
|
|
142
|
+
if 'effects' not in store: store['effects'] = []
|
|
143
|
+
|
|
144
|
+
if 'toast_data' in lite_data:
|
|
145
|
+
store['toasts'].append(lite_data['toast_data'])
|
|
146
|
+
if 'effect' in lite_data:
|
|
147
|
+
store['effects'].append(lite_data['effect'])
|
|
148
|
+
|
|
149
|
+
def progress(self, value=0, *args):
|
|
150
|
+
"""Display progress bar with Signal support"""
|
|
151
|
+
import html as html_lib
|
|
152
|
+
from ..state import State, ComputedState
|
|
153
|
+
|
|
154
|
+
cid = self._get_next_cid("progress")
|
|
155
|
+
|
|
156
|
+
def builder():
|
|
157
|
+
# Handle Signal
|
|
158
|
+
val_num = value
|
|
159
|
+
if isinstance(value, (State, ComputedState)):
|
|
160
|
+
token = rendering_ctx.set(cid)
|
|
161
|
+
val_num = value.value
|
|
162
|
+
rendering_ctx.reset(token)
|
|
163
|
+
elif callable(value):
|
|
164
|
+
token = rendering_ctx.set(cid)
|
|
165
|
+
val_num = value()
|
|
166
|
+
rendering_ctx.reset(token)
|
|
167
|
+
|
|
168
|
+
# Resolve text args
|
|
169
|
+
parts = []
|
|
170
|
+
if args:
|
|
171
|
+
token = rendering_ctx.set(cid)
|
|
172
|
+
for arg in args:
|
|
173
|
+
if isinstance(arg, (State, ComputedState)):
|
|
174
|
+
parts.append(str(arg.value))
|
|
175
|
+
elif callable(arg):
|
|
176
|
+
parts.append(str(arg()))
|
|
177
|
+
else:
|
|
178
|
+
parts.append(str(arg))
|
|
179
|
+
rendering_ctx.reset(token)
|
|
180
|
+
progress_text = " ".join(parts)
|
|
181
|
+
else:
|
|
182
|
+
progress_text = f"{val_num}%"
|
|
183
|
+
|
|
184
|
+
# XSS protection: escape text
|
|
185
|
+
escaped_text = html_lib.escape(str(progress_text))
|
|
186
|
+
|
|
187
|
+
html_output = f'''
|
|
188
|
+
<div style="margin-bottom:0.5rem;">
|
|
189
|
+
<div style="display:flex;justify-content:space-between;margin-bottom:0.25rem;">
|
|
190
|
+
<span style="font-size:0.875rem;color:var(--sl-text);">{escaped_text}</span>
|
|
191
|
+
<span style="font-size:0.875rem;color:var(--sl-text-muted);">{val_num}%</span>
|
|
192
|
+
</div>
|
|
193
|
+
<sl-progress-bar value="{val_num}"></sl-progress-bar>
|
|
194
|
+
</div>
|
|
195
|
+
'''
|
|
196
|
+
return Component("div", id=cid, content=html_output)
|
|
197
|
+
self._register_component(cid, builder)
|
|
198
|
+
|
|
199
|
+
def spinner(self, *args):
|
|
200
|
+
"""Display loading spinner"""
|
|
201
|
+
import html as html_lib
|
|
202
|
+
from ..state import State, ComputedState
|
|
203
|
+
|
|
204
|
+
cid = self._get_next_cid("spinner")
|
|
205
|
+
|
|
206
|
+
def builder():
|
|
207
|
+
parts = []
|
|
208
|
+
if args:
|
|
209
|
+
token = rendering_ctx.set(cid)
|
|
210
|
+
for arg in args:
|
|
211
|
+
if isinstance(arg, (State, ComputedState)):
|
|
212
|
+
parts.append(str(arg.value))
|
|
213
|
+
elif callable(arg):
|
|
214
|
+
parts.append(str(arg()))
|
|
215
|
+
else:
|
|
216
|
+
parts.append(str(arg))
|
|
217
|
+
rendering_ctx.reset(token)
|
|
218
|
+
text = " ".join(parts)
|
|
219
|
+
else:
|
|
220
|
+
text = "Loading..."
|
|
221
|
+
|
|
222
|
+
# XSS protection: escape text
|
|
223
|
+
escaped_text = html_lib.escape(str(text))
|
|
224
|
+
|
|
225
|
+
html_output = f'''
|
|
226
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;">
|
|
227
|
+
<sl-spinner style="font-size:1.5rem;"></sl-spinner>
|
|
228
|
+
<span style="color:var(--sl-text-muted);font-size:0.875rem;">{escaped_text}</span>
|
|
229
|
+
</div>
|
|
230
|
+
'''
|
|
231
|
+
return Component("div", id=cid, content=html_output)
|
|
232
|
+
self._register_component(cid, builder)
|
|
233
|
+
|
|
234
|
+
def status(self, label: str, state: str = "running", expanded: bool = True):
|
|
235
|
+
from ..context import fragment_ctx
|
|
236
|
+
|
|
237
|
+
cid = self._get_next_cid("status")
|
|
238
|
+
|
|
239
|
+
class StatusContext:
|
|
240
|
+
def __init__(self, app, status_id, label, state, expanded):
|
|
241
|
+
self.app = app
|
|
242
|
+
self.status_id = status_id
|
|
243
|
+
self.label = label
|
|
244
|
+
self.state = state
|
|
245
|
+
self.expanded = expanded
|
|
246
|
+
self.token = None
|
|
247
|
+
|
|
248
|
+
def __enter__(self):
|
|
249
|
+
# Register builder
|
|
250
|
+
def builder():
|
|
251
|
+
store = get_session_store()
|
|
252
|
+
|
|
253
|
+
# Collect nested content
|
|
254
|
+
htmls = []
|
|
255
|
+
# Check static
|
|
256
|
+
for cid_child, b in self.app.static_fragment_components.get(self.status_id, []):
|
|
257
|
+
htmls.append(b().render())
|
|
258
|
+
# Check session
|
|
259
|
+
for cid_child, b in store['fragment_components'].get(self.status_id, []):
|
|
260
|
+
htmls.append(b().render())
|
|
261
|
+
|
|
262
|
+
inner_html = "".join(htmls)
|
|
263
|
+
|
|
264
|
+
# Status icon and color based on state
|
|
265
|
+
if self.state == "running":
|
|
266
|
+
icon = '<sl-spinner style="font-size:1rem;"></sl-spinner>'
|
|
267
|
+
border_color = "var(--sl-primary)"
|
|
268
|
+
elif self.state == "complete":
|
|
269
|
+
icon = '<sl-icon name="check-circle-fill" style="color:#10b981;font-size:1rem;"></sl-icon>'
|
|
270
|
+
border_color = "#10b981"
|
|
271
|
+
elif self.state == "error":
|
|
272
|
+
icon = '<sl-icon name="x-circle-fill" style="color:#ef4444;font-size:1rem;"></sl-icon>'
|
|
273
|
+
border_color = "#ef4444"
|
|
274
|
+
else:
|
|
275
|
+
icon = '<sl-icon name="info-circle-fill" style="color:var(--sl-primary);font-size:1rem;"></sl-icon>'
|
|
276
|
+
border_color = "var(--sl-primary)"
|
|
277
|
+
|
|
278
|
+
# XSS protection: escape label
|
|
279
|
+
import html as html_lib
|
|
280
|
+
escaped_label = html_lib.escape(str(self.label))
|
|
281
|
+
|
|
282
|
+
# Build status container
|
|
283
|
+
html_output = f'''
|
|
284
|
+
<sl-details {"open" if self.expanded else ""} style="margin-bottom:1rem;">
|
|
285
|
+
<div slot="summary" style="display:flex;align-items:center;gap:0.5rem;font-weight:600;">
|
|
286
|
+
{icon}
|
|
287
|
+
<span>{escaped_label}</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div style="padding:0.5rem 0 0 1.5rem;border-left:2px solid {border_color};margin-left:0.5rem;">
|
|
290
|
+
{inner_html}
|
|
291
|
+
</div>
|
|
292
|
+
</sl-details>
|
|
293
|
+
'''
|
|
294
|
+
return Component("div", id=self.status_id, content=html_output)
|
|
295
|
+
|
|
296
|
+
self.app._register_component(self.status_id, builder)
|
|
297
|
+
|
|
298
|
+
self.token = fragment_ctx.set(self.status_id)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
302
|
+
if self.token:
|
|
303
|
+
fragment_ctx.reset(self.token)
|
|
304
|
+
|
|
305
|
+
def __getattr__(self, name):
|
|
306
|
+
return getattr(self.app, name)
|
|
307
|
+
|
|
308
|
+
return StatusContext(self, cid, label, state, expanded)
|