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.
- fmtr/tools/__init__.py +86 -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 +73 -12
- fmtr/tools/async_tools.py +4 -0
- fmtr/tools/av_tools.py +7 -0
- fmtr/tools/caching_tools.py +101 -3
- fmtr/tools/constants.py +41 -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 +22 -2
- fmtr/tools/datetime_tools.py +12 -0
- fmtr/tools/debugging_tools.py +60 -1
- 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 +3 -2
- fmtr/tools/function_tools.py +77 -1
- fmtr/tools/google_api_tools.py +15 -4
- fmtr/tools/ha_tools/__init__.py +8 -0
- fmtr/tools/ha_tools/constants.py +9 -0
- fmtr/tools/ha_tools/core.py +16 -0
- fmtr/tools/ha_tools/supervisor.py +16 -0
- fmtr/tools/ha_tools/utils.py +46 -0
- fmtr/tools/http_tools.py +52 -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 +122 -1
- fmtr/tools/logging_tools.py +99 -18
- fmtr/tools/mqtt_tools.py +89 -0
- fmtr/tools/networking_tools.py +73 -0
- 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} +217 -14
- fmtr/tools/path_tools/type_path_tools.py +3 -0
- fmtr/tools/pattern_tools.py +277 -0
- fmtr/tools/pdf_tools.py +39 -1
- fmtr/tools/settings_tools.py +27 -6
- fmtr/tools/setup_tools/__init__.py +8 -0
- fmtr/tools/setup_tools/setup_tools.py +481 -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 +64 -5
- fmtr/tools/youtube_tools.py +128 -0
- fmtr_tools-1.4.37.data/scripts/add-service +14 -0
- fmtr_tools-1.4.37.data/scripts/add-user-path +8 -0
- fmtr_tools-1.4.37.data/scripts/apt-headless +23 -0
- fmtr_tools-1.4.37.data/scripts/compose-update +10 -0
- fmtr_tools-1.4.37.data/scripts/docker-sandbox +43 -0
- fmtr_tools-1.4.37.data/scripts/docker-sandbox-init +23 -0
- fmtr_tools-1.4.37.data/scripts/docs-deploy +6 -0
- fmtr_tools-1.4.37.data/scripts/docs-serve +5 -0
- fmtr_tools-1.4.37.data/scripts/download +9 -0
- fmtr_tools-1.4.37.data/scripts/fmtr-test-script +3 -0
- fmtr_tools-1.4.37.data/scripts/ftu +3 -0
- fmtr_tools-1.4.37.data/scripts/ha-addon-launch +16 -0
- fmtr_tools-1.4.37.data/scripts/install-browser +8 -0
- fmtr_tools-1.4.37.data/scripts/parse-args +43 -0
- fmtr_tools-1.4.37.data/scripts/set-password +5 -0
- fmtr_tools-1.4.37.data/scripts/snips-install +14 -0
- fmtr_tools-1.4.37.data/scripts/ssh-auth +28 -0
- fmtr_tools-1.4.37.data/scripts/ssh-serve +15 -0
- fmtr_tools-1.4.37.data/scripts/vlc-tn +10 -0
- fmtr_tools-1.4.37.data/scripts/vm-launch +17 -0
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/METADATA +178 -54
- fmtr_tools-1.4.37.dist-info/RECORD +122 -0
- {fmtr_tools-1.1.1.dist-info → fmtr_tools-1.4.37.dist-info}/WHEEL +1 -1
- fmtr_tools-1.4.37.dist-info/entry_points.txt +6 -0
- fmtr_tools-1.4.37.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.4.37.dist-info}/licenses/LICENSE +0 -0
fmtr/tools/function_tools.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
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
|
fmtr/tools/google_api_tools.py
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
37
|
-
return
|
|
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
|
fmtr/tools/http_tools.py
ADDED
|
@@ -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,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)
|