aiohomematic 2026.1.29__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 (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
@@ -0,0 +1,719 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
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
+ - config_property: configuration-related properties.
10
+ - info_property: informational/metadata properties.
11
+ - state_property: dynamic state properties.
12
+ - hm_property: can be used to mark log_context or cached, where the other properties don't match
13
+
14
+ All decorators accept an optional keyword-only argument log_context. If set to
15
+ True, the property will be included in the LogContextMixin.log_context mapping.
16
+
17
+ Notes on caching
18
+ - Marked with cached=True always store on first access and invalidates on set/delete.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable, Mapping
24
+ import contextlib
25
+ from datetime import datetime
26
+ from enum import Enum, StrEnum
27
+ from functools import singledispatch
28
+ from typing import Any, Final, ParamSpec, Self, TypeVar, cast, overload
29
+ from weakref import WeakKeyDictionary
30
+
31
+ from aiohomematic._log_context_protocol import LogContextProtocol
32
+
33
+ __all__ = [
34
+ "DelegatedProperty",
35
+ "Kind",
36
+ "_GenericProperty",
37
+ "config_property",
38
+ "get_hm_property_by_kind",
39
+ "hm_property",
40
+ "info_property",
41
+ "state_property",
42
+ ]
43
+
44
+ P = ParamSpec("P")
45
+ T = TypeVar("T")
46
+ R = TypeVar("R")
47
+
48
+
49
+ class Kind(StrEnum):
50
+ """Enum for property feature flags."""
51
+
52
+ CONFIG = "config"
53
+ INFO = "info"
54
+ SIMPLE = "simple"
55
+ STATE = "state"
56
+
57
+
58
+ class DelegatedProperty[ValueT]:
59
+ """
60
+ Descriptor that delegates property access to a nested attribute path.
61
+
62
+ This descriptor simplifies forwarding properties that just return an
63
+ attribute from a nested object, eliminating boilerplate. It behaves
64
+ like a read-only @property and can be overridden by subclasses.
65
+
66
+ Supports the same features as the other property decorators:
67
+ - kind: Categorize as config/info/state/simple for get_hm_property_by_kind()
68
+ - cached: Cache the delegated value on first access
69
+ - log_context: Include in structured log context
70
+
71
+ Usage:
72
+ # Simple delegation:
73
+ interface: Final = DelegatedProperty[Interface](path="_config.interface")
74
+
75
+ # With caching and kind:
76
+ state: Final = DelegatedProperty[ClientState](
77
+ path="_state_machine.state",
78
+ kind=Kind.STATE,
79
+ cached=True,
80
+ )
81
+
82
+ # With log_context:
83
+ interface_id: Final = DelegatedProperty[str](
84
+ path="_config.interface_id",
85
+ kind=Kind.INFO,
86
+ log_context=True,
87
+ )
88
+
89
+ Note:
90
+ Do NOT use type annotations on the left side like `interface: Interface = ...`
91
+ as this confuses mypy. The generic type parameter provides type information.
92
+
93
+ """
94
+
95
+ __slots__ = ("_cache_attr", "_cached", "_doc", "_parts", "_path", "kind", "log_context")
96
+
97
+ __kwonly_check__ = False
98
+
99
+ def __init__(
100
+ self,
101
+ *,
102
+ path: str,
103
+ doc: str | None = None,
104
+ kind: Kind = Kind.SIMPLE,
105
+ cached: bool = False,
106
+ log_context: bool = False,
107
+ ) -> None:
108
+ """
109
+ Initialize the delegated property descriptor.
110
+
111
+ Args:
112
+ path: Dot-separated attribute path (e.g., "_config.interface").
113
+ doc: Optional docstring for the property.
114
+ kind: Categorize as config/info/state/simple.
115
+ cached: Enable per-instance caching of the delegated value.
116
+ log_context: Include this property in structured log context if True.
117
+
118
+ """
119
+ self._path: Final = path
120
+ self._parts: Final = tuple(path.split("."))
121
+ self._doc = doc
122
+ self.kind: Final = kind
123
+ self._cached: Final = cached
124
+ self.log_context = log_context
125
+ if cached:
126
+ # Use the property name (set in __set_name__) for cache attribute
127
+ # Fallback to path-based name if __set_name__ is not called
128
+ self._cache_attr = "" # Will be set in __set_name__
129
+
130
+ @overload
131
+ def __get__(self, instance: None, owner: type) -> Self: ...
132
+
133
+ @overload
134
+ def __get__(self, instance: object, owner: type) -> ValueT: ...
135
+
136
+ def __get__(self, instance: object | None, owner: type) -> ValueT | Self:
137
+ """Return the delegated attribute value."""
138
+ if instance is None:
139
+ return self
140
+
141
+ if not self._cached:
142
+ value: Any = instance
143
+ for part in self._parts:
144
+ value = getattr(value, part)
145
+ return cast(ValueT, value)
146
+
147
+ # Caching enabled - check cache first
148
+ cache_attr = self._cache_attr
149
+ try:
150
+ inst_dict = instance.__dict__
151
+ if cache_attr in inst_dict:
152
+ return cast(ValueT, inst_dict[cache_attr])
153
+
154
+ # Not cached yet, resolve and store
155
+ value = instance
156
+ for part in self._parts:
157
+ value = getattr(value, part)
158
+ inst_dict[cache_attr] = value
159
+ except AttributeError:
160
+ # Object uses __slots__, use slot for caching
161
+ try:
162
+ return cast(ValueT, getattr(instance, cache_attr))
163
+ except AttributeError:
164
+ # Cache slot exists but not set, compute and store
165
+ value = instance
166
+ for part in self._parts:
167
+ value = getattr(value, part)
168
+ setattr(instance, cache_attr, value)
169
+ return cast(ValueT, value)
170
+
171
+ def __set__(self, instance: object, value: Any) -> None:
172
+ """Raise AttributeError - this is a read-only property."""
173
+ raise AttributeError("can't set attribute") # i18n-exc: ignore
174
+
175
+ def __set_name__(self, owner: type, name: str) -> None:
176
+ """Set cache attribute name and validate cache slot exists when class is defined."""
177
+ if not self._cached:
178
+ return
179
+
180
+ # Set cache attribute name based on property name
181
+ self._cache_attr = f"_cached_{name}"
182
+
183
+ # Collect all slots from the class hierarchy
184
+ all_slots: set[str] = set()
185
+ has_dict = False
186
+
187
+ for cls in owner.__mro__:
188
+ if cls is object:
189
+ continue
190
+ if (cls_slots := getattr(cls, "__slots__", None)) is None:
191
+ # Class without __slots__ has __dict__
192
+ has_dict = True
193
+ continue
194
+ if isinstance(cls_slots, str):
195
+ all_slots.add(cls_slots)
196
+ else:
197
+ all_slots.update(cls_slots)
198
+ if "__dict__" in all_slots:
199
+ has_dict = True
200
+
201
+ # If class has __dict__, caching works via instance.__dict__
202
+ if has_dict:
203
+ return
204
+
205
+ # Check if cache slot exists in any class in the hierarchy
206
+ if (cache_attr := self._cache_attr) not in all_slots:
207
+ msg = f"Class {owner.__name__} uses __slots__ but is missing cache slot '{cache_attr}' required by DelegatedProperty(cached=True) on '{name}'"
208
+ raise TypeError(msg) # i18n-exc: ignore
209
+
210
+
211
+ class _GenericProperty[GETTER, SETTER](property):
212
+ """
213
+ Base descriptor used by all property decorators in this module.
214
+
215
+ Extends the built-in property to optionally cache the computed value on the
216
+ instance and to carry a log_context flag.
217
+
218
+ Args:
219
+ - fget/fset/fdel: Standard property callables.
220
+ - doc: Optional docstring of the property.
221
+ - cached: If True, the computed value is cached per instance and
222
+ invalidated when the descriptor receives a set/delete.
223
+ - log_context: If True, the property is included in get_attributes_for_log_context().
224
+
225
+ """
226
+
227
+ __kwonly_check__ = False
228
+
229
+ fget: Callable[[Any], GETTER] | None
230
+ fset: Callable[[Any, SETTER], None] | None
231
+ fdel: Callable[[Any], None] | None
232
+
233
+ def __init__(
234
+ self,
235
+ fget: Callable[[Any], GETTER] | None = None,
236
+ fset: Callable[[Any, SETTER], None] | None = None,
237
+ fdel: Callable[[Any], None] | None = None,
238
+ doc: str | None = None,
239
+ kind: Kind = Kind.SIMPLE,
240
+ cached: bool = False,
241
+ log_context: bool = False,
242
+ ) -> None:
243
+ """
244
+ Initialize the descriptor.
245
+
246
+ Mirrors the standard property signature and adds two options:
247
+ - kind: specify the kind of property (e.g. simple, cached, config, info, state).
248
+ - cached: enable per-instance caching of the computed value.
249
+ - log_context: mark this property as relevant for structured logging.
250
+ """
251
+ super().__init__(fget, fset, fdel, doc)
252
+ if doc is None and fget is not None:
253
+ doc = fget.__doc__
254
+ self.__doc__ = doc
255
+ self.kind: Final = kind
256
+ self._cached: Final = cached
257
+ self.log_context = log_context
258
+ self._cache_attr: str = ""
259
+ if cached:
260
+ if fget is not None:
261
+ func_name = fget.__name__
262
+ elif fset is not None:
263
+ func_name = fset.__name__
264
+ elif fdel is not None:
265
+ func_name = fdel.__name__
266
+ else:
267
+ func_name = "prop"
268
+ self._cache_attr = f"_cached_{func_name}"
269
+
270
+ def __delete__(self, instance: Any, /) -> None:
271
+ """Delete the attribute and invalidate cache if enabled."""
272
+ # Delete the cached value so it can be recomputed on next access.
273
+ if self._cached:
274
+ cache_attr = self._cache_attr
275
+ try:
276
+ instance.__dict__.pop(cache_attr, None)
277
+ except AttributeError:
278
+ # Object uses __slots__, reset slot to unset state
279
+ with contextlib.suppress(AttributeError):
280
+ delattr(instance, cache_attr)
281
+
282
+ if self.fdel is None:
283
+ raise AttributeError("can't delete attribute") # i18n-exc: ignore
284
+ self.fdel(instance)
285
+
286
+ @overload
287
+ def __get__(self, instance: None, owner: type[Any], /) -> Self: ...
288
+
289
+ @overload
290
+ def __get__(self, instance: object, owner: type[Any] | None = None, /) -> GETTER: ...
291
+
292
+ def __get__(self, instance: object | None, owner: type[Any] | None = None, /) -> GETTER | Self:
293
+ """
294
+ Return the attribute value.
295
+
296
+ If caching is enabled, compute on first access and return the per-instance
297
+ cached value on subsequent accesses.
298
+ """
299
+ if instance is None:
300
+ # Accessed from class, return the descriptor itself
301
+ return self
302
+
303
+ if (fget := self.fget) is None:
304
+ raise AttributeError("unreadable attribute") # i18n-exc: ignore
305
+
306
+ if not self._cached:
307
+ return fget(instance)
308
+
309
+ # Use direct __dict__ access when available for better performance
310
+ # Store cache_attr in local variable to avoid repeated attribute lookup
311
+ cache_attr = self._cache_attr
312
+
313
+ try:
314
+ inst_dict = instance.__dict__
315
+ # Use 'in' check first to distinguish between missing and None
316
+ if cache_attr in inst_dict:
317
+ return cast(GETTER, inst_dict[cache_attr])
318
+
319
+ # Not cached yet, compute and store
320
+ value = fget(instance)
321
+ inst_dict[cache_attr] = value
322
+ except AttributeError:
323
+ # Object uses __slots__, use slot for caching
324
+ try:
325
+ return cast(GETTER, getattr(instance, cache_attr))
326
+ except AttributeError:
327
+ # Cache slot exists but not set, compute and store
328
+ value = fget(instance)
329
+ setattr(instance, cache_attr, value)
330
+ return value
331
+
332
+ def __set__(self, instance: Any, value: Any, /) -> None:
333
+ """Set the attribute value and invalidate cache if enabled."""
334
+ # Delete the cached value so it can be recomputed on next access.
335
+ if self._cached:
336
+ cache_attr = self._cache_attr
337
+ try:
338
+ instance.__dict__.pop(cache_attr, None)
339
+ except AttributeError:
340
+ # Object uses __slots__, reset slot to unset state
341
+ with contextlib.suppress(AttributeError):
342
+ delattr(instance, cache_attr)
343
+
344
+ if self.fset is None:
345
+ raise AttributeError("can't set attribute") # i18n-exc: ignore
346
+ self.fset(instance, value)
347
+
348
+ def __set_name__(self, owner: type, name: str) -> None:
349
+ """Validate cache slot exists when class is defined."""
350
+ if not self._cached:
351
+ return
352
+
353
+ # Collect all slots from the class hierarchy
354
+ all_slots: set[str] = set()
355
+ has_dict = False
356
+
357
+ for cls in owner.__mro__:
358
+ if cls is object:
359
+ continue
360
+ if (cls_slots := getattr(cls, "__slots__", None)) is None:
361
+ # Class without __slots__ has __dict__
362
+ has_dict = True
363
+ continue
364
+ if isinstance(cls_slots, str):
365
+ all_slots.add(cls_slots)
366
+ else:
367
+ all_slots.update(cls_slots)
368
+ if "__dict__" in all_slots:
369
+ has_dict = True
370
+
371
+ # If class has __dict__, caching works via instance.__dict__
372
+ if has_dict:
373
+ return
374
+
375
+ # Check if cache slot exists in any class in the hierarchy
376
+ if (cache_attr := self._cache_attr) not in all_slots:
377
+ msg = f"Class {owner.__name__} uses __slots__ but is missing cache slot '{cache_attr}' required by @hm_property(cached=True) on '{name}'"
378
+ raise TypeError(msg) # i18n-exc: ignore
379
+
380
+ def deleter(self, fdel: Callable[[Any], None], /) -> _GenericProperty[GETTER, SETTER]:
381
+ """Return generic deleter."""
382
+ return type(self)(
383
+ fget=self.fget,
384
+ fset=self.fset,
385
+ fdel=fdel,
386
+ doc=self.__doc__,
387
+ kind=self.kind,
388
+ cached=self._cached,
389
+ log_context=self.log_context,
390
+ )
391
+
392
+ def getter(self, fget: Callable[[Any], GETTER], /) -> _GenericProperty[GETTER, SETTER]:
393
+ """Return generic getter."""
394
+ return type(self)(
395
+ fget=fget,
396
+ fset=self.fset,
397
+ fdel=self.fdel,
398
+ doc=self.__doc__,
399
+ kind=self.kind,
400
+ cached=self._cached,
401
+ log_context=self.log_context,
402
+ )
403
+
404
+ def setter(self, fset: Callable[[Any, SETTER], None], /) -> _GenericProperty[GETTER, SETTER]:
405
+ """Return generic setter."""
406
+ return type(self)(
407
+ fget=self.fget,
408
+ fset=fset,
409
+ fdel=self.fdel,
410
+ doc=self.__doc__,
411
+ kind=self.kind,
412
+ cached=self._cached,
413
+ log_context=self.log_context,
414
+ )
415
+
416
+
417
+ # ----- hm_property -----
418
+
419
+
420
+ @overload
421
+ def hm_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ... # kwonly: disable
422
+
423
+
424
+ @overload
425
+ def hm_property( # kwonly: disable
426
+ *, kind: Kind = ..., cached: bool = ..., log_context: bool = ...
427
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
428
+
429
+
430
+ def hm_property[PR]( # kwonly: disable
431
+ func: Callable[[Any], PR] | None = None,
432
+ *,
433
+ kind: Kind = Kind.SIMPLE,
434
+ cached: bool = False,
435
+ log_context: bool = False,
436
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
437
+ """
438
+ Decorate a method as a computed attribute.
439
+
440
+ Supports both usages:
441
+ - @hm_property
442
+ - @hm_property(kind=..., cached=True, log_context=True)
443
+
444
+ Args:
445
+ func: The function being decorated when used as @hm_property without
446
+ parentheses. When used as a factory (i.e., @hm_property(...)), this
447
+ is None and the returned callable expects the function to decorate.
448
+ kind: Specify the kind of property (e.g. simple, config, info, state).
449
+ cached: Optionally enable per-instance caching for this property.
450
+ log_context: Include this property in structured log context if True.
451
+
452
+ """
453
+ if func is None:
454
+
455
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
456
+ return _GenericProperty(f, kind=kind, cached=cached, log_context=log_context)
457
+
458
+ return wrapper
459
+ return _GenericProperty(func, kind=kind, cached=cached, log_context=log_context)
460
+
461
+
462
+ # ----- config_property -----
463
+
464
+
465
+ @overload
466
+ def config_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ... # kwonly: disable
467
+
468
+
469
+ @overload
470
+ def config_property( # kwonly: disable
471
+ *, cached: bool = ..., log_context: bool = ...
472
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
473
+
474
+
475
+ def config_property[PR]( # kwonly: disable
476
+ func: Callable[[Any], PR] | None = None,
477
+ *,
478
+ cached: bool = False,
479
+ log_context: bool = False,
480
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
481
+ """
482
+ Decorate a method as a configuration property.
483
+
484
+ Supports both usages:
485
+ - @config_property
486
+ - @config_property(cached=True, log_context=True)
487
+
488
+ Args:
489
+ func: The function being decorated when used as @config_property without
490
+ parentheses. When used as a factory (i.e., @config_property(...)), this is
491
+ None and the returned callable expects the function to decorate.
492
+ cached: Enable per-instance caching for this property when True.
493
+ log_context: Include this property in structured log context if True.
494
+
495
+ """
496
+ if func is None:
497
+
498
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
499
+ return _GenericProperty(f, kind=Kind.CONFIG, cached=cached, log_context=log_context)
500
+
501
+ return wrapper
502
+ return _GenericProperty(func, kind=Kind.CONFIG, cached=cached, log_context=log_context)
503
+
504
+
505
+ # ----- info_property -----
506
+
507
+
508
+ @overload
509
+ def info_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ... # kwonly: disable
510
+
511
+
512
+ @overload
513
+ def info_property( # kwonly: disable
514
+ *, cached: bool = ..., log_context: bool = ...
515
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
516
+
517
+
518
+ def info_property[PR]( # kwonly: disable
519
+ func: Callable[[Any], PR] | None = None,
520
+ *,
521
+ cached: bool = False,
522
+ log_context: bool = False,
523
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
524
+ """
525
+ Decorate a method as an informational/metadata property.
526
+
527
+ Supports both usages:
528
+ - @info_property
529
+ - @info_property(cached=True, log_context=True)
530
+
531
+ Args:
532
+ func: The function being decorated when used as @info_property without
533
+ parentheses. When used as a factory (i.e., @info_property(...)), this is
534
+ None and the returned callable expects the function to decorate.
535
+ cached: Enable per-instance caching for this property when True.
536
+ log_context: Include this property in structured log context if True.
537
+
538
+ """
539
+ if func is None:
540
+
541
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
542
+ return _GenericProperty(f, kind=Kind.INFO, cached=cached, log_context=log_context)
543
+
544
+ return wrapper
545
+ return _GenericProperty(func, kind=Kind.INFO, cached=cached, log_context=log_context)
546
+
547
+
548
+ # ----- state_property -----
549
+
550
+
551
+ @overload
552
+ def state_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ... # kwonly: disable
553
+
554
+
555
+ @overload
556
+ def state_property( # kwonly: disable
557
+ *, cached: bool = ..., log_context: bool = ...
558
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
559
+
560
+
561
+ def state_property[PR]( # kwonly: disable
562
+ func: Callable[[Any], PR] | None = None,
563
+ *,
564
+ cached: bool = False,
565
+ log_context: bool = False,
566
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
567
+ """
568
+ Decorate a method as a dynamic state property.
569
+
570
+ Supports both usages:
571
+ - @state_property
572
+ - @state_property(cached=True, log_context=True)
573
+
574
+ Args:
575
+ func: The function being decorated when used as @state_property without
576
+ parentheses. When used as a factory (i.e., @state_property(...)), this is
577
+ None and the returned callable expects the function to decorate.
578
+ cached: Enable per-instance caching for this property when True.
579
+ log_context: Include this property in structured log context if True.
580
+
581
+ """
582
+ if func is None:
583
+
584
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
585
+ return _GenericProperty(f, kind=Kind.STATE, cached=cached, log_context=log_context)
586
+
587
+ return wrapper
588
+ return _GenericProperty(func, kind=Kind.STATE, cached=cached, log_context=log_context)
589
+
590
+
591
+ # ----------
592
+
593
+
594
+ # Cache for per-class attribute names by decorator to avoid repeated dir() scans
595
+ # Use WeakKeyDictionary to allow classes to be garbage-collected without leaking cache entries.
596
+ # Structure: {cls: {decorator_class: (attr_name1, attr_name2, ...)}}
597
+ _PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[Kind, tuple[str, ...]]] = WeakKeyDictionary()
598
+
599
+
600
+ def get_hm_property_by_kind(*, data_object: Any, kind: Kind, context: bool = False) -> Mapping[str, Any]:
601
+ """
602
+ Collect properties from an object that are defined using a specific decorator.
603
+
604
+ Args:
605
+ data_object: The instance to inspect.
606
+ kind: The decorator class to use for filtering.
607
+ context: If True, only include properties where the descriptor has
608
+ log_context=True. When such a property's value implements LogContextProtocol,
609
+ its items are flattened into the result using a short prefix of the property
610
+ name (e.g. "p.key").
611
+
612
+ Returns:
613
+ Mapping[str, Any]: A mapping of attribute name to normalized value. Values are converted via
614
+ _get_text_value() to provide stable JSON/log-friendly types.
615
+
616
+ Notes:
617
+ Attribute NAMES are cached per (class, decorator) to avoid repeated dir()
618
+ scans. Values are never cached here since they are instance-dependent.
619
+ Getter exceptions are swallowed and represented as None so payload building
620
+ remains robust and side-effect free.
621
+
622
+ """
623
+ cls = data_object.__class__
624
+
625
+ # Get or create the per-class cache dict
626
+ if (decorator_cache := _PUBLIC_ATTR_CACHE.get(cls)) is None:
627
+ decorator_cache = {}
628
+ _PUBLIC_ATTR_CACHE[cls] = decorator_cache
629
+
630
+ if (names := decorator_cache.get(kind)) is None:
631
+ names = tuple(
632
+ y
633
+ for y in dir(cls)
634
+ if (gp := getattr(cls, y)) and isinstance(gp, _GenericProperty | DelegatedProperty) and gp.kind == kind
635
+ )
636
+ decorator_cache[kind] = names
637
+
638
+ result: dict[str, Any] = {}
639
+ for name in names:
640
+ if context and getattr(cls, name).log_context is False:
641
+ continue
642
+ try:
643
+ value = getattr(data_object, name)
644
+ if isinstance(value, LogContextProtocol):
645
+ result.update({f"{name[:1]}.{k}": v for k, v in value.log_context.items()})
646
+ else:
647
+ result[name] = _get_text_value(value)
648
+ except Exception:
649
+ # Avoid propagating side effects/errors from getters
650
+ result[name] = None
651
+ return result
652
+
653
+
654
+ @singledispatch
655
+ def _get_text_value(value: Any) -> Any: # kwonly: disable
656
+ """
657
+ Normalize values for payload/logging purposes.
658
+
659
+ Uses singledispatch for type-based conversion. Register new type handlers
660
+ with @_get_text_value.register(YourType).
661
+
662
+ Default behavior (unregistered types):
663
+ Returns value unchanged.
664
+
665
+ Registered conversions:
666
+ - list/tuple/set → tuple (items normalized recursively)
667
+ - Enum → str representation
668
+ - datetime → unix timestamp (float)
669
+
670
+ Args:
671
+ value: The input value to normalize into a log-/JSON-friendly representation.
672
+
673
+ Returns:
674
+ The normalized value, potentially converted as described above.
675
+
676
+ """
677
+ return value
678
+
679
+
680
+ @_get_text_value.register(list)
681
+ @_get_text_value.register(tuple)
682
+ @_get_text_value.register(set)
683
+ def _get_text_value_sequence(value: list[Any] | tuple[Any, ...] | set[Any]) -> tuple[Any, ...]: # kwonly: disable
684
+ """Convert sequence types to tuple with normalized items."""
685
+ return tuple(_get_text_value(v) for v in value)
686
+
687
+
688
+ @_get_text_value.register(Enum)
689
+ def _get_text_value_enum(value: Enum) -> str: # kwonly: disable
690
+ """Convert Enum to string representation."""
691
+ return str(value)
692
+
693
+
694
+ @_get_text_value.register(datetime)
695
+ def _get_text_value_datetime(value: datetime) -> float: # kwonly: disable
696
+ """Convert datetime to unix timestamp."""
697
+ return datetime.timestamp(value)
698
+
699
+
700
+ def get_hm_property_by_log_context(*, data_object: Any) -> Mapping[str, Any]:
701
+ """
702
+ Return combined log context attributes across all property categories.
703
+
704
+ Includes only properties declared with log_context=True and flattens
705
+ values that implement LogContextMixin by prefixing with a short key.
706
+
707
+ Args:
708
+ data_object: The instance from which to collect attributes marked for
709
+ log context across all property categories.
710
+
711
+ Returns:
712
+ Mapping[str, Any]: A mapping of attribute name to normalized value for logging.
713
+
714
+ """
715
+ result: dict[str, Any] = {}
716
+ for kind in Kind:
717
+ result.update(get_hm_property_by_kind(data_object=data_object, kind=kind, context=True))
718
+
719
+ return result