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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- 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
|