diff --git a/functions/lego_classes.py b/functions/lego_classes.py index af8a1b4a44c6bab706347094776674a1cf2fa96c..219614f5f18236333c67247b75d3f1e06e19e085 100644 --- a/functions/lego_classes.py +++ b/functions/lego_classes.py @@ -2,97 +2,192 @@ File consists of several classes for the different elements of a device. ''' from __future__ import annotations + +from enum import Enum, auto import uuid from typing import Any, Union, Literal, TypedDict, TypeVar, Type, List, Optional, Dict +import json + # TODO -# - Parents direkt setzen -# - Alle gemeinsamen JSON attribute im LegoItem constructor -# - enforce uuid # - Docstrings # - Beschreibung von Teilen (-> properties) # - Gute String Darstellung # - Minimalbeispiel für KPIs -# - Klassen analog zur deutschen Aufgabenstellung umbenennen (assembly, component) #13 #17 -# - Export als JSON # - Export als GraphViz -# - AggregationsEbene enum (system, assembly, subassembly, component) -class LegoItem: - def __init__(self, item_number: int, mass: float, delivery_time: int, **kwargs) -> None: - # , *args, **kwargs not handling additional/optional specs right now - self.id: uuid.UUID = uuid.uuid4() - self.properties: dict = kwargs - self.item_number: int = item_number +class ComponentCategory(Enum): + BATTERY = auto() + MOTOR = auto() + FRAME = auto() + WHEEL = auto() + AXLE = auto() + GEAR = auto() + + +class AggregationLayer(Enum): + SYSTEM = auto() + ASSEMBLY = auto() + SUBASSEMBLY = auto() + COMPONENT = auto() + + +class LegoComponent: + def __init__(self, name: str, category: ComponentCategory, lego_id: str, cost: float, mass: float, + delivery_time: int, layer: AggregationLayer = AggregationLayer.COMPONENT, **properties) -> None: + self.uuid: uuid.UUID = uuid.uuid4() + self.parent: None | LegoAssembly = None + self.name: str = name + self.category: ComponentCategory = category + self.lego_id: str = lego_id + self.cost: float = cost self.mass: float = mass self.delivery_time: int = delivery_time - # TODO: Set parent directly and not via id? This would allow for easier traversal of the tree - # Currently there is no way to search for parts and components in tree by id. - self.parent_id = None # This will be set when added to a component + self.layer: AggregationLayer = layer + self.properties: dict = properties + + def clone(self) -> LegoComponent: + clone = LegoComponent(self.name, self.category, self.lego_id, self.cost, self.mass, self.delivery_time, + self.layer, **self.properties) + return clone + + def get_root_assembly(self): + if self.parent is None: + return None + current_assembly = self.parent + while current_assembly.parent is not None: + current_assembly = current_assembly.parent + return current_assembly + + def to_dict(self) -> Dict: + ATTRIBUTES = ["uuid", "name", "category", "lego_id", "cost", "mass", "delivery_time", "layer", "properties"] + dict_ = {} + # store attributes + for attr in ATTRIBUTES: + dict_[attr] = getattr(self, attr) + dict_ = {"component": dict_} + return dict_ + + # TODO good string representation def __str__(self): + return self.__repr__() return ( - f"Item(id={self.id}, item_number={self.item_number}, " + f"Item(id={self.uuid}, item_number={self.lego_id}, " f"mass={self.mass}, delivery_time={self.delivery_time}, " - f"parent_id={self.parent_id})" + f"parent_id={self.parent})" ) + # TODO good repr representation def __repr__(self): - return f"Lego Item [{self.id}]" + return f"LegoComponent {self.name} [{self.uuid}]" -class LegoComponent: - def __init__(self, **kwargs) -> None: - self.id: uuid.UUID = uuid.uuid4() - self.properties: dict = kwargs - self.items: List[LegoItem] = [] +class LegoAssembly: + def __init__(self, name: str, layer: AggregationLayer, **properties) -> None: + self.uuid: uuid.UUID = uuid.uuid4() + self.parent: None | LegoAssembly = None + self.name: str = name + self.layer: AggregationLayer = layer + self.properties: dict = properties self.components: List[LegoComponent] = [] - self.parent_id: None | uuid.UUID = None - - def add_item(self, item: LegoItem) -> None: - if not isinstance(item, LegoItem): - raise TypeError(f"'item' should be of type LegoPart, got {type(item).__name__} instead.") - item.parent_id = self.id - self.items.append(item) + self.assemblies: List[LegoAssembly] = [] def add_component(self, component: LegoComponent) -> None: if not isinstance(component, LegoComponent): - raise TypeError(f"'component' should be of type LegoComponent, got {type(component).__name__} instead.") - component.parent_id = self.id + raise TypeError(f"Argument should be of type {LegoComponent.__name__}, " + f"got {type(component).__name__} instead.") + if self.get_root_assembly().contains_uuid(component.uuid): + raise AssertionError(f"This assembly or a subassembly already contains the component with ID " + f"{component.uuid}.") + component.parent = self self.components.append(component) - def children(self) -> Dict[str, List[LegoItem] | List[LegoComponent]]: - return {'items': self.items, 'components': self.components} + def add_assembly(self, assembly: LegoAssembly) -> None: + if not isinstance(assembly, LegoAssembly): + raise TypeError(f"Argument should be of type {LegoAssembly.__name__}, " + f"got {type(assembly).__name__} instead.") + if self.get_root_assembly().contains_uuid(assembly.uuid): + raise AssertionError(f"This assembly or a subassembly already contains the assembly with ID " + f"{assembly.uuid}.") + assembly.parent = self + self.assemblies.append(assembly) + + def children(self) -> Dict[str, List[LegoComponent] | List[LegoAssembly]]: + return {'components': self.components, 'assemblies': self.assemblies} + + def get_component_list(self, max_depth: int = -1) -> List[LegoComponent]: + component_list = [] + component_list.extend(self.components) + if max_depth > 0: + for assembly in self.assemblies: + component_list.extend(assembly.get_component_list(max_depth - 1)) + return component_list - def get_item_list(self) -> List[LegoItem]: - item_list = [] - item_list.extend(self.items) - for component in self.components: - item_list.extend(component.get_item_list()) - return item_list + def get_root_assembly(self) -> LegoAssembly: + current_assembly = self + while current_assembly.parent is not None: + current_assembly = current_assembly.parent + return current_assembly + def contains_uuid(self, uuid_: uuid.UUID): + # check component ids + component_ids = list(map(lambda c: c.uuid, self.components)) + if uuid_ in component_ids: + return True + # check assembly ids + assembly_ids = list(map(lambda a: a.uuid, self.assemblies)) + if uuid_ in assembly_ids: + return True + # recursively check assemblies + for assembly in self.assemblies: + if assembly.contains_uuid(uuid_): + return True + return False + + def to_dict(self) -> Dict: + ATTRIBUTES = ["uuid", "name", "layer", "properties"] + dict_ = {} + # store attributes + for attr in ATTRIBUTES: + dict_[attr] = getattr(self, attr) + # store components + dict_["components"] = [component.to_dict() for component in self.components] + dict_["assemblies"] = [assembly.to_dict() for assembly in self.assemblies] + + return {"assembly": dict_} + + # TODO find good string representation def __repr__(self): - return f"Lego Component [{self.id}]" + return f"LegoAssembly {self.name} [{self.uuid}]" -# TODO: Adjust default output when printing an item or component -def print_component_tree(root, level=0, is_last=False): +def print_assembly_tree(root, level=0, is_last=False): # print component - component_padding = "" + assembly_padding = "" if level > 0: - component_padding += "│ " * (level - 1) + assembly_padding += "│ " * (level - 1) if is_last: - component_padding += "└── " + assembly_padding += "└── " else: - component_padding += "├── " - print(f"{component_padding}{root}") + assembly_padding += "├── " + print(f"{assembly_padding}{root}") # recursively print child components - for i, component in enumerate(root.components): - is_last_ = i == len(root.components) - 1 and len(root.items) == 0 - print_component_tree(component, level + 1, is_last_) + for i, assembly in enumerate(root.assemblies): + is_last_ = i == len(root.assemblies) - 1 and len(root.components) == 0 + print_assembly_tree(assembly, level + 1, is_last_) # print items - for i, item in enumerate(root.items): - item_padding = "│ " * level - item_padding += "├── " if i < len(root.items) - 1 else "└── " - print(f"{item_padding}{item}") + for i, item in enumerate(root.components): + component_padding = "│ " * level if not is_last else " " + component_padding += "├── " if i < len(root.components) - 1 else "└── " + print(f"{component_padding}{item}") + + +class KPIEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, uuid.UUID): + return "kpi-" + str(o) + if isinstance(o, (ComponentCategory, AggregationLayer)): + return "kpi-" + o.name + return super().default(o)