Add support for JSON fragments (#107213)

This commit is contained in:
J. Nick Koston 2024-01-07 17:36:31 -10:00 committed by GitHub
parent 50edc334de
commit d04e2d56da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 289 additions and 103 deletions

View file

@ -87,7 +87,7 @@ from .helpers.deprecation import (
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
from .helpers.json import json_dumps
from .helpers.json import json_dumps, json_fragment
from .util import dt as dt_util, location
from .util.async_ import (
cancelling,
@ -996,8 +996,6 @@ class HomeAssistant:
class Context:
"""The context that triggered something."""
__slots__ = ("user_id", "parent_id", "id", "origin_event", "_as_dict")
def __init__(
self,
user_id: str | None = None,
@ -1009,23 +1007,37 @@ class Context:
self.user_id = user_id
self.parent_id = parent_id
self.origin_event: Event | None = None
self._as_dict: ReadOnlyDict[str, str | None] | None = None
def __eq__(self, other: Any) -> bool:
"""Compare contexts."""
return bool(self.__class__ == other.__class__ and self.id == other.id)
@cached_property
def _as_dict(self) -> dict[str, str | None]:
"""Return a dictionary representation of the context.
Callers should be careful to not mutate the returned dictionary
as it will mutate the cached version.
"""
return {
"id": self.id,
"parent_id": self.parent_id,
"user_id": self.user_id,
}
def as_dict(self) -> ReadOnlyDict[str, str | None]:
"""Return a dictionary representation of the context."""
if not self._as_dict:
self._as_dict = ReadOnlyDict(
{
"id": self.id,
"parent_id": self.parent_id,
"user_id": self.user_id,
}
)
return self._as_dict
"""Return a ReadOnlyDict representation of the context."""
return self._as_read_only_dict
@cached_property
def _as_read_only_dict(self) -> ReadOnlyDict[str, str | None]:
"""Return a ReadOnlyDict representation of the context."""
return ReadOnlyDict(self._as_dict)
@cached_property
def json_fragment(self) -> json_fragment:
"""Return a JSON fragment of the context."""
return json_fragment(json_dumps(self._as_dict))
class EventOrigin(enum.Enum):
@ -1042,8 +1054,6 @@ class EventOrigin(enum.Enum):
class Event:
"""Representation of an event within the bus."""
__slots__ = ("event_type", "data", "origin", "time_fired", "context", "_as_dict")
def __init__(
self,
event_type: str,
@ -1062,26 +1072,54 @@ class Event:
id=ulid_at_time(dt_util.utc_to_timestamp(self.time_fired))
)
self.context = context
self._as_dict: ReadOnlyDict[str, Any] | None = None
if not context.origin_event:
context.origin_event = self
def as_dict(self) -> ReadOnlyDict[str, Any]:
@cached_property
def _as_dict(self) -> dict[str, Any]:
"""Create a dict representation of this Event.
Callers should be careful to not mutate the returned dictionary
as it will mutate the cached version.
"""
return {
"event_type": self.event_type,
"data": self.data,
"origin": self.origin.value,
"time_fired": self.time_fired.isoformat(),
# _as_dict is marked as protected
# to avoid callers outside of this module
# from misusing it by mistake.
"context": self.context._as_dict, # pylint: disable=protected-access
}
def as_dict(self) -> ReadOnlyDict[str, Any]:
"""Create a ReadOnlyDict representation of this Event.
Async friendly.
"""
if not self._as_dict:
self._as_dict = ReadOnlyDict(
{
"event_type": self.event_type,
"data": ReadOnlyDict(self.data),
"origin": self.origin.value,
"time_fired": self.time_fired.isoformat(),
"context": self.context.as_dict(),
}
)
return self._as_dict
return self._as_read_only_dict
@cached_property
def _as_read_only_dict(self) -> ReadOnlyDict[str, Any]:
"""Create a ReadOnlyDict representation of this Event."""
as_dict = self._as_dict
data = as_dict["data"]
context = as_dict["context"]
# json_fragment will serialize data from a ReadOnlyDict
# or a normal dict so its ok to have either. We only
# mutate the cache if someone asks for the as_dict version
# to avoid storing multiple copies of the data in memory.
if type(data) is not ReadOnlyDict:
as_dict["data"] = ReadOnlyDict(data)
if type(context) is not ReadOnlyDict:
as_dict["context"] = ReadOnlyDict(context)
return ReadOnlyDict(as_dict)
@cached_property
def json_fragment(self) -> json_fragment:
"""Return an event as a JSON fragment."""
return json_fragment(json_dumps(self._as_dict))
def __repr__(self) -> str:
"""Return the representation."""
@ -1397,7 +1435,6 @@ class State:
self.context = context or Context()
self.state_info = state_info
self.domain, self.object_id = split_entity_id(self.entity_id)
self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None
@property
def name(self) -> str:
@ -1406,36 +1443,66 @@ class State:
"_", " "
)
def as_dict(self) -> ReadOnlyDict[str, Collection[Any]]:
@cached_property
def _as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the State.
Callers should be careful to not mutate the returned dictionary
as it will mutate the cached version.
"""
last_changed_isoformat = self.last_changed.isoformat()
if self.last_changed == self.last_updated:
last_updated_isoformat = last_changed_isoformat
else:
last_updated_isoformat = self.last_updated.isoformat()
return {
"entity_id": self.entity_id,
"state": self.state,
"attributes": self.attributes,
"last_changed": last_changed_isoformat,
"last_updated": last_updated_isoformat,
# _as_dict is marked as protected
# to avoid callers outside of this module
# from misusing it by mistake.
"context": self.context._as_dict, # pylint: disable=protected-access
}
def as_dict(
self,
) -> ReadOnlyDict[str, datetime.datetime | Collection[Any]]:
"""Return a ReadOnlyDict representation of the State.
Async friendly.
To be used for JSON serialization.
Can be used for JSON serialization.
Ensures: state == State.from_dict(state.as_dict())
"""
if not self._as_dict:
last_changed_isoformat = self.last_changed.isoformat()
if self.last_changed == self.last_updated:
last_updated_isoformat = last_changed_isoformat
else:
last_updated_isoformat = self.last_updated.isoformat()
self._as_dict = ReadOnlyDict(
{
"entity_id": self.entity_id,
"state": self.state,
"attributes": self.attributes,
"last_changed": last_changed_isoformat,
"last_updated": last_updated_isoformat,
"context": self.context.as_dict(),
}
)
return self._as_dict
return self._as_read_only_dict
@cached_property
def _as_read_only_dict(
self,
) -> ReadOnlyDict[str, datetime.datetime | Collection[Any]]:
"""Return a ReadOnlyDict representation of the State."""
as_dict = self._as_dict
context = as_dict["context"]
# json_fragment will serialize data from a ReadOnlyDict
# or a normal dict so its ok to have either. We only
# mutate the cache if someone asks for the as_dict version
# to avoid storing multiple copies of the data in memory.
if type(context) is not ReadOnlyDict:
as_dict["context"] = ReadOnlyDict(context)
return ReadOnlyDict(as_dict)
@cached_property
def as_dict_json(self) -> str:
"""Return a JSON string of the State."""
return json_dumps(self.as_dict())
return json_dumps(self._as_dict)
@cached_property
def json_fragment(self) -> json_fragment:
"""Return a JSON fragment of the State."""
return json_fragment(self.as_dict_json)
@cached_property
def as_compressed_state(self) -> dict[str, Any]:
@ -1449,7 +1516,10 @@ class State:
if state_context.parent_id is None and state_context.user_id is None:
context: dict[str, Any] | str = state_context.id
else:
context = state_context.as_dict()
# _as_dict is marked as protected
# to avoid callers outside of this module
# from misusing it by mistake.
context = state_context._as_dict # pylint: disable=protected-access
compressed_state = {
COMPRESSED_STATE_STATE: self.state,
COMPRESSED_STATE_ATTRIBUTES: self.attributes,