fmtr.tools 1.1.1__py3-none-any.whl → 1.3.81__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.
- fmtr/tools/__init__.py +68 -52
- fmtr/tools/ai_tools/__init__.py +2 -2
- fmtr/tools/ai_tools/agentic_tools.py +151 -32
- fmtr/tools/ai_tools/inference_tools.py +2 -1
- fmtr/tools/api_tools.py +8 -5
- fmtr/tools/caching_tools.py +101 -3
- fmtr/tools/constants.py +33 -0
- fmtr/tools/context_tools.py +23 -0
- fmtr/tools/data_modelling_tools.py +227 -14
- fmtr/tools/database_tools/__init__.py +6 -0
- fmtr/tools/database_tools/document.py +51 -0
- fmtr/tools/datatype_tools.py +21 -1
- fmtr/tools/datetime_tools.py +12 -0
- fmtr/tools/debugging_tools.py +60 -0
- fmtr/tools/dns_tools/__init__.py +7 -0
- fmtr/tools/dns_tools/client.py +97 -0
- fmtr/tools/dns_tools/dm.py +257 -0
- fmtr/tools/dns_tools/proxy.py +66 -0
- fmtr/tools/dns_tools/server.py +138 -0
- fmtr/tools/docker_tools/__init__.py +6 -0
- fmtr/tools/entrypoints/__init__.py +0 -0
- fmtr/tools/entrypoints/cache_hfh.py +3 -0
- fmtr/tools/entrypoints/ep_test.py +2 -0
- fmtr/tools/entrypoints/install_yamlscript.py +8 -0
- fmtr/tools/{console_script_tools.py → entrypoints/remote_debug_test.py} +1 -6
- fmtr/tools/entrypoints/shell_debug.py +8 -0
- fmtr/tools/environment_tools.py +2 -2
- fmtr/tools/function_tools.py +77 -1
- fmtr/tools/google_api_tools.py +15 -4
- fmtr/tools/http_tools.py +26 -0
- fmtr/tools/inherit_tools.py +27 -0
- fmtr/tools/interface_tools/__init__.py +8 -0
- fmtr/tools/interface_tools/context.py +13 -0
- fmtr/tools/interface_tools/controls.py +354 -0
- fmtr/tools/interface_tools/interface_tools.py +189 -0
- fmtr/tools/iterator_tools.py +29 -0
- fmtr/tools/logging_tools.py +43 -16
- fmtr/tools/packaging_tools.py +14 -0
- fmtr/tools/path_tools/__init__.py +12 -0
- fmtr/tools/path_tools/app_path_tools.py +40 -0
- fmtr/tools/{path_tools.py → path_tools/path_tools.py} +156 -12
- fmtr/tools/path_tools/type_path_tools.py +3 -0
- fmtr/tools/pattern_tools.py +260 -0
- fmtr/tools/pdf_tools.py +39 -1
- fmtr/tools/settings_tools.py +23 -4
- fmtr/tools/setup_tools/__init__.py +8 -0
- fmtr/tools/setup_tools/setup_tools.py +447 -0
- fmtr/tools/string_tools.py +92 -13
- fmtr/tools/tabular_tools.py +61 -0
- fmtr/tools/tools.py +27 -2
- fmtr/tools/version +1 -1
- fmtr/tools/version_tools/__init__.py +12 -0
- fmtr/tools/version_tools/version_tools.py +51 -0
- fmtr/tools/webhook_tools.py +17 -0
- fmtr/tools/yaml_tools.py +66 -5
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.3.81.dist-info}/METADATA +136 -54
- fmtr_tools-1.3.81.dist-info/RECORD +93 -0
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.3.81.dist-info}/WHEEL +1 -1
- fmtr_tools-1.3.81.dist-info/entry_points.txt +6 -0
- fmtr_tools-1.3.81.dist-info/top_level.txt +1 -0
- fmtr/tools/docker_tools.py +0 -30
- fmtr/tools/interface_tools.py +0 -64
- fmtr/tools/version_tools.py +0 -62
- fmtr_tools-1.1.1.dist-info/RECORD +0 -65
- fmtr_tools-1.1.1.dist-info/entry_points.txt +0 -3
- fmtr_tools-1.1.1.dist-info/top_level.txt +0 -2
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.3.81.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import TypeVar, Generic
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Inherit(Generic[T]):
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
Runtime inheritance. Acts like a wrapper around an instantiated base class of type T, and allows overriding methods in subclasses like regular inheritance.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, parent: T):
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
Set parent
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
object.__setattr__(self, "_parent", parent)
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name):
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
Since regular attribute access checks own methods first, we don't need to do anything fancy to fall back to the parent when not implemented.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
return getattr(self._parent, name)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from fmtr.tools.import_tools import MissingExtraMockModule
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from fmtr.tools.interface_tools.interface_tools import Base, update, progress
|
|
5
|
+
from fmtr.tools.interface_tools import controls
|
|
6
|
+
from fmtr.tools.interface_tools.context import Context
|
|
7
|
+
except ModuleNotFoundError as exception:
|
|
8
|
+
Interface = update = progress = controls = MissingExtraMockModule('interface', exception)
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import flet as ft
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from flet.core.gesture_detector import TapEvent
|
|
4
|
+
from flet.core.page import Page
|
|
5
|
+
from flet.core.types import ColorValue, IconValue
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from fmtr.tools.logging_tools import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SliderSteps(ft.Slider):
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
Slider control using step instead of divisions
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, *args, min=10, max=100, step=10, **kwargs):
|
|
20
|
+
self.step = step
|
|
21
|
+
divisions = (max - min) // step
|
|
22
|
+
super().__init__(*args, min=min, max=max, divisions=divisions, **kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Cell(ft.DataCell):
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
Context-aware, clickable data cell.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, series, column):
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
Store context
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
self.series = series
|
|
39
|
+
self.column = column
|
|
40
|
+
self.value = series[column]
|
|
41
|
+
|
|
42
|
+
super().__init__(self.gesture_detector)
|
|
43
|
+
|
|
44
|
+
@cached_property
|
|
45
|
+
def text(self):
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
Cell contents text
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
return ft.Text(str(self.value), color=self.color, bgcolor=self.bgcolor)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def color(self):
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
Basic conditional formatting
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
if self.value is None:
|
|
62
|
+
return ft.Colors.GREY
|
|
63
|
+
else:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def bgcolor(self):
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
Basic conditional formatting
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
@cached_property
|
|
76
|
+
def gesture_detector(self):
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
Make arbitrary content clickable
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
return ft.GestureDetector(content=self.text, on_tap=self.click_tap, on_double_tap=self.click_double_tap)
|
|
83
|
+
|
|
84
|
+
async def click_tap(self, event: TapEvent):
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
Default cell click behavior — override in subclass if needed
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
value = await self.click(event=event)
|
|
91
|
+
event.page.update()
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
async def click(self, event: Optional[TapEvent] = None):
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
Default cell click behavior — override in subclass if needed
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
logger.info(f"Clicked {self.column=} {self.series.name=} {self.value=}")
|
|
101
|
+
|
|
102
|
+
async def click_double_tap(self, event: TapEvent):
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
Default cell click behavior — override in subclass if needed
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
value = await self.double_click(event=event)
|
|
109
|
+
event.page.update()
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
async def double_click(self, event: Optional[TapEvent] = None):
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
Default cell double click behavior — override in subclass if needed
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
logger.info(f"Double-clicked {self.column=} {self.series.id=} {self.value=}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Row(ft.DataRow):
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
Instantiate a row from a series
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
TypesCells = {None: Cell}
|
|
129
|
+
|
|
130
|
+
def __init__(self, series):
|
|
131
|
+
self.series = series
|
|
132
|
+
super().__init__(self.cells_controls, color=self.row_color)
|
|
133
|
+
|
|
134
|
+
@cached_property
|
|
135
|
+
def cells_data(self) -> dict[str, list[Cell]]:
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
Cell controls lookup
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
data = {}
|
|
142
|
+
for col in self.series.index:
|
|
143
|
+
default = self.TypesCells[None]
|
|
144
|
+
TypeCell = self.TypesCells.get(col, default)
|
|
145
|
+
data.setdefault(col, []).append(TypeCell(self.series, col))
|
|
146
|
+
return data
|
|
147
|
+
|
|
148
|
+
@cached_property
|
|
149
|
+
def cells_controls(self) -> list[Cell]:
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
Flat list of controls
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
controls = []
|
|
156
|
+
for cells in self.cells_data.values():
|
|
157
|
+
for cell in cells:
|
|
158
|
+
controls.append(cell)
|
|
159
|
+
return controls
|
|
160
|
+
|
|
161
|
+
def __getitem__(self, item) -> list[Cell]:
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
Get cells by column name
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
return self.cells_data[item]
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def row_color(self):
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
Basic conditional formatting
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Column(ft.DataColumn):
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
Column stub
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, col_name: str):
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
Store context
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
self.col_name = col_name
|
|
194
|
+
|
|
195
|
+
super().__init__(label=self.text)
|
|
196
|
+
|
|
197
|
+
@cached_property
|
|
198
|
+
def text(self):
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
Cell contents text
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
return ft.Text(str(self.col_name), weight=self.weight)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def weight(self):
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
Default bold headers
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
return ft.FontWeight.BOLD
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class Table(ft.DataTable):
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
Dataframe with clickable cells
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
TypesRows = {None: Row}
|
|
224
|
+
TypesColumns = {None: Column}
|
|
225
|
+
|
|
226
|
+
def __init__(self, df): # todo move to submodule with tabular deps
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
Set columns/rows using relevant types
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
self.df = df
|
|
233
|
+
super().__init__(columns=self.columns_controls, rows=self.rows_controls)
|
|
234
|
+
|
|
235
|
+
@cached_property
|
|
236
|
+
def columns_data(self) -> dict[str, list[Column]]:
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
Columns controls lookup
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
data = {}
|
|
243
|
+
for col in self.df.columns:
|
|
244
|
+
default = self.TypesColumns[None]
|
|
245
|
+
TypeColumn = self.TypesColumns.get(col, default)
|
|
246
|
+
data.setdefault(col, []).append(TypeColumn(col))
|
|
247
|
+
return data
|
|
248
|
+
|
|
249
|
+
@cached_property
|
|
250
|
+
def columns_controls(self) -> list[Column]:
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
Flat list of controls
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
controls = []
|
|
257
|
+
for columns in self.columns_data.values():
|
|
258
|
+
for column in columns:
|
|
259
|
+
controls.append(column)
|
|
260
|
+
return controls
|
|
261
|
+
|
|
262
|
+
@cached_property
|
|
263
|
+
def rows_data(self) -> dict[str, list[Row]]:
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
Row controls lookup
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
data = {}
|
|
270
|
+
for index, row in self.df.iterrows():
|
|
271
|
+
default = self.TypesRows[None]
|
|
272
|
+
TypeRow = self.TypesRows.get(index, default)
|
|
273
|
+
data.setdefault(index, []).append(TypeRow(row))
|
|
274
|
+
return data
|
|
275
|
+
|
|
276
|
+
@cached_property
|
|
277
|
+
def rows_controls(self) -> list[Row]:
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
Flat list of controls
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
controls = []
|
|
284
|
+
for rows in self.rows_data.values():
|
|
285
|
+
for row in rows:
|
|
286
|
+
controls.append(row)
|
|
287
|
+
return controls
|
|
288
|
+
|
|
289
|
+
def __getitem__(self, item) -> list[Row]:
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
Get row by index
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
return self.rows_data[item]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass
|
|
300
|
+
class NotificationDatum:
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
Color and icon for notification bar
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
color: ColorValue
|
|
307
|
+
icon: IconValue
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class NotificationBar(ft.SnackBar):
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
Combined logging and notification bar: infers the appropriate UI representation from the log level
|
|
314
|
+
|
|
315
|
+
"""
|
|
316
|
+
DATA = {
|
|
317
|
+
logger.info: NotificationDatum(color=ft.Colors.BLUE, icon=ft.Icons.INFO),
|
|
318
|
+
logger.warning: NotificationDatum(color=ft.Colors.AMBER, icon=ft.Icons.WARNING),
|
|
319
|
+
logger.error: NotificationDatum(color=ft.Colors.RED, icon=ft.Icons.ERROR),
|
|
320
|
+
logger.debug: NotificationDatum(color=ft.Colors.GREY, icon=ft.Icons.BUG_REPORT),
|
|
321
|
+
logger.exception: NotificationDatum(color=ft.Colors.RED_ACCENT, icon=ft.Icons.REPORT),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
def __init__(self, msg: str, method=logger.info):
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
Log the message immediately, otherwise configure notification bar icon/color.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
self.msg = msg
|
|
331
|
+
self.method = method
|
|
332
|
+
self.method(msg)
|
|
333
|
+
|
|
334
|
+
icon = ft.Icon(self.data.icon, color=self.data.color)
|
|
335
|
+
text = ft.Text(self.msg)
|
|
336
|
+
content = ft.Row(controls=[icon, text])
|
|
337
|
+
super().__init__(content=content)
|
|
338
|
+
|
|
339
|
+
@cached_property
|
|
340
|
+
def data(self) -> NotificationDatum:
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
Fetching data using logging method.
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
return self.DATA[self.method]
|
|
347
|
+
|
|
348
|
+
def show(self, page: Page):
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
Show the notification on the relevant page.
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
page.open(self)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import TypeVar, Generic, Type
|
|
3
|
+
|
|
4
|
+
import flet as ft
|
|
5
|
+
from flet.core.control_event import ControlEvent
|
|
6
|
+
from flet.core.types import AppView
|
|
7
|
+
from flet.core.view import View
|
|
8
|
+
|
|
9
|
+
from fmtr.tools import environment_tools
|
|
10
|
+
from fmtr.tools.constants import Constants
|
|
11
|
+
from fmtr.tools.function_tools import MethodDecorator
|
|
12
|
+
from fmtr.tools.interface_tools.context import Context
|
|
13
|
+
from fmtr.tools.logging_tools import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class update(MethodDecorator):
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
Update the page after the decorated function is called.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def stop(self, instance, *args, **kwargs):
|
|
24
|
+
instance.page.update()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class progress(update):
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
Run the function while a progress indicator (e.g. spinner) is and within the object-defined context (e.g. logging span).
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def start(self, instance, *args, **kwargs):
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
Make progress visible and update.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
instance.progress.visible = True
|
|
41
|
+
instance.page.update()
|
|
42
|
+
|
|
43
|
+
def stop(self, instance, *args, **kwargs):
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
Make progress not visible and update.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
instance.progress.visible = False
|
|
50
|
+
super().stop(instance)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
T = TypeVar('T', bound=Context)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Base(Generic[T], ft.Column):
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
Simple interface base class.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
TITLE = 'Base Interface'
|
|
63
|
+
HOST = '0.0.0.0'
|
|
64
|
+
PORT = 8080
|
|
65
|
+
URL = Constants.FMTR_DEV_INTERFACE_URL if environment_tools.IS_DEV else None
|
|
66
|
+
APPVIEW = AppView.WEB_BROWSER
|
|
67
|
+
PATH_ASSETS = None
|
|
68
|
+
PATH_UPLOADS = None
|
|
69
|
+
SCROLL = ft.ScrollMode.AUTO
|
|
70
|
+
|
|
71
|
+
SECRET_KEY_KEY = 'FLET_SECRET_KEY'
|
|
72
|
+
ROUTE_ROOT = '/'
|
|
73
|
+
|
|
74
|
+
TypeContext: Type[T] = Context
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
async def new(cls, page: ft.Page):
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
Interface entry point/async constructor. Set relevant callbacks, and add instantiated self to page views.
|
|
81
|
+
|
|
82
|
+
Override this to work with `Context`, do async setup. Otherwise, override __init__ (which is regular Column __init__) for a simple interface.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
page.scroll = cls.SCROLL
|
|
86
|
+
page.title = cls.TITLE
|
|
87
|
+
page.on_connect = cls.on_connect
|
|
88
|
+
page.on_disconnect = cls.on_disconnect
|
|
89
|
+
page.on_route_change = cls.route
|
|
90
|
+
page.on_view_pop = cls.pop
|
|
91
|
+
page.theme = cls.get_theme()
|
|
92
|
+
|
|
93
|
+
context = cls.TypeContext(page=page)
|
|
94
|
+
self = cls()
|
|
95
|
+
self.context = context
|
|
96
|
+
|
|
97
|
+
page.controls.append(self)
|
|
98
|
+
page.update()
|
|
99
|
+
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def route(cls, event: ft.RouteChangeEvent):
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
Overridable router.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
logger.debug(f'Route change: {event=}')
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def pop(cls, view: View, page: ft.Page):
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
Overridable view pop.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
logger.debug(f'View popped: {page.route=} {len(page.views)=} {view=}')
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def on_connect(cls, event: ControlEvent):
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
Log connections
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
page = event.control
|
|
128
|
+
logger.warning(f'Connect: {page.client_user_agent=} {page.platform.name=}')
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def on_disconnect(cls, event: ControlEvent):
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
Log disconnections
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
page = event.control
|
|
138
|
+
logger.warning(f'Disconnect {page.client_user_agent=} {page.platform.name=}')
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def get_theme(self):
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
Overridable theme definition
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
text_style = ft.TextStyle(size=20)
|
|
149
|
+
theme = ft.Theme(
|
|
150
|
+
text_theme=ft.TextTheme(body_large=text_style),
|
|
151
|
+
)
|
|
152
|
+
return theme
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def launch(cls):
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
Launch via async constructor method
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
if cls.URL:
|
|
163
|
+
url = cls.URL
|
|
164
|
+
else:
|
|
165
|
+
url = f'http://{cls.HOST}:{cls.PORT}'
|
|
166
|
+
|
|
167
|
+
if not environment_tools.get(cls.SECRET_KEY_KEY, default=None):
|
|
168
|
+
os.environ["FLET_SECRET_KEY"] = os.urandom(12).hex()
|
|
169
|
+
|
|
170
|
+
logger.info(f"Launching {cls.TITLE} at {url}")
|
|
171
|
+
ft.app(cls.new, view=cls.APPVIEW, host=cls.HOST, port=cls.PORT, assets_dir=cls.PATH_ASSETS, upload_dir=cls.PATH_UPLOADS)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Test(Base[Context]):
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
Simple test interface, showing typing example.
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
TypeContext: Type[Context] = Context
|
|
181
|
+
|
|
182
|
+
TITLE = 'Test Interface'
|
|
183
|
+
|
|
184
|
+
def __init__(self):
|
|
185
|
+
controls = [ft.Text(self.TITLE)]
|
|
186
|
+
super().__init__(controls=controls)
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
Test.launch()
|
fmtr/tools/iterator_tools.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from itertools import chain, batched
|
|
2
2
|
from typing import List, Dict, Any
|
|
3
3
|
|
|
4
|
+
from fmtr.tools.datatype_tools import is_none
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def enlist(value) -> List[Any]:
|
|
6
8
|
"""
|
|
@@ -52,3 +54,30 @@ def rebatch(batches, size: int):
|
|
|
52
54
|
|
|
53
55
|
"""
|
|
54
56
|
return batched(chain.from_iterable(batches), size)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def strip_none(*items):
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
Remove nones from a list of arguments
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
return [item for item in items if not is_none(item)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def dedupe(items):
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
Deduplicate a list of items, retaining order
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
return list(dict.fromkeys(items))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_class_lookup(*classes, name_function=lambda cls: cls.__name__):
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
Dictionary of class names to classes
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
return {name_function(cls): cls for cls in classes}
|