fmtr.tools 1.1.1__py3-none-any.whl → 1.4.37__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.
Files changed (97) hide show
  1. fmtr/tools/__init__.py +86 -52
  2. fmtr/tools/ai_tools/__init__.py +2 -2
  3. fmtr/tools/ai_tools/agentic_tools.py +151 -32
  4. fmtr/tools/ai_tools/inference_tools.py +2 -1
  5. fmtr/tools/api_tools.py +73 -12
  6. fmtr/tools/async_tools.py +4 -0
  7. fmtr/tools/av_tools.py +7 -0
  8. fmtr/tools/caching_tools.py +101 -3
  9. fmtr/tools/constants.py +41 -0
  10. fmtr/tools/context_tools.py +23 -0
  11. fmtr/tools/data_modelling_tools.py +227 -14
  12. fmtr/tools/database_tools/__init__.py +6 -0
  13. fmtr/tools/database_tools/document.py +51 -0
  14. fmtr/tools/datatype_tools.py +22 -2
  15. fmtr/tools/datetime_tools.py +12 -0
  16. fmtr/tools/debugging_tools.py +60 -1
  17. fmtr/tools/dns_tools/__init__.py +7 -0
  18. fmtr/tools/dns_tools/client.py +97 -0
  19. fmtr/tools/dns_tools/dm.py +257 -0
  20. fmtr/tools/dns_tools/proxy.py +66 -0
  21. fmtr/tools/dns_tools/server.py +138 -0
  22. fmtr/tools/docker_tools/__init__.py +6 -0
  23. fmtr/tools/entrypoints/__init__.py +0 -0
  24. fmtr/tools/entrypoints/cache_hfh.py +3 -0
  25. fmtr/tools/entrypoints/ep_test.py +2 -0
  26. fmtr/tools/entrypoints/install_yamlscript.py +8 -0
  27. fmtr/tools/{console_script_tools.py → entrypoints/remote_debug_test.py} +1 -6
  28. fmtr/tools/entrypoints/shell_debug.py +8 -0
  29. fmtr/tools/environment_tools.py +3 -2
  30. fmtr/tools/function_tools.py +77 -1
  31. fmtr/tools/google_api_tools.py +15 -4
  32. fmtr/tools/ha_tools/__init__.py +8 -0
  33. fmtr/tools/ha_tools/constants.py +9 -0
  34. fmtr/tools/ha_tools/core.py +16 -0
  35. fmtr/tools/ha_tools/supervisor.py +16 -0
  36. fmtr/tools/ha_tools/utils.py +46 -0
  37. fmtr/tools/http_tools.py +52 -0
  38. fmtr/tools/inherit_tools.py +27 -0
  39. fmtr/tools/interface_tools/__init__.py +8 -0
  40. fmtr/tools/interface_tools/context.py +13 -0
  41. fmtr/tools/interface_tools/controls.py +354 -0
  42. fmtr/tools/interface_tools/interface_tools.py +189 -0
  43. fmtr/tools/iterator_tools.py +122 -1
  44. fmtr/tools/logging_tools.py +99 -18
  45. fmtr/tools/mqtt_tools.py +89 -0
  46. fmtr/tools/networking_tools.py +73 -0
  47. fmtr/tools/packaging_tools.py +14 -0
  48. fmtr/tools/path_tools/__init__.py +12 -0
  49. fmtr/tools/path_tools/app_path_tools.py +40 -0
  50. fmtr/tools/{path_tools.py → path_tools/path_tools.py} +217 -14
  51. fmtr/tools/path_tools/type_path_tools.py +3 -0
  52. fmtr/tools/pattern_tools.py +277 -0
  53. fmtr/tools/pdf_tools.py +39 -1
  54. fmtr/tools/settings_tools.py +27 -6
  55. fmtr/tools/setup_tools/__init__.py +8 -0
  56. fmtr/tools/setup_tools/setup_tools.py +481 -0
  57. fmtr/tools/string_tools.py +92 -13
  58. fmtr/tools/tabular_tools.py +61 -0
  59. fmtr/tools/tools.py +27 -2
  60. fmtr/tools/version +1 -1
  61. fmtr/tools/version_tools/__init__.py +12 -0
  62. fmtr/tools/version_tools/version_tools.py +51 -0
  63. fmtr/tools/webhook_tools.py +17 -0
  64. fmtr/tools/yaml_tools.py +64 -5
  65. fmtr/tools/youtube_tools.py +128 -0
  66. fmtr_tools-1.4.37.data/scripts/add-service +14 -0
  67. fmtr_tools-1.4.37.data/scripts/add-user-path +8 -0
  68. fmtr_tools-1.4.37.data/scripts/apt-headless +23 -0
  69. fmtr_tools-1.4.37.data/scripts/compose-update +10 -0
  70. fmtr_tools-1.4.37.data/scripts/docker-sandbox +43 -0
  71. fmtr_tools-1.4.37.data/scripts/docker-sandbox-init +23 -0
  72. fmtr_tools-1.4.37.data/scripts/docs-deploy +6 -0
  73. fmtr_tools-1.4.37.data/scripts/docs-serve +5 -0
  74. fmtr_tools-1.4.37.data/scripts/download +9 -0
  75. fmtr_tools-1.4.37.data/scripts/fmtr-test-script +3 -0
  76. fmtr_tools-1.4.37.data/scripts/ftu +3 -0
  77. fmtr_tools-1.4.37.data/scripts/ha-addon-launch +16 -0
  78. fmtr_tools-1.4.37.data/scripts/install-browser +8 -0
  79. fmtr_tools-1.4.37.data/scripts/parse-args +43 -0
  80. fmtr_tools-1.4.37.data/scripts/set-password +5 -0
  81. fmtr_tools-1.4.37.data/scripts/snips-install +14 -0
  82. fmtr_tools-1.4.37.data/scripts/ssh-auth +28 -0
  83. fmtr_tools-1.4.37.data/scripts/ssh-serve +15 -0
  84. fmtr_tools-1.4.37.data/scripts/vlc-tn +10 -0
  85. fmtr_tools-1.4.37.data/scripts/vm-launch +17 -0
  86. {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/METADATA +178 -54
  87. fmtr_tools-1.4.37.dist-info/RECORD +122 -0
  88. {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/WHEEL +1 -1
  89. fmtr_tools-1.4.37.dist-info/entry_points.txt +6 -0
  90. fmtr_tools-1.4.37.dist-info/top_level.txt +1 -0
  91. fmtr/tools/docker_tools.py +0 -30
  92. fmtr/tools/interface_tools.py +0 -64
  93. fmtr/tools/version_tools.py +0 -62
  94. fmtr_tools-1.1.1.dist-info/RECORD +0 -65
  95. fmtr_tools-1.1.1.dist-info/entry_points.txt +0 -3
  96. fmtr_tools-1.1.1.dist-info/top_level.txt +0 -2
  97. {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,8 @@
1
- from typing import Tuple
1
+ import functools
2
+ import inspect
3
+ from typing import Tuple, Callable, Self
4
+
5
+ from fmtr.tools import context_tools
2
6
 
3
7
 
4
8
  def combine_args_kwargs(args: dict=None, kwargs: dict=None) -> dict:
@@ -31,3 +35,75 @@ def split_args_kwargs(args_kwargs: dict) -> Tuple[list, dict]:
31
35
  return args, kwargs
32
36
 
33
37
 
38
+ class MethodDecorator:
39
+ """
40
+
41
+ Bound method decorator with overridable start/stop and context manager
42
+
43
+ """
44
+
45
+ CONTEXT_KEY = 'context'
46
+
47
+ def __init__(self):
48
+ """
49
+
50
+ Initialise the decorator itself with any arguments
51
+
52
+ """
53
+ self.func = None
54
+
55
+ def __call__(self, func: Callable) -> Self:
56
+ """
57
+
58
+ Add the (unbound) method.
59
+
60
+ """
61
+ self.func = func
62
+ functools.update_wrapper(self, func)
63
+ return self
64
+
65
+ def __get__(self, instance, owner):
66
+ """
67
+
68
+ Wrap bound method at runtime, call start/stop within context.
69
+
70
+ """
71
+ if instance is None: # Class method called.
72
+ return self.func
73
+
74
+ if inspect.iscoroutinefunction(self.func):
75
+ async def async_wrapper(*args, **kwargs):
76
+ with self.get_context(instance):
77
+ self.start(instance, *args, **kwargs)
78
+ result = await self.func(instance, *args, **kwargs)
79
+ self.stop(instance, *args, **kwargs)
80
+ return result
81
+
82
+ return async_wrapper
83
+
84
+ else:
85
+ def sync_wrapper(*args, **kwargs):
86
+ with self.get_context(instance):
87
+ self.start(instance, *args, **kwargs)
88
+ result = self.func(instance, *args, **kwargs)
89
+ self.stop(instance, *args, **kwargs)
90
+ return result
91
+
92
+ return sync_wrapper
93
+
94
+ def get_context(self, instance):
95
+ """
96
+
97
+ If the instance has a context attribute, use that - otherwise use a null context.
98
+
99
+ """
100
+ context = getattr(instance, self.CONTEXT_KEY, None)
101
+ if context:
102
+ return context
103
+ return context_tools.null()
104
+
105
+ def start(self, instance, *args, **kwargs):
106
+ pass
107
+
108
+ def stop(self, instance, *args, **kwargs):
109
+ pass
@@ -1,3 +1,4 @@
1
+ from google.auth.transport.requests import Request
1
2
  from google.oauth2.credentials import Credentials
2
3
  from google_auth_oauthlib.flow import InstalledAppFlow
3
4
  from googleapiclient.discovery import build
@@ -20,8 +21,12 @@ class Authenticator:
20
21
 
21
22
  @classmethod
22
23
  def auth(cls):
23
- msg = f'Doing auth for service {cls.SERVICE} ({cls.VERSION})...'
24
- logger.info(msg)
24
+ """
25
+
26
+ Do initial authentication or refresh token if expired.
27
+
28
+ """
29
+ logger.info(f'Doing auth for service {cls.SERVICE} ({cls.VERSION})...')
25
30
 
26
31
  PATH_CREDS = cls.PATH / 'credentials.json'
27
32
  PATH_TOKEN = cls.PATH / f'{cls.SERVICE}.json'
@@ -29,9 +34,15 @@ class Authenticator:
29
34
  if PATH_TOKEN.exists():
30
35
  data_token = PATH_TOKEN.read_json()
31
36
  credentials = Credentials.from_authorized_user_info(data_token, cls.SCOPES)
37
+ if credentials.expired:
38
+ with logger.span(f'Credentials expired for {cls.SERVICE}. Will refresh...'):
39
+ logger.warning(f'{cls.SERVICE}. {PATH_CREDS.exists()=} {PATH_TOKEN.exists()=} {credentials.valid=} {credentials.expired=} {credentials.expiry=}')
40
+ credentials.refresh(Request())
41
+ PATH_TOKEN.write_text(credentials.to_json())
32
42
  else:
33
43
  flow = InstalledAppFlow.from_client_secrets_file(PATH_CREDS, cls.SCOPES)
44
+ flow.authorization_url(prompt='consent', access_type='offline')
34
45
  credentials = flow.run_local_server(open_browser=False, port=cls.PORT)
35
46
  PATH_TOKEN.write_text(credentials.to_json())
36
- service = build(cls.SERVICE, cls.VERSION, credentials=credentials)
37
- return service
47
+
48
+ return build(cls.SERVICE, cls.VERSION, credentials=credentials)
@@ -0,0 +1,8 @@
1
+ from fmtr.tools.import_tools import MissingExtraMockModule
2
+
3
+ try:
4
+ from fmtr.tools.ha_tools import core, supervisor, constants
5
+ from fmtr.tools.ha_tools.utils import apply_addon_env
6
+
7
+ except ModuleNotFoundError as exception:
8
+ core = supervisor = constants = apply_addon_env = MissingExtraMockModule('ha', exception)
@@ -0,0 +1,9 @@
1
+ from fmtr.tools.path_tools.path_tools import root
2
+
3
+ SUPERVISOR_TOKEN_KEY = 'SUPERVISOR_TOKEN'
4
+ URL_SUPERVISOR_ADDON = "http://supervisor"
5
+ URL_CORE_ADDON = F"{URL_SUPERVISOR_ADDON}/core/api"
6
+ PATH_ADDON_ENV = root / 'addon.env'
7
+ PATH_ADDON_OPTIONS = root / 'data' / 'options.json'
8
+ PATH_ADDON_CONFIG = root / 'config'
9
+ PATH_ADDON_MEDIA = root / 'media'
@@ -0,0 +1,16 @@
1
+ import homeassistant_api
2
+
3
+ from fmtr.tools import environment_tools as env
4
+ from fmtr.tools.ha_tools import constants
5
+
6
+
7
+ class Client(homeassistant_api.Client):
8
+ """
9
+
10
+ Client stub
11
+
12
+ """
13
+
14
+ def __init__(self, *args, api_url: str = constants.URL_CORE_ADDON, token: str, **kwargs):
15
+ token = token or env.get(constants.SUPERVISOR_TOKEN_KEY)
16
+ super().__init__(*args, api_url=api_url, token=token, **kwargs)
@@ -0,0 +1,16 @@
1
+ import aiohasupervisor
2
+
3
+ from fmtr.tools import environment_tools as env
4
+ from fmtr.tools.ha_tools import constants
5
+
6
+
7
+ class Client(aiohasupervisor.SupervisorClient):
8
+ """
9
+
10
+ Client stub
11
+
12
+ """
13
+
14
+ def __init__(self, *args, api_url: str = constants.URL_SUPERVISOR_ADDON, token: str, **kwargs):
15
+ token = token or env.get(constants.SUPERVISOR_TOKEN_KEY)
16
+ super().__init__(*args, api_host=api_url, token=token, **kwargs)
@@ -0,0 +1,46 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from fmtr.tools.ha_tools import constants
5
+ from fmtr.tools.logging_tools import logger
6
+ from fmtr.tools.string_tools import ELLIPSIS
7
+
8
+
9
+ def apply_addon_env():
10
+ """
11
+
12
+ If we're inside an addon container, we need to source its environment file and convert its options.json to environment variables.
13
+
14
+ """
15
+ path = constants.PATH_ADDON_ENV
16
+ if path.exists():
17
+ logger.warning(f'Loading addon environment from "{path}"...')
18
+ load_dotenv(path)
19
+
20
+ for key, value in convert_options_data().items():
21
+ os.environ[key] = value
22
+
23
+
24
+ def convert_options_data() -> dict[str, str]:
25
+ """
26
+
27
+ Convert Home Assistant addon options.json to an environment-ready dict.
28
+
29
+ """
30
+ path = constants.PATH_ADDON_OPTIONS
31
+
32
+ data_env = {}
33
+
34
+ if not path.exists():
35
+ return data_env
36
+
37
+ data_json = path.read_json()
38
+
39
+ with logger.span(f'Converting addon "{path}" to environment variables...'):
40
+ for key, value in data_json.items():
41
+ key_env = key.upper()
42
+ val_env = str(value)
43
+ logger.debug(f'Converting {key_env}={ELLIPSIS}" to environment variable...')
44
+ data_env[key_env] = val_env
45
+
46
+ return data_env
@@ -0,0 +1,52 @@
1
+ import httpx
2
+ from functools import cached_property
3
+ from httpx_retries import RetryTransport, Retry
4
+
5
+ from fmtr.tools import logging_tools
6
+
7
+ logging_tools.logger.instrument_httpx()
8
+
9
+
10
+ class Client(httpx.Client):
11
+ """
12
+
13
+ Instrumented client base
14
+
15
+ """
16
+
17
+ TIMEOUT = 10
18
+
19
+ def __init__(self, *args, **kwargs):
20
+ super().__init__(*args, transport=self.transport, timeout=self.TIMEOUT, **kwargs)
21
+
22
+ @cached_property
23
+ def transport(self) -> RetryTransport:
24
+ """
25
+
26
+ Default Transport with retry
27
+
28
+ """
29
+ return RetryTransport(
30
+ retry=self.retry
31
+ )
32
+
33
+ @cached_property
34
+ def retry(self) -> Retry:
35
+ """
36
+
37
+ Default Retry
38
+
39
+ """
40
+ return Retry(
41
+ allowed_methods=Retry.RETRYABLE_METHODS,
42
+ backoff_factor=1.0
43
+ )
44
+
45
+
46
+ client = Client()
47
+
48
+ if __name__ == '__main__':
49
+ resp = client.get('https://postman-echo.com/delay/5')
50
+ resp.raise_for_status()
51
+ print(resp.json())
52
+ resp
@@ -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,13 @@
1
+ from dataclasses import dataclass
2
+
3
+ from flet.core.page import Page
4
+
5
+
6
+ @dataclass
7
+ class Context:
8
+ """
9
+
10
+ Base context class
11
+
12
+ """
13
+ page: Page
@@ -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)