aiohomematic 2025.9.1__py3-none-any.whl → 2025.9.3__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.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/caches/dynamic.py +1 -6
- aiohomematic/central/__init__.py +34 -23
- aiohomematic/central/xml_rpc_server.py +1 -1
- aiohomematic/client/__init__.py +35 -29
- aiohomematic/client/json_rpc.py +44 -12
- aiohomematic/client/xml_rpc.py +53 -20
- aiohomematic/const.py +2 -2
- aiohomematic/decorators.py +56 -21
- aiohomematic/model/__init__.py +1 -1
- aiohomematic/model/calculated/__init__.py +1 -1
- aiohomematic/model/calculated/climate.py +1 -1
- aiohomematic/model/calculated/data_point.py +3 -3
- aiohomematic/model/calculated/operating_voltage_level.py +7 -21
- aiohomematic/model/calculated/support.py +20 -0
- aiohomematic/model/custom/__init__.py +1 -1
- aiohomematic/model/custom/climate.py +18 -18
- aiohomematic/model/custom/cover.py +1 -1
- aiohomematic/model/custom/data_point.py +1 -1
- aiohomematic/model/custom/light.py +1 -1
- aiohomematic/model/custom/lock.py +1 -1
- aiohomematic/model/custom/siren.py +1 -1
- aiohomematic/model/custom/switch.py +1 -1
- aiohomematic/model/custom/valve.py +1 -1
- aiohomematic/model/data_point.py +24 -24
- aiohomematic/model/device.py +22 -21
- aiohomematic/model/event.py +3 -8
- aiohomematic/model/generic/__init__.py +1 -1
- aiohomematic/model/generic/binary_sensor.py +1 -1
- aiohomematic/model/generic/button.py +1 -1
- aiohomematic/model/generic/data_point.py +4 -6
- aiohomematic/model/generic/number.py +1 -1
- aiohomematic/model/generic/select.py +1 -1
- aiohomematic/model/generic/sensor.py +1 -1
- aiohomematic/model/generic/switch.py +4 -4
- aiohomematic/model/generic/text.py +1 -1
- aiohomematic/model/hub/binary_sensor.py +1 -1
- aiohomematic/model/hub/button.py +2 -2
- aiohomematic/model/hub/data_point.py +4 -7
- aiohomematic/model/hub/number.py +1 -1
- aiohomematic/model/hub/select.py +2 -2
- aiohomematic/model/hub/sensor.py +1 -1
- aiohomematic/model/hub/switch.py +3 -3
- aiohomematic/model/hub/text.py +1 -1
- aiohomematic/model/support.py +1 -40
- aiohomematic/model/update.py +5 -4
- aiohomematic/property_decorators.py +511 -0
- aiohomematic/support.py +71 -85
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.3.dist-info}/METADATA +7 -5
- aiohomematic-2025.9.3.dist-info/RECORD +78 -0
- aiohomematic_support/client_local.py +5 -5
- aiohomematic/model/decorators.py +0 -194
- aiohomematic-2025.9.1.dist-info/RECORD +0 -78
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.3.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.3.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.3.dist-info}/top_level.txt +0 -0
aiohomematic/model/hub/text.py
CHANGED
|
@@ -7,9 +7,9 @@ from __future__ import annotations
|
|
|
7
7
|
from typing import cast
|
|
8
8
|
|
|
9
9
|
from aiohomematic.const import DataPointCategory
|
|
10
|
-
from aiohomematic.model.decorators import state_property
|
|
11
10
|
from aiohomematic.model.hub.data_point import GenericSysvarDataPoint
|
|
12
11
|
from aiohomematic.model.support import check_length_and_log
|
|
12
|
+
from aiohomematic.property_decorators import state_property
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class SysvarDpText(GenericSysvarDataPoint):
|
aiohomematic/model/support.py
CHANGED
|
@@ -33,18 +33,12 @@ from aiohomematic.const import (
|
|
|
33
33
|
)
|
|
34
34
|
from aiohomematic.model import device as hmd
|
|
35
35
|
from aiohomematic.model.custom import definition as hmed
|
|
36
|
-
from aiohomematic.model.decorators import (
|
|
37
|
-
get_public_attributes_for_config_property,
|
|
38
|
-
get_public_attributes_for_info_property,
|
|
39
|
-
get_public_attributes_for_state_property,
|
|
40
|
-
)
|
|
41
36
|
from aiohomematic.support import to_bool
|
|
42
37
|
|
|
43
38
|
__all__ = [
|
|
44
39
|
"ChannelNameData",
|
|
45
40
|
"DataPointNameData",
|
|
46
41
|
"GenericParameterType",
|
|
47
|
-
"PayloadMixin",
|
|
48
42
|
"check_channel_is_the_only_primary_channel",
|
|
49
43
|
"convert_value",
|
|
50
44
|
"generate_channel_unique_id",
|
|
@@ -70,39 +64,6 @@ _BINARY_SENSOR_TRUE_VALUE_DICT_FOR_VALUE_LIST: Final[Mapping[tuple[str, ...], st
|
|
|
70
64
|
}
|
|
71
65
|
|
|
72
66
|
|
|
73
|
-
class PayloadMixin:
|
|
74
|
-
"""Mixin to add payload methods to class."""
|
|
75
|
-
|
|
76
|
-
__slots__ = ()
|
|
77
|
-
|
|
78
|
-
@property
|
|
79
|
-
def config_payload(self) -> Mapping[str, Any]:
|
|
80
|
-
"""Return the config payload."""
|
|
81
|
-
return {
|
|
82
|
-
key: value
|
|
83
|
-
for key, value in get_public_attributes_for_config_property(data_object=self).items()
|
|
84
|
-
if value is not None
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def info_payload(self) -> Mapping[str, Any]:
|
|
89
|
-
"""Return the info payload."""
|
|
90
|
-
return {
|
|
91
|
-
key: value
|
|
92
|
-
for key, value in get_public_attributes_for_info_property(data_object=self).items()
|
|
93
|
-
if value is not None
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@property
|
|
97
|
-
def state_payload(self) -> Mapping[str, Any]:
|
|
98
|
-
"""Return the state payload."""
|
|
99
|
-
return {
|
|
100
|
-
key: value
|
|
101
|
-
for key, value in get_public_attributes_for_state_property(data_object=self).items()
|
|
102
|
-
if value is not None
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
67
|
class ChannelNameData:
|
|
107
68
|
"""Dataclass for channel name parts."""
|
|
108
69
|
|
|
@@ -618,7 +579,7 @@ def get_index_of_value_from_value_list(
|
|
|
618
579
|
value: SYSVAR_TYPE, value_list: tuple[str, ...] | list[str] | None
|
|
619
580
|
) -> int | None:
|
|
620
581
|
"""Check if value is in value list."""
|
|
621
|
-
if value is not None and isinstance(value,
|
|
582
|
+
if value is not None and isinstance(value, str | StrEnum) and value_list is not None and value in value_list:
|
|
622
583
|
return value_list.index(value)
|
|
623
584
|
|
|
624
585
|
return None
|
aiohomematic/model/update.py
CHANGED
|
@@ -21,8 +21,9 @@ from aiohomematic.decorators import inspector
|
|
|
21
21
|
from aiohomematic.exceptions import AioHomematicException
|
|
22
22
|
from aiohomematic.model import device as hmd
|
|
23
23
|
from aiohomematic.model.data_point import CallbackDataPoint
|
|
24
|
-
from aiohomematic.model.
|
|
25
|
-
from aiohomematic.
|
|
24
|
+
from aiohomematic.model.support import DataPointPathData, generate_unique_id
|
|
25
|
+
from aiohomematic.property_decorators import config_property, state_property
|
|
26
|
+
from aiohomematic.support import PayloadMixin
|
|
26
27
|
|
|
27
28
|
__all__ = ["DpUpdate"]
|
|
28
29
|
|
|
@@ -130,12 +131,12 @@ class DpUpdate(CallbackDataPoint, PayloadMixin):
|
|
|
130
131
|
self._custom_id = None
|
|
131
132
|
self._device.unregister_firmware_update_callback(cb)
|
|
132
133
|
|
|
133
|
-
@inspector
|
|
134
|
+
@inspector
|
|
134
135
|
async def update_firmware(self, refresh_after_update_intervals: tuple[int, ...]) -> bool:
|
|
135
136
|
"""Turn the update on."""
|
|
136
137
|
return await self._device.update_firmware(refresh_after_update_intervals=refresh_after_update_intervals)
|
|
137
138
|
|
|
138
|
-
@inspector
|
|
139
|
+
@inspector
|
|
139
140
|
async def refresh_firmware_data(self) -> None:
|
|
140
141
|
"""Refresh device firmware data."""
|
|
141
142
|
await self._device.central.refresh_firmware_data(device_address=self._device.address)
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
3
|
+
"""
|
|
4
|
+
Decorators and helpers for declaring public attributes on data point classes.
|
|
5
|
+
|
|
6
|
+
This module provides four decorator factories that behave like the built-in
|
|
7
|
+
@property, but additionally annotate properties with a semantic category so they
|
|
8
|
+
can be automatically collected to build payloads and log contexts:
|
|
9
|
+
- cached_property: computed once per instance and cached until the value is
|
|
10
|
+
invalidated by a setter/deleter on the same descriptor.
|
|
11
|
+
- config_property: configuration-related properties.
|
|
12
|
+
- info_property: informational/metadata properties.
|
|
13
|
+
- state_property: dynamic state properties.
|
|
14
|
+
|
|
15
|
+
All decorators accept an optional keyword-only argument log_context. If set to
|
|
16
|
+
True, the property will be included in the LogContextMixin.log_context mapping.
|
|
17
|
+
|
|
18
|
+
Notes on caching
|
|
19
|
+
- cached_property always caches on first access and invalidates on set/delete.
|
|
20
|
+
- The other decorators can be created with cached=True to enable the same
|
|
21
|
+
behavior when desired.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Callable, Mapping
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from enum import Enum, StrEnum
|
|
29
|
+
from typing import Any, Final, ParamSpec, TypeVar, cast, overload
|
|
30
|
+
from weakref import WeakKeyDictionary
|
|
31
|
+
|
|
32
|
+
from aiohomematic import support as hms
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"config_property",
|
|
36
|
+
"get_hm_property_by_kind",
|
|
37
|
+
"info_property",
|
|
38
|
+
"state_property",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
P = ParamSpec("P")
|
|
42
|
+
T = TypeVar("T")
|
|
43
|
+
R = TypeVar("R")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Kind(StrEnum):
|
|
47
|
+
"""Enum for property feature flags."""
|
|
48
|
+
|
|
49
|
+
CONFIG = "config"
|
|
50
|
+
INFO = "info"
|
|
51
|
+
SIMPLE = "simple"
|
|
52
|
+
STATE = "state"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _GenericProperty[GETTER, SETTER](property):
|
|
56
|
+
"""
|
|
57
|
+
Base descriptor used by all property decorators in this module.
|
|
58
|
+
|
|
59
|
+
Extends the built-in property to optionally cache the computed value on the
|
|
60
|
+
instance and to carry a log_context flag.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
- fget/fset/fdel: Standard property callables.
|
|
64
|
+
- doc: Optional docstring of the property.
|
|
65
|
+
- cached: If True, the computed value is cached per instance and
|
|
66
|
+
invalidated when the descriptor receives a set/delete.
|
|
67
|
+
- log_context: If True, the property is included in get_attributes_for_log_context().
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
fget: Callable[[Any], GETTER] | None
|
|
72
|
+
fset: Callable[[Any, SETTER], None] | None
|
|
73
|
+
fdel: Callable[[Any], None] | None
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
fget: Callable[[Any], GETTER] | None = None,
|
|
78
|
+
fset: Callable[[Any, SETTER], None] | None = None,
|
|
79
|
+
fdel: Callable[[Any], None] | None = None,
|
|
80
|
+
doc: str | None = None,
|
|
81
|
+
kind: Kind = Kind.SIMPLE,
|
|
82
|
+
cached: bool = False,
|
|
83
|
+
log_context: bool = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Initialize the descriptor.
|
|
87
|
+
|
|
88
|
+
Mirrors the standard property signature and adds two options:
|
|
89
|
+
- kind: specify the kind of property (e.g. simple, cached, config, info, state).
|
|
90
|
+
- cached: enable per-instance caching of the computed value.
|
|
91
|
+
- log_context: mark this property as relevant for structured logging.
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(fget, fset, fdel, doc)
|
|
94
|
+
if doc is None and fget is not None:
|
|
95
|
+
doc = fget.__doc__
|
|
96
|
+
self.__doc__ = doc
|
|
97
|
+
self.kind: Final = kind
|
|
98
|
+
self._cached: Final = cached
|
|
99
|
+
self.log_context = log_context
|
|
100
|
+
if cached:
|
|
101
|
+
if fget is not None:
|
|
102
|
+
func_name = fget.__name__
|
|
103
|
+
elif fset is not None:
|
|
104
|
+
func_name = fset.__name__
|
|
105
|
+
elif fdel is not None:
|
|
106
|
+
func_name = fdel.__name__
|
|
107
|
+
else:
|
|
108
|
+
func_name = "prop"
|
|
109
|
+
self._cache_attr = f"_cached_{func_name}" # Default name of the cache attribute
|
|
110
|
+
|
|
111
|
+
def getter(self, fget: Callable[[Any], GETTER], /) -> _GenericProperty:
|
|
112
|
+
"""Return generic getter."""
|
|
113
|
+
return type(self)(
|
|
114
|
+
fget=fget,
|
|
115
|
+
fset=self.fset,
|
|
116
|
+
fdel=self.fdel,
|
|
117
|
+
doc=self.__doc__,
|
|
118
|
+
kind=self.kind,
|
|
119
|
+
cached=self._cached,
|
|
120
|
+
log_context=self.log_context,
|
|
121
|
+
) # pragma: no cover
|
|
122
|
+
|
|
123
|
+
def setter(self, fset: Callable[[Any, SETTER], None], /) -> _GenericProperty:
|
|
124
|
+
"""Return generic setter."""
|
|
125
|
+
return type(self)(
|
|
126
|
+
fget=self.fget,
|
|
127
|
+
fset=fset,
|
|
128
|
+
fdel=self.fdel,
|
|
129
|
+
doc=self.__doc__,
|
|
130
|
+
kind=self.kind,
|
|
131
|
+
cached=self._cached,
|
|
132
|
+
log_context=self.log_context,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def deleter(self, fdel: Callable[[Any], None], /) -> _GenericProperty:
|
|
136
|
+
"""Return generic deleter."""
|
|
137
|
+
return type(self)(
|
|
138
|
+
fget=self.fget,
|
|
139
|
+
fset=self.fset,
|
|
140
|
+
fdel=fdel,
|
|
141
|
+
doc=self.__doc__,
|
|
142
|
+
kind=self.kind,
|
|
143
|
+
cached=self._cached,
|
|
144
|
+
log_context=self.log_context,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def __get__(self, instance: Any, gtype: type | None = None, /) -> GETTER: # type: ignore[override]
|
|
148
|
+
"""
|
|
149
|
+
Return the attribute value.
|
|
150
|
+
|
|
151
|
+
If caching is enabled, compute on first access and return the per-instance
|
|
152
|
+
cached value on subsequent accesses.
|
|
153
|
+
"""
|
|
154
|
+
if instance is None:
|
|
155
|
+
# Accessed from class, return the descriptor itself
|
|
156
|
+
return cast(GETTER, self)
|
|
157
|
+
if self.fget is None:
|
|
158
|
+
raise AttributeError("unreadable attribute") # pragma: no cover
|
|
159
|
+
|
|
160
|
+
if not self._cached:
|
|
161
|
+
return self.fget(instance)
|
|
162
|
+
|
|
163
|
+
# If the cached value is not set yet, compute and store it
|
|
164
|
+
if not hasattr(instance, self._cache_attr):
|
|
165
|
+
value = self.fget(instance)
|
|
166
|
+
setattr(instance, self._cache_attr, value)
|
|
167
|
+
|
|
168
|
+
# Return the cached value
|
|
169
|
+
return cast(GETTER, getattr(instance, self._cache_attr))
|
|
170
|
+
|
|
171
|
+
def __set__(self, instance: Any, value: Any, /) -> None:
|
|
172
|
+
"""Set the attribute value and invalidate cache if enabled."""
|
|
173
|
+
# Delete the cached value so it can be recomputed on next access.
|
|
174
|
+
if self._cached and hasattr(instance, self._cache_attr):
|
|
175
|
+
delattr(instance, self._cache_attr)
|
|
176
|
+
|
|
177
|
+
if self.fset is None:
|
|
178
|
+
raise AttributeError("can't set attribute") # pragma: no cover
|
|
179
|
+
self.fset(instance, value)
|
|
180
|
+
|
|
181
|
+
def __delete__(self, instance: Any, /) -> None:
|
|
182
|
+
"""Delete the attribute and invalidate cache if enabled."""
|
|
183
|
+
|
|
184
|
+
# Delete the cached value so it can be recomputed on next access.
|
|
185
|
+
if self._cached and hasattr(instance, self._cache_attr):
|
|
186
|
+
delattr(instance, self._cache_attr)
|
|
187
|
+
|
|
188
|
+
if self.fdel is None:
|
|
189
|
+
raise AttributeError("can't delete attribute") # pragma: no cover
|
|
190
|
+
self.fdel(instance)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ----- hm_property -----
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@overload
|
|
197
|
+
def hm_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@overload
|
|
201
|
+
def hm_property(
|
|
202
|
+
*, kind: Kind = ..., cached: bool = ..., log_context: bool = ...
|
|
203
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def hm_property[PR](
|
|
207
|
+
func: Callable[[Any], PR] | None = None,
|
|
208
|
+
*,
|
|
209
|
+
kind: Kind = Kind.SIMPLE,
|
|
210
|
+
cached: bool = False,
|
|
211
|
+
log_context: bool = False,
|
|
212
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
213
|
+
"""
|
|
214
|
+
Decorate a method as a computed attribute.
|
|
215
|
+
|
|
216
|
+
Supports both usages:
|
|
217
|
+
- @hm_property
|
|
218
|
+
- @hm_property(kind=..., cached=True, log_context=True)
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
func: The function being decorated when used as @hm_property without
|
|
222
|
+
parentheses. When used as a factory (i.e., @hm_property(...)), this
|
|
223
|
+
is None and the returned callable expects the function to decorate.
|
|
224
|
+
kind: Specify the kind of property (e.g. simple, config, info, state).
|
|
225
|
+
cached: Optionally enable per-instance caching for this property.
|
|
226
|
+
log_context: Include this property in structured log context if True.
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
if func is None:
|
|
230
|
+
|
|
231
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
232
|
+
return _GenericProperty(f, kind=kind, cached=cached, log_context=log_context)
|
|
233
|
+
|
|
234
|
+
return wrapper
|
|
235
|
+
return _GenericProperty(func, kind=kind, cached=cached, log_context=log_context)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ----- cached_property -----
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@overload
|
|
242
|
+
def cached_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@overload
|
|
246
|
+
def cached_property(*, log_context: bool = ...) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def cached_property[PR](
|
|
250
|
+
func: Callable[[Any], PR] | None = None,
|
|
251
|
+
*,
|
|
252
|
+
log_context: bool = False,
|
|
253
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
254
|
+
"""
|
|
255
|
+
Decorate a method as a computed attribute with per-instance caching.
|
|
256
|
+
|
|
257
|
+
Supports both usages:
|
|
258
|
+
- @cached_property
|
|
259
|
+
- @cached_property(log_context=True)
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
func: The function being decorated when used as @cached_property without
|
|
263
|
+
parentheses. When used as a factory (i.e., @cached_property(...)), this
|
|
264
|
+
is None and the returned callable expects the function to decorate.
|
|
265
|
+
log_context: Include this property in structured log context if True.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
if func is None:
|
|
269
|
+
|
|
270
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
271
|
+
return _GenericProperty(f, kind=Kind.SIMPLE, cached=True, log_context=log_context)
|
|
272
|
+
|
|
273
|
+
return wrapper
|
|
274
|
+
return _GenericProperty(func, kind=Kind.SIMPLE, cached=True, log_context=log_context)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ----- config_property -----
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@overload
|
|
281
|
+
def config_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@overload
|
|
285
|
+
def config_property(
|
|
286
|
+
*, cached: bool = ..., log_context: bool = ...
|
|
287
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def config_property[PR](
|
|
291
|
+
func: Callable[[Any], PR] | None = None,
|
|
292
|
+
*,
|
|
293
|
+
cached: bool = False,
|
|
294
|
+
log_context: bool = False,
|
|
295
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
296
|
+
"""
|
|
297
|
+
Decorate a method as a configuration property.
|
|
298
|
+
|
|
299
|
+
Supports both usages:
|
|
300
|
+
- @config_property
|
|
301
|
+
- @config_property(cached=True, log_context=True)
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
func: The function being decorated when used as @config_property without
|
|
305
|
+
parentheses. When used as a factory (i.e., @config_property(...)), this is
|
|
306
|
+
None and the returned callable expects the function to decorate.
|
|
307
|
+
cached: Enable per-instance caching for this property when True.
|
|
308
|
+
log_context: Include this property in structured log context if True.
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
if func is None:
|
|
312
|
+
|
|
313
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
314
|
+
return _GenericProperty(f, kind=Kind.CONFIG, cached=cached, log_context=log_context)
|
|
315
|
+
|
|
316
|
+
return wrapper
|
|
317
|
+
return _GenericProperty(func, kind=Kind.CONFIG, cached=cached, log_context=log_context)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ----- info_property -----
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@overload
|
|
324
|
+
def info_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@overload
|
|
328
|
+
def info_property(
|
|
329
|
+
*, cached: bool = ..., log_context: bool = ...
|
|
330
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def info_property[PR](
|
|
334
|
+
func: Callable[[Any], PR] | None = None,
|
|
335
|
+
*,
|
|
336
|
+
cached: bool = False,
|
|
337
|
+
log_context: bool = False,
|
|
338
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
339
|
+
"""
|
|
340
|
+
Decorate a method as an informational/metadata property.
|
|
341
|
+
|
|
342
|
+
Supports both usages:
|
|
343
|
+
- @info_property
|
|
344
|
+
- @info_property(cached=True, log_context=True)
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
func: The function being decorated when used as @info_property without
|
|
348
|
+
parentheses. When used as a factory (i.e., @info_property(...)), this is
|
|
349
|
+
None and the returned callable expects the function to decorate.
|
|
350
|
+
cached: Enable per-instance caching for this property when True.
|
|
351
|
+
log_context: Include this property in structured log context if True.
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
if func is None:
|
|
355
|
+
|
|
356
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
357
|
+
return _GenericProperty(f, kind=Kind.INFO, cached=cached, log_context=log_context)
|
|
358
|
+
|
|
359
|
+
return wrapper
|
|
360
|
+
return _GenericProperty(func, kind=Kind.INFO, cached=cached, log_context=log_context)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ----- state_property -----
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@overload
|
|
367
|
+
def state_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@overload
|
|
371
|
+
def state_property(
|
|
372
|
+
*, cached: bool = ..., log_context: bool = ...
|
|
373
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def state_property[PR](
|
|
377
|
+
func: Callable[[Any], PR] | None = None,
|
|
378
|
+
*,
|
|
379
|
+
cached: bool = False,
|
|
380
|
+
log_context: bool = False,
|
|
381
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
382
|
+
"""
|
|
383
|
+
Decorate a method as a dynamic state property.
|
|
384
|
+
|
|
385
|
+
Supports both usages:
|
|
386
|
+
- @state_property
|
|
387
|
+
- @state_property(cached=True, log_context=True)
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
func: The function being decorated when used as @state_property without
|
|
391
|
+
parentheses. When used as a factory (i.e., @state_property(...)), this is
|
|
392
|
+
None and the returned callable expects the function to decorate.
|
|
393
|
+
cached: Enable per-instance caching for this property when True.
|
|
394
|
+
log_context: Include this property in structured log context if True.
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
if func is None:
|
|
398
|
+
|
|
399
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
400
|
+
return _GenericProperty(f, kind=Kind.STATE, cached=cached, log_context=log_context)
|
|
401
|
+
|
|
402
|
+
return wrapper
|
|
403
|
+
return _GenericProperty(func, kind=Kind.STATE, cached=cached, log_context=log_context)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ----------
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Cache for per-class attribute names by decorator to avoid repeated dir() scans
|
|
410
|
+
# Use WeakKeyDictionary to allow classes to be garbage-collected without leaking cache entries.
|
|
411
|
+
# Structure: {cls: {decorator_class: (attr_name1, attr_name2, ...)}}
|
|
412
|
+
_PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[Kind, tuple[str, ...]]] = WeakKeyDictionary()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def get_hm_property_by_kind(data_object: Any, kind: Kind, context: bool = False) -> Mapping[str, Any]:
|
|
416
|
+
"""
|
|
417
|
+
Collect properties from an object that are defined using a specific decorator.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
data_object: The instance to inspect.
|
|
421
|
+
kind: The decorator class to use for filtering.
|
|
422
|
+
context: If True, only include properties where the descriptor has
|
|
423
|
+
log_context=True. When such a property's value is a LogContextMixin, its
|
|
424
|
+
items are flattened into the result using a short prefix of the property
|
|
425
|
+
name (e.g. "p.key").
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Mapping[str, Any]: A mapping of attribute name to normalized value. Values are converted via
|
|
429
|
+
_get_text_value() to provide stable JSON/log-friendly types.
|
|
430
|
+
|
|
431
|
+
Notes:
|
|
432
|
+
Attribute NAMES are cached per (class, decorator) to avoid repeated dir()
|
|
433
|
+
scans. Values are never cached here since they are instance-dependent.
|
|
434
|
+
Getter exceptions are swallowed and represented as None so payload building
|
|
435
|
+
remains robust and side-effect free.
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
cls = data_object.__class__
|
|
439
|
+
|
|
440
|
+
# Get or create the per-class cache dict
|
|
441
|
+
if (decorator_cache := _PUBLIC_ATTR_CACHE.get(cls)) is None:
|
|
442
|
+
decorator_cache = {}
|
|
443
|
+
_PUBLIC_ATTR_CACHE[cls] = decorator_cache
|
|
444
|
+
|
|
445
|
+
if (names := decorator_cache.get(kind)) is None:
|
|
446
|
+
names = tuple(
|
|
447
|
+
y for y in dir(cls) if (gp := getattr(cls, y)) and isinstance(gp, _GenericProperty) and gp.kind == kind
|
|
448
|
+
)
|
|
449
|
+
decorator_cache[kind] = names
|
|
450
|
+
|
|
451
|
+
result: dict[str, Any] = {}
|
|
452
|
+
for name in names:
|
|
453
|
+
if context and getattr(cls, name).log_context is False:
|
|
454
|
+
continue
|
|
455
|
+
try:
|
|
456
|
+
value = getattr(data_object, name)
|
|
457
|
+
if isinstance(value, hms.LogContextMixin):
|
|
458
|
+
result.update({f"{name[:1]}.{k}": v for k, v in value.log_context.items()})
|
|
459
|
+
else:
|
|
460
|
+
result[name] = _get_text_value(value)
|
|
461
|
+
except Exception:
|
|
462
|
+
# Avoid propagating side effects/errors from getters
|
|
463
|
+
result[name] = None
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _get_text_value(value: Any) -> Any:
|
|
468
|
+
"""
|
|
469
|
+
Normalize values for payload/logging purposes.
|
|
470
|
+
|
|
471
|
+
- list/tuple/set are converted to tuples and their items normalized recursively
|
|
472
|
+
- Enum values are converted to their string representation
|
|
473
|
+
- datetime objects are converted to unix timestamps (float)
|
|
474
|
+
- all other types are returned unchanged
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
value: The input value to normalize into a log-/JSON-friendly representation.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Any: The normalized value, potentially converted as described above.
|
|
481
|
+
|
|
482
|
+
"""
|
|
483
|
+
if isinstance(value, list | tuple | set):
|
|
484
|
+
return tuple(_get_text_value(v) for v in value)
|
|
485
|
+
if isinstance(value, Enum):
|
|
486
|
+
return str(value)
|
|
487
|
+
if isinstance(value, datetime):
|
|
488
|
+
return datetime.timestamp(value)
|
|
489
|
+
return value
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def get_hm_property_by_log_context(data_object: Any) -> Mapping[str, Any]:
|
|
493
|
+
"""
|
|
494
|
+
Return combined log context attributes across all property categories.
|
|
495
|
+
|
|
496
|
+
Includes only properties declared with log_context=True and flattens
|
|
497
|
+
values that implement LogContextMixin by prefixing with a short key.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
data_object: The instance from which to collect attributes marked for
|
|
501
|
+
log context across all property categories.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Mapping[str, Any]: A mapping of attribute name to normalized value for logging.
|
|
505
|
+
|
|
506
|
+
"""
|
|
507
|
+
result: dict[str, Any] = {}
|
|
508
|
+
for kind in Kind:
|
|
509
|
+
result.update(get_hm_property_by_kind(data_object=data_object, kind=kind, context=True))
|
|
510
|
+
|
|
511
|
+
return result
|