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
|
@@ -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,5 +1,8 @@
|
|
|
1
1
|
from itertools import chain, batched
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, TypeVar, Generic, Iterable
|
|
4
|
+
|
|
5
|
+
from fmtr.tools.datatype_tools import is_none
|
|
3
6
|
|
|
4
7
|
|
|
5
8
|
def enlist(value) -> List[Any]:
|
|
@@ -52,3 +55,121 @@ def rebatch(batches, size: int):
|
|
|
52
55
|
|
|
53
56
|
"""
|
|
54
57
|
return batched(chain.from_iterable(batches), size)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def strip_none(*items):
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
Remove nones from a list of arguments
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
return [item for item in items if not is_none(item)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def dedupe(items):
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
Deduplicate a list of items, retaining order
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
return list(dict.fromkeys(items))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_class_lookup(*classes, name_function=lambda cls: cls.__name__):
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
Dictionary of class names to classes
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
return {name_function(cls): cls for cls in classes}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
IndexListT = TypeVar('IndexListT') # Generic type for list items
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class IndexList(list[IndexListT], Generic[IndexListT]):
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
List of objects selectable via attribute lookup, plus currently-selected item.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, iterable: Iterable[IndexListT] = ()):
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
Initialize with iterable
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
super().__init__(iterable)
|
|
104
|
+
self.current: IndexListT | None = self[0] if self else None
|
|
105
|
+
|
|
106
|
+
def __getattr__(self, name):
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
Return a lookup dict keyed on the specified field of each item in the self/list.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
return self.__dict__[name]
|
|
115
|
+
except KeyError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
if hasattr(list, name):
|
|
119
|
+
return getattr(self, name)
|
|
120
|
+
|
|
121
|
+
result = {}
|
|
122
|
+
for obj in self:
|
|
123
|
+
try:
|
|
124
|
+
value = getattr(obj, name)
|
|
125
|
+
except AttributeError:
|
|
126
|
+
value = obj[name] # assume dict-like
|
|
127
|
+
result[value] = obj
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
IterDifferT = TypeVar("IterDifferT")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class IterDiffer(Generic[IterDifferT]):
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
Compute added/removed differences between two iterables.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(self, before: Iterable[IterDifferT], after: Iterable[IterDifferT]):
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
Initialize with two iterables.
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
self.before: set[IterDifferT] = set(before)
|
|
148
|
+
self.after: set[IterDifferT] = set(after)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def added(self) -> set[IterDifferT]:
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
Items in `after` not in `before`.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
return self.after - self.before
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def removed(self) -> set[IterDifferT]:
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
Items in `before` not in `after`.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
return self.before - self.after
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def is_changed(self) -> bool:
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
True if any items added or removed.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
return bool(self.added or self.removed)
|
fmtr/tools/logging_tools.py
CHANGED
|
@@ -4,23 +4,24 @@ import os
|
|
|
4
4
|
from fmtr.tools import environment_tools
|
|
5
5
|
from fmtr.tools.constants import Constants
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
LEVEL_DEFAULT = logging.DEBUG if environment_tools.IS_DEBUG else logging.INFO
|
|
7
|
+
if environment_tools.IS_DEV:
|
|
8
|
+
STREAM_DEFAULT = ENVIRONMENT_DEFAULT = Constants.DEVELOPMENT
|
|
9
|
+
else:
|
|
10
|
+
STREAM_DEFAULT = None
|
|
11
|
+
ENVIRONMENT_DEFAULT = Constants.PRODUCTION
|
|
13
12
|
|
|
13
|
+
LEVEL_DEFAULT = logging.DEBUG if environment_tools.IS_DEV else logging.INFO
|
|
14
14
|
|
|
15
15
|
def get_logger(name, version=None, host=Constants.FMTR_OBS_HOST, key=None, org=Constants.ORG_NAME,
|
|
16
|
-
stream=STREAM_DEFAULT,
|
|
17
|
-
environment=ENVIRONMENT_DEFAULT, level=LEVEL_DEFAULT):
|
|
16
|
+
stream=STREAM_DEFAULT, environment=ENVIRONMENT_DEFAULT, level=LEVEL_DEFAULT):
|
|
18
17
|
"""
|
|
19
18
|
|
|
20
19
|
Get a pre-configured logfire logger, if dependency is present, otherwise default to native logger.
|
|
21
20
|
|
|
22
21
|
"""
|
|
23
22
|
|
|
23
|
+
stream = stream or name
|
|
24
|
+
|
|
24
25
|
try:
|
|
25
26
|
import logfire
|
|
26
27
|
except ImportError:
|
|
@@ -30,36 +31,116 @@ def get_logger(name, version=None, host=Constants.FMTR_OBS_HOST, key=None, org=C
|
|
|
30
31
|
|
|
31
32
|
return logger
|
|
32
33
|
|
|
34
|
+
logger = logfire
|
|
35
|
+
|
|
33
36
|
if key is None:
|
|
34
|
-
key = environment_tools.get(Constants.FMTR_OBS_API_KEY_KEY)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
key = environment_tools.get(Constants.FMTR_OBS_API_KEY_KEY, default=None)
|
|
38
|
+
|
|
39
|
+
if key:
|
|
40
|
+
url = f"https://{host}/api/{org}/v1/traces"
|
|
41
|
+
headers = f"Authorization=Basic {key},stream-name={stream}"
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = url
|
|
44
|
+
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = headers
|
|
45
|
+
os.environ["OTEL_EXPORTER_OTLP_INSECURE"] = str(False).lower()
|
|
41
46
|
|
|
42
47
|
if not version:
|
|
43
48
|
from fmtr.tools import version_tools
|
|
44
49
|
version = version_tools.read()
|
|
45
50
|
|
|
51
|
+
lev_name_otel = get_otel_level_name(level)
|
|
52
|
+
|
|
53
|
+
console_opts = logfire.ConsoleOptions(
|
|
54
|
+
colors='always',
|
|
55
|
+
min_log_level=lev_name_otel,
|
|
56
|
+
)
|
|
57
|
+
|
|
46
58
|
logfire.configure(
|
|
47
59
|
service_name=name,
|
|
48
60
|
service_version=version,
|
|
49
61
|
environment=environment,
|
|
50
62
|
send_to_logfire=False,
|
|
51
|
-
console=
|
|
63
|
+
console=console_opts,
|
|
64
|
+
scrubbing=logfire.ScrubbingOptions(callback=null_scrubber)
|
|
52
65
|
)
|
|
53
66
|
|
|
54
|
-
logging.getLogger(name).setLevel(level)
|
|
55
|
-
|
|
56
|
-
logger = logfire
|
|
57
67
|
return logger
|
|
58
68
|
|
|
59
69
|
|
|
70
|
+
def null_scrubber(match):
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
Effectively disable scrubbing
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
return match.value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_current_level(logger):
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
Get the current console log level.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
level = logger.DEFAULT_LOGFIRE_INSTANCE.config.console.min_log_level
|
|
86
|
+
return level
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_logger_names():
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
Fetch current native logger names
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
return list(logging.getLogger().manager.loggerDict.keys())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
OTEL_TO_NATIVE = {
|
|
99
|
+
1: 1, # trace
|
|
100
|
+
3: 5, # debug
|
|
101
|
+
5: 10, # debug (canonical)
|
|
102
|
+
6: 13, # warn
|
|
103
|
+
8: 17, # error
|
|
104
|
+
9: 20, # info
|
|
105
|
+
10: 23, # notice
|
|
106
|
+
11: 25, # success/loguru
|
|
107
|
+
13: 30, # warning
|
|
108
|
+
17: 40, # error
|
|
109
|
+
21: 50, # fatal
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_otel_level_name(native_level: int) -> str:
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
Convert a native Python logging level number to an OTEL/logfire level name.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
from logfire._internal import constants
|
|
121
|
+
|
|
122
|
+
otel_num = constants.LOGGING_TO_OTEL_LEVEL_NUMBERS[native_level]
|
|
123
|
+
name = constants.NUMBER_TO_LEVEL[otel_num]
|
|
124
|
+
return name
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_native_level_from_otel(otel_name: str) -> int:
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
Convert an OTEL/logfire level name to a native Python logging level number.
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
from logfire._internal import constants
|
|
134
|
+
|
|
135
|
+
otel_num = constants.LEVEL_NUMBERS[otel_name]
|
|
136
|
+
level = OTEL_TO_NATIVE[otel_num]
|
|
137
|
+
return level
|
|
138
|
+
|
|
139
|
+
|
|
60
140
|
logger = get_logger(name=Constants.LIBRARY_NAME)
|
|
61
141
|
|
|
62
142
|
if __name__ == '__main__':
|
|
63
143
|
logger.info('Hello World')
|
|
64
144
|
logger.warning('test warning')
|
|
65
145
|
logger.debug('Hello World')
|
|
146
|
+
logger
|
fmtr/tools/mqtt_tools.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import aiomqtt
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass, asdict
|
|
4
|
+
from paho.mqtt.client import CleanStartOption, MQTT_CLEAN_START_FIRST_ONLY
|
|
5
|
+
from typing import Literal, Self
|
|
6
|
+
|
|
7
|
+
from fmtr.tools.logging_tools import logger, get_current_level, get_native_level_from_otel
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger("mqtt")
|
|
10
|
+
LOGGER.handlers.clear()
|
|
11
|
+
LOGGER.addHandler(logger.LogfireLoggingHandler())
|
|
12
|
+
LOGGER.propagate = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Args:
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
The (serialisable subset of the) init args for Client (e.g. for init via Pydantic Settings)
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
hostname: str
|
|
23
|
+
port: int = 1883
|
|
24
|
+
username: str | None = None
|
|
25
|
+
password: str | None = None
|
|
26
|
+
identifier: str | None = None
|
|
27
|
+
clean_session: bool | None = None
|
|
28
|
+
transport: Literal["tcp", "websockets", "unix"] = "tcp"
|
|
29
|
+
timeout: float | None = None
|
|
30
|
+
keepalive: int = 60
|
|
31
|
+
bind_address: str = ""
|
|
32
|
+
bind_port: int = 0
|
|
33
|
+
clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY
|
|
34
|
+
max_queued_incoming_messages: int | None = None
|
|
35
|
+
max_queued_outgoing_messages: int | None = None
|
|
36
|
+
max_inflight_messages: int | None = None
|
|
37
|
+
max_concurrent_outgoing_calls: int | None = None
|
|
38
|
+
tls_insecure: bool | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Client(aiomqtt.Client):
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
Client stub
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
LOGGER = LOGGER
|
|
51
|
+
SYNC_LOG_LEVEL = False
|
|
52
|
+
Args = Args
|
|
53
|
+
|
|
54
|
+
def __init__(self, *args, **kwargs):
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
Seems a little goofy to sync with logfire on every init, but unsure how to do it better.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
if self.SYNC_LOG_LEVEL:
|
|
61
|
+
self.sync_log_level()
|
|
62
|
+
super().__init__(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
def sync_log_level(self):
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
Sync log level with logfire, which might have changed since handler set.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
level = get_current_level(logger)
|
|
71
|
+
level_no = get_native_level_from_otel(level)
|
|
72
|
+
self.LOGGER.setLevel(level_no)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_args(cls, args_obj: Args, **kwargs) -> Self:
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
Initialise from Args dataclass.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
args = asdict(args_obj) | kwargs
|
|
82
|
+
return cls(**args)
|
|
83
|
+
|
|
84
|
+
class Will(aiomqtt.Will):
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
Will stub
|
|
88
|
+
|
|
89
|
+
"""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
get_hostname = socket.gethostname
|
|
8
|
+
get_fqdn = socket.getfqdn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class MAC:
|
|
13
|
+
value: int = field(default_factory=uuid.getnode)
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_string(cls, s: str):
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
Initialise from a string.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
s = s.lower().replace(":", "").replace("-", "")
|
|
23
|
+
return cls(int(s, 16))
|
|
24
|
+
|
|
25
|
+
@cached_property
|
|
26
|
+
def int(self):
|
|
27
|
+
return self.value
|
|
28
|
+
|
|
29
|
+
@cached_property
|
|
30
|
+
def hex(self):
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
Plain 12-character hex string, no separators.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
return f"{self.value:012x}"
|
|
37
|
+
|
|
38
|
+
@cached_property
|
|
39
|
+
def components(self):
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
Return list of six 2-digit hex strings.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
h = self.hex
|
|
46
|
+
return [h[i:i + 2] for i in range(0, 12, 2)]
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def hex_colon(self):
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
Colon-separated hex string (default standard).
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
return self.string(":")
|
|
56
|
+
|
|
57
|
+
@cached_property
|
|
58
|
+
def is_random(self) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
Detect if this MAC is likely randomly generated by uuid.getnode().
|
|
62
|
+
Checks the "multicast" bit (bit 1 of the first byte).
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
return bool(self.value & (1 << 40))
|
|
66
|
+
|
|
67
|
+
def string(self, sep: str = ":") -> str:
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
Return MAC as a string with separator `sep`.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
return sep.join(self.components)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from fmtr.tools.import_tools import MissingExtraMockModule
|
|
2
|
+
from fmtr.tools.path_tools.path_tools import Path, PackagePaths, root
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from fmtr.tools.path_tools.app_path_tools import AppPaths
|
|
6
|
+
except ModuleNotFoundError as exception:
|
|
7
|
+
AppPaths = MissingExtraMockModule('path.app', exception)
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from fmtr.tools.path_tools.type_path_tools import guess
|
|
11
|
+
except ModuleNotFoundError as exception:
|
|
12
|
+
guess = MissingExtraMockModule('path.type', exception)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import appdirs
|
|
2
|
+
|
|
3
|
+
from fmtr.tools.path_tools import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AppPaths:
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
Wrap appdirs to return Path objects
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
PathType = Path
|
|
13
|
+
|
|
14
|
+
def user_data_dir(self, appname=None, appauthor=None, version=None, roaming=False):
|
|
15
|
+
path_str = appdirs.user_data_dir(appname=appname, appauthor=appauthor, version=version, roaming=roaming)
|
|
16
|
+
return self.PathType(path_str)
|
|
17
|
+
|
|
18
|
+
def user_config_dir(self, appname=None, appauthor=None, version=None, roaming=False):
|
|
19
|
+
path_str = appdirs.user_config_dir(appname=appname, appauthor=appauthor, version=version, roaming=roaming)
|
|
20
|
+
return self.PathType(path_str)
|
|
21
|
+
|
|
22
|
+
def site_config_dir(self, appname=None, appauthor=None, version=None):
|
|
23
|
+
path_str = appdirs.site_config_dir(appname=appname, appauthor=appauthor, version=version, multipath=False)
|
|
24
|
+
return self.PathType(path_str)
|
|
25
|
+
|
|
26
|
+
def site_data_dir(self, appname=None, appauthor=None, version=None):
|
|
27
|
+
path_str = appdirs.site_data_dir(appname=appname, appauthor=appauthor, version=version, multipath=False)
|
|
28
|
+
return self.PathType(path_str)
|
|
29
|
+
|
|
30
|
+
def user_cache_dir(self, appname=None, appauthor=None, version=None):
|
|
31
|
+
path_str = appdirs.user_cache_dir(appname=appname, appauthor=appauthor, version=version)
|
|
32
|
+
return self.PathType(path_str)
|
|
33
|
+
|
|
34
|
+
def user_state_dir(self, appname=None, appauthor=None, version=None):
|
|
35
|
+
path_str = appdirs.user_state_dir(appname=appname, appauthor=appauthor, version=version)
|
|
36
|
+
return self.PathType(path_str)
|
|
37
|
+
|
|
38
|
+
def user_log_dir(self, appname=None, appauthor=None, version=None):
|
|
39
|
+
path_str = appdirs.user_log_dir(appname=appname, appauthor=appauthor, version=version)
|
|
40
|
+
return self.PathType(path_str)
|