dars-framework 1.2.3__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.
- dars/__init__.py +0 -0
- dars/all.py +69 -0
- dars/cli/__init__.py +0 -0
- dars/cli/doctor/__init__.py +1 -0
- dars/cli/doctor/detect.py +154 -0
- dars/cli/doctor/doctor.py +176 -0
- dars/cli/doctor/installers.py +100 -0
- dars/cli/doctor/persist.py +62 -0
- dars/cli/doctor/preflight.py +33 -0
- dars/cli/doctor/ui.py +54 -0
- dars/cli/hot_reload.py +33 -0
- dars/cli/main.py +1107 -0
- dars/cli/preview.py +448 -0
- dars/cli/translations.py +531 -0
- dars/components/__init__.py +0 -0
- dars/components/advanced/__init__.py +8 -0
- dars/components/advanced/accordion.py +26 -0
- dars/components/advanced/card.py +33 -0
- dars/components/advanced/modal.py +45 -0
- dars/components/advanced/navbar.py +44 -0
- dars/components/advanced/table.py +25 -0
- dars/components/advanced/tabs.py +31 -0
- dars/components/basic/__init__.py +34 -0
- dars/components/basic/button.py +55 -0
- dars/components/basic/checkbox.py +35 -0
- dars/components/basic/container.py +29 -0
- dars/components/basic/datepicker.py +139 -0
- dars/components/basic/image.py +36 -0
- dars/components/basic/input.py +57 -0
- dars/components/basic/link.py +31 -0
- dars/components/basic/markdown.py +86 -0
- dars/components/basic/page.py +20 -0
- dars/components/basic/progressbar.py +18 -0
- dars/components/basic/radiobutton.py +35 -0
- dars/components/basic/select.py +82 -0
- dars/components/basic/slider.py +63 -0
- dars/components/basic/spinner.py +12 -0
- dars/components/basic/text.py +23 -0
- dars/components/basic/textarea.py +46 -0
- dars/components/basic/tooltip.py +19 -0
- dars/components/layout/__init__.py +0 -0
- dars/components/layout/anchor.py +13 -0
- dars/components/layout/flex.py +26 -0
- dars/components/layout/grid.py +45 -0
- dars/config.py +134 -0
- dars/core/__init__.py +0 -0
- dars/core/app.py +957 -0
- dars/core/component.py +284 -0
- dars/core/events.py +102 -0
- dars/core/js_bridge.py +99 -0
- dars/core/properties.py +127 -0
- dars/core/state.py +309 -0
- dars/dars_tests/apps_test/health_check.py +56 -0
- dars/dars_tests/run_tests.py +275 -0
- dars/dars_tests/tests/test_advanced_components.py +69 -0
- dars/dars_tests/tests/test_basic_components.py +88 -0
- dars/dars_tests/tests/test_core_and_cli.py +17 -0
- dars/dars_tests/tests/test_layout_components.py +58 -0
- dars/dars_tests/tests/test_version_check.py +21 -0
- dars/docs/__init__.py +0 -0
- dars/docs/app.md +290 -0
- dars/docs/cli.md +80 -0
- dars/docs/components.md +1679 -0
- dars/docs/custom_components.md +30 -0
- dars/docs/events.md +45 -0
- dars/docs/exporters.md +162 -0
- dars/docs/getting_started.md +79 -0
- dars/docs/index.md +18 -0
- dars/docs/scripts.md +593 -0
- dars/docs/state_management.md +57 -0
- dars/exporters/__init__.py +0 -0
- dars/exporters/base.py +96 -0
- dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
- dars/exporters/web/OLD/html_css_js_old.py +1406 -0
- dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
- dars/exporters/web/__init__.py +0 -0
- dars/exporters/web/html_css_js.py +2675 -0
- dars/exporters/web/vdom.py +251 -0
- dars/js_lib.py +206 -0
- dars/scripts/__init__.py +0 -0
- dars/scripts/dscript.py +26 -0
- dars/scripts/script.py +39 -0
- dars/security.py +195 -0
- dars/templates/__init__.py +0 -0
- dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- dars/templates/examples/README.md +4 -0
- dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
- dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
- dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
- dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
- dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
- dars/templates/examples/advanced/__init__.py +0 -0
- dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
- dars/templates/examples/basic/Forms/form_components.py +516 -0
- dars/templates/examples/basic/Forms/simple_form.py +379 -0
- dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
- dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
- dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
- dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
- dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
- dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
- dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
- dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
- dars/templates/examples/basic/__init__.py +0 -0
- dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
- dars/templates/examples/demo/complete_app.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
- dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
- dars/templates/examples/markdown/__init__.py +0 -0
- dars/templates/html/__init__.py +0 -0
- dars/version.py +2 -0
- dars_framework-1.2.3.dist-info/METADATA +15 -0
- dars_framework-1.2.3.dist-info/RECORD +118 -0
- dars_framework-1.2.3.dist-info/WHEEL +5 -0
- dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
- dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
- dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/core/state.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from dars.scripts.script import InlineScript
|
|
3
|
+
|
|
4
|
+
# Global registry collected at authoring time (Python)
|
|
5
|
+
STATE_BOOTSTRAP: List[Dict[str, Any]] = []
|
|
6
|
+
|
|
7
|
+
class DarsState:
|
|
8
|
+
def __init__(self, name: str, id: Optional[str], states: Optional[List[Any]], is_custom: bool = False):
|
|
9
|
+
self.name = name
|
|
10
|
+
self.id = id
|
|
11
|
+
self.states = states or []
|
|
12
|
+
self.is_custom = is_custom
|
|
13
|
+
self.rules: Dict[str, Dict[str, Any]] = {}
|
|
14
|
+
self._bootstrap_ref: Optional[Dict[str, Any]] = None
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
17
|
+
d = {
|
|
18
|
+
"name": self.name,
|
|
19
|
+
"id": self.id,
|
|
20
|
+
"states": self.states,
|
|
21
|
+
"isCustom": self.is_custom,
|
|
22
|
+
}
|
|
23
|
+
try:
|
|
24
|
+
d["defaultIndex"] = 0
|
|
25
|
+
d["defaultValue"] = (self.states[0] if isinstance(self.states, list) and len(self.states) > 0 else None)
|
|
26
|
+
except Exception:
|
|
27
|
+
d["defaultIndex"] = 0
|
|
28
|
+
d["defaultValue"] = None
|
|
29
|
+
if self.rules:
|
|
30
|
+
d["rules"] = self.rules
|
|
31
|
+
return d
|
|
32
|
+
|
|
33
|
+
def state(self, idx: Optional[int] = None, cComp: bool = False, render: Optional[Any] = None, goto: Optional[Any] = None) -> InlineScript:
|
|
34
|
+
"""
|
|
35
|
+
Convenience: returns an InlineScript that, when added to a page/app,
|
|
36
|
+
triggers a state change via the JS runtime. Intended for quick prototyping.
|
|
37
|
+
|
|
38
|
+
- idx: target state index/value
|
|
39
|
+
- cComp: if True, performs a full HTML replace (custom component flow)
|
|
40
|
+
- render: HTML string to inject when cComp=True
|
|
41
|
+
"""
|
|
42
|
+
target_id = self.id or ""
|
|
43
|
+
|
|
44
|
+
def _escape_js_str(s: str) -> str:
|
|
45
|
+
return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "")
|
|
46
|
+
|
|
47
|
+
# Compute HTML if needed
|
|
48
|
+
html_val = None
|
|
49
|
+
if cComp and render is not None:
|
|
50
|
+
try:
|
|
51
|
+
# DeferredAttr -> clone component with attrs
|
|
52
|
+
if hasattr(render, 'clone_with') and callable(getattr(render, 'clone_with')):
|
|
53
|
+
render = render.clone_with()
|
|
54
|
+
# If it's a Component instance, render it to HTML
|
|
55
|
+
try:
|
|
56
|
+
from dars.core.component import Component as _DarsComponent
|
|
57
|
+
if isinstance(render, _DarsComponent):
|
|
58
|
+
from dars.exporters.web.html_css_js import HTMLCSSJSExporter
|
|
59
|
+
_exp = HTMLCSSJSExporter()
|
|
60
|
+
html_val = _exp.render_component(render)
|
|
61
|
+
except Exception:
|
|
62
|
+
html_val = None
|
|
63
|
+
if html_val is None and isinstance(render, str):
|
|
64
|
+
html_val = render
|
|
65
|
+
except Exception:
|
|
66
|
+
html_val = None
|
|
67
|
+
|
|
68
|
+
# Build payload
|
|
69
|
+
parts = [f"id: '{_escape_js_str(target_id)}'", f"name: '{_escape_js_str(self.name)}'"]
|
|
70
|
+
if idx is not None:
|
|
71
|
+
parts.append(f"state: {idx}")
|
|
72
|
+
if goto is not None:
|
|
73
|
+
if isinstance(goto, str):
|
|
74
|
+
parts.append(f"goto: '{_escape_js_str(goto)}'")
|
|
75
|
+
else:
|
|
76
|
+
parts.append(f"goto: {goto}")
|
|
77
|
+
if cComp:
|
|
78
|
+
html_str = _escape_js_str(html_val or "")
|
|
79
|
+
parts.append("useCustomRender: true")
|
|
80
|
+
parts.append(f"html: '{html_str}'")
|
|
81
|
+
payload = ", ".join(parts)
|
|
82
|
+
|
|
83
|
+
code = (
|
|
84
|
+
"(async () => {\n"
|
|
85
|
+
" try {\n"
|
|
86
|
+
" let ch = window.__DARS_CHANGE_FN;\n"
|
|
87
|
+
" if (!ch) {\n"
|
|
88
|
+
" if (window.Dars && typeof window.Dars.change === 'function') {\n"
|
|
89
|
+
" ch = window.Dars.change.bind(window.Dars);\n"
|
|
90
|
+
" } else {\n"
|
|
91
|
+
" const m = await import('./lib/dars.min.js');\n"
|
|
92
|
+
" ch = (m.change || (m.default && m.default.change));\n"
|
|
93
|
+
" }\n"
|
|
94
|
+
" if (typeof ch === 'function') window.__DARS_CHANGE_FN = ch;\n"
|
|
95
|
+
" }\n"
|
|
96
|
+
f" if (typeof ch === 'function') ch({{{payload}}});\n"
|
|
97
|
+
" } catch (e) { /* noop */ }\n"
|
|
98
|
+
"})();\n"
|
|
99
|
+
)
|
|
100
|
+
return InlineScript(code, module=True)
|
|
101
|
+
|
|
102
|
+
# --- cState: define rules/mods for a given state index ---
|
|
103
|
+
def cState(self, idx: int, mods: Optional[List[Dict[str, Any]]] = None) -> 'CStateRuleBuilder':
|
|
104
|
+
key = str(idx)
|
|
105
|
+
if idx == 0:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"Default state (index 0) is immutable. Do not define cState(0). "
|
|
108
|
+
"Configure the component's default directly on the instance instead."
|
|
109
|
+
)
|
|
110
|
+
if key not in self.rules:
|
|
111
|
+
self.rules[key] = {}
|
|
112
|
+
if mods:
|
|
113
|
+
existing = list(self.rules[key].get('mods', []))
|
|
114
|
+
existing.extend(mods)
|
|
115
|
+
self.rules[key]['mods'] = existing
|
|
116
|
+
# mirror into bootstrap ref if exists
|
|
117
|
+
if self._bootstrap_ref is not None:
|
|
118
|
+
self._bootstrap_ref.setdefault('rules', {})
|
|
119
|
+
self._bootstrap_ref['rules'][key] = self.rules[key]
|
|
120
|
+
return CStateRuleBuilder(self, key)
|
|
121
|
+
|
|
122
|
+
# sugar: direct goto builder for rules
|
|
123
|
+
def goto(self, value: Any) -> 'CStateRuleBuilder':
|
|
124
|
+
# attach as default rule for current state if exists, else for state 0
|
|
125
|
+
key = str(0)
|
|
126
|
+
if key not in self.rules:
|
|
127
|
+
self.rules[key] = {}
|
|
128
|
+
self.rules[key]['goto'] = value
|
|
129
|
+
if self._bootstrap_ref is not None:
|
|
130
|
+
self._bootstrap_ref.setdefault('rules', {})
|
|
131
|
+
self._bootstrap_ref['rules'][key] = self.rules[key]
|
|
132
|
+
return CStateRuleBuilder(self, key)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Mod:
|
|
136
|
+
@staticmethod
|
|
137
|
+
def inc(target: Any, prop: str = 'text', by: int = 1) -> Dict[str, Any]:
|
|
138
|
+
tid = getattr(target, 'id', None) or str(target)
|
|
139
|
+
return {"op": "inc", "target": tid, "prop": prop, "by": by}
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def dec(target: Any, prop: str = 'text', by: int = 1) -> Dict[str, Any]:
|
|
143
|
+
return Mod.inc(target, prop=prop, by=(-abs(by)))
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def set(target: Any, **attrs) -> Dict[str, Any]:
|
|
147
|
+
tid = getattr(target, 'id', None) or str(target)
|
|
148
|
+
return {"op": "set", "target": tid, "attrs": attrs}
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def toggle_class(target: Any, name: str, on: Optional[bool] = None) -> Dict[str, Any]:
|
|
152
|
+
tid = getattr(target, 'id', None) or str(target)
|
|
153
|
+
d: Dict[str, Any] = {"op": "toggleClass", "target": tid, "name": name}
|
|
154
|
+
if on is not None:
|
|
155
|
+
d['on'] = bool(on)
|
|
156
|
+
return d
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def append_text(target: Any, value: str) -> Dict[str, Any]:
|
|
160
|
+
tid = getattr(target, 'id', None) or str(target)
|
|
161
|
+
return {"op": "appendText", "target": tid, "value": value}
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def prepend_text(target: Any, value: str) -> Dict[str, Any]:
|
|
165
|
+
tid = getattr(target, 'id', None) or str(target)
|
|
166
|
+
return {"op": "prependText", "target": tid, "value": value}
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def call(target: Any, state: Any = None, goto: Any = None) -> Dict[str, Any]:
|
|
170
|
+
"""Invoke another dState's state change.
|
|
171
|
+
- target: DarsState instance or state name string; if a component is passed, use its id.
|
|
172
|
+
- state: target state index/value.
|
|
173
|
+
- goto: relative/absolute goto directive (e.g., '+1').
|
|
174
|
+
The runtime will resolve the state by name first (registry), falling back to id.
|
|
175
|
+
"""
|
|
176
|
+
name: Optional[str] = None
|
|
177
|
+
sid: Optional[str] = None
|
|
178
|
+
try:
|
|
179
|
+
# DarsState instance
|
|
180
|
+
if hasattr(target, 'name') and hasattr(target, 'id'):
|
|
181
|
+
name = getattr(target, 'name', None)
|
|
182
|
+
sid = getattr(target, 'id', None)
|
|
183
|
+
elif isinstance(target, str):
|
|
184
|
+
name = target
|
|
185
|
+
else:
|
|
186
|
+
# Maybe a component; try id
|
|
187
|
+
sid = getattr(target, 'id', None) or str(target)
|
|
188
|
+
except Exception:
|
|
189
|
+
name = None
|
|
190
|
+
sid = None
|
|
191
|
+
d: Dict[str, Any] = {"op": "call"}
|
|
192
|
+
if name:
|
|
193
|
+
d['name'] = name
|
|
194
|
+
if sid:
|
|
195
|
+
d['id'] = sid
|
|
196
|
+
if state is not None:
|
|
197
|
+
d['state'] = state
|
|
198
|
+
if goto is not None:
|
|
199
|
+
d['goto'] = goto
|
|
200
|
+
return d
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class CStateRuleBuilder:
|
|
204
|
+
def __init__(self, st: DarsState, key: str):
|
|
205
|
+
self.st = st
|
|
206
|
+
self.key = key
|
|
207
|
+
|
|
208
|
+
def _ensure(self):
|
|
209
|
+
if self.key not in self.st.rules:
|
|
210
|
+
self.st.rules[self.key] = {}
|
|
211
|
+
if 'mods' not in self.st.rules[self.key]:
|
|
212
|
+
self.st.rules[self.key]['mods'] = []
|
|
213
|
+
|
|
214
|
+
def inc(self, target: Any, prop: str = 'text', by: int = 1) -> 'CStateRuleBuilder':
|
|
215
|
+
self._ensure()
|
|
216
|
+
self.st.rules[self.key]['mods'].append(Mod.inc(target, prop, by))
|
|
217
|
+
if self.st._bootstrap_ref is not None:
|
|
218
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
219
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def dec(self, target: Any, prop: str = 'text', by: int = 1) -> 'CStateRuleBuilder':
|
|
223
|
+
self._ensure()
|
|
224
|
+
self.st.rules[self.key]['mods'].append(Mod.dec(target, prop, by))
|
|
225
|
+
if self.st._bootstrap_ref is not None:
|
|
226
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
227
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
228
|
+
return self
|
|
229
|
+
|
|
230
|
+
def set(self, target: Any, **attrs) -> 'CStateRuleBuilder':
|
|
231
|
+
self._ensure()
|
|
232
|
+
self.st.rules[self.key]['mods'].append(Mod.set(target, **attrs))
|
|
233
|
+
if self.st._bootstrap_ref is not None:
|
|
234
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
235
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
def toggle_class(self, target: Any, name: str, on: Optional[bool] = None) -> 'CStateRuleBuilder':
|
|
239
|
+
self._ensure()
|
|
240
|
+
self.st.rules[self.key]['mods'].append(Mod.toggle_class(target, name, on))
|
|
241
|
+
if self.st._bootstrap_ref is not None:
|
|
242
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
243
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def append_text(self, target: Any, value: str) -> 'CStateRuleBuilder':
|
|
247
|
+
self._ensure()
|
|
248
|
+
self.st.rules[self.key]['mods'].append(Mod.append_text(target, value))
|
|
249
|
+
if self.st._bootstrap_ref is not None:
|
|
250
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
251
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def prepend_text(self, target: Any, value: str) -> 'CStateRuleBuilder':
|
|
255
|
+
self._ensure()
|
|
256
|
+
self.st.rules[self.key]['mods'].append(Mod.prepend_text(target, value))
|
|
257
|
+
if self.st._bootstrap_ref is not None:
|
|
258
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
259
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
def call(self, target: Any, state: Any = None, goto: Any = None) -> 'CStateRuleBuilder':
|
|
263
|
+
"""Append a cross-state call op to this rule."""
|
|
264
|
+
self._ensure()
|
|
265
|
+
self.st.rules[self.key]['mods'].append(Mod.call(target, state=state, goto=goto))
|
|
266
|
+
if self.st._bootstrap_ref is not None:
|
|
267
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
268
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
269
|
+
return self
|
|
270
|
+
|
|
271
|
+
def goto(self, value: Any) -> 'CStateRuleBuilder':
|
|
272
|
+
if self.key not in self.st.rules:
|
|
273
|
+
self.st.rules[self.key] = {}
|
|
274
|
+
self.st.rules[self.key]['goto'] = value
|
|
275
|
+
if self.st._bootstrap_ref is not None:
|
|
276
|
+
self.st._bootstrap_ref.setdefault('rules', {})
|
|
277
|
+
self.st._bootstrap_ref['rules'][self.key] = self.st.rules[self.key]
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def dState(name: str, component: Any = None, id: Optional[str] = None, states: Optional[List[Any]] = None, is_custom: bool = False) -> DarsState:
|
|
282
|
+
"""
|
|
283
|
+
Declare a state associated with a component or an element id.
|
|
284
|
+
- name: state name (unique enough per app).
|
|
285
|
+
- component: a Dars component instance; if provided and has .id, it is used.
|
|
286
|
+
- id: explicit target id when component is not provided.
|
|
287
|
+
- states: optional list of possible state values (metadata).
|
|
288
|
+
- is_custom: mark as custom component to indicate full HTML replace flows.
|
|
289
|
+
|
|
290
|
+
Returns a DarsState object (for ergonomics), and records the state
|
|
291
|
+
in a global bootstrap list consumed by the exporter.
|
|
292
|
+
"""
|
|
293
|
+
target_id = None
|
|
294
|
+
try:
|
|
295
|
+
if component is not None and hasattr(component, 'id'):
|
|
296
|
+
target_id = getattr(component, 'id')
|
|
297
|
+
except Exception:
|
|
298
|
+
target_id = None
|
|
299
|
+
if not target_id:
|
|
300
|
+
target_id = id
|
|
301
|
+
|
|
302
|
+
st = DarsState(name=name, id=target_id, states=states, is_custom=is_custom)
|
|
303
|
+
try:
|
|
304
|
+
d = st.to_dict()
|
|
305
|
+
STATE_BOOTSTRAP.append(d)
|
|
306
|
+
st._bootstrap_ref = d
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
return st
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dars.all import *
|
|
2
|
+
|
|
3
|
+
app = App(title="Dars-HealthCheck", theme="dark")
|
|
4
|
+
# Crear componentes
|
|
5
|
+
container = Container(
|
|
6
|
+
Text(
|
|
7
|
+
text="Dars Health Check",
|
|
8
|
+
style={
|
|
9
|
+
'font-size': '48px',
|
|
10
|
+
'color': '#2c3e50',
|
|
11
|
+
'margin-bottom': '20px',
|
|
12
|
+
'font-weight': 'bold',
|
|
13
|
+
'text-align': 'center'
|
|
14
|
+
}
|
|
15
|
+
),
|
|
16
|
+
Text(
|
|
17
|
+
text="Dars Health Check",
|
|
18
|
+
style={
|
|
19
|
+
'font-size': '20px',
|
|
20
|
+
'color': '#7f8c8d',
|
|
21
|
+
'margin-bottom': '40px',
|
|
22
|
+
'text-align': 'center'
|
|
23
|
+
}
|
|
24
|
+
),
|
|
25
|
+
|
|
26
|
+
Button(
|
|
27
|
+
text="Check",
|
|
28
|
+
on_click= dScript("""alert('Health Check')"""),
|
|
29
|
+
on_mouse_enter=dScript("""this.style.backgroundColor = '#2980b9';"""),
|
|
30
|
+
on_mouse_leave=dScript("""this.style.backgroundColor = '#3498db';"""),
|
|
31
|
+
style={
|
|
32
|
+
'background-color': '#3498db',
|
|
33
|
+
'color': 'white',
|
|
34
|
+
'padding': '15px 30px',
|
|
35
|
+
'border': 'none',
|
|
36
|
+
'border-radius': '8px',
|
|
37
|
+
'font-size': '18px',
|
|
38
|
+
'cursor': 'pointer',
|
|
39
|
+
'transition': 'background-color 0.3s'
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
style={
|
|
43
|
+
'display': 'flex',
|
|
44
|
+
'flex-direction': 'column',
|
|
45
|
+
'align-items': 'center',
|
|
46
|
+
'justify-content': 'center',
|
|
47
|
+
'min-height': '100vh',
|
|
48
|
+
'background-color': '#f0f2f5',
|
|
49
|
+
'font-family': 'Arial, sans-serif'
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
app.set_root(container)
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
app.rTimeCompile()
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Runner that executes the test files and prints a colored report using rich
|
|
2
|
+
import runpy, glob, importlib.util, os, sys, traceback, subprocess, time, threading
|
|
3
|
+
import requests
|
|
4
|
+
import signal
|
|
5
|
+
import webbrowser
|
|
6
|
+
import shutil
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.prompt import Prompt
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
def run_unit_tests(unit_test_paths=None):
|
|
15
|
+
"""Run unit test files, optionally from specific paths"""
|
|
16
|
+
if unit_test_paths is None:
|
|
17
|
+
# Default behavior: run all tests in tests directory
|
|
18
|
+
tests = sorted(glob.glob(os.path.join(os.path.dirname(__file__), 'tests', 'test_*.py')))
|
|
19
|
+
else:
|
|
20
|
+
# Use provided test paths
|
|
21
|
+
tests = unit_test_paths
|
|
22
|
+
|
|
23
|
+
results = []
|
|
24
|
+
for t in tests:
|
|
25
|
+
name = os.path.basename(t)
|
|
26
|
+
try:
|
|
27
|
+
console.print(Panel(f'Running {name}', style='cyan'))
|
|
28
|
+
ns = runpy.run_path(t, run_name='__main__')
|
|
29
|
+
results.append((name, True, None))
|
|
30
|
+
console.print(f'[green]PASS[/green] {name}')
|
|
31
|
+
except Exception as e:
|
|
32
|
+
results.append((name, False, traceback.format_exc()))
|
|
33
|
+
console.print(f'[red]FAIL[/red] {name}')
|
|
34
|
+
console.print(traceback.format_exc())
|
|
35
|
+
return results
|
|
36
|
+
|
|
37
|
+
def check_server(port=8000, timeout=10):
|
|
38
|
+
"""Check if the server is running on the given port"""
|
|
39
|
+
start_time = time.time()
|
|
40
|
+
while time.time() - start_time < timeout:
|
|
41
|
+
try:
|
|
42
|
+
response = requests.get(f'http://localhost:{port}', timeout=2)
|
|
43
|
+
if response.status_code < 500:
|
|
44
|
+
return True
|
|
45
|
+
except requests.exceptions.RequestException:
|
|
46
|
+
time.sleep(0.5)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def run_app_with_timeout(app_file, timeout=15):
|
|
50
|
+
"""Run a Dars app with rTimeCompile and capture its output with a timeout"""
|
|
51
|
+
try:
|
|
52
|
+
env = os.environ.copy()
|
|
53
|
+
env['PYTHONIOENCODING'] = 'utf-8'
|
|
54
|
+
|
|
55
|
+
process = subprocess.Popen(
|
|
56
|
+
[sys.executable, app_file],
|
|
57
|
+
stdout=subprocess.PIPE,
|
|
58
|
+
stderr=subprocess.PIPE,
|
|
59
|
+
cwd=os.path.dirname(app_file) or None, # evita cwd = "" en Windows
|
|
60
|
+
env=env,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Esperar a que el servidor se inicie o timeout
|
|
64
|
+
server_started = False
|
|
65
|
+
start_time = time.time()
|
|
66
|
+
|
|
67
|
+
while time.time() - start_time < timeout:
|
|
68
|
+
if check_server(8000, 2):
|
|
69
|
+
server_started = True
|
|
70
|
+
break
|
|
71
|
+
# Verificar si el proceso ha terminado (lo que indicaría un error)
|
|
72
|
+
if process.poll() is not None:
|
|
73
|
+
break
|
|
74
|
+
time.sleep(1)
|
|
75
|
+
|
|
76
|
+
# Recoger la salida (aunque no haya terminado, para capturar errores iniciales)
|
|
77
|
+
try:
|
|
78
|
+
stdout, stderr = process.communicate(timeout=2)
|
|
79
|
+
except subprocess.TimeoutExpired:
|
|
80
|
+
stdout, stderr = b"", b""
|
|
81
|
+
|
|
82
|
+
stdout_str = stdout.decode('utf-8', errors='replace') if stdout else ''
|
|
83
|
+
stderr_str = stderr.decode('utf-8', errors='replace') if stderr else ''
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
'success': server_started,
|
|
87
|
+
'process': process,
|
|
88
|
+
'stdout': stdout_str,
|
|
89
|
+
'stderr': stderr_str
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return {
|
|
94
|
+
'success': False,
|
|
95
|
+
'error': str(e),
|
|
96
|
+
'stdout': '',
|
|
97
|
+
'stderr': ''
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def safe_read_file(file_path):
|
|
102
|
+
"""Leer un archivo de forma segura manejando diferentes codificaciones"""
|
|
103
|
+
encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
|
|
104
|
+
|
|
105
|
+
for encoding in encodings:
|
|
106
|
+
try:
|
|
107
|
+
with open(file_path, 'r', encoding=encoding) as f:
|
|
108
|
+
return f.read()
|
|
109
|
+
except UnicodeDecodeError:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Si todas las codificaciones fallan, usar modo binario con reemplazo de errores
|
|
113
|
+
with open(file_path, 'rb') as f:
|
|
114
|
+
return f.read().decode('utf-8', errors='replace')
|
|
115
|
+
|
|
116
|
+
def run_app_tests(app_test_paths=None):
|
|
117
|
+
"""Run Dars application tests that use rTimeCompile, optionally from specific paths"""
|
|
118
|
+
if app_test_paths is None:
|
|
119
|
+
# Default behavior: run all apps in apps_test directory
|
|
120
|
+
apps_test_dir = os.path.join(os.path.dirname(__file__), 'apps_test')
|
|
121
|
+
if not os.path.exists(apps_test_dir):
|
|
122
|
+
console.print(f"[yellow]apps_test directory not found: {apps_test_dir}[/yellow]")
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
apps = sorted(glob.glob(os.path.join(apps_test_dir, '*.py')))
|
|
126
|
+
else:
|
|
127
|
+
# Use provided app paths
|
|
128
|
+
apps = app_test_paths
|
|
129
|
+
|
|
130
|
+
results = []
|
|
131
|
+
|
|
132
|
+
for app_file in apps:
|
|
133
|
+
name = os.path.basename(app_file)
|
|
134
|
+
try:
|
|
135
|
+
console.print(Panel(f'Testing Dars App: {name}', style='magenta'))
|
|
136
|
+
|
|
137
|
+
# Verificar si la aplicación usa rTimeCompile de forma segura
|
|
138
|
+
content = safe_read_file(app_file)
|
|
139
|
+
uses_rTimeCompile = 'rTimeCompile' in content
|
|
140
|
+
|
|
141
|
+
if not uses_rTimeCompile:
|
|
142
|
+
console.print(f"[yellow]Skipping {name} - does not use rTimeCompile[/yellow]")
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Ejecutar la aplicación con timeout
|
|
146
|
+
result = run_app_with_timeout(app_file, timeout=20)
|
|
147
|
+
|
|
148
|
+
if result['success']:
|
|
149
|
+
# Éxito: el servidor se inició correctamente
|
|
150
|
+
results.append((name, True, "App started server successfully on port 8000"))
|
|
151
|
+
console.print(f'[green]PASS[/green] {name}')
|
|
152
|
+
|
|
153
|
+
# Abrir el navegador para verificación visual
|
|
154
|
+
console.print("[green]Opening browser for visual verification...[/green]")
|
|
155
|
+
webbrowser.open('http://localhost:8000')
|
|
156
|
+
|
|
157
|
+
# Preguntar al usuario si quiere terminar el proceso
|
|
158
|
+
console.print("\n[bold]Please verify the application in your browser.[/bold]")
|
|
159
|
+
console.print("After verification, you can:")
|
|
160
|
+
console.print("1. Press 'y' to terminate the process and continue with tests")
|
|
161
|
+
console.print("2. Press 'n' to keep the server running and continue")
|
|
162
|
+
|
|
163
|
+
response = Prompt.ask("\nTerminate process and continue?", choices=["y", "n"], default="y")
|
|
164
|
+
|
|
165
|
+
if response.lower() == "y":
|
|
166
|
+
# Terminar el proceso
|
|
167
|
+
if 'process' in result and result['process'].poll() is None:
|
|
168
|
+
console.print("[yellow]Terminating app process...[/yellow]")
|
|
169
|
+
try:
|
|
170
|
+
# Enviar señal de interrupción (equivalente a Ctrl+C)
|
|
171
|
+
if os.name == 'nt': # Windows
|
|
172
|
+
result['process'].terminate()
|
|
173
|
+
else: # Unix
|
|
174
|
+
result['process'].send_signal(signal.SIGINT)
|
|
175
|
+
|
|
176
|
+
# Esperar a que termine
|
|
177
|
+
result['process'].wait(timeout=10)
|
|
178
|
+
console.print("[green]App process terminated successfully[/green]")
|
|
179
|
+
|
|
180
|
+
# Limpiar carpeta dars_preview
|
|
181
|
+
preview_dir = os.path.join(os.path.dirname(app_file), "dars_preview")
|
|
182
|
+
if os.path.exists(preview_dir):
|
|
183
|
+
shutil.rmtree(preview_dir)
|
|
184
|
+
console.print("[green]Preview directory cleaned up[/green]")
|
|
185
|
+
|
|
186
|
+
except:
|
|
187
|
+
console.print("[red]Force killing app process...[/red]")
|
|
188
|
+
result['process'].kill()
|
|
189
|
+
else:
|
|
190
|
+
console.print("[yellow]Keeping server running...[/yellow]")
|
|
191
|
+
else:
|
|
192
|
+
# Fallo: el servidor no se inició
|
|
193
|
+
error_msg = result['stderr'] or result['stdout'] or "Unknown error"
|
|
194
|
+
results.append((name, False, error_msg))
|
|
195
|
+
console.print(f'[red]FAIL[/red] {name}: {error_msg}')
|
|
196
|
+
|
|
197
|
+
# Terminar el proceso si todavía está ejecutándose (solo si no elegimos mantenerlo)
|
|
198
|
+
if 'process' in result and result['process'].poll() is None:
|
|
199
|
+
console.print("[yellow]Terminating app process...[/yellow]")
|
|
200
|
+
try:
|
|
201
|
+
# Enviar señal de interrupción (equivalente a Ctrl+C)
|
|
202
|
+
if os.name == 'nt': # Windows
|
|
203
|
+
result['process'].terminate()
|
|
204
|
+
else: # Unix
|
|
205
|
+
result['process'].send_signal(signal.SIGINT)
|
|
206
|
+
|
|
207
|
+
# Esperar a que termine
|
|
208
|
+
result['process'].wait(timeout=10)
|
|
209
|
+
console.print("[green]App process terminated successfully[/green]")
|
|
210
|
+
|
|
211
|
+
# Limpiar carpeta dars_preview
|
|
212
|
+
preview_dir = os.path.join(os.path.dirname(app_file), "dars_preview")
|
|
213
|
+
if os.path.exists(preview_dir):
|
|
214
|
+
shutil.rmtree(preview_dir)
|
|
215
|
+
console.print("[green]Preview directory cleaned up[/green]")
|
|
216
|
+
|
|
217
|
+
except:
|
|
218
|
+
console.print("[red]Force killing app process...[/red]")
|
|
219
|
+
result['process'].kill()
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
results.append((name, False, str(e)))
|
|
223
|
+
console.print(f'[red]FAIL[/red] {name}: {e}')
|
|
224
|
+
|
|
225
|
+
return results
|
|
226
|
+
|
|
227
|
+
def main(unit_test_paths=None, app_test_paths=None):
|
|
228
|
+
"""
|
|
229
|
+
Run Dars test suite with optional specific test paths
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
unit_test_paths (list): Optional list of paths to unit test files
|
|
233
|
+
app_test_paths (list): Optional list of paths to application test files
|
|
234
|
+
"""
|
|
235
|
+
# Run unit tests
|
|
236
|
+
console.print(Panel("Running Unit Tests", style="cyan"))
|
|
237
|
+
unit_results = run_unit_tests(unit_test_paths)
|
|
238
|
+
|
|
239
|
+
# Run app tests
|
|
240
|
+
console.print(Panel("Running App Tests (rTimeCompile)", style="magenta"))
|
|
241
|
+
app_results = run_app_tests(app_test_paths)
|
|
242
|
+
|
|
243
|
+
# Combine results
|
|
244
|
+
all_results = unit_results + app_results
|
|
245
|
+
|
|
246
|
+
# Print results table
|
|
247
|
+
table = Table(title='Dars Test Suite Results')
|
|
248
|
+
table.add_column('Test File')
|
|
249
|
+
table.add_column('Result')
|
|
250
|
+
table.add_column('Details', overflow='fold')
|
|
251
|
+
|
|
252
|
+
for name, ok, details in all_results:
|
|
253
|
+
table.add_row(name, '[green]PASS[/green]' if ok else '[red]FAIL[/red]', details or '')
|
|
254
|
+
|
|
255
|
+
console.print(table)
|
|
256
|
+
|
|
257
|
+
# Exit code
|
|
258
|
+
any_fail = any(not ok for _, ok, _ in all_results)
|
|
259
|
+
if any_fail:
|
|
260
|
+
console.print('[bold red]Some tests failed.[/bold red]')
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
else:
|
|
263
|
+
console.print('[bold green]All tests passed.[/bold green]')
|
|
264
|
+
|
|
265
|
+
if __name__ == '__main__':
|
|
266
|
+
# Parse command line arguments for custom test paths
|
|
267
|
+
import argparse
|
|
268
|
+
parser = argparse.ArgumentParser(description='Run Dars Framework tests')
|
|
269
|
+
parser.add_argument('--unit-tests', nargs='+', help='Paths to specific unit test files')
|
|
270
|
+
parser.add_argument('--app-tests', nargs='+', help='Paths to specific application test files')
|
|
271
|
+
|
|
272
|
+
args = parser.parse_args()
|
|
273
|
+
|
|
274
|
+
# Run tests with optional paths
|
|
275
|
+
main(unit_test_paths=args.unit_tests, app_test_paths=args.app_tests)
|