ex4nicegui 0.2.18__py3-none-any.whl → 0.3.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.
- ex4nicegui/__init__.py +1 -1
- ex4nicegui/bi/__init__.py +3 -0
- ex4nicegui/bi/dataSource.py +174 -0
- ex4nicegui/bi/dataSourceFacade.py +263 -0
- ex4nicegui/bi/index.py +76 -0
- ex4nicegui/bi/protocols.py +111 -0
- ex4nicegui/bi/types.py +12 -0
- ex4nicegui/reactive/EChartsComponent/ECharts.js +1 -2
- ex4nicegui/reactive/officials/echarts.py +1 -1
- ex4nicegui/utils/clientScope.py +42 -0
- ex4nicegui/utils/signals.py +50 -5
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.0.dist-info}/METADATA +19 -19
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.0.dist-info}/RECORD +16 -9
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.0.dist-info}/WHEEL +1 -1
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.0.dist-info}/LICENSE +0 -0
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.0.dist-info}/top_level.txt +0 -0
ex4nicegui/__init__.py
CHANGED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, cast
|
|
2
|
+
from ex4nicegui import to_ref, ref_computed, on
|
|
3
|
+
from nicegui import globals, Client
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from . import types
|
|
7
|
+
from .protocols import IDataSourceAble
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DataSourceInfo:
|
|
12
|
+
source: "DataSource"
|
|
13
|
+
update_callback: types._TSourceBuildFn
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Filter:
|
|
18
|
+
callback: types._TFilterCallback
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ComponentInfoKey:
|
|
23
|
+
client_id: types._TNgClientID
|
|
24
|
+
element_id: types._TElementID
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ComponentInfo:
|
|
29
|
+
key: ComponentInfoKey
|
|
30
|
+
update_callback: types._TComponentUpdateCallback
|
|
31
|
+
filter: Optional[Filter] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ComponentMap:
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._client_map: Dict[
|
|
37
|
+
types._TNgClientID, Dict[types._TElementID, ComponentInfo]
|
|
38
|
+
] = {}
|
|
39
|
+
|
|
40
|
+
def has_client_record(self, client_id: types._TNgClientID):
|
|
41
|
+
return client_id in self._client_map
|
|
42
|
+
|
|
43
|
+
def add_info(self, info: ComponentInfo):
|
|
44
|
+
client_id = info.key.client_id
|
|
45
|
+
element_id = info.key.element_id
|
|
46
|
+
|
|
47
|
+
if client_id not in self._client_map:
|
|
48
|
+
self._client_map[client_id] = {element_id: info}
|
|
49
|
+
|
|
50
|
+
element_map = self._client_map[client_id]
|
|
51
|
+
|
|
52
|
+
if element_id not in element_map:
|
|
53
|
+
element_map[element_id] = info
|
|
54
|
+
|
|
55
|
+
def remove_client(self, client_id: types._TNgClientID):
|
|
56
|
+
if client_id in self._client_map:
|
|
57
|
+
del self._client_map[client_id]
|
|
58
|
+
|
|
59
|
+
def has_record(self, client_id: types._TNgClientID, element_id: types._TElementID):
|
|
60
|
+
return (
|
|
61
|
+
client_id in self._client_map and element_id in self._client_map[client_id]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def set_filter(
|
|
65
|
+
self,
|
|
66
|
+
client_id: types._TNgClientID,
|
|
67
|
+
element_id: types._TElementID,
|
|
68
|
+
filter: Filter,
|
|
69
|
+
):
|
|
70
|
+
self._client_map[client_id][element_id].filter = filter
|
|
71
|
+
|
|
72
|
+
def get_all_info(self):
|
|
73
|
+
return (
|
|
74
|
+
info for ele_map in self._client_map.values() for info in ele_map.values()
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DataSource:
|
|
79
|
+
_global_id_count: types._TDataSourceId = 0
|
|
80
|
+
|
|
81
|
+
def __init__(self, data: IDataSourceAble) -> None:
|
|
82
|
+
DataSource._global_id_count += 1
|
|
83
|
+
self.__id = DataSource._global_id_count
|
|
84
|
+
|
|
85
|
+
self._idataSource = data
|
|
86
|
+
|
|
87
|
+
data_fn = lambda: data.get_data()
|
|
88
|
+
data_fn = ref_computed(data_fn)
|
|
89
|
+
|
|
90
|
+
self.__filters = to_ref(cast(List[Filter], []))
|
|
91
|
+
|
|
92
|
+
@ref_computed
|
|
93
|
+
def apply_filters():
|
|
94
|
+
df = data_fn.value
|
|
95
|
+
for f in self.__filters.value:
|
|
96
|
+
df = f.callback(df)
|
|
97
|
+
|
|
98
|
+
return df
|
|
99
|
+
|
|
100
|
+
self.__filtered_data = apply_filters
|
|
101
|
+
|
|
102
|
+
self.__data = data_fn
|
|
103
|
+
self._component_map = ComponentMap()
|
|
104
|
+
|
|
105
|
+
@on(data_fn)
|
|
106
|
+
def _():
|
|
107
|
+
self.__notify_update()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def data(self):
|
|
111
|
+
return self.__data.value
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def filtered_data(self):
|
|
115
|
+
return self.__filtered_data.value
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def id(self):
|
|
119
|
+
return self.__id
|
|
120
|
+
|
|
121
|
+
def _register_component(
|
|
122
|
+
self,
|
|
123
|
+
element_id: types._TElementID,
|
|
124
|
+
update_callback: types._TComponentUpdateCallback,
|
|
125
|
+
):
|
|
126
|
+
ng_client = globals.get_client()
|
|
127
|
+
client_id = ng_client.id
|
|
128
|
+
|
|
129
|
+
if not self._component_map.has_client_record(client_id):
|
|
130
|
+
|
|
131
|
+
@ng_client.on_disconnect
|
|
132
|
+
def _(e: Client):
|
|
133
|
+
if not e.shared:
|
|
134
|
+
self._component_map.remove_client(e.id)
|
|
135
|
+
|
|
136
|
+
self._component_map.add_info(
|
|
137
|
+
ComponentInfo(ComponentInfoKey(client_id, element_id), update_callback)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def send_filter(self, element_id: types._TElementID, filter: Filter):
|
|
143
|
+
client_id = globals.get_client().id
|
|
144
|
+
|
|
145
|
+
if not self._component_map.has_record(client_id, element_id):
|
|
146
|
+
raise ValueError("element not register")
|
|
147
|
+
|
|
148
|
+
self._component_map.set_filter(client_id, element_id, filter)
|
|
149
|
+
|
|
150
|
+
self.__notify_update([ComponentInfoKey(client_id, element_id)])
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def __notify_update(self, ignore_keys: Optional[List[ComponentInfoKey]] = None):
|
|
154
|
+
ignore_keys = ignore_keys or []
|
|
155
|
+
ignore_ids_set = set(ignore_keys)
|
|
156
|
+
|
|
157
|
+
# nodify every component
|
|
158
|
+
for current_info in self._component_map.get_all_info():
|
|
159
|
+
if current_info.key in ignore_ids_set:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# apply filters ,except current target
|
|
163
|
+
filters = [
|
|
164
|
+
info.filter.callback
|
|
165
|
+
for info in self._component_map.get_all_info()
|
|
166
|
+
if (info.key != current_info.key) and info.filter
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
new_data = self._idataSource.apply_filters(self.__data.value, filters)
|
|
170
|
+
current_info.update_callback(new_data)
|
|
171
|
+
|
|
172
|
+
self.__filters.value = [
|
|
173
|
+
info.filter for info in self._component_map.get_all_info() if info.filter
|
|
174
|
+
]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable, Dict, TypeVar, Generic, Union, cast
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
from ex4nicegui import ref_computed
|
|
5
|
+
from ex4nicegui.reactive import rxui
|
|
6
|
+
from .dataSource import DataSource, Filter
|
|
7
|
+
from ex4nicegui.reactive.EChartsComponent.ECharts import echarts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_TData = TypeVar("_TData")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataSourceFacade(Generic[_TData]):
|
|
14
|
+
def __init__(self, ds: DataSource) -> None:
|
|
15
|
+
self._dataSource = ds
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def data(self) -> _TData:
|
|
19
|
+
"""Data without any filtering"""
|
|
20
|
+
return cast(_TData, self._dataSource.data)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def filtered_data(self) -> _TData:
|
|
24
|
+
"""Data after filtering"""
|
|
25
|
+
return cast(_TData, self._dataSource.filtered_data)
|
|
26
|
+
|
|
27
|
+
def ui_select(
|
|
28
|
+
self, column: str, *, clearable=True, multiple=True, **kwargs
|
|
29
|
+
) -> ui.select:
|
|
30
|
+
"""
|
|
31
|
+
Creates a user interface select box.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
column (str): The column name of the data source.
|
|
35
|
+
clearable (bool, optional): Whether to allow clearing the content of the select box. Default is True.
|
|
36
|
+
multiple (bool, optional): Whether to allow multiple selections.
|
|
37
|
+
**kwargs: Additional optional parameters that will be passed to the ui.select constructor.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ui.select: An instance of a user interface select box.
|
|
41
|
+
"""
|
|
42
|
+
options = self._dataSource._idataSource.duplicates_column_values(
|
|
43
|
+
self.data, column
|
|
44
|
+
)
|
|
45
|
+
kwargs.update(
|
|
46
|
+
{
|
|
47
|
+
"options": options,
|
|
48
|
+
"multiple": multiple,
|
|
49
|
+
"clearable": clearable,
|
|
50
|
+
"label": column,
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
cp = ui.select(**kwargs).props("use-chips outlined")
|
|
55
|
+
|
|
56
|
+
def onchange(e):
|
|
57
|
+
value = None
|
|
58
|
+
if e.args:
|
|
59
|
+
if isinstance(e.args, list):
|
|
60
|
+
value = [arg["label"] for arg in e.args]
|
|
61
|
+
else:
|
|
62
|
+
value = e.args["label"]
|
|
63
|
+
|
|
64
|
+
cp.value = value
|
|
65
|
+
|
|
66
|
+
def data_filter(data):
|
|
67
|
+
if cp.value is None or not cp.value:
|
|
68
|
+
return data
|
|
69
|
+
|
|
70
|
+
cond = None
|
|
71
|
+
if isinstance(cp.value, list):
|
|
72
|
+
cond = data[column].isin(cp.value)
|
|
73
|
+
else:
|
|
74
|
+
cond = data[column] == cp.value
|
|
75
|
+
return data[cond]
|
|
76
|
+
|
|
77
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
78
|
+
|
|
79
|
+
cp.on("update:modelValue", onchange)
|
|
80
|
+
|
|
81
|
+
def on_source_update(data):
|
|
82
|
+
options = self._dataSource._idataSource.duplicates_column_values(
|
|
83
|
+
data, column
|
|
84
|
+
)
|
|
85
|
+
value = cp.value
|
|
86
|
+
|
|
87
|
+
# Make the value within the options
|
|
88
|
+
if isinstance(value, list):
|
|
89
|
+
value = list(set(value) & set(options))
|
|
90
|
+
else:
|
|
91
|
+
if value not in options:
|
|
92
|
+
value = ""
|
|
93
|
+
|
|
94
|
+
cp.set_options(options, value=value)
|
|
95
|
+
|
|
96
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
97
|
+
|
|
98
|
+
return cp
|
|
99
|
+
|
|
100
|
+
def ui_aggrid(self, **kwargs):
|
|
101
|
+
"""
|
|
102
|
+
Creates aggrid table.
|
|
103
|
+
|
|
104
|
+
Parameters:
|
|
105
|
+
**kwargs: Additional optional parameters that will be passed to the ui.aggrid constructor.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
ui.aggrid: aggrid table.
|
|
109
|
+
"""
|
|
110
|
+
kwargs.update(
|
|
111
|
+
{"options": self._dataSource._idataSource.get_aggrid_options(self.data)}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
cp = ui.aggrid(**kwargs)
|
|
115
|
+
|
|
116
|
+
def on_source_update(data):
|
|
117
|
+
cp._props["options"] = self._dataSource._idataSource.get_aggrid_options(
|
|
118
|
+
data
|
|
119
|
+
)
|
|
120
|
+
cp.update()
|
|
121
|
+
|
|
122
|
+
on_source_update(self.filtered_data)
|
|
123
|
+
|
|
124
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
125
|
+
|
|
126
|
+
return cp
|
|
127
|
+
|
|
128
|
+
def ui_radio(self, column: str, **kwargs):
|
|
129
|
+
"""
|
|
130
|
+
Creates radio Selection.
|
|
131
|
+
|
|
132
|
+
Parameters:
|
|
133
|
+
column (str): The column name of the data source.
|
|
134
|
+
**kwargs: Additional optional parameters that will be passed to the ui.radio constructor.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
ui.radio: An radio Selection.
|
|
138
|
+
"""
|
|
139
|
+
options = self._dataSource._idataSource.duplicates_column_values(
|
|
140
|
+
self.data, column
|
|
141
|
+
)
|
|
142
|
+
kwargs.update({"options": options})
|
|
143
|
+
|
|
144
|
+
cp = ui.radio(**kwargs)
|
|
145
|
+
|
|
146
|
+
def onchange(e):
|
|
147
|
+
cp.value = cp.options[e.args]
|
|
148
|
+
|
|
149
|
+
def data_filter(data):
|
|
150
|
+
if cp.value not in cp.options:
|
|
151
|
+
return data
|
|
152
|
+
cond = data[column] == cp.value
|
|
153
|
+
return data[cond]
|
|
154
|
+
|
|
155
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
156
|
+
|
|
157
|
+
cp.on("update:modelValue", onchange)
|
|
158
|
+
|
|
159
|
+
def on_source_update(data):
|
|
160
|
+
options = self._dataSource._idataSource.duplicates_column_values(
|
|
161
|
+
data, column
|
|
162
|
+
)
|
|
163
|
+
value = cp.value
|
|
164
|
+
if value not in options:
|
|
165
|
+
value = ""
|
|
166
|
+
|
|
167
|
+
cp.set_options(options, value=value)
|
|
168
|
+
|
|
169
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
170
|
+
|
|
171
|
+
return cp
|
|
172
|
+
|
|
173
|
+
def ui_slider(self, column: str, **kwargs):
|
|
174
|
+
"""
|
|
175
|
+
Creates Slider.
|
|
176
|
+
|
|
177
|
+
Parameters:
|
|
178
|
+
column (str): The column name of the data source.
|
|
179
|
+
**kwargs: Additional optional parameters that will be passed to the ui.slider constructor.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
ui.radio: An Slider.
|
|
183
|
+
"""
|
|
184
|
+
self._dataSource._idataSource.slider_check(self.data, column)
|
|
185
|
+
|
|
186
|
+
min, max = self._dataSource._idataSource.slider_min_max(self.data, column)
|
|
187
|
+
kwargs.update({"min": min, "max": max})
|
|
188
|
+
|
|
189
|
+
cp = ui.slider(**kwargs).props("label label-always switch-label-side")
|
|
190
|
+
|
|
191
|
+
def onchange():
|
|
192
|
+
def data_filter(data):
|
|
193
|
+
if cp.value is None or cp.value < min:
|
|
194
|
+
return data
|
|
195
|
+
cond = data[column] == cp.value
|
|
196
|
+
return data[cond]
|
|
197
|
+
|
|
198
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
199
|
+
|
|
200
|
+
cp.on("change", onchange)
|
|
201
|
+
|
|
202
|
+
def on_source_update(data):
|
|
203
|
+
min, max = self._dataSource._idataSource.slider_min_max(data, column)
|
|
204
|
+
if min is None or max is None:
|
|
205
|
+
cp.value = None
|
|
206
|
+
else:
|
|
207
|
+
cp._props["min"] = min
|
|
208
|
+
cp._props["max"] = max
|
|
209
|
+
|
|
210
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
211
|
+
|
|
212
|
+
return cp
|
|
213
|
+
|
|
214
|
+
def ui_echarts(
|
|
215
|
+
self, fn: Callable[[Any], Union[Dict, "pyecharts.Base"]] # pyright: ignore
|
|
216
|
+
) -> echarts:
|
|
217
|
+
"""Create charts
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
fn (Callable[[Any], Union[Dict, "pyecharts.Base"]]): builder function.
|
|
221
|
+
|
|
222
|
+
## Examples
|
|
223
|
+
|
|
224
|
+
Support pyecharts
|
|
225
|
+
|
|
226
|
+
```py
|
|
227
|
+
import pandas as pd
|
|
228
|
+
from ex4nicegui import bi
|
|
229
|
+
from pyecharts.charts import Bar
|
|
230
|
+
|
|
231
|
+
df = pd.DataFrame({"name": list("abcdc"), "value": range(5)})
|
|
232
|
+
ds = bi.data_source(df)
|
|
233
|
+
|
|
234
|
+
@ds.ui_echarts
|
|
235
|
+
def bar(data: pd.DataFrame):
|
|
236
|
+
c = (
|
|
237
|
+
Bar()
|
|
238
|
+
.add_xaxis(data["name"].tolist())
|
|
239
|
+
.add_yaxis("value", data["value"].tolist())
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return c
|
|
243
|
+
|
|
244
|
+
bar.classes("h-[20rem]")
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
@ref_computed
|
|
250
|
+
def chart_options():
|
|
251
|
+
options = fn(self.filtered_data)
|
|
252
|
+
if isinstance(options, Dict):
|
|
253
|
+
return options
|
|
254
|
+
|
|
255
|
+
import simplejson as json
|
|
256
|
+
from pyecharts.charts.chart import Base
|
|
257
|
+
|
|
258
|
+
if isinstance(options, Base):
|
|
259
|
+
return cast(Dict, json.loads(options.dump_options()))
|
|
260
|
+
|
|
261
|
+
cp = rxui.echarts(chart_options) # type: ignore
|
|
262
|
+
|
|
263
|
+
return cp.element # type: ignore
|
ex4nicegui/bi/index.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from .dataSourceFacade import DataSourceFacade
|
|
2
|
+
from .dataSource import DataSource
|
|
3
|
+
from .protocols import CallableDataSourceAble, DataFrameDataSourceAble
|
|
4
|
+
from typing import Callable, cast, TypeVar, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_TData = TypeVar("_TData")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def data_source(data: Union[Callable[..., _TData], _TData]) -> DataSourceFacade[_TData]:
|
|
11
|
+
"""Create a data source
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
data (_TData): Any supported data
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
TypeError: Throw an error when the data type is not supported
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
DataSourceFacade[_TData]: _description_
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
pandas dataframe source:
|
|
27
|
+
```python
|
|
28
|
+
df = pd.DataFrame({"name": list("abcdc"), "value": range(5)})
|
|
29
|
+
ds = bi.data_source(df)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
Link multiple data sources
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
```python
|
|
38
|
+
df = pd.DataFrame({"name": list("abcdc"), "value": range(5)})
|
|
39
|
+
ds = bi.data_source(df)
|
|
40
|
+
|
|
41
|
+
@bi.data_source
|
|
42
|
+
def ds_other():
|
|
43
|
+
# ds.filtered_data is DataFrame after filtering
|
|
44
|
+
where = ds.filtered_data[''].isin(['b','c','d'])
|
|
45
|
+
return ds.filtered_data[where]
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
Now, when `ds` changes, it will trigger changes to `ds_other` and thus drive the related interface components to change.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# select box of data source 'ds'
|
|
55
|
+
# it change will trigger changes to table
|
|
56
|
+
ds.ui_select('name')
|
|
57
|
+
|
|
58
|
+
# table of data 'ds_other'
|
|
59
|
+
ds_other.ui_aggrid()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
ds_protocol = None
|
|
65
|
+
|
|
66
|
+
if type(data).__name__ == "DataFrame":
|
|
67
|
+
ds_protocol = DataFrameDataSourceAble(data)
|
|
68
|
+
|
|
69
|
+
if isinstance(data, Callable):
|
|
70
|
+
ds_protocol = CallableDataSourceAble(data)
|
|
71
|
+
|
|
72
|
+
if ds_protocol is None:
|
|
73
|
+
raise TypeError(f"not support type[{type(data)}]")
|
|
74
|
+
|
|
75
|
+
ds = DataSource(ds_protocol)
|
|
76
|
+
return cast(DataSourceFacade[_TData], DataSourceFacade(ds))
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing_extensions import Protocol
|
|
2
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from ex4nicegui.bi.types import _TFilterCallback
|
|
5
|
+
from .types import _TFilterCallback
|
|
6
|
+
from ex4nicegui.utils import common as utils_common
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IDataSourceAble(Protocol):
|
|
10
|
+
def get_data(self):
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
def apply_filters(self, data, filters: List[_TFilterCallback]):
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
def duplicates_column_values(self, data, column_name: str) -> List:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def get_aggrid_options(self, data) -> Dict:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def slider_check(self, data, column_name: str) -> None:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def slider_min_max(
|
|
26
|
+
self, data, column_name: str
|
|
27
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DataFrameDataSourceAble(IDataSourceAble):
|
|
32
|
+
def __init__(self, df) -> None:
|
|
33
|
+
self.data = df
|
|
34
|
+
|
|
35
|
+
def get_data(self):
|
|
36
|
+
return self.data
|
|
37
|
+
|
|
38
|
+
def apply_filters(self, data, filters: List[_TFilterCallback]):
|
|
39
|
+
new_data = data
|
|
40
|
+
for f in filters:
|
|
41
|
+
new_data = f(new_data)
|
|
42
|
+
|
|
43
|
+
return new_data
|
|
44
|
+
|
|
45
|
+
def duplicates_column_values(self, data, column_name: str) -> List:
|
|
46
|
+
return data[column_name].drop_duplicates().tolist()
|
|
47
|
+
|
|
48
|
+
def get_aggrid_options(self, data) -> Dict:
|
|
49
|
+
df = utils_common.convert_dataframe(data)
|
|
50
|
+
return {
|
|
51
|
+
"columnDefs": [{"field": col} for col in df.columns],
|
|
52
|
+
"rowData": df.to_dict("records"),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def slider_check(self, data, column_name: str) -> None:
|
|
56
|
+
from pandas.api.types import is_numeric_dtype
|
|
57
|
+
|
|
58
|
+
if not is_numeric_dtype(data[column_name]):
|
|
59
|
+
raise ValueError(f"column[{column_name}] must be numeric type")
|
|
60
|
+
|
|
61
|
+
def slider_min_max(
|
|
62
|
+
self, data, column_name: str
|
|
63
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
64
|
+
import numpy as np
|
|
65
|
+
|
|
66
|
+
min, max = data[column_name].min(), data[column_name].max()
|
|
67
|
+
|
|
68
|
+
if np.isnan(min) or np.isnan(max):
|
|
69
|
+
return None, None
|
|
70
|
+
|
|
71
|
+
return min, max
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CallableDataSourceAble(IDataSourceAble):
|
|
75
|
+
def __init__(self, fn: Callable) -> None:
|
|
76
|
+
self.data_fn = fn
|
|
77
|
+
|
|
78
|
+
def get_data(self):
|
|
79
|
+
return self.data_fn()
|
|
80
|
+
|
|
81
|
+
def apply_filters(self, data, filters: List[_TFilterCallback]):
|
|
82
|
+
new_data = data
|
|
83
|
+
for f in filters:
|
|
84
|
+
new_data = f(new_data)
|
|
85
|
+
|
|
86
|
+
return new_data
|
|
87
|
+
|
|
88
|
+
def duplicates_column_values(self, data, column_name: str) -> List:
|
|
89
|
+
return data[column_name].drop_duplicates().tolist()
|
|
90
|
+
|
|
91
|
+
def get_aggrid_options(self, data) -> Dict:
|
|
92
|
+
df = data
|
|
93
|
+
return {
|
|
94
|
+
"columnDefs": [{"field": col} for col in df.columns],
|
|
95
|
+
"rowData": df.to_dict("records"),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def slider_check(self, data, column_name: str) -> None:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def slider_min_max(
|
|
102
|
+
self, data, column_name: str
|
|
103
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
104
|
+
import numpy as np
|
|
105
|
+
|
|
106
|
+
min, max = data[column_name].min(), data[column_name].max()
|
|
107
|
+
|
|
108
|
+
if np.isnan(min) or np.isnan(max):
|
|
109
|
+
return None, None
|
|
110
|
+
|
|
111
|
+
return min, max
|
ex4nicegui/bi/types.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Callable, TypeVar
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_TData = TypeVar("_TData")
|
|
5
|
+
|
|
6
|
+
_TElementID = int
|
|
7
|
+
_TNgClientID = str
|
|
8
|
+
_TComponentUpdateCallback = Callable[[_TData], None]
|
|
9
|
+
_TFilterCallback = Callable[[_TData], _TData]
|
|
10
|
+
|
|
11
|
+
_TDataSourceId = int
|
|
12
|
+
_TSourceBuildFn = Callable[..., _TData]
|
|
@@ -43967,8 +43967,7 @@ const TX = { class: "echart-container relative" }, CX = /* @__PURE__ */ GL({
|
|
|
43967
43967
|
l.target || t("chartClickBlank");
|
|
43968
43968
|
});
|
|
43969
43969
|
function s() {
|
|
43970
|
-
|
|
43971
|
-
i == null || i.resize({ width: l.width - 5, height: l.height - 5 });
|
|
43970
|
+
i == null || i.resize();
|
|
43972
43971
|
}
|
|
43973
43972
|
ZL("resize", () => {
|
|
43974
43973
|
s();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
from signe.core.effect import Effect
|
|
3
|
+
from signe.core.scope import IScope
|
|
4
|
+
from nicegui import globals as ng_globals, Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_TClientID = str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NgClientScope(IScope):
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._effects: List[Effect] = []
|
|
13
|
+
|
|
14
|
+
def add_effect(self, effect: Effect):
|
|
15
|
+
self._effects.append(effect)
|
|
16
|
+
|
|
17
|
+
def dispose(self):
|
|
18
|
+
for effect in self._effects:
|
|
19
|
+
effect.dispose()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NgClientScopeManager:
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._client_scope_map: Dict[_TClientID, NgClientScope] = {}
|
|
25
|
+
|
|
26
|
+
def get_scope(self):
|
|
27
|
+
if len(ng_globals.get_slot_stack()) <= 0:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
client = ng_globals.get_client()
|
|
31
|
+
if client.shared:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if client.id not in self._client_scope_map:
|
|
35
|
+
self._client_scope_map[client.id] = NgClientScope()
|
|
36
|
+
|
|
37
|
+
@client.on_disconnect
|
|
38
|
+
def _(e: Client):
|
|
39
|
+
if e.id in self._client_scope_map:
|
|
40
|
+
self._client_scope_map[e.id].dispose()
|
|
41
|
+
|
|
42
|
+
return self._client_scope_map[client.id]
|
ex4nicegui/utils/signals.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
from signe import createSignal, effect, computed, on as signe_on
|
|
1
|
+
from signe import createSignal, effect as signe_effect, computed, on as signe_on
|
|
2
2
|
from signe.core.signal import Signal, SignalOption
|
|
3
|
+
from signe.core.effect import Effect
|
|
3
4
|
from signe import utils as signe_utils
|
|
5
|
+
from .clientScope import NgClientScopeManager
|
|
4
6
|
from signe.types import TSetter, TGetter
|
|
5
7
|
from typing import (
|
|
6
8
|
Any,
|
|
9
|
+
Dict,
|
|
7
10
|
TypeVar,
|
|
8
11
|
Generic,
|
|
9
12
|
overload,
|
|
@@ -17,6 +20,8 @@ from nicegui import ui
|
|
|
17
20
|
|
|
18
21
|
T = TypeVar("T")
|
|
19
22
|
|
|
23
|
+
_CLIENT_SCOPE_MANAGER = NgClientScopeManager()
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
class ReadonlyRef(Generic[T]):
|
|
22
27
|
def __init__(self, getter: TGetter[T]) -> None:
|
|
@@ -115,6 +120,39 @@ def ref(value: T):
|
|
|
115
120
|
return cast(Ref[T], Ref(s.getValue, s.setValue, s))
|
|
116
121
|
|
|
117
122
|
|
|
123
|
+
@overload
|
|
124
|
+
def effect(
|
|
125
|
+
fn: None = ...,
|
|
126
|
+
*,
|
|
127
|
+
priority_level=1,
|
|
128
|
+
debug_trigger: Optional[Callable] = None,
|
|
129
|
+
) -> signe_utils._TEffect_Fn[None]:
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@overload
|
|
134
|
+
def effect(
|
|
135
|
+
fn: Callable[..., None],
|
|
136
|
+
*,
|
|
137
|
+
priority_level=1,
|
|
138
|
+
debug_trigger: Optional[Callable] = None,
|
|
139
|
+
) -> Effect[None]:
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def effect(
|
|
144
|
+
fn: Optional[Callable[..., None]] = None,
|
|
145
|
+
*,
|
|
146
|
+
priority_level=1,
|
|
147
|
+
debug_trigger: Optional[Callable] = None,
|
|
148
|
+
) -> Union[signe_utils._TEffect_Fn[None], Effect[None]]:
|
|
149
|
+
kws = {
|
|
150
|
+
"debug_trigger": debug_trigger,
|
|
151
|
+
"priority_level": priority_level,
|
|
152
|
+
}
|
|
153
|
+
return signe_effect(fn, **kws, scope=_CLIENT_SCOPE_MANAGER.get_scope())
|
|
154
|
+
|
|
155
|
+
|
|
118
156
|
@overload
|
|
119
157
|
def ref_computed(
|
|
120
158
|
fn: Callable[[], T],
|
|
@@ -122,6 +160,7 @@ def ref_computed(
|
|
|
122
160
|
desc="",
|
|
123
161
|
debug_trigger: Optional[Callable[..., None]] = None,
|
|
124
162
|
priority_level: int = 1,
|
|
163
|
+
debug_name: Optional[str] = None,
|
|
125
164
|
) -> ReadonlyRef[T]:
|
|
126
165
|
...
|
|
127
166
|
|
|
@@ -133,6 +172,7 @@ def ref_computed(
|
|
|
133
172
|
desc="",
|
|
134
173
|
debug_trigger: Optional[Callable[..., None]] = None,
|
|
135
174
|
priority_level: int = 1,
|
|
175
|
+
debug_name: Optional[str] = None,
|
|
136
176
|
) -> Callable[[Callable[..., T]], ReadonlyRef[T]]:
|
|
137
177
|
...
|
|
138
178
|
|
|
@@ -143,14 +183,16 @@ def ref_computed(
|
|
|
143
183
|
desc="",
|
|
144
184
|
debug_trigger: Optional[Callable[..., None]] = None,
|
|
145
185
|
priority_level: int = 1,
|
|
186
|
+
debug_name: Optional[str] = None,
|
|
146
187
|
) -> Union[ReadonlyRef[T], Callable[[Callable[..., T]], ReadonlyRef[T]]]:
|
|
147
188
|
kws = {
|
|
148
189
|
"debug_trigger": debug_trigger,
|
|
149
190
|
"priority_level": priority_level,
|
|
191
|
+
"debug_name": debug_name,
|
|
150
192
|
}
|
|
151
193
|
|
|
152
194
|
if fn:
|
|
153
|
-
getter = computed(fn, **kws)
|
|
195
|
+
getter = computed(fn, **kws, scope=_CLIENT_SCOPE_MANAGER.get_scope())
|
|
154
196
|
return cast(DescReadonlyRef[T], DescReadonlyRef(getter, desc))
|
|
155
197
|
else:
|
|
156
198
|
|
|
@@ -192,20 +234,23 @@ class effect_refreshable:
|
|
|
192
234
|
re_func.refresh()
|
|
193
235
|
|
|
194
236
|
if len(self._refs) == 0:
|
|
195
|
-
runner =
|
|
237
|
+
runner = signe_effect(runner)
|
|
196
238
|
else:
|
|
197
239
|
runner = on(self._refs)(runner)
|
|
198
240
|
|
|
199
241
|
return runner
|
|
200
242
|
|
|
201
243
|
|
|
202
|
-
def on(
|
|
244
|
+
def on(
|
|
245
|
+
refs: Union[ReadonlyRef, Sequence[ReadonlyRef]],
|
|
246
|
+
effect_kws: Optional[Dict[str, Any]] = None,
|
|
247
|
+
):
|
|
203
248
|
if not isinstance(refs, Sequence):
|
|
204
249
|
refs = [refs]
|
|
205
250
|
|
|
206
251
|
getters = [getattr(r, "_ReadonlyRef___getter") for r in refs]
|
|
207
252
|
|
|
208
253
|
def wrap(fn: Callable):
|
|
209
|
-
return signe_on(getters, fn)
|
|
254
|
+
return signe_on(getters, fn, effect_kws=effect_kws)
|
|
210
255
|
|
|
211
256
|
return wrap
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: ex4nicegui
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: ...
|
|
5
|
-
Home-page:
|
|
6
|
-
Author: carson_jia
|
|
7
|
-
Author-email: 568166495@qq.com
|
|
8
|
-
License: MIT license
|
|
9
|
-
Keywords: nicegui,ex4nicegui,webui
|
|
10
|
-
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Natural Language :: English
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Requires-Python: >=3.8
|
|
15
|
-
License-File: LICENSE
|
|
16
|
-
Requires-Dist: signe (>=0.
|
|
17
|
-
Requires-Dist: nicegui (>=1.3.13)
|
|
18
|
-
Requires-Dist: typing-extensions
|
|
19
|
-
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ex4nicegui
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: ...
|
|
5
|
+
Home-page:
|
|
6
|
+
Author: carson_jia
|
|
7
|
+
Author-email: 568166495@qq.com
|
|
8
|
+
License: MIT license
|
|
9
|
+
Keywords: nicegui,ex4nicegui,webui
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: signe (>=0.2.3)
|
|
17
|
+
Requires-Dist: nicegui (>=1.3.13)
|
|
18
|
+
Requires-Dist: typing-extensions
|
|
19
|
+
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
ex4nicegui/__init__.py,sha256=
|
|
1
|
+
ex4nicegui/__init__.py,sha256=r2UGFOv2DxiLcgASHLLVpmZJALQYsF2VTWGRSQQMoRw,396
|
|
2
|
+
ex4nicegui/bi/__init__.py,sha256=OXA3np4-3CaujOLAxza8vK0mrV0nIfNP-TQBlfbq9k8,61
|
|
3
|
+
ex4nicegui/bi/dataSource.py,sha256=yue_zPniAmjq8-z4gXg718kfCXU1CU5gCwj1AufWoEc,5039
|
|
4
|
+
ex4nicegui/bi/dataSourceFacade.py,sha256=S6D7h82o5loIFqHkDN4gtC1knDUuqzRF_O7t38qRibw,8024
|
|
5
|
+
ex4nicegui/bi/index.py,sha256=Zuo8V_7IZnayo8vxEMFbx-BkZTkEca-QhHVrmVTHjuc,1883
|
|
6
|
+
ex4nicegui/bi/protocols.py,sha256=YiLYC62nFb3UsGu4Ly9AkM-kj1TuG1vlqKLJb23xe90,3175
|
|
7
|
+
ex4nicegui/bi/types.py,sha256=R6mfDqD9dONJNq1_3uPcMFUsTBr6Rdpu5KFIfsvNSN4,277
|
|
2
8
|
ex4nicegui/experimental_/__init__.py,sha256=LSDd_U6eQ9g9St9kC4daau3MFGlVCRHGZJC4E0JRH34,36
|
|
3
9
|
ex4nicegui/experimental_/gridLayout/__init__.py,sha256=48y_Pm0xxgC_PRnixQB5R_5rPL4FuyeoeOao_W7pm7A,49
|
|
4
10
|
ex4nicegui/experimental_/gridLayout/index.py,sha256=zFXuvFroo5EC1CFjt-b4hMiEy67hGP5J1GYTKH6kpUU,4737
|
|
@@ -18,7 +24,7 @@ ex4nicegui/reactive/local_file_picker.py,sha256=DWNzm_IP02sY-nZWN6WEWJxlwpABW6tN
|
|
|
18
24
|
ex4nicegui/reactive/q_pagination.py,sha256=ITXBrjLnI1a5bz3Rbn7j8lZs9UJaFuMHrM9_FW_V7NA,1217
|
|
19
25
|
ex4nicegui/reactive/rxui.py,sha256=NZUgvItxqqgzHKrt4oGZnxxV9dlEudGiv4J3fhJdvdQ,24
|
|
20
26
|
ex4nicegui/reactive/usePagination.py,sha256=IP1NeLxaH3413KTEjtbyuzq0FVdtnKQsTZqM-W7iEgY,2468
|
|
21
|
-
ex4nicegui/reactive/EChartsComponent/ECharts.js,sha256=
|
|
27
|
+
ex4nicegui/reactive/EChartsComponent/ECharts.js,sha256=qj3HUngg-pcTjNDCiz7jgz-O2Lx3aW0HhvrcsDEb0a0,1581667
|
|
22
28
|
ex4nicegui/reactive/EChartsComponent/ECharts.py,sha256=LhThgTo7TSjaXIaWcG6eJojuPWyhUkDkw3tJIuCkJTw,2968
|
|
23
29
|
ex4nicegui/reactive/EChartsComponent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
30
|
ex4nicegui/reactive/UseDraggable/UseDraggable.js,sha256=D2_4c64qYwkqG_JzL1ZAwoNZDoz6qtHfPA_Z5RIvmIw,5235
|
|
@@ -37,7 +43,7 @@ ex4nicegui/reactive/officials/color_picker.py,sha256=s6zUBkCAqAnBnoLWS3bXFIqmCK5
|
|
|
37
43
|
ex4nicegui/reactive/officials/column.py,sha256=3RLvVKNaDtOb8df4uS3xRfwJJPuH1ndXk_Y4Gry0Tjo,413
|
|
38
44
|
ex4nicegui/reactive/officials/date.py,sha256=4muMUxoXwV_OYBoe-ucB5g20msT2zjbiyfGNZbKPdoU,2700
|
|
39
45
|
ex4nicegui/reactive/officials/drawer.py,sha256=7XqP8UmkvgxglV1-vUwbo5Sx2Fz7CPr1UqBfriEagDE,2443
|
|
40
|
-
ex4nicegui/reactive/officials/echarts.py,sha256=
|
|
46
|
+
ex4nicegui/reactive/officials/echarts.py,sha256=HmQqzcvIFum6C80_b6IPmLLy-D16l_8y1IaHYFOsylk,2198
|
|
41
47
|
ex4nicegui/reactive/officials/expansion.py,sha256=Z2aKsrtUpkO0Z4kO9kPwcu7piBcE_d62OAC2oVDFTGE,1528
|
|
42
48
|
ex4nicegui/reactive/officials/grid.py,sha256=6brGijR9ZLqOhe5r2w4BF81R8I4kJPZxZVkbQjXwlOU,925
|
|
43
49
|
ex4nicegui/reactive/officials/html.py,sha256=7CQWKu_t3MdDJX21fTC3xTMAOcg0gKZrKJsaSCpZ0e4,1687
|
|
@@ -61,10 +67,11 @@ ex4nicegui/reactive/useMouse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
|
61
67
|
ex4nicegui/tools/__init__.py,sha256=Ue6ATQC9BuQlJEcs2JnuFXZh4DYh9twKc4F7zpIPhjE,40
|
|
62
68
|
ex4nicegui/tools/debug.py,sha256=HCKlVzhHx5av-983ADgwgMkScKwTreSluLA7uikGYa0,4887
|
|
63
69
|
ex4nicegui/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
|
+
ex4nicegui/utils/clientScope.py,sha256=plgHxcCY1y76bK31u5zfOTX8cnYC1Jlx-N3cAgCY2G4,1143
|
|
64
71
|
ex4nicegui/utils/common.py,sha256=5fsaOkoj-Ild1LGsInZXra66gJLVoVcZGAIG6YOeM6E,430
|
|
65
|
-
ex4nicegui/utils/signals.py,sha256=
|
|
66
|
-
ex4nicegui-0.
|
|
67
|
-
ex4nicegui-0.
|
|
68
|
-
ex4nicegui-0.
|
|
69
|
-
ex4nicegui-0.
|
|
70
|
-
ex4nicegui-0.
|
|
72
|
+
ex4nicegui/utils/signals.py,sha256=S15wA6WPLBmohn1Z5EpFOxC1KvksOf5ir86PuhRM_Oc,5969
|
|
73
|
+
ex4nicegui-0.3.0.dist-info/LICENSE,sha256=0KDDElS2dl-HIsWvbpy8ywbLzJMBFzXLev57LnMIZXs,1094
|
|
74
|
+
ex4nicegui-0.3.0.dist-info/METADATA,sha256=WfHwYH3zUfTli4m0P_D8wDRdls-AMIZqwwcoggnjBKw,533
|
|
75
|
+
ex4nicegui-0.3.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
76
|
+
ex4nicegui-0.3.0.dist-info/top_level.txt,sha256=VFwMiO9AFjj5rfLMJwN1ipLRASk9fJXB8tM6DNrpvPQ,11
|
|
77
|
+
ex4nicegui-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|