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
@@ -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()
@@ -1,5 +1,8 @@
1
1
  from itertools import chain, batched
2
- from typing import List, Dict, Any
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)
@@ -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
- DEVELOPMENT = "development"
8
- PRODUCTION = "production"
9
- STREAM_DEFAULT = DEVELOPMENT
10
- ENVIRONMENT_DEFAULT = DEVELOPMENT
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
- url = f"https://{host}/api/{org}/v1/traces"
36
- headers = f"Authorization=Basic {key},stream-name={stream}"
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
- os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = url
39
- os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = headers
40
- os.environ["OTEL_EXPORTER_OTLP_INSECURE"] = str(False).lower()
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=logfire.ConsoleOptions(colors='always' if environment_tools.IS_DEBUG else 'auto')
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
@@ -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,14 @@
1
+ import importlib.metadata
2
+
3
+
4
+ def get_version(module):
5
+ """
6
+
7
+ Retrieve the version of a specified module.
8
+
9
+ """
10
+
11
+ if type(module) is not str:
12
+ module = module.__name__
13
+ version = importlib.metadata.version(module)
14
+ return version
@@ -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)