"""
Elementary class implementing hierarchy.
Author: Juha Meskanen
Date: 2024-10-26
"""
from __future__ import annotations
from typing import Callable, Any, Dict, List, Optional, cast
from typing_extensions import override
from .url import URL
from .masterpiece import MasterPiece
[docs]
class Composite(MasterPiece):
"""Class implementing hierarchy. Objects of this class can consist of children.
This class can be used for grouping masterpieces into larger entities to model
any real world apparatus. Hierarchical entities can be manipulated exactly the
same way as the most primitive objects, e.g. copied, serialized, or manipulated
via do() method:
Example:
::
sensors = Composite("motionsensors")
sensors.add(ShellyMotionSensor("downstairs"))
sensors.add(ShellyMotionSensor("upstairs"))
def some_action(node: MasterPiece, context : MyContext) -> bool:
...
return 1 # continue traversal
# Run traversal
sensors.do(some_action, my_context)
"""
[docs]
def __init__(
self,
name: str = "group",
payload: Optional[MasterPiece] = None,
children: Optional[list[MasterPiece]] = None,
parent: Optional[MasterPiece] = None, # New parameter
) -> None:
super().__init__(name, payload)
self.children: List[MasterPiece] = children or []
self.role: str = "union"
self.parent = parent
for child in self.children:
child.parent = self
[docs]
@override
def to_dict(self) -> Dict[str, Any]:
data = super().to_dict()
data["_group"] = {
"role": self.role,
"children": [child.to_dict() for child in self.children],
}
return data
[docs]
@override
def from_dict(self, data: Dict[str, Any]) -> None:
"""Recursively deserialize the group from a dictionary, including its
children.
Args:
data (dict): data to deserialize from.
"""
super().from_dict(data)
for key, value in data.get("_group", {}).items():
if key == "children":
for child_dict in value:
child = MasterPiece.instantiate(child_dict["_class"])
self.add(child)
child.from_dict(child_dict)
else:
setattr(self, key, value)
[docs]
@override
def do(
self,
action: Callable[["MasterPiece", Dict[str, Any]], bool],
context: Dict[str, Any],
) -> bool:
"""
Recursively traverses the tree, from root to leaf, left to right direction,
calling the provided `action` on each node.
:param action: A callable that takes (node, context) and returns a boolean.
:param context: Any context data that the action may use.
:returns: None
"""
if not super().do(action, context):
return False
for child in self.children:
if not child.do(action, context):
return False
return True
[docs]
@override
def root(self) -> Composite:
root: Composite = cast(
Composite, super().root()
) # Explicitly cast to Composite
return root
[docs]
@override
def resolve_url(self, url: URL) -> Optional[MasterPiece]:
# First, run the common find method from MasterPiece
result = super().resolve_url(url)
if result:
return result
segments = url.segments
if len(segments) == 0:
return self # If URL is empty, return the current object
if segments[0] == ".":
# Current object, continue search with the rest of the segments
return self.resolve_url(URL("/".join(segments[1:])))
if segments[0] == "..":
# Parent object, move to the parent and strip the ".."
if self.parent:
return self.parent.resolve_url(URL("/".join(segments[1:])))
return None # No parent to traverse up
# Handle child objects (search through children for the first segment)
for child in self.children:
if child.name == segments[0]:
# If the first segment matches the child, recurse into it
return child.resolve_url(URL("/".join(segments[1:])))
return None # No match found in the children
[docs]
@override
def run_forever(self) -> None:
"""
Dispatches first the call to all children and then to the super class.
It is up to the sub classes to implement the actual functionality
for this method.
"""
self.start_children()
super().run_forever()
self.shutdown_children()
[docs]
@override
def run(self) -> None:
"""
Dispatches first the call to all children and then to the super class.
It is up to the sub classes to implement the actual functionality
for this method.
"""
self.start_children()
super().run()
self.shutdown_children()
[docs]
@override
def shutdown(self) -> None:
"""Shuts down the object. First, it dispatches the call to all child objects,
then calls the superclass method to stop the associated payload object, if one exists.
"""
self.shutdown_children()
super().shutdown()
[docs]
def add(self, h: Optional[MasterPiece]) -> None:
"""Add new object as children. The object to be inserted
must be derived from MasterPiece base class.
Args:
h (T): object to be inserted.
"""
if h:
self.children.append(h)
h.parent = self
else:
raise ValueError(f"None cannot be inserted to {self.name}")
[docs]
def start_children(self) -> None:
"""Start all children."""
i: int = 0
for s in self.children:
self.info(f"Starting up {i} {s.name}")
s.run()
i = i + 1
self.info(f"All {i} children successfully started")
[docs]
def shutdown_children(self) -> None:
"""Shuts down the children."""
i: int = 0
self.info("Shutting down children")
for s in self.children:
self.info(f"Shutting down thread {i} {s.name}")
s.shutdown()
i = i + 1
self.info(f"All {i} children successfully shut down")