ex4nicegui 0.5.2__py3-none-any.whl → 0.5.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.
ex4nicegui/__init__.py CHANGED
@@ -17,10 +17,12 @@ from ex4nicegui.utils.signals import (
17
17
  deep_ref,
18
18
  is_setter_ref,
19
19
  batch,
20
+ is_reactive,
20
21
  )
21
22
 
22
23
 
23
24
  __all__ = [
25
+ "is_reactive",
24
26
  "rxui",
25
27
  "ref_computed",
26
28
  "effect",
@@ -41,4 +43,4 @@ __all__ = [
41
43
  "is_setter_ref",
42
44
  ]
43
45
 
44
- __version__ = "0.5.2"
46
+ __version__ = "0.5.3"
@@ -1,4 +1,5 @@
1
1
  from .gsap import set_defaults, from_, to, new, run_script
2
+ from .timeline import Timeline as timeline
2
3
 
3
4
 
4
5
  __all__ = [
@@ -7,4 +8,5 @@ __all__ = [
7
8
  "to",
8
9
  "new",
9
10
  "run_script",
11
+ "timeline",
10
12
  ]
@@ -0,0 +1,56 @@
1
+ import { gsap } from "gsap";
2
+ import { convertDynamicProperties } from "../../static/utils/dynamic_properties.js";
3
+
4
+
5
+ export default {
6
+ template: `<template></template>`,
7
+ data() {
8
+
9
+ const tl = gsap.timeline(this.defaults)
10
+ this.tl = tl
11
+
12
+ return {
13
+ }
14
+
15
+ },
16
+ mounted() {
17
+ document.addEventListener('DOMContentLoaded', () => {
18
+ /**
19
+ * @type any[]
20
+ */
21
+ const tasks = this.tasks
22
+ tasks.forEach(t => {
23
+ this[t.method](t.targets, t.vars, t.position)
24
+ })
25
+
26
+ this.scriptTasks.forEach(script => {
27
+ const fn = new Function('return ' + script)()
28
+ fn(this.tl, gsap)
29
+ })
30
+
31
+ })
32
+ },
33
+
34
+ methods: {
35
+ from(targets, vars, position) {
36
+ convertDynamicProperties(vars, false)
37
+ this.tl.from(targets, vars, position)
38
+ },
39
+ to(targets, vars, position) {
40
+ convertDynamicProperties(vars, false)
41
+ this.tl.to(targets, vars, position)
42
+ },
43
+ runScript(script) {
44
+ const fn = new Function('return ' + script)()
45
+ fn(this.tl, gsap)
46
+ },
47
+ callTimeline(name, ...args) {
48
+ this.tl[name](...args)
49
+ }
50
+ },
51
+ props: {
52
+ defaults: Object,
53
+ tasks: Array,
54
+ scriptTasks: Array,
55
+ },
56
+ };
@@ -0,0 +1,78 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Dict, Optional, Union
4
+ from nicegui.element import Element
5
+ from nicegui import context as ng_context
6
+
7
+
8
+ class Timeline(
9
+ Element,
10
+ component="timeline.js",
11
+ exposed_libraries=["../libs/gsap/gsap.mjs"],
12
+ extra_libraries=["../libs/gsap/*.js", "../libs/gsap/utils/*.js"],
13
+ ):
14
+ def __init__(
15
+ self,
16
+ defaults: Optional[Dict] = None,
17
+ ) -> None:
18
+ super().__init__()
19
+ self._props["defaults"] = defaults or {}
20
+ self._props["tasks"] = []
21
+ self._props["scriptTasks"] = []
22
+
23
+ def from_(self, targets: str, vars: Dict, position: Optional[str] = None):
24
+ self.__try_run_task("from", targets, vars, position)
25
+ return self
26
+
27
+ def to(self, targets: str, vars: Dict, position: Optional[str] = None):
28
+ self.__try_run_task("to", targets, vars, position)
29
+ return self
30
+
31
+ def run_script(self, script: Union[str, Path]):
32
+ if isinstance(script, Path):
33
+ script = script.read_text("utf8")
34
+
35
+ script = os.linesep.join([s for s in script.splitlines() if s])
36
+ self.__try_run_script_task(script)
37
+ return self
38
+
39
+ def __try_run_script_task(self, script: str):
40
+ def fn():
41
+ self.run_method("runScript", script)
42
+
43
+ if ng_context.get_client().has_socket_connection:
44
+ fn()
45
+ else:
46
+ tasks = self._props["scriptTasks"]
47
+ tasks.append(script)
48
+
49
+ def __try_run_task(
50
+ self, name: str, targets: str, vars: Dict, position: Optional[str] = None
51
+ ):
52
+ def fn():
53
+ self.run_method(name, targets, vars)
54
+
55
+ if ng_context.get_client().has_socket_connection:
56
+ fn()
57
+ else:
58
+ tasks = self._props["tasks"]
59
+ tasks.append(
60
+ {"method": name, "targets": targets, "vars": vars, "position": position}
61
+ )
62
+
63
+ def pause(self):
64
+ self.run_method("callTimeline", "pause")
65
+
66
+ def play(self):
67
+ self.run_method("callTimeline", "play")
68
+
69
+ def resume(
70
+ self,
71
+ ):
72
+ self.run_method("callTimeline", "resume")
73
+
74
+ def seek(self, position: Optional[str] = None, suppressEvents=True):
75
+ self.run_method("callTimeline", "seek", position, suppressEvents)
76
+
77
+ def reverse(self):
78
+ self.run_method("callTimeline", "reverse")
@@ -44,7 +44,8 @@ from .officials.number import NumberBindableUi as number
44
44
  from .officials.grid import GridBindableUi as grid
45
45
  from .officials.expansion import ExpansionBindableUi as expansion
46
46
  from .officials.linear_progress import LinearProgressBindableUi as linear_progress
47
-
47
+ from .officials.knob import KnobBindableUi as knob
48
+ from .officials.circular_progress import CircularProgressBindableUi as circular_progress
48
49
  from .q_pagination import PaginationBindableUi as q_pagination
49
50
 
50
51
  from .local_file_picker import local_file_picker
@@ -56,11 +57,14 @@ from .dropZone.dropZone import use_drag_zone
56
57
  from .fileWatcher import FilesWatcher
57
58
  from .mermaid.mermaid import Mermaid as mermaid
58
59
  from .vfor import vfor, VforStore
60
+ from .vmodel import vmodel
59
61
 
60
62
  pagination = q_pagination
61
63
 
62
64
 
63
65
  __all__ = [
66
+ "circular_progress",
67
+ "knob",
64
68
  "UploadResult",
65
69
  "local_file_picker",
66
70
  "use_draggable",
@@ -70,6 +74,7 @@ __all__ = [
70
74
  "FilesWatcher",
71
75
  "vfor",
72
76
  "VforStore",
77
+ "vmodel",
73
78
  "html",
74
79
  "aggird",
75
80
  "button",
@@ -0,0 +1,64 @@
1
+ from typing import (
2
+ Optional,
3
+ )
4
+ from ex4nicegui.reactive.utils import ParameterClassifier
5
+ from ex4nicegui.utils.apiEffect import ui_effect
6
+
7
+ from ex4nicegui.utils.signals import (
8
+ ReadonlyRef,
9
+ _TMaybeRef as TMaybeRef,
10
+ to_value,
11
+ )
12
+ from nicegui import ui
13
+ from .base import BindableUi, DisableableMixin
14
+
15
+
16
+ class CircularProgressBindableUi(
17
+ BindableUi[ui.circular_progress],
18
+ DisableableMixin,
19
+ ):
20
+ def __init__(
21
+ self,
22
+ value: TMaybeRef[float] = 0.0,
23
+ *,
24
+ min: TMaybeRef[float] = 0.0, # pylint: disable=redefined-builtin
25
+ max: TMaybeRef[float] = 1.0, # pylint: disable=redefined-builtin
26
+ size: Optional[TMaybeRef[str]] = "xl",
27
+ show_value: TMaybeRef[bool] = True,
28
+ color: Optional[TMaybeRef[str]] = "primary",
29
+ ) -> None:
30
+ pc = ParameterClassifier(
31
+ locals(),
32
+ maybeRefs=[
33
+ "value",
34
+ "min",
35
+ "max",
36
+ "color",
37
+ "size",
38
+ "show_value",
39
+ ],
40
+ )
41
+
42
+ value_kws = pc.get_values_kws()
43
+ element = ui.circular_progress(**value_kws)
44
+ super().__init__(element) # type: ignore
45
+
46
+ for key, value in pc.get_bindings().items():
47
+ self.bind_prop(key, value) # type: ignore
48
+
49
+ @property
50
+ def value(self):
51
+ return self.element.value
52
+
53
+ def bind_prop(self, prop: str, ref_ui: ReadonlyRef):
54
+ if prop == "value":
55
+ return self.bind_value(ref_ui)
56
+
57
+ return super().bind_prop(prop, ref_ui)
58
+
59
+ def bind_value(self, ref_ui: ReadonlyRef[float]):
60
+ @ui_effect
61
+ def _():
62
+ self.element.set_value(to_value(ref_ui))
63
+
64
+ return self
@@ -1,18 +1,33 @@
1
1
  from typing import (
2
2
  Any,
3
3
  )
4
+ from ex4nicegui.reactive.utils import ParameterClassifier
5
+ from ex4nicegui.utils.signals import (
6
+ _TMaybeRef as TMaybeRef,
7
+ )
4
8
  from nicegui import ui
5
9
  from .base import BindableUi
6
10
 
7
11
 
8
12
  class ColumnBindableUi(BindableUi[ui.column]):
9
- def __init__(
10
- self,
11
- ) -> None:
12
- element = ui.column()
13
+ def __init__(self, *, wrap: TMaybeRef[bool] = True) -> None:
14
+ pc = ParameterClassifier(locals(), maybeRefs=["wrap"], events=[])
15
+ element = ui.column(**pc.get_values_kws())
13
16
 
14
17
  super().__init__(element)
15
18
 
19
+ for key, value in pc.get_bindings().items():
20
+ self.bind_prop(key, value) # type: ignore
21
+
22
+ def bind_prop(self, prop: str, ref_ui: TMaybeRef):
23
+ if prop == "wrap":
24
+ return self.bind_wrap(ref_ui)
25
+
26
+ return super().bind_prop(prop, ref_ui)
27
+
28
+ def bind_wrap(self, ref_ui: TMaybeRef):
29
+ self.bind_classes({"wrap": ref_ui})
30
+
16
31
  def __enter__(self):
17
32
  self.element.__enter__()
18
33
  return self
@@ -0,0 +1,75 @@
1
+ from typing import (
2
+ Any,
3
+ Callable,
4
+ Optional,
5
+ )
6
+ from ex4nicegui.reactive.utils import ParameterClassifier
7
+ from ex4nicegui.utils.apiEffect import ui_effect
8
+
9
+ from ex4nicegui.utils.signals import (
10
+ ReadonlyRef,
11
+ _TMaybeRef as TMaybeRef,
12
+ to_value,
13
+ )
14
+ from nicegui import ui
15
+ from .base import BindableUi, DisableableMixin
16
+
17
+
18
+ class KnobBindableUi(
19
+ BindableUi[ui.knob],
20
+ DisableableMixin,
21
+ ):
22
+ def __init__(
23
+ self,
24
+ value: TMaybeRef[float] = 0.0,
25
+ *,
26
+ min: TMaybeRef[float] = 0.0, # pylint: disable=redefined-builtin
27
+ max: TMaybeRef[float] = 1.0, # pylint: disable=redefined-builtin
28
+ step: TMaybeRef[float] = 0.01,
29
+ color: Optional[TMaybeRef[str]] = "primary",
30
+ center_color: Optional[TMaybeRef[str]] = None,
31
+ track_color: Optional[TMaybeRef[str]] = None,
32
+ size: Optional[TMaybeRef[str]] = None,
33
+ show_value: TMaybeRef[bool] = False,
34
+ on_change: Optional[Callable[..., Any]] = None,
35
+ ) -> None:
36
+ pc = ParameterClassifier(
37
+ locals(),
38
+ maybeRefs=[
39
+ "value",
40
+ "min",
41
+ "max",
42
+ "step",
43
+ "color",
44
+ "center_color",
45
+ "track_color",
46
+ "size",
47
+ "show_value",
48
+ ],
49
+ v_model=("value", "on_change"),
50
+ events=["on_change"],
51
+ )
52
+
53
+ value_kws = pc.get_values_kws()
54
+ element = ui.knob(**value_kws)
55
+ super().__init__(element) # type: ignore
56
+
57
+ for key, value in pc.get_bindings().items():
58
+ self.bind_prop(key, value) # type: ignore
59
+
60
+ @property
61
+ def value(self):
62
+ return self.element.value
63
+
64
+ def bind_prop(self, prop: str, ref_ui: ReadonlyRef):
65
+ if prop == "value":
66
+ return self.bind_value(ref_ui)
67
+
68
+ return super().bind_prop(prop, ref_ui)
69
+
70
+ def bind_value(self, ref_ui: ReadonlyRef[float]):
71
+ @ui_effect
72
+ def _():
73
+ self.element.set_value(to_value(ref_ui))
74
+
75
+ return self
@@ -4,7 +4,6 @@ from typing import (
4
4
  Optional,
5
5
  TypeVar,
6
6
  Dict,
7
- Union,
8
7
  )
9
8
  from ex4nicegui.reactive.utils import ParameterClassifier
10
9
  from ex4nicegui.utils.apiEffect import ui_effect
@@ -26,7 +25,7 @@ class NumberBindableUi(BindableUi[ui.number]):
26
25
  label: Optional[TMaybeRef[str]] = None,
27
26
  *,
28
27
  placeholder: Optional[TMaybeRef[str]] = None,
29
- value: TMaybeRef[Union[float, None]] = None,
28
+ value: Optional[TMaybeRef[float]] = None,
30
29
  min: Optional[TMaybeRef[float]] = None,
31
30
  max: Optional[TMaybeRef[float]] = None,
32
31
  step: Optional[TMaybeRef[float]] = None,
@@ -1,17 +1,31 @@
1
1
  from typing import (
2
2
  Any,
3
3
  )
4
+ from ex4nicegui.reactive.utils import ParameterClassifier
5
+ from ex4nicegui.utils.signals import (
6
+ _TMaybeRef as TMaybeRef,
7
+ )
4
8
  from nicegui import ui
5
9
  from .base import BindableUi
6
10
 
7
11
 
8
12
  class RowBindableUi(BindableUi[ui.row]):
9
- def __init__(
10
- self,
11
- ) -> None:
12
- element = ui.row()
13
+ def __init__(self, *, wrap: TMaybeRef[bool] = True) -> None:
14
+ pc = ParameterClassifier(locals(), maybeRefs=["wrap"], events=[])
15
+ element = ui.row(**pc.get_values_kws())
13
16
 
14
17
  super().__init__(element)
18
+ for key, value in pc.get_bindings().items():
19
+ self.bind_prop(key, value) # type: ignore
20
+
21
+ def bind_prop(self, prop: str, ref_ui: TMaybeRef):
22
+ if prop == "wrap":
23
+ return self.bind_wrap(ref_ui)
24
+
25
+ return super().bind_prop(prop, ref_ui)
26
+
27
+ def bind_wrap(self, ref_ui: TMaybeRef):
28
+ self.bind_classes({"wrap": ref_ui})
15
29
 
16
30
  def __enter__(self):
17
31
  self.element.__enter__()
@@ -1,9 +1,33 @@
1
- from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, cast
1
+ from typing import (
2
+ Any,
3
+ Callable,
4
+ Dict,
5
+ Iterable,
6
+ List,
7
+ Optional,
8
+ Protocol,
9
+ Tuple,
10
+ cast,
11
+ runtime_checkable,
12
+ Union,
13
+ )
2
14
 
3
15
  from ex4nicegui.utils.signals import is_ref, to_value, is_setter_ref
4
16
  from nicegui.events import handle_event
5
17
 
6
18
 
19
+ @runtime_checkable
20
+ class GetItemProtocol(Protocol):
21
+ def __getitem__(self, key):
22
+ ...
23
+
24
+
25
+ @runtime_checkable
26
+ class SetItemProtocol(Protocol):
27
+ def __setitem__(self, key, value):
28
+ ...
29
+
30
+
7
31
  def _convert_kws_ref2value(kws: Dict) -> Dict:
8
32
  return {key: to_value(value) for key, value in kws.items()}
9
33
 
@@ -74,3 +98,18 @@ class ParameterClassifier:
74
98
  for k, v in self._args.items()
75
99
  if (k in self.maybeRefs and (is_ref(v) or isinstance(v, Callable)))
76
100
  }
101
+
102
+
103
+ def get_attribute(obj: Union[object, GetItemProtocol], name: Union[str, int]) -> Any:
104
+ if isinstance(obj, (GetItemProtocol)):
105
+ return obj[name]
106
+ return getattr(obj, name) # type: ignore
107
+
108
+
109
+ def set_attribute(
110
+ obj: Union[object, SetItemProtocol], name: Union[str, int], value: Any
111
+ ) -> None:
112
+ if isinstance(obj, SetItemProtocol):
113
+ obj[name] = value
114
+ else:
115
+ setattr(obj, name, value) # type: ignore
@@ -2,13 +2,18 @@ from __future__ import annotations
2
2
  from nicegui.element import Element
3
3
  from nicegui import ui
4
4
  from ex4nicegui.utils.clientScope import _CLIENT_SCOPE_MANAGER
5
- from ex4nicegui.utils.signals import ReadonlyRef, on, to_ref, to_ref_wrapper
5
+ from ex4nicegui.utils.signals import (
6
+ TReadonlyRef,
7
+ on,
8
+ to_ref,
9
+ to_ref_wrapper,
10
+ TGetterOrReadonlyRef,
11
+ )
6
12
  from typing import (
7
13
  Any,
8
14
  Callable,
9
15
  Dict,
10
16
  List,
11
- Mapping,
12
17
  Optional,
13
18
  TypeVar,
14
19
  Generic,
@@ -17,11 +22,11 @@ from typing import (
17
22
  )
18
23
  from functools import partial
19
24
  from dataclasses import dataclass
20
- from signe.core.reactive import DictProxy as signe_DictProxy
21
25
  from signe.core.scope import Scope
26
+ from .utils import get_attribute
22
27
 
23
28
  _T = TypeVar("_T")
24
- _T_data = ReadonlyRef[List[Any]]
29
+ _T_data = TGetterOrReadonlyRef[List[Any]]
25
30
 
26
31
 
27
32
  class VforStore(Generic[_T]):
@@ -33,29 +38,17 @@ class VforStore(Generic[_T]):
33
38
  def row_index(self):
34
39
  return self._data_index
35
40
 
36
- def get(self, attr: Optional[str] = None) -> _T:
37
- item = self._source.value[self._data_index.value]
38
-
39
- if attr:
40
- setter = None
41
- if isinstance(item, signe_DictProxy):
42
-
43
- def base_setter(value):
44
- item[attr] = value
41
+ def get(self) -> TReadonlyRef[_T]:
42
+ def base_setter(value):
43
+ self._source.value[self._data_index.value] = value
45
44
 
46
- setter = base_setter
47
- else:
48
- setter = lambda x: _set_attribute(item, attr, x) # noqa: E731
49
-
50
- return cast(
51
- _T,
52
- to_ref_wrapper(
53
- lambda: _get_attribute(item, attr),
54
- setter,
55
- ),
56
- )
45
+ wrapper = to_ref_wrapper(
46
+ lambda: self._source.value[self._data_index.value],
47
+ base_setter,
48
+ )
49
+ wrapper._is_readonly = True
57
50
 
58
- return item
51
+ return cast(TReadonlyRef, wrapper)
59
52
 
60
53
  def update(self, index: int):
61
54
  self._data_index.value = index
@@ -73,25 +66,12 @@ class VforContainer(Element, component="vfor.js"):
73
66
  pass
74
67
 
75
68
 
76
- def _get_attribute(obj: Union[object, Mapping], name: str) -> Any:
77
- if isinstance(obj, Mapping):
78
- return obj[name]
79
- return getattr(obj, name)
80
-
81
-
82
- def _set_attribute(obj: Union[object, Mapping], name: str, value: Any) -> None:
83
- if isinstance(obj, dict):
84
- obj[name] = value
85
- else:
86
- setattr(obj, name, value)
87
-
88
-
89
69
  def _get_key_with_index(idx: int, data: Any):
90
70
  return idx
91
71
 
92
72
 
93
73
  def _get_key_with_getter(attr: str, idx: int, data: Any):
94
- return _get_attribute(data, attr)
74
+ return get_attribute(data, attr)
95
75
 
96
76
 
97
77
  class vfor(Generic[_T]):
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+ import inspect
3
+ from io import BytesIO
4
+ from ex4nicegui.utils.signals import (
5
+ RefWrapper,
6
+ TRef,
7
+ to_raw,
8
+ is_setter_ref,
9
+ to_ref_wrapper,
10
+ to_value,
11
+ is_reactive,
12
+ is_setter_ref,
13
+ to_ref_wrapper,
14
+ to_value,
15
+ )
16
+ from typing import (
17
+ Any,
18
+ Callable,
19
+ TypeVar,
20
+ Union,
21
+ cast,
22
+ Tuple,
23
+ NamedTuple,
24
+ )
25
+
26
+ from types import FrameType
27
+ import tokenize
28
+ import executing
29
+ import ast
30
+ import warnings
31
+ from .utils import get_attribute, set_attribute
32
+
33
+ _T = TypeVar("_T")
34
+
35
+
36
+ def get_caller() -> FrameType:
37
+ return inspect.currentframe().f_back.f_back # type: ignore
38
+
39
+
40
+ def get_args_code(caller: FrameType) -> str:
41
+ node = executing.Source.executing(caller).node
42
+
43
+ assert isinstance(node, ast.Call)
44
+
45
+ source = inspect.getsource(inspect.getmodule(caller)) # type: ignore
46
+ key_code = ast.get_source_segment(source, node.args[0])
47
+ return key_code # type: ignore
48
+
49
+
50
+ class CodeInfo(NamedTuple):
51
+ model: str
52
+ is_ref: bool
53
+ keys: Tuple[Union[str, int], ...] = tuple() # type: ignore
54
+
55
+
56
+ def normalize(token: tokenize.TokenInfo):
57
+ if token.type == tokenize.NUMBER:
58
+ return int(token.string)
59
+
60
+ if token.type == tokenize.STRING:
61
+ return token.string[1:-1]
62
+
63
+ return token.string
64
+
65
+
66
+ def parse_code(code: str) -> CodeInfo:
67
+ tokens = [
68
+ t
69
+ for t in tokenize.tokenize(BytesIO(code.encode("utf8")).readline)
70
+ if t.type in (tokenize.NAME, tokenize.NUMBER, tokenize.OP, tokenize.STRING)
71
+ ]
72
+
73
+ model = tokens[0].string
74
+ is_ref = (
75
+ len(tokens) >= 3
76
+ and tokens[1].type == tokenize.OP
77
+ and tokens[1].string == "."
78
+ and tokens[2].string == "value"
79
+ )
80
+
81
+ keys_search_start = 3 if is_ref else 1
82
+
83
+ keys = tuple(
84
+ normalize(token)
85
+ for token in tokens[keys_search_start:]
86
+ if token.type != tokenize.OP
87
+ )
88
+
89
+ return CodeInfo(model, is_ref, keys)
90
+
91
+
92
+ def create_writeable_wrapper(expr, ref_data, attrs: Tuple[Union[str, int], ...]):
93
+ if not attrs:
94
+
95
+ def maybe_ref_getter():
96
+ return to_value(expr)
97
+
98
+ def reactive_getter():
99
+ return to_raw(expr)
100
+
101
+ def setter(value):
102
+ pass
103
+
104
+ wrapper = to_ref_wrapper(
105
+ reactive_getter if is_reactive(expr) else maybe_ref_getter,
106
+ setter,
107
+ )
108
+ wrapper._is_readonly = False
109
+ return wrapper
110
+
111
+ def getter():
112
+ obj = get_attribute(to_value(ref_data), attrs[0])
113
+ for attr in attrs[1:]:
114
+ obj = get_attribute(obj, attr)
115
+
116
+ return obj
117
+
118
+ def setter(value):
119
+ obj = to_value(ref_data)
120
+
121
+ for attr in attrs[:-1]:
122
+ obj = get_attribute(obj, attr)
123
+
124
+ set_attribute(obj, attrs[-1], value)
125
+
126
+ wrapper = to_ref_wrapper(
127
+ getter,
128
+ setter,
129
+ )
130
+ wrapper._is_readonly = False
131
+ return wrapper
132
+
133
+
134
+ def vmodel(expr: Any, *attrs: Union[str, int]) -> TRef[Any]:
135
+ """Create a two-way binding on a form input element or a component.
136
+
137
+ @see - https://github.com/CrystalWindSnake/ex4nicegui/blob/main/README.en.md#vmodel
138
+ @中文文档 - https://gitee.com/carson_add/ex4nicegui/tree/main/#vmodel
139
+
140
+ Args:
141
+ expr (Any): _description_
142
+
143
+ ## Examples
144
+ ```python
145
+ from ex4nicegui.reactive import rxui
146
+ from ex4nicegui import deep_ref
147
+
148
+ data = deep_ref({"a": 1, "b": [1, 2, 3, 4]})
149
+
150
+ rxui.label(lambda: f"{data.value=!s}")
151
+
152
+ # No binding effect
153
+ rxui.input(value=data.value["a"])
154
+
155
+ # readonly binding
156
+ rxui.input(value=lambda: data.value["a"])
157
+
158
+ # two-way binding
159
+ rxui.input(value=rxui.vmodel(data.value["a"]))
160
+ ```
161
+
162
+ """
163
+
164
+ assert not isinstance(expr, Callable), "argument expr cannot be a function"
165
+
166
+ if isinstance(expr, RefWrapper):
167
+ expr._is_readonly = False
168
+
169
+ if is_setter_ref(expr):
170
+ if attrs:
171
+ wrapper = create_writeable_wrapper(expr, expr, attrs)
172
+
173
+ return cast(
174
+ TRef,
175
+ wrapper,
176
+ )
177
+
178
+ return cast(
179
+ TRef,
180
+ expr,
181
+ )
182
+
183
+ caller = get_caller()
184
+ code = get_args_code(caller)
185
+
186
+ info = parse_code(code)
187
+ ref_data = caller.f_locals.get(info.model) or caller.f_globals.get(info.model)
188
+ assert ref_data is not None, f"{info.model} not found"
189
+ all_attrs = (*info.keys, *attrs)
190
+
191
+ if not all_attrs:
192
+ warn_mes = ""
193
+ if is_reactive(expr) or is_setter_ref(expr):
194
+ warn_mes = rf"""Expression missing the key,result is read-only binding.Maybe you meant `{code}['key']`"""
195
+ elif not info.is_ref:
196
+ warn_mes = """Maybe you don't need to use vmodel"""
197
+ else:
198
+ warn_mes = rf"""No binding.Maybe you meant `{info.model}`"""
199
+
200
+ warnings.warn(
201
+ warn_mes,
202
+ stacklevel=2,
203
+ )
204
+
205
+ wrapper = create_writeable_wrapper(expr, ref_data, all_attrs)
206
+
207
+ return cast(
208
+ TRef,
209
+ wrapper,
210
+ )
@@ -3,6 +3,7 @@ from functools import partial
3
3
  import types
4
4
  from weakref import WeakValueDictionary
5
5
  import signe
6
+ from signe.core.protocols import ComputedResultProtocol
6
7
  from .clientScope import _CLIENT_SCOPE_MANAGER
7
8
  from typing import (
8
9
  Any,
@@ -20,23 +21,27 @@ from typing import (
20
21
  from nicegui import ui
21
22
  from .effect import effect
22
23
  from .scheduler import get_uiScheduler
24
+ import warnings
23
25
 
24
26
  T = TypeVar("T")
25
27
 
26
28
 
27
- TReadonlyRef = signe.TGetterSignal[T]
29
+ TReadonlyRef = ComputedResultProtocol[T]
28
30
  ReadonlyRef = TReadonlyRef[T]
29
31
  DescReadonlyRef = TReadonlyRef[T]
30
32
 
31
33
  TGetterOrReadonlyRef = signe.TGetter[T]
32
34
 
33
35
 
36
+ is_reactive = signe.is_reactive
37
+
38
+
34
39
  def reactive(obj: T) -> T:
35
40
  return signe.reactive(obj, get_uiScheduler())
36
41
 
37
42
 
38
43
  class RefWrapper(Generic[T]):
39
- __slot__ = ("_getter_fn", "_setter_fn")
44
+ __slot__ = ("_getter_fn", "_setter_fn", "")
40
45
 
41
46
  def __init__(
42
47
  self,
@@ -57,12 +62,17 @@ class RefWrapper(Generic[T]):
57
62
  self._getter_fn = lambda: getter_or_ref
58
63
  self._setter_fn = lambda x: None
59
64
 
65
+ self._is_readonly = False
66
+
60
67
  @property
61
68
  def value(self) -> T:
62
69
  return cast(T, self._getter_fn())
63
70
 
64
71
  @value.setter
65
72
  def value(self, new_value: T):
73
+ if self._is_readonly:
74
+ warnings.warn("readonly ref cannot be assigned.")
75
+ return
66
76
  return self._setter_fn(new_value)
67
77
 
68
78
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ex4nicegui
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Extension library based on nicegui, providing data responsive,BI functionality modules
5
5
  Home-page:
6
6
  Author: carson_jia
@@ -14,9 +14,10 @@ Classifier: Programming Language :: Python :: 3.8
14
14
  Requires-Python: >=3.8
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: signe (>=0.4.8)
17
+ Requires-Dist: signe (>=0.4.9)
18
18
  Requires-Dist: nicegui (>=1.4.0)
19
19
  Requires-Dist: typing-extensions
20
+ Requires-Dist: executing
20
21
 
21
22
  # ex4nicegui
22
23
  [中文 README](./README.md)
@@ -389,16 +390,58 @@ ui.button("change b", on_click=change_b)
389
390
 
390
391
  ---
391
392
 
393
+ ## functionality
394
+
395
+ ### vmodel
396
+ Create two-way bindings on form input elements or components.
397
+
398
+ Bidirectional bindings are supported by default for `ref` simple value types
399
+ ```python
400
+ from ex4nicegui import rxui, to_ref, deep_ref
401
+
402
+ data = to_ref("init")
403
+
404
+ rxui.label(lambda: f"{data.value=}")
405
+ # two-way binding
406
+ rxui.input(value=data)
407
+ ```
408
+ - Simple value types are generally immutable values such as `str`, `int`, etc.
409
+
410
+ When using complex data structures, `deep_ref` is used to keep nested values responsive.
411
+ ```python
412
+ data = deep_ref({"a": 1, "b": [1, 2, 3, 4]})
413
+
414
+ rxui.label(lambda: f"{data.value=!s}")
415
+
416
+ # No binding effect
417
+ rxui.input(value=data.value["a"])
418
+
419
+ # readonly binding
420
+ rxui.input(value=lambda: data.value["a"])
421
+
422
+ # two-way binding
423
+ rxui.input(value=rxui.vmodel(data.value["a"]))
424
+ ```
425
+
426
+ - The first input box will be completely unresponsive because the code is equivalent to passing in a value `1` directly
427
+ - The second input box will get read responsiveness due to the use of a function.
428
+ - The third input box, wrapped in `rxui.vmodel`, can be bi-directionally bound.
429
+
430
+ Most use `vmodel` with `vfor`, see [todo list examples](./examples/todomvc/)
431
+
432
+ ---
433
+
392
434
  ### vfor
393
435
  Render list components based on list responsive data. Each component is updated on demand. Data items support dictionaries or objects of any type
394
436
 
395
437
  ```python
396
438
  from nicegui import ui
397
439
  from ex4nicegui.reactive import rxui
398
- from ex4nicegui import to_ref, ref_computed
440
+ from ex4nicegui import deep_ref, ref_computed
441
+ from typing import Dict
399
442
 
400
443
  # refs
401
- items = to_ref(
444
+ items = deep_ref(
402
445
  [
403
446
  {"id": 1, "message": "foo", "done": False},
404
447
  {"id": 2, "message": "bar", "done": True},
@@ -414,7 +457,6 @@ def done_count_info():
414
457
  def check():
415
458
  for item in items.value:
416
459
  item["done"] = not item["done"]
417
- items.value = items.value
418
460
 
419
461
 
420
462
  # ui
@@ -423,54 +465,33 @@ ui.button("check", on_click=check)
423
465
 
424
466
 
425
467
  @rxui.vfor(items,key='id')
426
- def _(store: rxui.VforStore):
468
+ def _(store: rxui.VforStore[Dict]):
427
469
  # function to build the interface for each row of data
428
- msg_ref = store.get("message") # Get responsive object with `store.get`
470
+ item = store.get() # Get responsive object with `store.get`
471
+ mes = rxui.vmodel(item.value['message'])
429
472
 
430
473
  # Enter the content of the input box,
431
474
  # you can see the title of the radio box changes synchronously
432
475
  with ui.card():
433
- rxui.input(value=msg_ref)
434
- rxui.checkbox(text=msg_ref, value=store.get("done"))
476
+ with ui.row():
477
+ rxui.input(value=mes)
478
+ rxui.label(lambda: f"{mes.value=!s}")
479
+ rxui.checkbox(text=mes, value=rxui.vmodel(item.value['done']))
435
480
 
436
481
  ```
437
482
 
438
483
  - `rxui.vfor` decorator to custom function
439
484
  - The first argument is passed to the responsive list. Each item in the list can be a dictionary or other object (`dataclasses` etc.)
440
485
  - Second parameter `key`: In order to be able to keep track of the identity of each node, and thus reuse and reorder existing elements, you can provide a unique key for the block corresponding to each element. The default(`None`) is to use the list element index.
441
- - The custom function takes one argument. The current row's attribute can be retrieved via `store.get`, which is a responsive object.
486
+ - The custom function takes one argument. The current row's can be retrieved via `store.get`, which is a responsive object.
442
487
 
443
488
 
444
489
  > vfor are created only when new data is added.
445
490
 
446
- In the above example, you'll notice that when the checkbox is clicked, the text of the number of completed counts (`done_count_info`) doesn't change synchronously
447
-
448
- This is because responsive data changes in the `vfor` function do not affect the data source list. This is a restriction to prevent writing overly complex bi-directional data flow response logic.
449
-
450
- We should make changes to the data source list via events in the function
451
-
452
- ```python
453
- ...
454
-
455
- @rxui.vfor(items, key="id")
456
- def _(store: rxui.VforStore):
457
- msg_ref = store.get("message")
458
-
459
- def on_check_change(e):
460
- items.value[store.row_index]["done"] = e.value
461
- items.value = items.value
462
-
463
- with ui.card():
464
- rxui.input(value=msg_ref)
465
- rxui.checkbox(text=msg_ref, value=store.get("done"),on_change=on_check_change)
466
-
467
- ```
468
491
 
469
492
  ---
470
493
 
471
494
 
472
- ## functionality
473
-
474
495
  ### Bind class names
475
496
 
476
497
  All component classes provide `bind_classes` for binding `class`, supporting three different data structures.
@@ -1,4 +1,4 @@
1
- ex4nicegui/__init__.py,sha256=gp1J8Cq4-9IsL9kkzTskxFLXFLwFxp6qpYekJisFm8g,698
1
+ ex4nicegui/__init__.py,sha256=sbp4UdZz1sLg5LIpMSt87LwclMQb13_IDD9mewNPRuE,736
2
2
  ex4nicegui/bi/__init__.py,sha256=eu-2CuzzrcHCyKQOfoo87v6C9nSwFDdeLhjY0cRV13M,315
3
3
  ex4nicegui/bi/dataSource.py,sha256=sQzuQvNWBf20n9cz--1OX5gxT6RUAhwrq_xTQEpSa0o,7691
4
4
  ex4nicegui/bi/dataSourceFacade.py,sha256=6NuAyrTIk_L2Tz9IRFpuHKRQY_OwJINDzxoM6JYOc14,8186
@@ -22,8 +22,10 @@ ex4nicegui/bi/elements/ui_table.py,sha256=aKL26xbiCIQG_-KFEozXjbB4G88FdOK25k2H0k
22
22
  ex4nicegui/experimental_/__init__.py,sha256=HODL0f70HUzVrfRwUzdCwxTp_9mYr4D1nnzd8jevlMw,69
23
23
  ex4nicegui/experimental_/gridLayout/__init__.py,sha256=c9k-zykhKW3Ol6QECUoKqJW9QEuhA9xPi8s4Dm4m7SU,125
24
24
  ex4nicegui/experimental_/gridLayout/index.py,sha256=zFXuvFroo5EC1CFjt-b4hMiEy67hGP5J1GYTKH6kpUU,4737
25
- ex4nicegui/gsap/__init__.py,sha256=k9JKpx3sv3k9CnohAMnBA5COKSevrs_p7IPbApGbBeM,157
25
+ ex4nicegui/gsap/__init__.py,sha256=xBApQlq9st9exhgcSkqCzWeS6bc1eKiPJfYP25MSInw,218
26
26
  ex4nicegui/gsap/gsap.py,sha256=-1sncG2_37LYN6RosnHZAgYSI899HMj9FB6MQWUj8yw,4675
27
+ ex4nicegui/gsap/timeline.js,sha256=CB300drH7UUSfy_WDeuWHSNh3WX-vwbRtKBLL1Ak43o,1241
28
+ ex4nicegui/gsap/timeline.py,sha256=B3CvN83CzJMMFMVdZZC2UHR0l_I90amRQ0Sf-P623zo,2421
27
29
  ex4nicegui/gsap/wrapGsap.js,sha256=0Iz7qh8aA-h3svV7fW4U5k_pX7zXCcIdHDaG7NNvgLU,1045
28
30
  ex4nicegui/layout/__init__.py,sha256=YT76Ec7p4aFOGms6wc19207flBeyI6jrq7Kg_FQ2wnQ,278
29
31
  ex4nicegui/layout/gridFlex/GridFlex.js,sha256=ljkxGFucBUIPksMAT5w_35sxGogC7OzxzXnOw21Z3_k,4468
@@ -64,15 +66,16 @@ ex4nicegui/libs/gsap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
64
66
  ex4nicegui/libs/gsap/utils/matrix.js,sha256=77scrxbQZXx4ex5HkvnT9IkhMG1rQoDNp4TSYgUeYVk,15235
65
67
  ex4nicegui/libs/gsap/utils/paths.js,sha256=2SPaRHQ7zgba9cH8hGhkTYPCZdrrEhE2qhh6ECAEvSA,49314
66
68
  ex4nicegui/libs/gsap/utils/strings.js,sha256=47G9slz5ltG9mDSwrfQDtWzzdV5QJ-AIMLRMNK0VSiM,10472
67
- ex4nicegui/reactive/__init__.py,sha256=07i_FuQx4Mv_RuME9jBqUUpcDJylq8tCH6m1cE6qduY,3117
69
+ ex4nicegui/reactive/__init__.py,sha256=osdHgTwRTI2jOjHtbOpMjdQlvGiJW-1vaqcPPLhzbdQ,3339
68
70
  ex4nicegui/reactive/fileWatcher.py,sha256=gjeZhgar02f-qGQa47Tj5SMaCP_ftRtSU898XUmXl1U,1472
69
71
  ex4nicegui/reactive/local_file_picker.py,sha256=6kh7pW4RZk1gqYjxkOvTfJ-2T_KwZznr8AekcZoxthw,6171
70
72
  ex4nicegui/reactive/q_pagination.py,sha256=WcSYpB75NH3lffw3HXwZRcvnh0l6EA1Eqp5bcJKIJaw,1505
71
73
  ex4nicegui/reactive/rxui.py,sha256=gZ8ZEjGuJFKcedEZhcm4PIZguNkY-Wv5yQx80QnsBKI,31
72
74
  ex4nicegui/reactive/usePagination.py,sha256=Gop9KV780DcQK5bH4zQZ5sRpV3jOD2sRX_Bpmb1LTFI,2598
73
- ex4nicegui/reactive/utils.py,sha256=2lPWEA_Lx17J0I64TqYx6h3xKTZzmaJRgZcpKsgk1yw,2552
75
+ ex4nicegui/reactive/utils.py,sha256=GSk_eUH-iNUi18Tg7MsBVnMK5basgX6EQgbSD1zxsZw,3315
74
76
  ex4nicegui/reactive/vfor.js,sha256=OmpeLZdjV_GiG5HdglMXGBKpgrKYh3UIeugpszAVdWw,52
75
- ex4nicegui/reactive/vfor.py,sha256=UnOsarN-sY7xKn-R5BXvVZyxTQLBv6RKR0HpVzVry24,6127
77
+ ex4nicegui/reactive/vfor.py,sha256=hyvbmMJtC8usSYeq023bCL8kOrmnFw09YOS_r6GWBzk,5495
78
+ ex4nicegui/reactive/vmodel.py,sha256=FH1rXjbzD6r0MeFdf-3GDvz8JJ_qgmI7rTo8itw_StY,5147
76
79
  ex4nicegui/reactive/EChartsComponent/ECharts.js,sha256=HMmgXlDl548H4JpozDSbLWv-opvPi3OdJz_XY_MHbnY,3326
77
80
  ex4nicegui/reactive/EChartsComponent/ECharts.py,sha256=hoUZ7h-KOUKNd-1BybSX4xxv33gYuTkky70yEzv-oIA,6344
78
81
  ex4nicegui/reactive/EChartsComponent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -91,8 +94,9 @@ ex4nicegui/reactive/officials/base.py,sha256=1d6ePyz4ZiOsZYLKtCr04Aw4_9YyYl2HpCN
91
94
  ex4nicegui/reactive/officials/button.py,sha256=12FZ2cQGFsTsivSHT9YfDD_wP_D4xrpHHfKmVNNKLJE,1660
92
95
  ex4nicegui/reactive/officials/card.py,sha256=8-tBwm3xfVybolQ87i8lAYUpBV6FdaVdeSH6xu0736U,1275
93
96
  ex4nicegui/reactive/officials/checkbox.py,sha256=BcSbWuMTA2UgwOeuBTtpvzqdOivb2xbkLazfGqwuboo,1562
97
+ ex4nicegui/reactive/officials/circular_progress.py,sha256=BVLjCJtEcwa2pqGZ4nC-w-8FlkTS5IuqlL_tY6xOXNY,1787
94
98
  ex4nicegui/reactive/officials/color_picker.py,sha256=fSRICdzdH16w3BIGyAk3CtaauYqpPfUHCRc2kq-w-ZA,3402
95
- ex4nicegui/reactive/officials/column.py,sha256=3RLvVKNaDtOb8df4uS3xRfwJJPuH1ndXk_Y4Gry0Tjo,413
99
+ ex4nicegui/reactive/officials/column.py,sha256=CfoJigEdpRoGaJTY_O7wbK8cp8ufX8mYUKNRUnPWUr0,1040
96
100
  ex4nicegui/reactive/officials/date.py,sha256=jp75WgZaICnCHLS_wpF3uJI2kaGeNqmRLj3qpn5IIyg,2681
97
101
  ex4nicegui/reactive/officials/drawer.py,sha256=y9_gbDDy_6Y7mRfSB8BGawM-_g6kECM1ieZXMX2YCQo,2370
98
102
  ex4nicegui/reactive/officials/echarts.py,sha256=rBYHL6K_uAp4A4r_qacXt_bFukOMPWxdN_9xjEXG4RY,9683
@@ -103,11 +107,12 @@ ex4nicegui/reactive/officials/html.py,sha256=yasDeGMJyyGmCpJoiwmG-QszfLSOsHLYWSh
103
107
  ex4nicegui/reactive/officials/icon.py,sha256=L0yHo4Ute7PNJw2R2vn8PJGhIjreqeoZnS4p6tijmg0,1559
104
108
  ex4nicegui/reactive/officials/image.py,sha256=qtdtrjZ2OPF1mmtrL-RSgH3hNcXbRdwH9oD-RyFYVJM,1120
105
109
  ex4nicegui/reactive/officials/input.py,sha256=d338LH7tYArhUQYXJleytIOXsDieXEm4yLxOrjRbxLM,4030
110
+ ex4nicegui/reactive/officials/knob.py,sha256=ZCKXUbFQUhdsRoTADk2gqdEwmi1CeW9Jmc7AGJyV1Yg,2153
106
111
  ex4nicegui/reactive/officials/label.py,sha256=7xhvQIa-3Zm44xJIoKeMys1M6d6AMJTeE5l9THPICGo,1398
107
112
  ex4nicegui/reactive/officials/linear_progress.py,sha256=3ew4uqUIDrDH0PdX42K7rnKQL4khmZgxmJfmWX6qMDs,2128
108
- ex4nicegui/reactive/officials/number.py,sha256=DUb9mQy2KpVLKkdIOXqNHRz30WA48wWVip4zC_OXADQ,2216
113
+ ex4nicegui/reactive/officials/number.py,sha256=Ro20LiSjzeLkcPM5oIe3phulLH5Gsw3YwYBCyRl9bqI,2201
109
114
  ex4nicegui/reactive/officials/radio.py,sha256=35KCAE0JhzBczaCGrlufZystgpzljobOoIw1H7js1KA,1929
110
- ex4nicegui/reactive/officials/row.py,sha256=ZBPITfHbJmAdAWuIZFl2H1XFS9pJam57PJ_zDZWhueE,404
115
+ ex4nicegui/reactive/officials/row.py,sha256=pwFJBdv3_D5-E-oga-OG5t57ahHcYXGSI_-UofWg_j0,1029
111
116
  ex4nicegui/reactive/officials/select.py,sha256=Fj4V96ju2tpsP1ok0RFokN1K7TxzNS9YLVCz55O788Y,2980
112
117
  ex4nicegui/reactive/officials/slider.py,sha256=6E6UeyX_hXIDz_-0v6WEIfTM5oqzukonRyqDRJHT-PA,2954
113
118
  ex4nicegui/reactive/officials/switch.py,sha256=-QDXbOQllnWRE28pAH74qrgpN1TXs-1snLDqN6pdiVM,1605
@@ -126,9 +131,9 @@ ex4nicegui/utils/clientScope.py,sha256=-KJnrZGqdAQUNNbQbqMAqNzWHCVon-lC3aaQGuaTd
126
131
  ex4nicegui/utils/common.py,sha256=7P0vboDadLun6EMxNi3br9rKJgKt0QT4sy_66cHEwb4,994
127
132
  ex4nicegui/utils/effect.py,sha256=MgvWuAP3OFs2bR4ef6uXPwGCkKORUK-4hmx1oSwl04Y,2310
128
133
  ex4nicegui/utils/scheduler.py,sha256=Wa963Df3UDvWHjXXoVYGIBevIILzCFoz-yAWjvxeyfQ,1218
129
- ex4nicegui/utils/signals.py,sha256=nzjw0uy9fnUUc4aol1dzweH3F4uGhNSZspW1iTZ-TBw,10924
130
- ex4nicegui-0.5.2.dist-info/LICENSE,sha256=0KDDElS2dl-HIsWvbpy8ywbLzJMBFzXLev57LnMIZXs,1094
131
- ex4nicegui-0.5.2.dist-info/METADATA,sha256=2Me59obBt8Ee7791n8OU-jwwBVMrJT9s4JVbsfu6WW4,26040
132
- ex4nicegui-0.5.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
133
- ex4nicegui-0.5.2.dist-info/top_level.txt,sha256=VFwMiO9AFjj5rfLMJwN1ipLRASk9fJXB8tM6DNrpvPQ,11
134
- ex4nicegui-0.5.2.dist-info/RECORD,,
134
+ ex4nicegui/utils/signals.py,sha256=Eruq0WUH-G7dpnfHZ2y9Uf-mS6L_RBVcsaqbK8FKfvE,11193
135
+ ex4nicegui-0.5.3.dist-info/LICENSE,sha256=0KDDElS2dl-HIsWvbpy8ywbLzJMBFzXLev57LnMIZXs,1094
136
+ ex4nicegui-0.5.3.dist-info/METADATA,sha256=hGb8ZKAD6yb90SRQ6tALuCBNwsGaPwbJFK6rGsWboG0,26519
137
+ ex4nicegui-0.5.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
138
+ ex4nicegui-0.5.3.dist-info/top_level.txt,sha256=VFwMiO9AFjj5rfLMJwN1ipLRASk9fJXB8tM6DNrpvPQ,11
139
+ ex4nicegui-0.5.3.dist-info/RECORD,,