diff --git a/changelog.md b/changelog.md index ee265bd01..93e266fff 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ * `EnergySource.phases` * `PowerElectronicsConnection.phases` * ContactDetails are now Identifiable and no longer have default id generation. The constructor now requires a string. The `id` field is deprecated, to be replaced with mrid. +* Removing items with backref from a containing object's list automatically clears the backref field on the item - it is not accessible after removal. ### New Features * Added a `lint` tox environment that runs `ruff check .` to enforce code quality standards. The test environments now depend on lint passing first, so CI will fail if any new lint violations are introduced. @@ -23,6 +24,8 @@ * Added `skip_install = true` to the lint environment to avoid installing package dependencies that aren't needed for linting. * Added E402 noqa comments for intentional mid-file imports used to avoid circular dependency issues in `dataclassy/dataclass.py`, `context_value_computer.py`, `queue_condition.py`, `direction_logger.py`, and `test_network_trace.py`. * Switched from using `dataclassy` to native dataclasses +* Every UML relationship is now a custom list wrapper that takes no memory and has a reference to the backing field - usage interface is nearly identical to lists +* UML relationships can now automatically set backref values where needed (eg `Cut.ac_line_segment`). They also clear the backref field when the item is removed. ### Fixes * added python3.14 to compatibility list. diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index bb5bd2339..114dcc8e3 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -127,6 +127,7 @@ from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import * from zepben.ewb.model.cim.iec61970.base.core.base_voltage import * +from zepben.ewb.model.cim.iec61970.base.core.terminal import * from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import * from zepben.ewb.model.cim.iec61970.base.core.connectivity_node import * from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import * @@ -144,7 +145,6 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import * from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import * from zepben.ewb.model.cim.iec61970.base.core.substation import * -from zepben.ewb.model.cim.iec61970.base.core.terminal import * from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram import * from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import * diff --git a/src/zepben/ewb/boilerplate/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md index 53220a622..571788402 100644 --- a/src/zepben/ewb/boilerplate/MANIFESTO.md +++ b/src/zepben/ewb/boilerplate/MANIFESTO.md @@ -6,13 +6,13 @@ Yes, this is not your usual manifest We have removed the dataclassy library from the SDK, migrating to using the actual Python dataclass decorator. This was motivated by lack of ongoing support for dataclassy, as well as no-one on the team wanting to deal with its mess of magic code. -Our Python version of CIM has some very specific requirements leading to some unorthodox decisions (eg custom inits). Whatever your "why does it work this way" is, it has likely been considered, and many are provided below. +Our Python version of CIM has some very specific requirements leading to some unorthodox decisions (eg custom inits). Whatever your "why does it work this way" is, it has likely been considered, and many are answered below. This document is to serve as a guide in case you need to modify the SDK and are unfamiliar with how things are implemented around here. The following are some major decisions we have made and how they influence your potential contribution. ## Slots -Due to the immense size of grid models we are dealing with, every byte of memory matters, lest you fry your RAM with 120% load. By default, Python stores its values as a dictionary, which takes up hella space. Slots exist to fix that - a `__slots__` class variable describes the fixed memory layout of a class, making it rigid and a lot more compact. Every single one of our CIM classes uses slots, and all the new ones should too. `@dataclass` with slots enabled saves us specifying them explicitly at the cost of transparency - every field of such a class is a descriptor, and overwrites don't always work the way you expect them to. (For example, `@property` overwrite of a static field in a superclass is completely ignored by Python). +Due to the immense size of grid models we are dealing with, every byte of memory matters, lest you fry your RAM with 120% load. By default, Python stores instance values as a dictionary, which takes up hella space. Slots exist to fix that - a `__slots__` class variable describes the fixed attribute layout of a class, and through some dark magic (static instance-hashed attribute dictionaries) removes a lot of the memory overhead. Every single one of our CIM classes uses slots, and all the new ones should too. `@dataclass` with slots enabled saves us specifying them explicitly at the cost of transparency - every field of such a class is a descriptor, and overwrites don't always work the way you expect them to. (For example, `@property` overwrite of a static field in a superclass is completely ignored by Python). Our chosen approach is delegating as much functionality as we can to the native Python `@dataclass`, which lets you rely on its documentation and AI's knowledge of its quirks. It might be weird, but it is weird in consistent ways. No more reading a random biologist's library to figure out why your ConnectivityNode just did a barrel roll! @@ -20,7 +20,7 @@ Our chosen approach is delegating as much functionality as we can to the native We have chosen to use dataclasses simply because we have a lot of attributes, and any non-dataclass Python init requires a ridiculous amount of code duplication. This allows us to make use of its slot construction, as well as default values for fields that don't need to be passed in the init. -Unfortunately, dataclass generates custom `__init__` functions for the classes, but ONLY if you don't have an existing init already. That's why slightly tweaking class instantiation is quite complicated (requiring a custom decorator to rebuild the class from the original and the dataclass combined - I tried this). Instead, we went for a "clean slate" base class init approach, which, while stripping some of the dataclass functionality, is a lot more transparent when it explodes in your face. +Unfortunately, dataclass generates custom `__init__` functions for the classes, but ONLY if you don't have an existing init already. That's why slightly tweaking class instantiation is quite complicated (requiring a custom decorator to rebuild the class from the original and the dataclass combined - I tried this). Instead, we went for a "clean slate" base class init approach (explained below), which, while stripping some of the dataclass functionality, is a lot more transparent when it explodes in your face. ## Instantiation @@ -38,16 +38,40 @@ Since `@dataclass` generates an init for every subclass without one, the `@zb_da Positional args are very hard to get consistent with the use of `@dataclass` - to know the order, you must parse the inheritance tree. `@property` values are not included in dataclass inits by default, meaning that adding them as an argument would create arbitrary ordering, and that is a readability nightmare. Moreover, we have custom inits in subclasses, making maintaining the arg order functionally impossible. -That is why, from this version onwards, any argument that is not the object's `mrid` (or equivalent identifier) must be passed as a keyword argument (`MyClass("mrid", 42)` bad, `MyClass("mrid", thing=42)` good). +That is why, from this version onwards, any argument that is not the object's `mrid` (or equivalent identifier) must be passed as a keyword argument (`MyClass("mRID", 42)` bad, `MyClass("mRID", thing=42)` good). While it would be possible to bring args back if all subclass inits are removed (probable future feature), it is a terrible idea. Do not do it unless you are very sure of yourself. ## Eq, Hash, Str, Repr -CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses mrid equality, and `__str__` and `__rer__` represent the class as `{}` eg `Terminal{terminal_mrid}`. +CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mRID}`. ## Descriptors -Properties are bulky and limited in functionality. There is a more general python concept called a Descriptor. This is any class implementing `__get__` and/or (usually and) `__set__` methods, which overwrite accessing a class member provided they are attached to the class and not its instance. This allows us to, for example, implement internally nullable fields. +Properties are bulky and limited in functionality. There is a more general python concept called a Descriptor. This is any class implementing `__get__` and/or (usually and) `__set__` methods, which overwrite accessing a class member (provided they are attached to the class and not its instance). This allows us to, for example, implement internally nullable fields. -These fields technically require no space in the object, since they access other values internally, but `@dataclass` assigns a slot to anything with a type. Obviously, if you want type hinting on member access, descriptor fields need to be typed. `@remove_descriptor_annotations` decorator takes care of that by stripping the types off of anything defining the aforementioned methods before it gets passed to a dataclass. Since this happens at runtime, IDE type-checking still gets the types, and `@dataclass` is no longer confused about which fields should be created. +These fields technically require no space in the object, since they access other values internally, but `@dataclass` assigns a slot to anything with a type. Obviously, if you want type hinting on member access, descriptor fields need to be typed. `@remove_descriptor_annotations` decorator (ordinarily part of `@zb_dataclass`) takes care of that by stripping the types off of anything defining the aforementioned methods before it gets passed to a dataclass. Since this happens at runtime, IDE type-checking still gets the types, and `@dataclass` is no longer confused about which fields should be created. + +## Lists + +Since CIM is UML-based, there are a lot of one-to-many and many-to-many class relationships, represented with lists. Most such relationships are usually empty. Python empty list is 56 bytes, whereas `None` is 16, so we want the lists to be nullable. + +Consequently, we have created a hierarchy of nullable descriptors backed by private fields. Each descriptor implements a list-like interface, allowing the user to interact with the relations normally, blissfully unaware of the nullable insanity going on under the hood. + +Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some of these lists: + +- *mRID collision checks*: for mRID collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. +- *backfill*: some items contained in lists link back to the owner item. In that case, we perform a backfill to do this automatically. We also ensure that the item is not already linked to another owner. +- *validation*: some lists are created with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. +- *sorting*: passing a sorting lambda to ordered collections automatically re-sorts the list after an item is added. + +Here are the concrete implementations of the lists that are used in the SDK: + +- `LazyList`: A nullable list of objects that has validation and sorting built in. Typically used for non-`Identifiable` relationships. +- `LazyIndexList`: `LazyList` with index-based insertion and deletion, for strictly ordered relationships such as diagram points. + +The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mRID`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) + +- `LazyMridList`: A `LazyList` with an mRID check and backfill added. `O(N)` mRID lookup. +- `MridList`: An mRID collection backed by a non-nullable list. Useful for relationships that are most likely to be filled (eg `ConnectivityNode.terminals`). `O(N)` mRID lookup. +- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful for large-scale collections (for which the lookup speedup is significant) diff --git a/src/zepben/ewb/boilerplate/__init__.py b/src/zepben/ewb/boilerplate/__init__.py index 7ee764916..35e3122c5 100644 --- a/src/zepben/ewb/boilerplate/__init__.py +++ b/src/zepben/ewb/boilerplate/__init__.py @@ -2,4 +2,3 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. - diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 1ecf6d965..9a52eb6f3 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -2,18 +2,10 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations from dataclasses import Field -from typing import TypeVar - -T = TypeVar("T", bound=type) - class BackedDescriptor: @@ -24,89 +16,28 @@ class BackedDescriptor: """ def __init__(self, private_field): - if not isinstance(private_field, Field): - raise TypeError("private_field parameter of the Descriptor constructor has to be an instance of dataclass Field.") - self.private_field: Field = private_field + if not isinstance(private_field, Field) and not isinstance(private_field, BackedDescriptor): + raise TypeError(f"private_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {private_field}") + self.private_field: Field | 'BackedDescriptor' = private_field + self._backing_name: str | None = None self.name = None def __set_name__(self, owner, name): - self.name = name + if name is None: + return + self.__name__ = self.name = name + if not self._backing_name: + self._backing_name = self.private_field.name def __get__(self, instance, _): - return getattr(instance, self.private_field.name) + if instance is None: + return self + return getattr(instance, self._backing_name) def __set__(self, instance, value): - return setattr(instance, self.private_field.name, value) - - - -def remove_descriptor_annotations(cls: T) -> T: - """ - Remove annotations for class attributes that are data descriptors. - - This is intended for descriptor attributes that should remain class-level - descriptors (classes defining ``__get__``/``__set__`` methods, controlling attribute access), - rather than becoming dataclass fields/slots. Dataclasses decide - which fields to create from ``__annotations__``. By removing annotations for - descriptor-backed attributes before ``@dataclass`` runs, those attributes are - left alone and can continue to behave as descriptors. - - Use this decorator below ``@dataclass`` so that it is called first. Python - applies decorators from the bottom up:: - - @dataclass - @remove_descriptor_annotations - class MyClass: - _x: int = field(default=0) - - x: int = MyDescriptor("_x") - - In the example above, ``x`` is annotated, but its class value is a descriptor. - Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as - a dataclass field and may try to include it in generated fields, slots, init, - repr, etc. With this decorator, the annotation for ``x`` is removed before - dataclass processing, while normal fields such as ``_x`` are left intact. - - Dataclass ``Field`` instances are themselves descriptors and are thus skipped. - In the example above, the slot for ``_x`` is still created. - - Before decoration:: - - MyClass.__annotations__ == { - "_x": int, - "x": int, - } - - After ``remove_descriptor_annotations`` runs:: - - MyClass.__annotations__ == { - "_x": int, - } - - The class is then passed to ``@dataclass`` with only real dataclass fields - remaining in ``__annotations__``. - """ - # Get editable annotations - original_annotations = dict(getattr(cls, "__annotations__", {})) - tweaked_annotations = dict(original_annotations) - - # Get the current values of class fields. Before @dataclass is run, we see Descriptor instances here - cls_dict = vars(cls) - - for name in list(tweaked_annotations): - try: - value = cls_dict[name] - # Skip dataclass fields - they need annotations - if isinstance(value, Field): - continue - # Any descriptor needs to implement get or set - most likely both. - if hasattr(value, "__get__") or hasattr(value, "__set__"): - tweaked_annotations.pop(name) - # Values without defaults will error out - definitely not descriptors - except KeyError: - pass + if self._backing_name is None: + raise ValueError(f"Descriptor {self} is not yet aware of the supporting field - `__set__` cannot be called") + return setattr(instance, self._backing_name, value) - # Update annotations on the class - cls.__annotations__ = tweaked_annotations - return cls +Alias = BackedDescriptor diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py new file mode 100644 index 000000000..c0cca6157 --- /dev/null +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -0,0 +1,63 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import Field +from types import MemberDescriptorType +from typing import Any, Callable, TypeVar + +from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.collections.mrid_collection import S + + +F = TypeVar("F", bound=Callable[..., Any]) + + +class Backfill: + def __init__(self, backfill_prop: property) -> None: + if backfill_prop.fget is None: + raise TypeError(f"Cannot backfill a property without a getter: {backfill_prop!r}") + + self.backfill_prop = backfill_prop + + def _get_backing_name(self) -> str: + name = self.backfill_prop.fget.__name__ + + target = getattr(self.backfill_prop.fget, "_internal_target", None) + backing_name = name if target is None else ( + getattr(target, "name", None) + or getattr(target, "__name__", None) + or getattr(getattr(target, "fget", None), "__name__", None) + ) + + if backing_name is None: + raise TypeError(f"Cannot determine backing name for {target!r}") + return backing_name + + + def apply(self, element: S, owner: Any) -> None: + backing_name = self._get_backing_name() + if getattr(element, backing_name) is None: + setattr(element, backing_name, owner) + + ref = getattr(element, backing_name) + if ref is not owner: + raise ValueError(f"{element} `{self.backfill_prop.fget.__name__}` property references {ref}, expected {owner}.") + + def clear(self, element: S) -> None: + backing_name = self._get_backing_name() + setattr(element, backing_name, None) + + +def internal(target: Any) -> Callable[[F], F]: + if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"target parameter of the target decorator has to be an instance of property or dataclass Field, instead is {target}") + + if isinstance(target, property) and target.fget is None: + raise TypeError(f"Cannot backfill a property without a getter: {target}") + + def dec(func: Callable): + setattr(func, "_internal_target", target) + return func + return dec diff --git a/src/zepben/ewb/boilerplate/collections/__init__.py b/src/zepben/ewb/boilerplate/collections/__init__.py new file mode 100644 index 000000000..35e3122c5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py new file mode 100644 index 000000000..148c8fb36 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -0,0 +1,50 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Collection, Iterable, Generic, Iterator, Callable, TypeVar + + +T = TypeVar("T") + + +class AbstractBackedCollection(Collection[T], Generic[T], ABC): + + @abstractmethod + def _get_collection(self) -> Collection[T]: + ... + + @abstractmethod + def append(self, item: T, /) -> None: + """Append an item to the collection.""" + ... + + def extend(self, items: Iterable[T] | None, /) -> None: + """Append each item to the collection.""" + for element in items or []: + self.append(element) + + @abstractmethod + def remove(self, item: T, /) -> None: + """Remove an item from the collection.""" + ... + + @abstractmethod + def clear(self) -> None: + """Remove all items from the collection.""" + ... + + def __len__(self) -> int: + return len(self._get_collection()) + + def __iter__(self) -> Iterator[T]: + return iter(self._get_collection()) + + def __contains__(self, item: object) -> bool: + return item in self._get_collection() + + def for_each_indexed(self, action: Callable[[int, T], object]) -> None: + """Call the `action` on each item in the list.""" + for index, item in enumerate(self._get_collection()): + action(index, item) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py new file mode 100644 index 000000000..49f348e0b --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -0,0 +1,34 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Sequence, Generic, overload + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection, T + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + collection = self._get_collection() + if isinstance(index, slice): + return collection[index] + return collection[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py new file mode 100644 index 000000000..421eac1a8 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -0,0 +1,91 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList, T + + +class LazyIndexList(LazyList[T]): + """ + Lazy collection with list-style index-based insertion and deletion. + + It retains the nullable backing-list behaviour of ``LazyList``, + creating the backing list when an item is inserted and resetting it to + ``None`` when the final item is deleted. + + For example:: + + container.items.insert(0, "value") + assert container._items == ["value"] + + del container.items[0] + assert container._items is None + """ + def __init__( + self, + private_field: list[T] | None, + element_description: str, + validate=None, + ) -> None: + super().__init__(private_field, validate=validate, sort_by=None) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert an item into the collection at a given index. + Run optional validation. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self._instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + if self.validate is not None: + self.validate(self._instance, item) + + existing = getattr(self._instance, self._backing_name) + + if existing is None: + existing = [item] + setattr(self._instance, self._backing_name, existing) + else: + existing.insert(index, item) + + def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self._instance, self._backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item + + def __delitem__(self, index: int, /) -> None: + """Remove the item at the given index.""" + self.remove(self[index]) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py new file mode 100644 index 000000000..c9cf9cec2 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -0,0 +1,87 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TypeVar + +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + + +T = TypeVar("T") + + +class LazyList(_IterableWrapper[T], AbstractBackedList[T]): + """ + Concrete collection wrapper that treats its backing field as a nullable + list. + + A backing value of ``None`` is exposed as an empty collection. The backing + list is created when the first item is appended and reset to ``None`` when + the last item is removed or the collection is cleared. + + For example:: + + class Container: + _items = field(default=None) + items = LazyList(_items) + + container = Container() + + assert list(container.items) == [] + assert container._items is None + + container.items.append("value") + assert container._items == ["value"] + + container.items.clear() + assert container._items is None + """ + def __init__( + self, + private_field: list[T] | None, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self) -> list[T] | None: + return getattr(self._instance, self._backing_name) + + def _get_collection(self) -> list[T]: + return getattr(self._instance, self._backing_name) or [] + + def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ + if self.validate is not None: + self.validate(self._instance, item) + + existing = getattr(self._instance, self._backing_name) + if existing is None: + existing = [item] + setattr(self._instance, self._backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def remove(self, item: T) -> None: + existing = self._get_collection() + existing.remove(item) + if not existing: + self.clear() + + def clear(self) -> None: + setattr(self._instance, self._backing_name, None) + + def __repr__(self) -> str: + if self._instance is None: + return object.__repr__(self) + return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py new file mode 100644 index 000000000..33c9f41b0 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -0,0 +1,51 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.mrid_collection import S, MridCollection + + +class LazyMridList(LazyList[S], MridCollection[S]): + """ + Nullable list implementation of :class:`MridCollection`. + + Inherits mRID lookup and uniqueness semantics from ``MridCollection`` and + lazy backing-list behavior from ``LazyList``. + """ + def __init__( + self, + private_field: list[S] | None, + element_description: str, + backfill: Backfill | None = None, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field, validate, sort_by) + self.element_description = element_description + self.backfill = backfill + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + existing = self._get() + if existing is None: + return None + found = next((element for element in existing if element.mrid == mrid), None) + return found + + def append(self, item: S) -> None: + """ + Append an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + Sort the collection if key lambda is provided. + """ + if not self._can_add_by_mrid(item): + return + + if self.backfill is not None: + self.backfill.apply(item, self._instance) + + super().append(item) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py new file mode 100644 index 000000000..a5e5b36eb --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -0,0 +1,105 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TypeVar, ValuesView + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + +T = TypeVar("T") + + +class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + """ + Lazy mRID collection backed by a nullable dictionary. + + Items are stored by their ``mrid`` while iteration exposes the dictionary + values. A backing value of ``None`` is treated as an empty collection. The + dictionary is created when the first item is appended and reset to ``None`` + when the collection becomes empty. + + For example:: + + container.items.append(item) + + assert container._items == {item.mrid: item} + assert container.items.get_by_mrid(item.mrid) is item + + container.items.remove(item) + assert container._items is None + """ + def __init__( + self, + private_field: dict[str, S] | None, + element_description: str, + backfill: Backfill | None = None, + validate=None + ) -> None: + super().__init__(private_field) + self.element_description = element_description + self.backfill = backfill + self.validate = validate + + def _get(self) -> dict[str, S] | None: + return getattr(self._instance, self._backing_name) + + def _get_or_empty(self) -> dict[str, S]: + return getattr(self._instance, self._backing_name) or {} + + def _get_collection(self) -> ValuesView[S]: + return self._get_or_empty().values() + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + return self._get_or_empty().get(mrid, None) + + def get_by_mrid(self, mrid: str) -> S: + return self._get_or_empty()[mrid] + + def append(self, item: S) -> None: + """ + Add an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + """ + if not self._can_add_by_mrid(item): + return + + if self.backfill is not None: + self.backfill.apply(item, self._instance) + + if self.validate is not None: + self.validate(self._instance, item) + + existing = getattr(self._instance, self._backing_name) + if existing is None: + existing = {item.mrid: item} + setattr(self._instance, self._backing_name, existing) + else: + existing[item.mrid] = item + + def __len__(self) -> int: + return len(self._get_or_empty()) + + def __contains__(self, item: object) -> bool: + return self._get_or_empty().get(getattr(item, "mrid", None)) == item + + def remove(self, item) -> None: + existing = self._get_or_empty() + + del existing[item.mrid] + if not existing: + self.clear() + + def clear(self) -> None: + setattr(self._instance, self._backing_name, None) + + def __repr__(self) -> str: + if self._instance is None: + return object.__repr__(self) + return repr(self._get_or_empty()) + + def __getitem__(self, item) -> S: + return (self._get_or_empty())[item] diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py new file mode 100644 index 000000000..867fe1b48 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -0,0 +1,54 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Any, Protocol, TypeVar + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection + + +class HasMrid(Protocol): + mrid: str + + +S = TypeVar("S", bound=HasMrid) + + +class MridCollection(AbstractBackedCollection[S], ABC): + """ + Collection of objects identified by a unique ``mrid``. + + Provides lookup by mRID and rejects distinct objects with duplicate mRIDs. + """ + + _instance: Any + element_description: str + + @abstractmethod + def _safe_get_by_mrid(self, mrid: str) -> S | None: ... + + def get_by_mrid(self, mrid: str) -> S: + """ + Get an element matching given ``mrid`` + + raises KeyError if one is not present + """ + res = self._safe_get_by_mrid(mrid) + if res is None: + raise KeyError(mrid) + return res + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self._instance}." + ) + + return False diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py new file mode 100644 index 000000000..971ad9226 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -0,0 +1,94 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import Sequence + +from typing_extensions import Self + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + + +class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): + """ + mRID collection backed by a non-nullable list. + + Items are kept in insertion order and retrieved by mRID using a linear search. + Appending enforces mRID uniqueness and can optionally apply backfill, + validation, and sorting. + + Unlike ``LazyMridList``, clearing the collection leaves an empty backing list + rather than resetting the backing field to ``None``. + """ + def __init__( + self, + private_field: list[S], + element_description: str, + backfill: Backfill | None = None, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field) + self.element_description = element_description + self.backfill = backfill + self.validate = validate + self.sort_by = sort_by + self._backing_list = None + + def __get__(self, instance, owner=None) -> Self: + obj = super().__get__(instance, owner) + if obj is not self: + # noinspection PyUnresolvedReferences + obj.__post_init__() + return obj + + def __post_init__(self) -> None: + self._backing_list = getattr(self._instance, self._backing_name) + + def _get_collection(self) -> Sequence[S]: + return self._backing_list + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + found = next((element for element in self._backing_list if element.mrid == mrid), None) + return found + + def append(self, item: S) -> None: + """ + Append an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + Sort the collection if key lambda is provided. + """ + if not self._can_add_by_mrid(item): + return + + if self.backfill is not None: + self.backfill.apply(item, self._instance) + + if self.validate is not None: + self.validate(self._instance, item) + + self._backing_list.append(item) + + if self.sort_by is not None: + self._backing_list.sort(key=self.sort_by) + + def remove(self, item: S) -> None: + self._backing_list.remove(item) + if self.backfill is not None: + self.backfill.clear(item) + + def clear(self) -> None: + if self.backfill is not None: + for item in self._backing_list: + self.backfill.clear(item) + self._backing_list.clear() + + def __repr__(self) -> str: + if self._instance is None: + return object.__repr__(self) + return repr(self._backing_list) diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py new file mode 100644 index 000000000..a388be93b --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -0,0 +1,110 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from abc import ABC +from dataclasses import Field +from typing import Any, TypeVar + +from typing_extensions import Self, deprecated + +from zepben.ewb import BackedDescriptor, resolve_default +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection + + +class _Wrapper(BackedDescriptor): + """ + Descriptor base for collection views backed by another dataclass field. + + Access through an instance creates a short-lived wrapper bound to that + instance and its backing field. For example:: + + class Container: + _items = field(default=None) + items = LazyList(_items) + + container = Container() + + # Calls Container.items.__get__(container, Container), returning a + # wrapper whose _instance is container and _backing_name is "_items". + bound_items = container.items + bound_items.append("value") + + assert container._items == ["value"] + + Access through the class returns the original shared descriptor:: + + assert isinstance(Container.items, LazyList) + """ + + _instance: Any + _backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._instance = None + self._backing_name = None + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + if instance is None: + return self + obj = type(self)(*self._init_args, **self._init_kwargs) + obj._instance = instance + obj._backing_name = self.private_field.name + return obj + + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + It implements a minimalistic callable fget with a name. + TODO: Remove in a separate PR fixing tests + """ + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + def fget(instance): + return self.__get__(instance) + self.fget = fget + + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + self.fget.__name__ = name + self.fget.__qualname__ = f"{owner.__qualname__}.{name}" + + @deprecated("Lists are no longer a property") + def fget(self, instance): ... + +T = TypeVar("T") + +class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value) -> None: + if instance is None: + return + if not hasattr(instance, self._backing_name) and isinstance(self.private_field, Field): + resolve_default(instance, self.private_field) + elif getattr(instance, self._backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 58067ae4a..82bb2a2b3 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -2,7 +2,13 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import dataclass, fields, MISSING + +from dataclasses import dataclass, fields, MISSING, Field +from typing import TypeVar, cast + + + +T = TypeVar("T") def _is_set(obj: object, name: str) -> bool: @@ -12,7 +18,111 @@ def _is_set(obj: object, name: str) -> bool: return False return True -@dataclass(slots=True) +def remove_descriptor_annotations(cls: type[T]) -> T: + """ + Remove annotations for class attributes that are data descriptors. + + This is intended for descriptor attributes that should remain class-level + descriptors (classes defining ``__get__``/``__set__`` methods, controlling attribute access), + rather than becoming dataclass fields/slots. Dataclasses decide + which fields to create from ``__annotations__``. By removing annotations for + descriptor-backed attributes before ``@dataclass`` runs, those attributes are + left alone and can continue to behave as descriptors. + + Use this decorator below ``@dataclass`` so that it is called first. Python + applies decorators from the bottom up:: + + @dataclass + class MyClass: + _x: int = field(default=0) + + x: int = MyDescriptor("_x") + + In the example above, ``x`` is annotated, but its class value is a descriptor. + Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as + a dataclass field and may try to include it in generated fields, slots, init, + repr, etc. With this decorator, the annotation for ``x`` is removed before + dataclass processing, while normal fields such as ``_x`` are left intact. + + Dataclass ``Field`` instances are themselves descriptors and are thus skipped. + In the example above, the slot for ``_x`` is still created. + + Before decoration:: + + MyClass.__annotations__ == { + "_x": int, + "x": int, + } + + After ``remove_descriptor_annotations`` runs:: + + MyClass.__annotations__ == { + "_x": int, + } + + The class is then passed to ``@dataclass`` with only real dataclass fields + remaining in ``__annotations__``. + """ + # Get editable annotations + original_annotations = dict(getattr(cls, "__annotations__", {})) + tweaked_annotations = dict(original_annotations) + + # Get the current values of class fields. Before @dataclass is run, we see Descriptor instances here + cls_dict = vars(cls) + + for name in list(tweaked_annotations): + try: + value = cls_dict[name] + # Skip dataclass fields - they need annotations + if isinstance(value, Field): + continue + # Any descriptor needs to implement get or set - most likely both. + if hasattr(value, "__get__") or hasattr(value, "__set__"): + tweaked_annotations.pop(name) + # Values without defaults will error out - definitely not descriptors + except KeyError: + pass + + # Update annotations on the class + cls.__annotations__ = tweaked_annotations + + return cls + +def zb_dataclass(cls: type[T]) -> type[T]: + """ + Shorthand alias for ``@dataclass(init=False, eq=False, slots=True, repr=False)`` + Allows us to modify dataclass parameters for all of CIM from a single reference point + """ + cls = remove_descriptor_annotations(cls) + # The cast is purely for type checkers to be aware of the true class of cls + return cast( + type[T], + dataclass( + init=False, + eq=False, + slots=True, + repr=False, + )(cls), + ) + +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Note: custom descriptors are not included in `fields(cls)`, so we don't need to handle them here + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) + + +@zb_dataclass class DataclassBase: """ Instantiate the default fields and interpret the kwargs like ``@dataclass`` does @@ -24,44 +134,20 @@ class DataclassBase: For more motivation, refer to `MANIFESTO.md` """ def __init__(self, **kwargs) -> None: - # Assign all of the kwargs manually. - # str-based setattr triggers descriptors, allowing us to intercept __set__. - for attr, value in kwargs.items(): - setattr(self, attr, value) - - # Manually assign defaults in dataclass fields - for f in fields(type(self)): - # We cannot just check kwargs because fields could be set in subclass __init__'s - if _is_set(self, f.name): - continue - - # Set field defaults - if f.default is not MISSING: - setattr(self, f.name, f.default) - elif f.default_factory is not MISSING: - setattr(self, f.name, f.default_factory()) - - # Ignore custom descriptors - elif hasattr(f, '__get__'): - continue - - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {f.name!r} " - f"for {type(self).__name__}" - ) - # Currently post init implementation requires a lot of extra logic, # which would mess with readability. If you require it - add it. if callable(getattr(self, "__post_init__", None)): raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") + # Manually assign defaults in dataclass fields + for f in fields(type(self)): + # We cannot just check kwargs because fields could be set in subclass __init__'s + if _is_set(self, f.name) or f.name in kwargs: + continue -# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. -zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) -""" -Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` -Allows us to modify dataclass parameters for all of CIM from a single reference point -""" + resolve_default(self, f) + # Assign all of the kwargs manually. + # str-based setattr triggers descriptors, allowing us to intercept __set__. + for attr, value in kwargs.items(): + setattr(self, attr, value) diff --git a/src/zepben/ewb/boilerplate/relations/__init__.py b/src/zepben/ewb/boilerplate/relations/__init__.py new file mode 100644 index 000000000..35e3122c5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py new file mode 100644 index 000000000..0fa921cd1 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -0,0 +1,21 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import SinglePhaseKind +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList + + +class AcLineSegmentPhaseList(LazyMridList): + def get_by_phase(self, phase: SinglePhaseKind): + """ + The individual phase models for an AcLineSegment. + `phase` the phase of the required [AcLineSegmentPhase] + """ + res = next((it for it in self if it.phase == phase), None) + if res is None: + raise KeyError(phase) + return res diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py new file mode 100644 index 000000000..233a59867 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -0,0 +1,24 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import BatteryControlMode +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList + + +class BatteryControlList(LazyMridList): + def get_by_mode(self, control_mode: BatteryControlMode): + """ + Get the `BatteryControl` identified by its `control_mode` + + `control_mode` the `BatteryControlMode` of the desired `BatteryControl` + Returns The `BatteryControl` with the specified `control_mode` if it exists + Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. + """ + for control in self: + if control.control_mode == control_mode: + return control + raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py new file mode 100644 index 000000000..85954a1d2 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -0,0 +1,38 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList + +if TYPE_CHECKING: + from zepben.ewb import CurveData + + +class CurveDataList(LazyList): + + def get(self, x: float) -> 'CurveData': + """ + Get the :class:`CurveData` identified by its `x_value`. + + :param x: The X value of the required :class:`CurveData`. + :returns: The :class:`CurveData` with the specified `x` if it exists. + :raises KeyError: When no `CurveData` was found with `x`. + """ + curve_data = next((it for it in self if it.x_value == x), None) + if curve_data: + return curve_data + raise KeyError(x) + + def remove_data_at(self, x: float) -> 'CurveData': + """ + Disassociate a :class:`CurveData` from this collection based on its `x_value`. + + :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. + :returns: A reference to the removed :class:`CurveData`. + :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. + """ + data = self.get(x) + self.remove(data) + return data diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py new file mode 100644 index 000000000..f1f2951e4 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -0,0 +1,36 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList +if TYPE_CHECKING: + from zepben.ewb import SinglePhaseKind, PhaseImpedanceData + + +class PhaseImpedanceDataList(LazyList): + def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: + """ + Get the matrix entry for the corresponding to and from phases. + + :param from_phase: The from_phase to lookup. + :param to_phase: The to_phase to lookup. + :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. + :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. + """ + phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) + if phase_impedance_data: + return phase_impedance_data + + raise KeyError((from_phase, to_phase)) + + @property + def diagonal(self): + """ + Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. + """ + return (pid for pid in self if pid.from_phase == pid.to_phase) diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py new file mode 100644 index 000000000..ede88484f --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -0,0 +1,42 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +if TYPE_CHECKING: + from zepben.ewb import PowerTransformerEnd, Terminal + + +class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): + + def get_by_num(self, end_number: int) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. + + `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. + Returns The `PowerTransformerEnd` referred to by `end_number` + Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. + """ + end = next((it for it in self if it.end_number == end_number), None) + if end: + return end + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self._instance)}") + + def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. + + `terminal` The `terminal` to find a `PowerTransformerEnd` for. + Returns The `PowerTransformerEnd` connected to the specified `terminal` + Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. + """ + + end = next((it for it in self if it.terminal == terminal), None) + if end: + return end + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self._instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py new file mode 100644 index 000000000..3242be310 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -0,0 +1,18 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import Terminal +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList + + +class TerminalsList(LazyMridList[Terminal]): + + def get_by_sequence_number(self, sequence_number: int) -> Terminal: + term = next((it for it in self if it.sequence_number == sequence_number), None) + if term is None: + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self._instance)}") + return term diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py new file mode 100644 index 000000000..f62647491 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -0,0 +1,27 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import TransformerEndRatedS, TransformerCoolingType +from zepben.ewb.boilerplate.collections.lazy_list import LazyList + + +class TransformerEndRatedSList(LazyList[TransformerEndRatedS]): + + def get_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + return next((rating for rating in self if rating.cooling_type == cooling_type), None) + + def remove_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + rating = self.get_by_cooling_type(cooling_type) + if rating is not None: + self.remove(rating) + return rating diff --git a/src/zepben/ewb/database/sqlite/network/network_database_reader.py b/src/zepben/ewb/database/sqlite/network/network_database_reader.py index 3f9ba1765..ad1005b02 100644 --- a/src/zepben/ewb/database/sqlite/network/network_database_reader.py +++ b/src/zepben/ewb/database/sqlite/network/network_database_reader.py @@ -139,7 +139,7 @@ def _validate_equipment_containers(self): for it in missing_containers: count_by_class[type(it).__name__] += 1 - for (className, count) in count_by_class: + for (className, count) in count_by_class.items(): self._logger.warning(f"{count} {className}s were missing an equipment container.") if count_by_class: diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index 1936a7f02..cf500fa23 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -7,12 +7,15 @@ __all__ = ["RelayInfo"] -from typing import Optional, List, Generator, Callable, Any +from dataclasses import field +from typing import Optional, List, Callable, Any +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo -from zepben.ewb.util import ngen, nlen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass @@ -26,20 +29,12 @@ class RelayInfo(AssetInfo): reclose_fast: Optional[bool] = None """True if reclose_delays are associated with a fast Curve, false otherwise.""" - _reclose_delays: Optional[List[float]] = None + _reclose_delays: Optional[List[float]] = field(default=None) - def __init__(self, *args, reclose_delays: Optional[List[float]] = None, **kwargs): - super(RelayInfo, self).__init__(*args, **kwargs) - if reclose_delays: - for index, delay in enumerate(reclose_delays): - self.add_delay(delay, index) - - @property - def reclose_delays(self) -> Generator[float, None, None]: - """ - The reclose delays for this curve and relay type. The index of the list is the reclose step, and the value is the overall delay time. - """ - return ngen(self._reclose_delays) + reclose_delays: LazyIndexList[float] = LazyIndexList( + _reclose_delays, + "float" + ) def set_delays(self, delays: List[float]) -> RelayInfo: """ @@ -48,83 +43,53 @@ def set_delays(self, delays: List[float]) -> RelayInfo: :param delays: The delays to set. The provided list will be copied. :return: A reference to this :class:`RelayInfo` to allow fluent use. """ - self._reclose_delays = delays.copy() + self.reclose_delays.clear() + self.reclose_delays.extend(delays) return self - def num_delays(self) -> int: - """ - Get the number of reclose delays for this :class:`RelayInfo` - """ - return nlen(self._reclose_delays) - - def get_delay(self, index: int) -> float: - """ - Get the reclose delay at the specified index, if it exists. Otherwise, this returns + # region deprecated methods - :param index: The index of the delay to retrieve. - :return: The reclose delay at `index` if it exists, otherwise None. - """ - if self._reclose_delays: - return self._reclose_delays[index] - else: - raise IndexError(index) + # region reclose_delays boilerplate - def for_each_delay(self, action: Callable[[int, float], Any]): - """ - Call the `action` on each delay in the `reclose_delays` collection - - :param action: An action to apply to each delay in the `reclose_delays` collection, taking the index of the delay, and the delay itself. - """ - for index, point in enumerate(self._reclose_delays): - action(index, point) - - def add_delay(self, delay: float, index: int = None) -> RelayInfo: - """ - Add a reclose delay. + @deprecated("Use len(reclose_delays) instead.") + def num_delays(self) -> int: + return len(self.reclose_delays) - :param delay: The delay in seconds to add. - :param index: The index into the list to add the delay at. Defaults to the end of the list. - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - if index is None: - index = self.num_delays() - require(0 <= index <= self.num_delays(), - lambda: f"Unable to add float to {str(self)}. Index number {index} " - f"is invalid. Expected a value between 0 and {self.num_delays()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._reclose_delays = list() if self._reclose_delays is None else self._reclose_delays - self._reclose_delays.insert(index, delay) + @deprecated("Use reclose_delays[index] instead.") + def get_delay(self, index: int) -> float: + return self.reclose_delays[index] + + @deprecated("Use reclose_delays.for_each_indexed(action) instead.") + def for_each_delay( + self, + action: Callable[[int, float], Any], + ) -> None: + self.reclose_delays.for_each_indexed(action) + + @deprecated("Use reclose_delays.append(delay)") + def add_delay( + self, + delay: float, + index: int | None = None, + ) -> RelayInfo: + if index is None: index = len(self.reclose_delays) + self.reclose_delays.insert(index, delay) return self + @deprecated("Use reclose_delays.remove(delay) instead.") def remove_delay(self, delay: float) -> RelayInfo: - """ - Remove a delay from the list. - - :param delay: The delay to remove. - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - self._reclose_delays = safe_remove(self._reclose_delays, delay) + self.reclose_delays.remove(delay) return self + @deprecated("Use reclose_delays.pop(index) instead.") def remove_delay_at(self, index: int) -> float: - """ - Remove a delay from the list. - - :param index: The index of the delay to remove. - :return: The delay that was removed, or `None` if no delay was present at `index`. - :raises IndexError: If `sequence_number` is out of range. - """ - if self._reclose_delays: - delay = self._reclose_delays.pop(index) - self._reclose_delays = self._reclose_delays if self._reclose_delays else None - return delay - raise IndexError(index) + return self.reclose_delays.pop(index) + @deprecated("Use reclose_delays.clear() instead.") def clear_delays(self) -> RelayInfo: - """ - Clear all reclose delays. - - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - self._reclose_delays = None + self.reclose_delays.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index c7cbe845b..4d03083a8 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -5,18 +5,20 @@ __all__ = ["ContactDetails"] -from typing import Generator, Any +from dataclasses import field +from typing import Any from typing_extensions import deprecated from zepben.ewb import zb_dataclass +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -from zepben.ewb.util import ngen, nlen @zbex @@ -63,41 +65,29 @@ def id(self) -> str: business_name: str | None = None """[ZBEX] The business name of this contact.""" - _phone_numbers: list[TelephoneNumber] | None = None + _phone_numbers: list[TelephoneNumber] | None = field(default=None) - _electronic_addresses: list[ElectronicAddress] | None = None + _electronic_addresses: list[ElectronicAddress] | None = field(default=None) - def __init__(self, id: str|None=None, *args, phone_numbers: list[TelephoneNumber] = None, electronic_addresses: list[ElectronicAddress] = None, **kwargs): + def __init__(self, id: str|None=None, *args, **kwargs): if id is not None: if "mrid" in kwargs: raise TypeError("ContactDetails.id is an alias for mrid. Do not pass both to the constructor!") kwargs["mrid"] = id super(ContactDetails, self).__init__(*args, **kwargs) - for number in phone_numbers or []: - self.add_phone_number(number) - - for email in electronic_addresses or []: - self.add_electronic_address(email) - def __str__(self): - return f"ContactDetails({self.id})" + return f"ContactDetails({self.mrid})" def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) - @zbex - @property - def phone_numbers(self) -> Generator[TelephoneNumber, None, None]: - """[ZBEX] Phone numbers.""" - return ngen(self._phone_numbers) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyList(_phone_numbers) + """[ZBEX] Phone numbers.""" - @zbex - @property - def electronic_addresses(self) -> Generator[ElectronicAddress, None, None]: - """[ZBEX] Electronic addresses.""" - return ngen(self._electronic_addresses) + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyList(_electronic_addresses) + """[ZBEX] Electronic addresses.""" def __eq__(self, other: Any) -> bool: # @@ -118,79 +108,65 @@ def __eq__(self, other: Any) -> bool: self._electronic_addresses == other._electronic_addresses, )) + + # region deprecated list boilerplate + + # region phone_numbers boilerplate + + @deprecated("Use len(phone_numbers) instead.") def num_phone_numbers(self) -> int: - """Get the number of entries in the ``TelephoneNumber`` collection.""" - return nlen(self._phone_numbers) - - def add_phone_number(self, phone_number: TelephoneNumber) -> "ContactDetails": - """ - Add an ``TelephoneNumber`` to this ``ContactDetails``. - - :param phone_number: The ``TelephoneNumber`` to add. - :return: This ``ContactDetails`` for fluent use. - """ - if self._phone_numbers is None: - self._phone_numbers = [] - self._phone_numbers.append(phone_number) + return len(self.phone_numbers) + + @deprecated("Use phone_numbers.append(phone_number) instead.") + def add_phone_number( + self, + phone_number: TelephoneNumber, + ) -> "ContactDetails": + self.phone_numbers.append(phone_number) return self - def remove_phone_number(self, phone_number: TelephoneNumber) -> bool: - """ - Remove an ``TelephoneNumber`` from this ``ContactDetails``. - - :param phone_number: The ``TelephoneNumber`` to remove. - :return: True if the ``TelephoneNumber`` was removed. - """ - if not self._phone_numbers: - raise KeyError(phone_number) - self._phone_numbers.remove(phone_number) - if self.num_phone_numbers == 0: - self._phone_numbers = None + @deprecated("Use phone_numbers.remove(phone_number) instead.") + def remove_phone_number( + self, + phone_number: TelephoneNumber, + ) -> bool: + self.phone_numbers.remove(phone_number) return True + @deprecated("Use phone_numbers.clear() instead.") def clear_phone_numbers(self) -> "ContactDetails": - """ - Clear all ``TelephoneNumber``'s from this ``ContactDetails``. - :return: this ``ContactDetails`` for fluent use. - """ - self._phone_numbers = None + self.phone_numbers.clear() return self + # endregion + + # region electronic_addresses boilerplate + + @deprecated("Use len(electronic_addresses) instead.") def num_electronic_addresses(self) -> int: - """Get the number of entries in the [ElectronicAddress] collection.""" - return nlen(self._electronic_addresses) - - def add_electronic_address(self, electronic_address: ElectronicAddress) -> "ContactDetails": - """ - Add an ``ElectronicAddress`` to this ``ContactDetails``. - - :param electronic_address: The ``ElectronicAddress`` to add. - :return: this ``ContactDetails`` for fluent use. - """ - if self._electronic_addresses is None: - self._electronic_addresses = [] - self._electronic_addresses.append(electronic_address) + return len(self.electronic_addresses) + + @deprecated("Use electronic_addresses.append(electronic_address) instead.") + def add_electronic_address( + self, + electronic_address: ElectronicAddress, + ) -> "ContactDetails": + self.electronic_addresses.append(electronic_address) return self - def remove_electronic_address(self, electronic_address: ElectronicAddress) -> bool: - """ - Remove an ``ElectronicAddress`` from this ``ContactDetails``. - - :param electronic_address: The ``ElectronicAddress`` to remove. - :return: True if the ``ElectronicAddress`` was removed. - """ - if not self._electronic_addresses: - raise KeyError(electronic_address) - self._electronic_addresses.remove(electronic_address) - if self.num_electronic_addresses == 0: - self._electronic_addresses = None + @deprecated("Use electronic_addresses.remove(electronic_address) instead.") + def remove_electronic_address( + self, + electronic_address: ElectronicAddress, + ) -> bool: + self.electronic_addresses.remove(electronic_address) return True + @deprecated("Use electronic_addresses.clear() instead.") def clear_electronic_addresses(self) -> "ContactDetails": - """ - Clear all ``ElectronicAddress``'s from this ``ContactDetails``. - - :return: this ``ContactDetails`` for fluent use. - """ - self._electronic_addresses = None + self.electronic_addresses.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py index 16c669dc1..29355bff7 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py @@ -27,10 +27,6 @@ class PanDemandResponseFunction(EndDeviceFunction): _appliance_bitmask: Optional[int] = None - def __init__(self, *args, appliance: Union[int, ControlledAppliance] = None, **kwargs): - super(PanDemandResponseFunction, self).__init__(*args, **kwargs) - if appliance is not None: - self.appliance = appliance @property def appliance(self) -> Optional[ControlledAppliance]: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 523435c10..828b1c269 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -7,12 +7,15 @@ __all__ = ["Loop"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import safe_remove, ngen, nlen, get_by_mrid from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @@ -28,182 +31,121 @@ class Loop(IdentifiedObject): to many customers for more than a short time. """ - _circuits: Optional[List[Circuit]] = None - _substations: Optional[List[Substation]] = None - _energizing_substations: Optional[List[Substation]] = None - - def __init__(self, *args, circuits: List[Circuit] = None, substations: List[Substation] = None, energizing_substations: List[Substation] = None, **kwargs): - super(Loop, self).__init__(*args, **kwargs) - if circuits: - for term in circuits: - self.add_circuit(term) - - if substations: - for sub in substations: - self.add_substation(sub) - - if energizing_substations: - for sub in energizing_substations: - self.add_energizing_substation(sub) - - @property - def circuits(self) -> Generator[Circuit, None, None]: - """ - [ZBEX] Sub-transmission `Circuit`s that form part of this loop. - """ - return ngen(self._circuits) - - @property - def substations(self) -> Generator[Substation, None, None]: - """ - [ZBEX] The `Substation`s that are powered by this `Loop`. - """ - return ngen(self._substations) - - @property - def energizing_substations(self) -> Generator[Substation, None, None]: - """ - [ZBEX] The `Substation`s that normally energize this `Loop`. - """ - return ngen(self._energizing_substations) + _circuits: Optional[List[Circuit]] = field(default=None) + _substations: Optional[List[Substation]] = field(default=None) + _energizing_substations: Optional[List[Substation]] = field(default=None) + + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "An Circuit", + ) + + substations: MridCollection[Substation] = LazyMridList( + _substations, + "An Substation", + ) + + energizing_substations: MridCollection[Substation] = LazyMridList( + _energizing_substations, + "An Substation", + ) + + + + + + + + + + + + + + + + + # region deprecated list boilerplate + # region circuits boilerplate + + @deprecated("Use len(obj.circuits) instead.") def num_circuits(self): - """Return the number of end `Circuit`s associated with this `Loop`""" - return nlen(self._circuits) + return len(self.circuits) + @deprecated("Use obj.circuits.get_by_mrid(mrid) instead.") def get_circuit(self, mrid: str) -> Circuit: - """ - Get the `Circuit` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Circuit` - Returns The `Circuit` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._circuits, mrid) + return self.circuits.get_by_mrid(mrid) + @deprecated("Use obj.circuits.append(circuit) instead.") def add_circuit(self, circuit: Circuit) -> Loop: - """ - Associate an `Circuit` with this `Loop` - - `circuit` the `Circuit` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Circuit` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(circuit, self.get_circuit, "An Circuit"): - return self - self._circuits = list() if self._circuits is None else self._circuits - self._circuits.append(circuit) + self.circuits.append(circuit) return self + @deprecated("Use obj.circuits.remove(circuit) instead.") def remove_circuit(self, circuit: Circuit) -> Loop: - """ - Disassociate `circuit` from this `Loop` - - `circuit` the `Circuit` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `circuit` was not associated with this `Loop`. - """ - self._circuits = safe_remove(self._circuits, circuit) + self.circuits.remove(circuit) return self + @deprecated("Use obj.circuits.clear() instead.") def clear_circuits(self) -> Loop: - """ - Clear all end circuits. - Returns A reference to this `Loop` to allow fluent use. - """ - self._circuits = None + self.circuits.clear() return self + # endregion circuits boilerplate + + # region substations boilerplate + + @deprecated("Use len(obj.substations) instead.") def num_substations(self): - """Return the number of end `Substation`s associated with this `Loop`""" - return nlen(self._substations) + return len(self.substations) + @deprecated("Use obj.substations.get_by_mrid(mrid) instead.") def get_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._substations, mrid) + return self.substations.get_by_mrid(mrid) + @deprecated("Use obj.substations.append(substation) instead.") def add_substation(self, substation: Substation) -> Loop: - """ - Associate an `Substation` with this `Loop` - - `substation` the `Substation` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(substation, self.get_substation, "An Substation"): - return self - self._substations = list() if self._substations is None else self._substations - self._substations.append(substation) + self.substations.append(substation) return self + @deprecated("Use obj.substations.remove(substation) instead.") def remove_substation(self, substation: Substation) -> Loop: - """ - Disassociate `substation` from this `Loop` - - `substation` the `Substation` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Loop`. - """ - self._substations = safe_remove(self._substations, substation) + self.substations.remove(substation) return self + @deprecated("Use obj.substations.clear() instead.") def clear_substations(self) -> Loop: - """ - Clear all end substations. - Returns A reference to this `Loop` to allow fluent use. - """ - self._substations = None + self.substations.clear() return self + # endregion substations boilerplate + + # region energizing_substations boilerplate + + @deprecated("Use len(obj.energizing_substations) instead.") def num_energizing_substations(self): - """Return the number of end `Substation`s associated with this `Loop`""" - return nlen(self._energizing_substations) + return len(self.energizing_substations) + @deprecated("Use obj.energizing_substations.get_by_mrid(mrid) instead.") def get_energizing_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energizing_substations, mrid) + return self.energizing_substations.get_by_mrid(mrid) + @deprecated("Use obj.energizing_substations.append(substation) instead.") def add_energizing_substation(self, substation: Substation) -> Loop: - """ - Associate an `Substation` with this `Loop` - - `substation` the `Substation` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(substation, self.get_energizing_substation, "An Substation"): - return self - self._energizing_substations = list() if self._energizing_substations is None else self._energizing_substations - self._energizing_substations.append(substation) + self.energizing_substations.append(substation) return self + @deprecated("Use obj.energizing_substations.remove(substation) instead.") def remove_energizing_substation(self, substation: Substation) -> Loop: - """ - Disassociate `substation` from this `Loop` - - `substation` the `Substation` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Loop`. - """ - self._energizing_substations = safe_remove(self._energizing_substations, substation) + self.energizing_substations.remove(substation) return self + @deprecated("Use obj.energizing_substations.clear() instead.") def clear_energizing_substations(self) -> Loop: - """ - Clear all end energizing_substations. - Returns A reference to this `Loop` to allow fluent use. - """ - self._energizing_substations = None + self.energizing_substations.clear() return self + + # endregion energizing_substations boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index f03a6e156..deab4947b 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -8,18 +8,21 @@ __all__ = ["LvFeeder"] import typing -from typing import Generator, Optional, Dict, List +from dataclasses import field +from typing import Optional, Dict +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import safe_remove_by_id, nlen, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if typing.TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal - from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation @zb_dataclass @@ -40,40 +43,18 @@ class LvFeeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """[ZBEX] The normal head terminal or terminals of this LvFeeder""" - _normal_energizing_feeders: Dict[str, Feeder] | None = None + _normal_energizing_feeders_by_id: Dict[str, Feeder] | None = field(default=None) """[ZBEX] The feeders that energize this LV feeder in the normal state of the network.""" - _current_equipment: Dict[str, Equipment] | None = None + _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """[ZBEX] The equipment contained in this LvFeeder in the current state of the network.""" - _current_energizing_feeders: Dict[str, Feeder] | None = None + _current_energizing_feeders_by_id: Dict[str, Feeder] | None = field(default=None) """[ZBEX] The feeders that energize this LV feeder in the current state of the network.""" normal_energizing_lv_substation: 'LvSubstation | None' = None """[ZBEX] The normally energizing LvSubstation for this LvFeeder""" - def __init__( - self, - *args, - normal_head_terminal: Terminal = None, - normal_energizing_feeders: List[Feeder] = None, - current_equipment: List[Equipment] = None, - current_energizing_feeders: List[Feeder] = None, - **kwargs - ): - super(LvFeeder, self).__init__(*args, **kwargs) - if normal_head_terminal: - self.normal_head_terminal = normal_head_terminal - if normal_energizing_feeders: - for feeder in normal_energizing_feeders: - self.add_normal_energizing_feeder(feeder) - if current_equipment: - for eq in current_equipment: - self.add_current_equipment(eq) - if current_energizing_feeders: - for feeder in current_energizing_feeders: - self.add_current_energizing_feeder(feeder) - @property def normal_head_terminal(self) -> Optional[Terminal]: """ @@ -88,185 +69,101 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"normal_head_terminal for {str(self)} has already been set to {self._normal_head_terminal}, cannot reset this field to {term}") - @property - def normal_energizing_feeders(self) -> Generator[Feeder, None, None]: - """ - [ZBEX] The HV/MV feeders that normally energize this LV feeder. - """ - return ngen(self._normal_energizing_feeders) + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" - @property - def current_energizing_feeders(self) -> Generator[Feeder, None, None]: - """ - [ZBEX] The HV/MV feeders that currently energize this LV feeder. - """ - return ngen(self._current_energizing_feeders) + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return ngen(self._current_equipment) + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" + # region deprecated list boilerplate + # region current_equipment boilerplate + + @deprecated("Use len(current_equipment) instead") def num_current_equipment(self): - """ - Returns The number of `Equipment` associated with this `LvFeeder` in the current state of the network. - """ - return nlen(self._current_equipment) + return len(self.current_equipment) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` contained in this `LvFeeder` in the current state of the network, identified by `mrid` - - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._current_equipment: - raise KeyError(mrid) - try: - return self._current_equipment[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_current_equipment(self, equipment: Equipment) -> LvFeeder: - """ - Associate `equipment` with this `LvFeeder` in the current state of the network. + return self.current_equipment.get_by_mrid(mrid) - `equipment` the `Equipment` to associate with this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `LvFeeder`. - """ - if self._validate_reference(equipment, self.get_current_equipment, "An Equipment"): - return self - self._current_equipment = dict() if self._current_equipment is None else self._current_equipment - self._current_equipment[equipment.mrid] = equipment + @deprecated("Use current_equipment.append(equipment) instead") + def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.append(equipment) return self - def remove_current_equipment(self, equipment: Equipment) -> LvFeeder: - """ - Disassociate `equipment` from this `LvFeeder` in the current state of the network. - - `equipment` The `Equipment` to disassociate from this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `LvFeeder`. - """ - self._current_equipment = safe_remove_by_id(self._current_equipment, equipment) + @deprecated("Use current_equipment.remove(equipment) instead") + def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.remove(equipment) return self - def clear_current_equipment(self) -> LvFeeder: - """ - Clear all `Equipment` from this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - """ - self._current_equipment = None + @deprecated("Use current_equipment.clear() instead") + def clear_current_equipment(self) -> EquipmentContainer: + self.current_equipment.clear() return self - def num_normal_energizing_feeders(self) -> int: - """ - Get the number of HV/MV feeders that normally energize this LV feeder. - """ - return nlen(self._normal_energizing_feeders) + # endregion + # region normal_energizing_feeders boilerplate - def get_normal_energizing_feeder(self, mrid: str) -> Feeder: - """ - Energizing feeder using the normal state of the network. + @deprecated("Use len(normal_energizing_feeders) instead") + def num_normal_energizing_feeders(self): + return len(self.normal_energizing_feeders) - @param mrid: The mrid of the `Feeder`. - @return A matching `Feeder` that energizes this `LvFeeder` in the normal state of the network. - @raise A `KeyError` if no matching `Feeder` was found. - """ - if not self._normal_energizing_feeders: - raise KeyError(mrid) - try: - return self._normal_energizing_feeders[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_normal_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Associate this `LvFeeder` with a `Feeder` in the normal state of the network. + @deprecated("Use normal_energizing_feeders.get_by_mrid(mrid) instead") + def get_normal_energizing_feeder(self, mrid: str) -> Feeder: + return self.normal_energizing_feeders.get_by_mrid(mrid) - @param feeder: the HV/MV feeder to associate with this LV feeder in the normal state of the network. - @return: This `LvFeeder` for fluent use. - """ - if self._validate_reference(feeder, self.get_normal_energizing_feeder, "A Feeder"): - return self - self._normal_energizing_feeders = dict() if self._normal_energizing_feeders is None else self._normal_energizing_feeders - self._normal_energizing_feeders[feeder.mrid] = feeder + @deprecated("Use normal_energizing_feeders.append(lv_feeder) instead") + def add_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.normal_energizing_feeders.append(lv_feeder) return self - def remove_normal_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Disassociate this `LvFeeder` from a `Feeder` in the normal state of the network. - - @param feeder: the HV/MV feeder to disassociate from this LV feeder in the normal state of the network. - @return: This `LvFeeder` for fluent use. - @raise: A `ValueError` if `feeder` is not found in the normal energizing feeders collection. - """ - self._normal_energizing_feeders = safe_remove_by_id(self._normal_energizing_feeders, feeder) + @deprecated("Use normal_energizing_feeders.remove(lv_feeder) instead") + def remove_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.normal_energizing_feeders.remove(lv_feeder) return self + @deprecated("Use normal_energizing_feeders.clear() instead") def clear_normal_energizing_feeders(self) -> LvFeeder: - """ - Clear all `Feeder`s associated with `LvFeeder` in the normal state of the network. - - @return: This `LvFeeder` for fluent use. - """ - self._normal_energizing_feeders = None + self.normal_energizing_feeders.clear() return self - def num_current_energizing_feeders(self) -> int: - """ - Get the number of HV/MV feeders that currently energize this LV feeder. - """ - return nlen(self._current_energizing_feeders) + # endregion + # region current_energizing_feeders boilerplate - def get_current_energizing_feeder(self, mrid: str) -> Feeder: - """ - Energizing feeder using the current state of the network. + @deprecated("Use len(current_energizing_feeders) instead") + def num_current_energizing_feeders(self): + return len(self.current_energizing_feeders) - @param mrid: The mrid of the `Feeder`. - @return A matching `Feeder` that energizes this `LvFeeder` in the current state of the network. - @raise A `KeyError` if no matching `Feeder` was found. - """ - if not self._current_energizing_feeders: - raise KeyError(mrid) - try: - return self._current_energizing_feeders[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_current_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Associate this `LvFeeder` with a `Feeder` in the current state of the network. + @deprecated("Use current_energizing_feeders.get_by_mrid(mrid) instead") + def get_current_energizing_feeder(self, mrid: str) -> Feeder: + return self.current_energizing_feeders.get_by_mrid(mrid) - @param feeder: the HV/MV feeder to associate with this LV feeder in the current state of the network. - @return: This `LvFeeder` for fluent use. - """ - if self._validate_reference(feeder, self.get_current_energizing_feeder, "A Feeder"): - return self - self._current_energizing_feeders = dict() if self._current_energizing_feeders is None else self._current_energizing_feeders - self._current_energizing_feeders[feeder.mrid] = feeder + @deprecated("Use current_energizing_feeders.append(lv_feeder) instead") + def add_current_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.current_energizing_feeders.append(lv_feeder) return self - def remove_current_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Disassociate this `LvFeeder` from a `Feeder` in the current state of the network. - - @param feeder: the HV/MV feeder to disassociate from this LV feeder in the current state of the network. - @return: This `LvFeeder` for fluent use. - @raise: A `ValueError` if `feeder` is not found in the current energizing feeders collection. - """ - self._current_energizing_feeders = safe_remove_by_id(self._current_energizing_feeders, feeder) + @deprecated("Use current_energizing_feeders.remove(lv_feeder) instead") + def remove_current_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.current_energizing_feeders.remove(lv_feeder) return self + @deprecated("Use current_energizing_feeders.clear() instead") def clear_current_energizing_feeders(self) -> LvFeeder: - """ - Clear all `Feeder`s associated with `LvFeeder` in the current state of the network. - - @return: This `LvFeeder` for fluent use. - """ - self._current_energizing_feeders = None + self.current_energizing_feeders.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 9afc8a44e..c18713852 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -3,11 +3,17 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ['LvSubstation'] +from dataclasses import field from typing import Generator, TYPE_CHECKING -from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder @@ -28,48 +34,27 @@ class LvSubstation(EquipmentContainer): :var current_energizing_feeders: [ZBEX] the feeders that currently energize the substation. also used for naming purposes. """ - _normal_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = None - _current_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = None - _normal_energized_lv_feeders_by_id: dict[str | None, LvFeeder] | None = None - - def __init__( - self, - *args, - normal_energizing_feeders: list['Feeder'] | None = None, - current_energizing_feeders: list['Feeder'] | None = None, - normal_energized_lv_feeders: list[LvFeeder] | None = None, - **kwargs - ): - super(LvSubstation, self).__init__(*args, **kwargs) - if normal_energizing_feeders: - for lv_feeder in normal_energizing_feeders: - self.add_normal_energizing_feeder(lv_feeder) - if current_energizing_feeders: - for lv_feeder in current_energizing_feeders: - self.add_current_energizing_feeder(lv_feeder) - if normal_energized_lv_feeders: - for lv_feeder in normal_energized_lv_feeders: - self.add_normal_energized_lv_feeder(lv_feeder) - - @zbex - @property - def normal_energizing_feeders(self) -> Generator["Feeder", None, None]: - """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - return ngen(self._normal_energizing_feeders_by_id) - - @zbex - @property - def normal_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - return ngen(self._normal_energized_lv_feeders_by_id) - - @zbex - @property - def current_energizing_feeders(self) -> Generator['Feeder', None, None]: - """ - [ZBEX] The HV/MV feeders that currently energize this LV substation. - """ - return ngen(self._current_energizing_feeders_by_id) + _normal_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = field(default=None) + _current_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = field(default=None) + _normal_energized_lv_feeders_by_id: dict[str | None, LvFeeder] | None = field(default=None) + + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" + + normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _normal_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" + + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: """ @@ -83,143 +68,84 @@ def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): yield lv_feeder - def num_normal_energizing_feeders(self) -> int: - """Get the number of entries in the normal ``Feeder`` collection.""" - return nlen(self._normal_energizing_feeders_by_id) - def get_normal_energizing_feeder(self, mrid: str) -> 'Feeder | None': - """ - Energizing feeder using the normal state of the network. + # region deprecated list boilerplate + # region normal_energizing_feeders boilerplate - :param mrid: the mRID of the required normal ``Feeder`` - :returns: The ``Feeder`` with the specified ``mrid`` if it exists, otherwise null - """ - return get_by_mrid(self._normal_energizing_feeders_by_id, mrid) - - def add_normal_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Associate this ``LvSubstation`` with a ``Feeder`` in the normal state of the network. + @deprecated("Use len(normal_energizing_feeders) instead") + def num_normal_energizing_feeders(self): + return len(self.normal_energizing_feeders) - :param feeder: the HV/MV feeder to associate with this ``LvSubstation`` in the normal state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(feeder, self.get_normal_energizing_feeder, "A Feeder"): - return self + @deprecated("Use normal_energizing_feeders.get_by_mrid(mrid) instead") + def get_normal_energizing_feeder(self, mrid: str) -> Feeder: + return self.normal_energizing_feeders.get_by_mrid(mrid) - if self._normal_energizing_feeders_by_id is None: - self._normal_energizing_feeders_by_id = dict() - self._normal_energizing_feeders_by_id[feeder.mrid] = feeder + @deprecated("Use normal_energizing_feeders.append(lv_feeder) instead") + def add_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.normal_energizing_feeders.append(lv_feeder) return self - def remove_normal_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from a ``Feeder`` in the normal state of the network. - - :param feeder: the HV/MV feeder to disassociate from this ``LvSubstation`` in the normal state of the network. - :returns: true if a matching feeder is removed from the collection. - """ - self._normal_energizing_feeders_by_id = safe_remove_by_id(self._normal_energizing_feeders_by_id, feeder) + @deprecated("Use normal_energizing_feeders.remove(lv_feeder) instead") + def remove_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.normal_energizing_feeders.remove(lv_feeder) return self - def clear_normal_energizing_feeders(self) -> "LvSubstation": - """ - Clear all ``Feeder``'s associated with this ``LvSubstation`` in the normal state of the network. - - :returns: This ``LvSubstation`` for fluent use. - """ - self._normal_energizing_feeders_by_id = None + @deprecated("Use normal_energizing_feeders.clear() instead") + def clear_normal_energizing_feeders(self) -> LvSubstation: + self.normal_energizing_feeders.clear() return self - def num_current_energizing_feeders(self) -> int: - """ - Get the number of entries in the current ``Feeder`` collection. - """ - return nlen(self._current_energizing_feeders_by_id) + # endregion + # region current_energizing_feeders boilerplate - def get_current_energizing_feeder(self, mrid: str) -> 'Feeder | None': - """ - Retrieve an energizing feeder using the current state of the network. + @deprecated("Use len(current_energizing_feeders) instead") + def num_current_energizing_feeders(self): + return len(self.current_energizing_feeders) - :param mrid: the mRID of the required current ``Feeder`` - :returns: The ``Feeder`` with the specified ``mRID`` if it exists, otherwise null - """ - return get_by_mrid(self._current_energizing_feeders_by_id, mrid) + @deprecated("Use current_energizing_feeders.get_by_mrid(mrid) instead") + def get_current_energizing_feeder(self, mrid: str) -> Feeder: + return self.current_energizing_feeders.get_by_mrid(mrid) - def add_current_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Associate this ``LvSubstation`` with a ``Feeder`` in the current state of the network. - - :param feeder: the HV/MV feeder to associate with this ``LvSubstation`` in the current state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(feeder, self.get_current_energizing_feeder, "A Feeder"): - return self - if self._current_energizing_feeders_by_id is None: - self._current_energizing_feeders_by_id = dict() - self._current_energizing_feeders_by_id[feeder.mrid] = feeder + @deprecated("Use current_energizing_feeders.append(lv_feeder) instead") + def add_current_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.current_energizing_feeders.append(lv_feeder) return self - def remove_current_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from a ``Feeder`` in the current state of the network. - - :param feeder: the HV/MV feeder to disassociate from this LvSubstation the current state of the network. - :returns: true if a matching feeder is removed from the collection. - """ - self._current_energizing_feeders_by_id = safe_remove_by_id(self._current_energizing_feeders_by_id, feeder) + @deprecated("Use current_energizing_feeders.remove(lv_feeder) instead") + def remove_current_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.current_energizing_feeders.remove(lv_feeder) return self - def clear_current_energizing_feeders(self) -> "LvSubstation": - """ - Clear all ``Feeder``'s associated with this ``LvSubstation`` in the current state of the network. - - :returns: This ``LvSubstation`` for fluent use. - """ - self._current_energizing_feeders_by_id = None + @deprecated("Use current_energizing_feeders.clear() instead") + def clear_current_energizing_feeders(self) -> LvSubstation: + self.current_energizing_feeders.clear() return self - def num_normal_energized_lv_feeders(self) -> int: - """Get the number of entries in the normal ``LvFeeder`` collection.""" - return nlen(self._normal_energized_lv_feeders_by_id) + # endregion + # region normal_energized_lv_feeders boilerplate - def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder | None: - """ - Retrieve an energized ``LvFeeder`` using the normal state of the network. + @deprecated("Use len(normal_energized_lv_feeders) instead") + def num_normal_energized_lv_feeders(self): + return len(self.normal_energized_lv_feeders) - :param mrid: the mRID of the required normal ``LvFeeder`` - :returns: The ``LvFeeder`` with the specified ``mRID`` if it exists, otherwise null - """ - return get_by_mrid(self._normal_energized_lv_feeders_by_id, mrid) + @deprecated("Use normal_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.normal_energized_lv_feeders.get_by_mrid(mrid) - def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> "LvSubstation": - """ - Associate this ``LvSubstation`` with an ``LvFeeder`` in the normal state of the network. - - :param lv_feeder: the ``LvFeeder`` to associate with this feeder in the normal state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_normal_energized_lv_feeder, "An LvFeeder"): - return self - - if self._normal_energized_lv_feeders_by_id is None: - self._normal_energized_lv_feeders_by_id = dict() - self._normal_energized_lv_feeders_by_id[lv_feeder.mrid] = lv_feeder + @deprecated("Use normal_energized_lv_feeders.append(lv_feeder) instead") + def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> LvSubstation: + self.normal_energized_lv_feeders.append(lv_feeder) return self - def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from an ``LvFeeder`` in the normal state of the network. - - :param lv_feeder: the ``LvFeeder`` to disassociate from this HV/MV feeder in the normal state of the network. - """ - self._normal_energized_lv_feeders_by_id = safe_remove_by_id(self._normal_energized_lv_feeders_by_id, lv_feeder) + @deprecated("Use normal_energized_lv_feeders.remove(lv_feeder) instead") + def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> LvSubstation: + self.normal_energized_lv_feeders.remove(lv_feeder) return self - def clear_normal_energized_lv_feeders(self) -> "LvSubstation": - """ - Clear all ``LvFeeder``'s associated with this ``LvSubstation`` in the normal state of the network. - - :returns: This ``LvSubstation`` for fluent use. - """ - self._normal_energized_lv_feeders_by_id = None + @deprecated("Use normal_energized_lv_feeders.clear() instead") + def clear_normal_energized_lv_feeders(self) -> LvSubstation: + self.normal_energized_lv_feeders.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index cd0eb9908..b92f7fe12 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -8,20 +8,23 @@ __all__ = ["ProtectionRelayFunction"] import sys -import warnings -from typing import Optional, List, Generator, Iterable, Callable, TYPE_CHECKING, Any +from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta + +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList if sys.version_info >= (3, 13): from warnings import deprecated else: from typing_extensions import deprecated +from dataclasses import field from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.extensions.iec61970.base.protection.power_direction_kind import PowerDirectionKind from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.util import require, nlen, ngen, safe_remove, get_by_mrid +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo @@ -59,47 +62,16 @@ class ProtectionRelayFunction(PowerSystemResource, metaclass=ABCMeta): power_direction: PowerDirectionKind = PowerDirectionKind.UNKNOWN """[ZBEX] The flow of the power direction used by this ProtectionRelayFunction.""" - _sensors: Optional[List[Sensor]] = None + _sensors: Optional[List[Sensor]] = field(default=None) - _protected_switches: Optional[List[ProtectedSwitch]] = None + _protected_switches: Optional[List[ProtectedSwitch]] = field(default=None) - _schemes: Optional[List[ProtectionRelayScheme]] = None + _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) - _time_limits: Optional[List[float]] = None + _time_limits: Optional[List[float]] = field(default=None) - _thresholds: Optional[List[RelaySetting]] = None + _thresholds: Optional[List[RelaySetting]] = field(default=None) - def __init__( - self, - *args, - sensors: Iterable[Sensor] = None, - protected_switches: Iterable[ProtectedSwitch] = None, - schemes: Iterable[ProtectionRelayScheme] = None, - time_limits: Iterable[float] = None, - thresholds: Iterable[RelaySetting] = None, - relay_info: RelayInfo | None = None, - **kwargs - ): - super(ProtectionRelayFunction, self).__init__(*args, **kwargs) - - if sensors is not None: - for sensor in sensors: - self.add_sensor(sensor) - if protected_switches is not None: - for protected_switch in protected_switches: - self.add_protected_switch(protected_switch) - if schemes is not None: - for scheme in schemes: - self.add_scheme(scheme) - if time_limits is not None: - for time_limit in time_limits: - self.add_time_limit(time_limit) - if thresholds is not None: - for threshold in thresholds: - self.add_threshold(threshold) - if relay_info is not None: - warnings.warn("relay_info is deprecated, use asset_info instead.") - self.asset_info = relay_info @property @deprecated("use asset_info instead.") @@ -112,355 +84,202 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - @property - def thresholds(self) -> Generator[RelaySetting, None, None]: - """ - [ZBEX] Yields all the thresholds[:class:`RelaySettings`] for this :class:`ProtectionRelayFunction`. The order of thresholds corresponds to the order of time limits. + thresholds: LazyIndexList[RelaySetting] = LazyIndexList( + _thresholds, + "RelaySetting" + ) - :return: A generator that iterates over all thresholds[:class:`RelaySettings`] for this relay function. - """ - return ngen(self._thresholds) + time_limits: LazyIndexList[float] = LazyIndexList( + _time_limits, + "float" + ) - @property - def time_limits(self) -> Generator[float, None, None]: - """ - [ZBEX] Yields all the time limits (in seconds) for this relay function. Order of entries corresponds to the order of entries in thresholds. + sensors: MridCollection[Sensor] = LazyMridList( + _sensors, + "A Sensor", + ) - :return: A generator that iterates over all time limits for this relay function. - """ - return ngen(self._time_limits) + protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( + _protected_switches, + "A ProtectedSwitch", + ) - @property - def sensors(self) -> Generator[Sensor, None, None]: - """ - [ZBEX] Yields all the :class:`Sensors` for this relay function. + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - :return: A generator that iterates over all :class:`Sensors` for this relay function. - """ - return ngen(self._sensors) - @property - def protected_switches(self) -> Generator[ProtectedSwitch, None, None]: - """ - [ZBEX] Yields the :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. + # region deprecated list boilerplate - :return: A generator that iterates over all :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - """ - return ngen(self._protected_switches) - - @property - def schemes(self) -> Generator[ProtectionRelayScheme, None, None]: - """ - [ZBEX] Yields the :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. - - :return: A generator that iterates over all :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. - """ - return ngen(self._schemes) + # region thresholds boilerplate + @deprecated("Use thresholds.for_each_indexed(action)") def for_each_threshold(self, action: Callable[[int, RelaySetting], Any]): - """ - Call the `action` on each :class:`RelaySetting` in the `thresholds` collection - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ - for index, point in enumerate(self.thresholds): - action(index, point) - - def add_threshold(self, threshold: RelaySetting, sequence_number: int = None) -> ProtectionRelayFunction: - """ - Add a threshold[:class:`RelaySetting`] to this :class:`ProtectionRelayFunction`'s list of thresholds. - - :param threshold: The threshold[:class:`RelaySetting`] to add to this :class:`ProtectionRelayFunction`. - :param sequence_number: The sequence number of the `threshold` being added. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if sequence_number is None: - sequence_number = self.num_thresholds() - require(0 <= sequence_number <= self.num_thresholds(), - lambda: f"Unable to add RelaySetting to {str(self)}. Sequence number {sequence_number} " - f"is invalid. Expected a value between 0 and {self.num_thresholds()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._thresholds = list() if self._thresholds is None else self._thresholds - self._thresholds.insert(sequence_number, threshold) + self.thresholds.for_each_indexed(action) + + @deprecated("Use thresholds.append(threshold)") + def add_threshold( + self, + threshold: RelaySetting, + sequence_number: int = None, + ) -> ProtectionRelayFunction: + if sequence_number is None: sequence_number = len(self.thresholds) + self.thresholds.insert(sequence_number, threshold) return self + @deprecated("Use len(thresholds) instead.") def num_thresholds(self) -> int: - """ - Get the number of thresholds for this :class:`ProtectionRelayFunction`. - - :return: The number of thresholds for this `ProtectionRelayFunction`. - """ - return nlen(self._thresholds) + return len(self.thresholds) + @deprecated("Use thresholds[sequence_number] instead.") def get_threshold(self, sequence_number: int) -> RelaySetting: - """ - Get the threshold[:class:`RelaySetting`] for this :class:`ProtectionRelayFunction` by its `sequence_number`. - - :param sequence_number: The sequence_number of the threshold :class:`RelaySetting` for this :class:`ProtectionRelayFunction`. - :returns: The threshold[:class:`RelaySetting`] for this :class:`ProtectionRelayFunction` with sequence number `sequence_number` - :raises IndexError: if no :class:`RelaySetting` was found with sequence_number `sequence_number`. - """ - if self._thresholds is not None: - return self._thresholds[sequence_number] - else: - raise IndexError(sequence_number) - - def remove_threshold(self, threshold: RelaySetting) -> ProtectionRelayFunction: - """ - Removes a threshold[:class:`RelaySetting`] from this :class:`ProtectionRelayFunction`. - - :param threshold: The threshold[:class:`RelaySetting`] to disassociate from this :class:`ProtectionRelayFunction`. - :returns: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._thresholds = safe_remove(self._thresholds, threshold) - return self + return self.thresholds[sequence_number] - def remove_threshold_at(self, sequence_number: int) -> RelaySetting: - """ - Removes a threshold[:class:`RelaySetting`] from this :class:`ProtectionRelayFunction`. + @deprecated("Use thresholds.remove(threshold) instead.") + def remove_threshold( + self, + threshold: RelaySetting, + ) -> ProtectionRelayFunction: + self.thresholds.remove(threshold) + return self - :param sequence_number: The sequence_number of the threshold[:class:`RelaySetting`] to disassociate from this :class:`ProtectionRelayFunction`. - :returns: A reference to removed threshold[:class:`RelaySetting`]. - :raises IndexError: If `sequence_number` is out of range. - """ - threshold = self.get_threshold(sequence_number) - self._thresholds = safe_remove(self._thresholds, threshold) - return threshold + @deprecated("Use thresholds.pop(sequence_number) instead.") + def remove_threshold_at( + self, + sequence_number: int, + ) -> RelaySetting: + return self.thresholds.pop(sequence_number) + @deprecated("Use thresholds.clear() instead.") def clear_thresholds(self) -> ProtectionRelayFunction: - """ - Removes all thresholds from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._thresholds = None + self.thresholds.clear() return self + # endregion + + # region time_limits boilerplate + @deprecated("Use time_limits.for_each_indexed(action)") def for_each_time_limit(self, action: Callable[[int, float], Any]): - """ - Call the `action` on each time limit in the `time_limits` collection - - :param action: An action to apply to each time limit in the `time_limits` collection, taking the index of the limit, and the limit itself. - """ - for index, limit in enumerate(self.time_limits): - action(index, limit) - - def add_time_limit(self, time_limit: float, index: int = None) -> ProtectionRelayFunction: - """ - Add a time limit. - - :param time_limit: The time limit in seconds to add to this :class:`ProtectionRelayFunction`. - :param index: The index into the list to add the time limit at. Defaults to the end of the list. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if index is None: - index = self.num_time_limits() - require(0 <= index <= self.num_time_limits(), - lambda: f"Unable to add float to {str(self)}. Sequence number {index} " - f"is invalid. Expected a value between 0 and {self.num_time_limits()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._time_limits = list() if self._time_limits is None else self._time_limits - self._time_limits.insert(index, time_limit) + self.time_limits.for_each_indexed(action) + + @deprecated( + "Use time_limits.append(time_limit)") + def add_time_limit( + self, + time_limit: float, + index: int = None, + ) -> ProtectionRelayFunction: + if index is None: index = len(self.time_limits) + self.time_limits.insert(index, time_limit) return self + @deprecated("Use len(time_limits) instead.") def num_time_limits(self) -> int: - return nlen(self._time_limits) - - def get_time_limit(self, index: int): - """ - Get the time limit for this :class:`ProtectionRelayFunction` by its `index`. - - :param index: The index of the desired time limit. - :returns: The time limit with the specified `index` if it exists. - :raises IndexError: if no time limit was found with provided index. - """ - if self._time_limits is not None: - return self._time_limits[index] - else: - raise IndexError(index) - - def remove_time_limit(self, time_limit: float) -> ProtectionRelayFunction: - """ - Remove a time limit from the list. - - :param time_limit: The time limit to remove. - :returns: A reference to this `ProtectionRelayFunction` to allow fluent use. - """ - self._time_limits = safe_remove(self._time_limits, time_limit) + return len(self.time_limits) + + @deprecated("Use time_limits[index] instead.") + def get_time_limit(self, index: int) -> float: + return self.time_limits[index] + + @deprecated("Use time_limits.remove(time_limit) instead.") + def remove_time_limit( + self, + time_limit: float, + ) -> ProtectionRelayFunction: + self.time_limits.remove(time_limit) return self + @deprecated("Use time_limits.pop(index) instead.") def remove_time_limit_at(self, index: int) -> float: - """ - Remove a time limit from the list. - - :param index: The time limit to remove. - :returns: The time limit that was removed, or `None` if no time limit was present at `index`. - :raises IndexError: If `sequence_number` is out of range. - """ - if self._time_limits: - limit = self._time_limits.pop(index) - self._time_limits = self._time_limits if self._time_limits else None - return limit - raise IndexError(index) + return self.time_limits.pop(index) + @deprecated("Use time_limits.clear() instead.") def clear_time_limits(self) -> ProtectionRelayFunction: - """ - Removes all time limits from this :class:`ProtectionRelayFunction`. - - :returns: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._time_limits = None + self.time_limits.clear() return self - def num_sensors(self) -> int: - """ - Get the number of :class:`Sensors` for this :class:`ProtectionRelayFunction`. + # endregion - :return: The number of :class:`Sensors` for this :class:`ProtectionRelayFunction`. - """ - return nlen(self._sensors) + # region sensors boilerplate - def get_sensor(self, mrid: str) -> Sensor: - """ - Get a sensor :class:`Sensor` for this :class:`ProtectionRelayFunction` by its mrid. + @deprecated("Use len(obj.sensors) instead.") + def num_sensors(self) -> int: + return len(self.sensors) - :param mrid: The mrid of the desired :class:`Sensor`. - :returns: The :class:`Sensor` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._sensors, mrid) + @deprecated("Use obj.sensors.get_by_mrid(mrid) instead.") + def get_sensor(self, mrid: str) -> Sensor: + return self.sensors.get_by_mrid(mrid) + @deprecated("Use obj.sensors.append(sensor) instead.") def add_sensor(self, sensor: Sensor) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`Sensor`. - - :param sensor: The :class:`Sensor` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(sensor, self.get_sensor, "A Sensor"): - return self - self._sensors = list() if self._sensors is None else self._sensors - self._sensors.append(sensor) + self.sensors.append(sensor) return self + @deprecated("Use obj.sensors.remove(sensor) instead.") def remove_sensor(self, sensor: Optional[Sensor]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`Sensor`. - - :param sensor: The :class:`Sensor` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If sensor was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._sensors, sensor) + self.sensors.remove(sensor) return self + @deprecated("Use obj.sensors.clear() instead.") def clear_sensors(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`Sensors` from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = None + self.sensors.clear() return self - def num_protected_switches(self) -> int: - """ - Get the number of :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. + # endregion sensors boilerplate - :return: The number of :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - """ - return nlen(self._protected_switches) + # region protected_switches boilerplate - def get_protected_switch(self, mrid: str) -> ProtectedSwitch: - """ - Get a :class:`ProtectedSwitch` operated by this :class:`ProtectionRelayFunction` by its mrid. + @deprecated("Use len(obj.protected_switches) instead.") + def num_protected_switches(self) -> int: + return len(self.protected_switches) - :param mrid: The mrid of the desired :class:`ProtectedSwitch`. - :returns: The :class:`ProtectedSwitch` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._protected_switches, mrid) + @deprecated("Use obj.protected_switches.get_by_mrid(mrid) instead.") + def get_protected_switch(self, mrid: str) -> ProtectedSwitch: + return self.protected_switches.get_by_mrid(mrid) + @deprecated("Use obj.protected_switches.append(protected_switch) instead.") def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`ProtectedSwitch` it operates. - - :param protected_switch: The :class:`ProtectedSwitch` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(protected_switch, self.get_protected_switch, "A ProtectedSwitch"): - return self - self._protected_switches = list() if self._protected_switches is None else self._protected_switches - self._protected_switches.append(protected_switch) + self.protected_switches.append(protected_switch) return self + @deprecated("Use obj.protected_switches.remove(protected_switch) instead.") def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. - - :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._protected_switches, protected_switch) + self.protected_switches.remove(protected_switch) return self + @deprecated("Use obj.protected_switches.clear() instead.") def clear_protected_switches(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`ProtectedSwitches` from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._protected_switches = None + self.protected_switches.clear() return self - def num_schemes(self) -> int: - """ - Get the number of :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. + # endregion protected_switches boilerplate - :return: The number of:class:`ProtectionRelaySchemes` operated by this :class:`ProtectionRelayFunction`. - """ - return nlen(self._schemes) + # region schemes boilerplate - def get_scheme(self, mrid: str) -> ProtectionRelayScheme: - """ - Get a :class:`ProtectionRelayScheme` this :class:`ProtectionRelayFunction` operates under by its mRID. + @deprecated("Use len(obj.schemes) instead.") + def num_schemes(self) -> int: + return len(self.schemes) - :param mrid: The mRID of the desired :class:`ProtectionRelayScheme`. - :returns: The :class:`ProtectionRelayScheme` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._schemes, mrid) + @deprecated("Use obj.schemes.get_by_mrid(mrid) instead.") + def get_scheme(self, mrid: str) -> ProtectionRelayScheme: + return self.schemes.get_by_mrid(mrid) + @deprecated("Use obj.schemes.append(scheme) instead.") def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`ProtectionRelayScheme` it operates under. - - :param scheme: The :class:`ProtectionRelayScheme` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(scheme, self.get_scheme, "A ProtectionRelayScheme"): - return self - self._schemes = list() if self._schemes is None else self._schemes - self._schemes.append(scheme) + self.schemes.append(scheme) return self + @deprecated("Use obj.schemes.remove(scheme) instead.") def remove_scheme(self, scheme: Optional[ProtectionRelayScheme]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectionRelayScheme`. - - :param scheme: The :class:`ProtectionRelayScheme` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If scheme was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._schemes = safe_remove(self._schemes, scheme) + self.schemes.remove(scheme) return self + @deprecated("Use obj.schemes.clear() instead.") def clear_schemes(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`ProtectionRelaySchemes` from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._schemes = None + self.schemes.clear() return self + + # endregion schemes boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index 2d6a42987..01252e8f9 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -7,12 +7,15 @@ __all__ = ["ProtectionRelayScheme"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem @@ -30,70 +33,40 @@ class ProtectionRelayScheme(IdentifiedObject): system: Optional[ProtectionRelaySystem] = None """[ZBEX] The system this scheme belongs to.""" - _functions: Optional[List[ProtectionRelayFunction]] = None + _functions: Optional[List[ProtectionRelayFunction]] = field(default=None) - def __init__(self, *args, functions: Optional[List[ProtectionRelayFunction]] = None, **kwargs): - super(ProtectionRelayScheme, self).__init__(*args, **kwargs) - if functions is not None: - for function in functions: - self.add_function(function) + functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _functions, + "A ProtectionRelayFunction", + ) - @property - def functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - [ZBEX] 6Yields all the functions operated as part of this :class:`ProtectionRelayScheme`. - :return: A generator that iterates over all functions operated as part of this :class:`ProtectionRelayScheme`. - """ - return ngen(self._functions) + # region deprecated list boilerplate + # region functions boilerplate + @deprecated("Use len(obj.functions) instead.") def num_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunctions` operated as part of this :class:`ProtectionRelayScheme`. - - :return: The number of :class:`ProtectionRelayFunctions` operated as part of this :class:`ProtectionRelayScheme`. - """ - return nlen(self._functions) + return len(self.functions) + @deprecated("Use obj.functions.get_by_mrid(mrid) instead.") def get_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` operated as part of this :class:`ProtectionRelayScheme`. - - :param mrid: The mrid of the desired :class:`ProtectionRelayFunction`. - :returns: The :class:`ProtectionRelayFunction` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._functions, mrid) + return self.functions.get_by_mrid(mrid) + @deprecated("Use obj.functions.append(function) instead.") def add_function(self, function: ProtectionRelayFunction) -> ProtectionRelayScheme: - """ - Associate a :class:`ProtectionRelayFunction` with this :class:`ProtectionRelayScheme`. - - :param function: The :class:`ProtectionRelayFunction` to associate with this :class:`ProtectionRelayScheme`. - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - if self._validate_reference(function, self.get_function, "A ProtectionRelayFunction"): - return self - self._functions = list() if self._functions is None else self._functions - self._functions.append(function) + self.functions.append(function) return self + @deprecated("Use obj.functions.remove(function) instead.") def remove_function(self, function: Optional[ProtectionRelayFunction]) -> ProtectionRelayScheme: - """ - Disassociate this :class:`ProtectionRelayScheme` from a :class:`ProtectionRelayFunction`. - - :param function: The :class:`ProtectionRelayFunction` to disassociate from this :class:`ProtectionRelayScheme`. - :raises ValueError: If function was not associated with this :class:`ProtectionRelayScheme`. - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - self._functions = safe_remove(self._functions, function) + self.functions.remove(function) return self + @deprecated("Use obj.functions.clear() instead.") def clear_function(self) -> ProtectionRelayScheme: - """ - Disassociate all :class:`ProtectionRelayFunctions` from this :class:`ProtectionRelayScheme`. - - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - self._functions = None + self.functions.clear() return self + + # endregion functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index d0245a0e2..ec0f4fda5 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -7,13 +7,16 @@ __all__ = ["ProtectionRelaySystem"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme @@ -30,70 +33,40 @@ class ProtectionRelaySystem(Equipment): protection_kind: ProtectionKind = ProtectionKind.UNKNOWN """[ZBEX] The kind of protection being provided by this protection equipment.""" - _schemes: Optional[List[ProtectionRelayScheme]] = None + _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) - def __init__(self, *args, schemes: Optional[List[ProtectionRelayScheme]] = None, **kwargs): - super(ProtectionRelaySystem, self).__init__(*args, **kwargs) - if schemes is not None: - for scheme in schemes: - self.add_scheme(scheme) + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - @property - def schemes(self) -> Generator[ProtectionRelayScheme, None, None]: - """ - [ZBEX] Yields all the schemes implemented by this :class:`ProtectionRelaySystem`. - :return: A generator that iterates over all the schemes implemented by this :class:`ProtectionRelaySystem`. - """ - return ngen(self._schemes) + # region deprecated list boilerplate + # region schemes boilerplate + @deprecated("Use len(obj.schemes) instead.") def num_schemes(self) -> int: - """ - Get the number of :class:`ProtectionRelaySchemes` for this :class:`ProtectionRelaySystem`. - - :return: The number of :class:`ProtectionRelaySchemes` for this :class:`ProtectionRelaySystem`. - """ - return nlen(self._schemes) + return len(self.schemes) + @deprecated("Use obj.schemes.get_by_mrid(mrid) instead.") def get_scheme(self, mrid: str) -> ProtectionRelayScheme: - """ - Get a :class:`ProtectionRelayScheme` for this :class:`ProtectionRelaySystem` by its mRID. - - :param mrid: The mRID of the desired :class:`ProtectionRelayScheme`. - :returns: The :class:`ProtectionRelayScheme` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._schemes, mrid) + return self.schemes.get_by_mrid(mrid) + @deprecated("Use obj.schemes.append(scheme) instead.") def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelaySystem: - """ - Add a :class:`ProtectionRelayScheme` to this :class:`ProtectionRelaySystem`. - - :param scheme: The :class:`ProtectionRelayScheme` to add. - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - if self._validate_reference(scheme, self.get_scheme, "A ProtectionRelayScheme"): - return self - self._schemes = list() if self._schemes is None else self._schemes - self._schemes.append(scheme) + self.schemes.append(scheme) return self + @deprecated("Use obj.schemes.remove(scheme) instead.") def remove_scheme(self, scheme: Optional[ProtectionRelayScheme]) -> ProtectionRelaySystem: - """ - Remove a :class:`ProtectionRelayScheme` from this :class:`ProtectionRelaySystem`. - - :param scheme: The :class:`ProtectionRelayScheme` to remove. - :raises ValueError: If scheme was not associated with this :class:`ProtectionRelaySystem`. - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - self._schemes = safe_remove(self._schemes, scheme) + self.schemes.remove(scheme) return self + @deprecated("Use obj.schemes.clear() instead.") def clear_scheme(self) -> ProtectionRelaySystem: - """ - Remove all :class:`ProtectionRelaySchemes` from this :class:`ProtectionRelaySystem`. - - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - self._schemes = None + self.schemes.clear() return self + + # endregion schemes boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index c774d5006..04d5077c2 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -7,12 +7,15 @@ __all__ = ["PowerTransformerInfo"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo @@ -22,21 +25,14 @@ class PowerTransformerInfo(AssetInfo): """Set of power transformer data, from an equipment library.""" - _transformer_tank_infos: Optional[List[TransformerTankInfo]] = None + _transformer_tank_infos: Optional[List[TransformerTankInfo]] = field(default=None) """Data for all the tanks described by this power transformer data.""" - def __init__(self, *args, transformer_tank_infos: List[TransformerTankInfo] = None, **kwargs): - super(PowerTransformerInfo, self).__init__(*args, **kwargs) - if transformer_tank_infos: - for ti in transformer_tank_infos: - self.add_transformer_tank_info(ti) + transformer_tank_infos: MridCollection[TransformerTankInfo] = LazyMridList( + _transformer_tank_infos, + "A TransformerTankInfo", + ) - @property - def transformer_tank_infos(self) -> Generator[TransformerTankInfo, None, None]: - """ - The `TransformerTankInfo`s of this `PowerTransformerInfo`. - """ - return ngen(self._transformer_tank_infos) def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance]: """ @@ -51,55 +47,32 @@ def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance] else: return None + # region deprecated list boilerplate + # region transformer_tank_infos boilerplate + + @deprecated("Use len(obj.transformer_tank_infos) instead.") def num_transformer_tank_infos(self): - """ - Get the number of `TransformerTankInfo`s associated with this `PowerTransformerInfo`. - """ - return nlen(self._transformer_tank_infos) + return len(self.transformer_tank_infos) + @deprecated("Use obj.transformer_tank_infos.get_by_mrid(mrid) instead.") def get_transformer_tank_info(self, mrid: str) -> TransformerTankInfo: - """ - Get the `TransformerTankInfo` for this `PowerTransformerInfo` identified by `mrid`. - - `mrid` the mRID of the required `TransformerTankInfo` - Returns The `TransformerTankInfo` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._transformer_tank_infos, mrid) + return self.transformer_tank_infos.get_by_mrid(mrid) + @deprecated("Use obj.transformer_tank_infos.append(tti) instead.") def add_transformer_tank_info(self, tti: TransformerTankInfo) -> PowerTransformerInfo: - """ - `tti` The `TransformerTankInfo` to - associate with this `PowerTransformerInfo`. - - Returns A reference to this `PowerTransformerInfo` to allow fluent use. - - Raises `ValueError` if another `TransformerTankInfo` with the same `mrid` already - exists in this `PowerTransformerInfo` - """ - if self._validate_reference(tti, self.get_transformer_tank_info, "A TransformerTankInfo"): - return self - - self._transformer_tank_infos = list() if self._transformer_tank_infos is None else self._transformer_tank_infos - self._transformer_tank_infos.append(tti) + self.transformer_tank_infos.append(tti) return self + @deprecated("Use obj.transformer_tank_infos.remove(tti) instead.") def remove_transformer_tank_info(self, tti: TransformerTankInfo) -> PowerTransformerInfo: - """ - Disassociate an `TransformerTankInfo` from this `PowerTransformerInfo`. - - `tti` the `TransformerTankInfo` to - disassociate with this `PowerTransformerInfo`. - Raises `ValueError` if `tti` was not associated with this `PowerTransformerInfo`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._transformer_tank_infos = safe_remove(self._transformer_tank_infos, tti) + self.transformer_tank_infos.remove(tti) return self + @deprecated("Use obj.transformer_tank_infos.clear() instead.") def clear_transformer_tank_infos(self) -> PowerTransformerInfo: - """ - Clears all `TransformerTankInfo`. - Returns self - """ - self._transformer_tank_infos = None + self.transformer_tank_infos.clear() return self + + # endregion transformer_tank_infos boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index 1d827d563..4f29eb367 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -7,12 +7,15 @@ __all__ = ["TransformerTankInfo"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import nlen, ngen, safe_remove, get_by_mrid from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo @@ -26,21 +29,14 @@ class TransformerTankInfo(AssetInfo): power_transformer_info: Optional[PowerTransformerInfo] = None """Power transformer data that this tank description is part of.""" - _transformer_end_infos: Optional[List[TransformerEndInfo]] = None + _transformer_end_infos: Optional[List[TransformerEndInfo]] = field(default=None) """Data for all the ends described by this transformer tank data.""" - def __init__(self, *args, transformer_end_infos: List[TransformerEndInfo] = None, **kwargs): - super(TransformerTankInfo, self).__init__(*args, **kwargs) - if transformer_end_infos: - for tei in transformer_end_infos: - self.add_transformer_end_info(tei) + transformer_end_infos: MridCollection[TransformerEndInfo] = LazyMridList( + _transformer_end_infos, + "A TransformerEndInfo", + ) - @property - def transformer_end_infos(self) -> Generator[TransformerEndInfo, None, None]: - """ - The `TransformerEndInfo`s of this `TransformerTankInfo`. - """ - return ngen(self._transformer_end_infos) def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance]: """ @@ -56,55 +52,32 @@ def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance] else: return None + # region deprecated list boilerplate + # region transformer_end_infos boilerplate + + @deprecated("Use len(obj.transformer_end_infos) instead.") def num_transformer_end_infos(self): - """ - Get the number of `TransformerEndInfo`s associated with this `TransformerTankInfo`. - """ - return nlen(self._transformer_end_infos) + return len(self.transformer_end_infos) + @deprecated("Use obj.transformer_end_infos.get_by_mrid(mrid) instead.") def get_transformer_end_info(self, mrid: str) -> TransformerEndInfo: - """ - Get the `TransformerEndInfo` for this `TransformerTankInfo` identified by `mrid`. - - `mrid` the mRID of the required `TransformerEndInfo` - Returns The `TransformerEndInfo` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._transformer_end_infos, mrid) + return self.transformer_end_infos.get_by_mrid(mrid) + @deprecated("Use obj.transformer_end_infos.append(tei) instead.") def add_transformer_end_info(self, tei: TransformerEndInfo) -> TransformerTankInfo: - """ - `tei` The `TransformerEndInfo` to - associate with this `TransformerTankInfo`. - - Returns A reference to this `TransformerTankInfo` to allow fluent use. - - Raises `ValueError` if another `TransformerEndInfo` with the same `mrid` already - exists in this `TransformerTankInfo` - """ - if self._validate_reference(tei, self.get_transformer_end_info, "A TransformerEndInfo"): - return self - - self._transformer_end_infos = list() if self._transformer_end_infos is None else self._transformer_end_infos - self._transformer_end_infos.append(tei) + self.transformer_end_infos.append(tei) return self + @deprecated("Use obj.transformer_end_infos.remove(tei) instead.") def remove_transformer_end_info(self, tei: TransformerEndInfo) -> TransformerTankInfo: - """ - Disassociate an `TransformerEndInfo` from this `TransformerTankInfo`. - - `tei` the `TransformerEndInfo` to - disassociate with this `TransformerTankInfo`. - Raises `ValueError` if `tei` was not associated with this `TransformerTankInfo`. - Returns A reference to this `TransformerTankInfo` to allow fluent use. - """ - self._transformer_end_infos = safe_remove(self._transformer_end_infos, tei) + self.transformer_end_infos.remove(tei) return self + @deprecated("Use obj.transformer_end_infos.clear() instead.") def clear_transformer_end_infos(self) -> TransformerTankInfo: - """ - Clears all `TransformerEndInfo`. - Returns self - """ - self._transformer_end_infos = None + self.transformer_end_infos.clear() return self + + # endregion transformer_end_infos boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index 9ca727541..ea8989474 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -7,12 +7,15 @@ __all__ = ["Asset"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole @@ -32,126 +35,74 @@ class Asset(IdentifiedObject, metaclass=ABCMeta): location: Optional[Location] = None """`zepben.ewb.model.cim.iec61968.common.location.Location` of this asset""" - _organisation_roles: Optional[List[AssetOrganisationRole]] = None + _organisation_roles: Optional[List[AssetOrganisationRole]] = field(default=None) - _power_system_resources: Optional[List[PowerSystemResource]] = None + _power_system_resources: Optional[List[PowerSystemResource]] = field(default=None) - def __init__(self, *args, organisation_roles: List[AssetOrganisationRole] = None, power_system_resources: List[PowerSystemResource] = None, **kwargs): - super(Asset, self).__init__(*args, **kwargs) - if organisation_roles: - for role in organisation_roles: - self.add_organisation_role(role) + organisation_roles: MridCollection[AssetOrganisationRole] = LazyMridList( + _organisation_roles, + "An AssetOrganisationRole", + ) - if power_system_resources: - for resource in power_system_resources: - self.add_power_system_resource(resource) + power_system_resources: MridCollection[PowerSystemResource] = LazyMridList( + _power_system_resources, + "An PowerSystemResource", + ) - @property - def organisation_roles(self) -> Generator[AssetOrganisationRole, None, None]: - """ - The `AssetOrganisationRole`s of this `Asset`. - """ - return ngen(self._organisation_roles) - @property - def power_system_resources(self) -> Generator[PowerSystemResource, None, None]: - """ - The `PowerSystemResource`s of this `Asset`. - """ - return ngen(self._power_system_resources) + # region deprecated list boilerplate + # region organisation_roles boilerplate + @deprecated("Use len(obj.organisation_roles) instead.") def num_organisation_roles(self) -> int: - """ - Get the number of `AssetOrganisationRole`s associated with this `Asset`. - """ - return nlen(self._organisation_roles) + return len(self.organisation_roles) + @deprecated("Use obj.organisation_roles.get_by_mrid(mrid) instead.") def get_organisation_role(self, mrid: str) -> AssetOrganisationRole: - """ - Get the `AssetOrganisationRole` for this asset identified by `mrid`. - - `mrid` the mRID of the required `AssetOrganisationRole` - Returns The `AssetOrganisationRole` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._organisation_roles, mrid) + return self.organisation_roles.get_by_mrid(mrid) + @deprecated("Use obj.organisation_roles.append(role) instead.") def add_organisation_role(self, role: AssetOrganisationRole) -> Asset: - """ - `role` The `AssetOrganisationRole` to associate with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - Raises `ValueError` if another `AssetOrganisationRole` with the same `mrid` already exists in this `Asset` - """ - if self._validate_reference(role, self.get_organisation_role, "An AssetOrganisationRole"): - return self - - self._organisation_roles = list() if self._organisation_roles is None else self._organisation_roles - self._organisation_roles.append(role) + self.organisation_roles.append(role) return self + @deprecated("Use obj.organisation_roles.remove(role) instead.") def remove_organisation_role(self, role: AssetOrganisationRole) -> Asset: - """ - Disassociate an `AssetOrganisationRole` from this `Asset`. - - `role` the `AssetOrganisationRole` to disassociate from this `Asset`. - Raises `ValueError` if `role` was not associated with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._organisation_roles = safe_remove(self._organisation_roles, role) + self.organisation_roles.remove(role) return self + @deprecated("Use obj.organisation_roles.clear() instead.") def clear_organisation_roles(self) -> Asset: - """ - Clear all organisation roles. - Returns self - """ - self._organisation_roles = None + self.organisation_roles.clear() return self + # endregion organisation_roles boilerplate + + # region power_system_resources boilerplate + + @deprecated("Use len(obj.power_system_resources) instead.") def num_power_system_resources(self) -> int: - """ - Get the number of `PowerSystemResource`s associated with this `Asset`. - """ - return nlen(self._power_system_resources) + return len(self.power_system_resources) + @deprecated("Use obj.power_system_resources.get_by_mrid(mrid) instead.") def get_power_system_resource(self, mrid: str) -> PowerSystemResource: - """ - Get the `PowerSystemResource` for this asset identified by `mrid`. - - `mrid` the mRID of the required `PowerSystemResource` - Returns The `PowerSystemResource` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_system_resources, mrid) + return self.power_system_resources.get_by_mrid(mrid) + @deprecated("Use obj.power_system_resources.append(resource) instead.") def add_power_system_resource(self, resource: PowerSystemResource) -> Asset: - """ - `resource` The `PowerSystemResource` to associate with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - Raises `ValueError` if another `PowerSystemResource` with the same `mrid` already exists in this `Asset` - """ - if self._validate_reference(resource, self.get_power_system_resource, "An PowerSystemResource"): - return self - - self._power_system_resources = list() if self._power_system_resources is None else self._power_system_resources - self._power_system_resources.append(resource) + self.power_system_resources.append(resource) return self + @deprecated("Use obj.power_system_resources.remove(resource) instead.") def remove_power_system_resource(self, resource: PowerSystemResource) -> Asset: - """ - Disassociate an `PowerSystemResource` from this `Asset`. - - `resource` the `PowerSystemResource` to disassociate from this `Asset`. - Raises `ValueError` if `resource` was not associated with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._power_system_resources = safe_remove(self._power_system_resources, resource) + self.power_system_resources.remove(resource) return self + @deprecated("Use obj.power_system_resources.clear() instead.") def clear_power_system_resources(self) -> Asset: - """ - Clear all power system resources. - Returns self - """ - self._power_system_resources = None + self.power_system_resources.clear() return self + + # endregion power_system_resources boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index cceb97065..c20302742 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -7,13 +7,16 @@ __all__ = ["Location"] -from typing import List, Optional, Generator, Callable, Any +from dataclasses import field +from typing import List, Optional, Callable, Any +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import require, nlen, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass @@ -25,107 +28,86 @@ class Location(IdentifiedObject): main_address: Optional[StreetAddress] = None """Main address of the location.""" - _position_points: Optional[List[PositionPoint]] = None + _position_points: Optional[List[PositionPoint]] = field(default=None) - def __init__(self, *args, position_points: List[PositionPoint] = None, **kwargs): - """ - `position_points` A list of `PositionPoint`s to associate with this `Location`. - """ + def __init__(self, *args, position_points=None, **kwargs): super(Location, self).__init__(*args, **kwargs) - if position_points: - for point in position_points: - self.add_point(point) - - @property - def points(self) -> Generator[PositionPoint, None, None]: - """ - Returns Generator over the `PositionPoint`s of this `Location`. - """ - for point in ngen(self._position_points): - yield point - - def for_each_point(self, action: Callable[[int, PositionPoint], Any]): - """ - Call the `action` on each :class:`PositionPoint` in the `points` collection - - :param action: An action to apply to each :class:`PositionPoint` in the `points` collection, taking the index of the point, and the point itself. - """ - for index, point in enumerate(self.points): - action(index, point) - + self.points.extend(position_points) + + points: LazyIndexList[PositionPoint] = LazyIndexList( + _position_points, + "PositionPoint", + ) + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. + + # region points boilerplate + + @deprecated("Use points.for_each_indexed(action) instead.") + def for_each_point( + self, + action: Callable[[int, PositionPoint], Any], + ): + self.points.for_each_indexed(action) + + @deprecated("Use len(points) instead.") def num_points(self): - """ - Returns The number of `PositionPoint`s in this `Location` - """ - return nlen(self._position_points) + return len(self.points) + @deprecated("Use points[sequence_number] instead.") def get_point(self, sequence_number: int) -> PositionPoint: - """ - Get the `sequence_number` `PositionPoint` for this `Location`. - - `sequence_number` The sequence number of the `PositionPoint` to get. - Returns The `PositionPoint` identified by `sequence_number` - Raises IndexError if this `Location` didn't contain `sequence_number` points. - """ - return self._position_points[sequence_number] + return self.points[sequence_number] - def __getitem__(self, item): - return self.get_point(item) + @deprecated("Use points[item] instead.") + def __getitem__(self, item: int) -> PositionPoint: + return self.points[item] + @deprecated("Use points.append(point) instead.") def add_point(self, point: PositionPoint) -> Location: - """ - Associate a `PositionPoint` with this `Location`, assigning it a sequence_number of `num_points`. - `point` The `PositionPoint` to associate with this `Location`. - Returns A reference to this `Location` to allow fluent use. - """ - return self.insert_point(point) - - def insert_point(self, point: PositionPoint, sequence_number: int = None) -> Location: - """ - Associate a `PositionPoint` with this `Location` - - `point` The `PositionPoint` to associate with this `Location`. - `sequence_number` The sequence number of the `PositionPoint`. - Returns A reference to this `Location` to allow fluent use. - Raises `ValueError` if `sequence_number` < 0 or > `num_points()`. - """ - if sequence_number is None: - sequence_number = self.num_points() - require(0 <= sequence_number <= self.num_points(), - lambda: f"Unable to add PositionPoint to {str(self)}. Sequence number {sequence_number} " - f"is invalid. Expected a value between 0 and {self.num_points()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._position_points = list() if self._position_points is None else self._position_points - self._position_points.insert(sequence_number, point) + self.points.append(point) return self - def __setitem__(self, key, value): - return self.insert_point(value, key) + @deprecated("Use points.insert(sequence_number, point)") + def insert_point( + self, + point: PositionPoint, + sequence_number: int | None = None, + ) -> Location: + if sequence_number is None: sequence_number = len(self.points) + self.points.insert(sequence_number, point) - def remove_point(self, point: PositionPoint) -> Location: - """ - Remove a `PositionPoint` from this `Location` - `point` The `PositionPoint` to remove. - Raises `ValueError` if `point` was not part of this `Location` - Returns A reference to this `Location` to allow fluent use. - """ - self._position_points = safe_remove(self._position_points, point) return self - def remove_point_by_sequence_number(self, sequence_number: int) -> PositionPoint: - """ - Remove a :class:`PositionPoint` from this :class:`Location` by its sequence number. + @deprecated("Use points.insert(key, value) instead.") + def __setitem__( + self, + key: int, + value: PositionPoint, + ) -> None: + self.points.insert(key, value) - NOTE: This will update the sequence numbers of all items located after the removed sequence number. + @deprecated("Use points.remove(point) instead.") + def remove_point(self, point: PositionPoint) -> Location: + self.points.remove(point) + return self - :param sequence_number: The sequence number of the `PositionPoint` to remove. - :return: The :class:`PositionPoint` that was removed, or null if there was no :class:`PositionPoint` for the given `sequenceNumber`. - :raises IndexError: If no :class:`PositionPoint` with the specified `sequence_number` was not associated with this :class:`Location`. - """ - point = self.get_point(sequence_number) - self._position_points = safe_remove(self._position_points, point) - return point + @deprecated("Use points.pop(sequence_number) instead.") + def remove_point_by_sequence_number( + self, + sequence_number: int, + ) -> PositionPoint: + return self.points.pop(sequence_number) + @deprecated("Use points.clear() instead.") def clear_points(self) -> Location: - self._position_points = None + self.points.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index f1425e257..90c2a0a4e 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -7,12 +7,15 @@ __all__ = ["Customer"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement @@ -30,66 +33,43 @@ class Customer(OrganisationRole): special_need: Optional[str] = None """A special service need such as life support, hospitals, etc.""" - _customer_agreements: Optional[List[CustomerAgreement]] = None + _customer_agreements: Optional[List[CustomerAgreement]] = field(default=None) - def __init__(self, *args, customer_agreements: List[CustomerAgreement] = None, **kwargs): + def __init__(self, *args, customer_agreements=None, **kwargs): super(Customer, self).__init__(*args, **kwargs) - if customer_agreements: - for agreement in customer_agreements: - self.add_agreement(agreement) + self.agreements.extend(customer_agreements) - @property - def agreements(self) -> Generator[CustomerAgreement, None, None]: - """ - The `CustomerAgreement`s for this `Customer`. - """ - return ngen(self._customer_agreements) + agreements: MridCollection[CustomerAgreement] = LazyMridList( + _customer_agreements, + "A CustomerAgreement", + ) + # region deprecated list boilerplate + # region agreements boilerplate + + @deprecated("Use len(obj.agreements) instead.") def num_agreements(self) -> int: - """ - Get the number of `CustomerAgreement`s associated with this `Customer`. - """ - return nlen(self._customer_agreements) + return len(self.agreements) + @deprecated("Use obj.agreements.get_by_mrid(mrid) instead.") def get_agreement(self, mrid: str) -> CustomerAgreement: - """ - Get the `CustomerAgreement` for this `Customer` identified by `mrid`. - - `mrid` the mRID of the required `customer_agreement.CustomerAgreement` - Returns the `CustomerAgreement` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._customer_agreements, mrid) + return self.agreements.get_by_mrid(mrid) + @deprecated("Use obj.agreements.append(customer_agreement) instead.") def add_agreement(self, customer_agreement: CustomerAgreement) -> Customer: - """ - Associate a `CustomerAgreement` with this `Customer`. - `customer_agreement` The `customer_agreement.CustomerAgreement` to associate with this `Customer`. - Returns A reference to this `Customer` to allow fluent use. - Raises `ValueError` if another `CustomerAgreement` with the same `mrid` already exists for this `Customer` - """ - if self._validate_reference(customer_agreement, self.get_agreement, "A CustomerAgreement"): - return self - - self._customer_agreements = list() if self._customer_agreements is None else self._customer_agreements - self._customer_agreements.append(customer_agreement) + self.agreements.append(customer_agreement) return self + @deprecated("Use obj.agreements.remove(customer_agreement) instead.") def remove_agreement(self, customer_agreement: CustomerAgreement) -> Customer: - """ - Disassociate `customer_agreement` from this `Customer`. - - `customer_agreement` the `customer_agreement.CustomerAgreement` to disassociate with this `Customer`. - Returns A reference to this `Customer` to allow fluent use. - Raises `ValueError` if `customer_agreement` was not associated with this `Customer`. - """ - self._customer_agreements = safe_remove(self._customer_agreements, customer_agreement) + self.agreements.remove(customer_agreement) return self + @deprecated("Use obj.agreements.clear() instead.") def clear_agreements(self) -> Customer: - """ - Clear all customer agreements. - Returns self - """ - self._customer_agreements = None + self.agreements.clear() return self + + # endregion agreements boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 4bfaf89da..538806a6b 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -7,11 +7,14 @@ __all__ = ["CustomerAgreement"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.agreement import Agreement -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer @@ -29,15 +32,7 @@ class CustomerAgreement(Agreement): _customer: Optional[Customer] = None """The `zepben.ewb.model.cim.iec61968.customers.customer.Customer` that has this `CustomerAgreement`.""" - _pricing_structures: Optional[List[PricingStructure]] = None - - def __init__(self, *args, customer: Customer = None, pricing_structures: List[PricingStructure] = None, **kwargs): - super(CustomerAgreement, self).__init__(*args, **kwargs) - if customer: - self.customer = customer - if pricing_structures: - for ps in pricing_structures: - self.add_pricing_structure(ps) + _pricing_structures: Optional[List[PricingStructure]] = field(default=None) @property def customer(self): @@ -51,59 +46,39 @@ def customer(self, cust): else: raise ValueError(f"customer for {str(self)} has already been set to {self._customer}, cannot reset this field to {cust}") - @property - def pricing_structures(self) -> Generator[PricingStructure, None, None]: - """ - The `PricingStructure`s of this `CustomerAgreement`. - """ - return ngen(self._pricing_structures) + pricing_structures: MridCollection[PricingStructure] = LazyMridList( + _pricing_structures, + "A PricingStructure", + ) + + + # region deprecated list boilerplate + # region pricing_structures boilerplate + + @deprecated("Use len(obj.pricing_structures) instead.") def num_pricing_structures(self): - """ - The number of `PricingStructure`s associated with this `CustomerAgreement` - """ - return nlen(self._pricing_structures) + return len(self.pricing_structures) + @deprecated("Use obj.pricing_structures.get_by_mrid(mrid) instead.") def get_pricing_structure(self, mrid: str) -> PricingStructure: - """ - Get the `PricingStructure` for this `CustomerAgreement` identified by `mrid` - - `mrid` the mRID of the required `PricingStructure` - Returns the `PricingStructure` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._pricing_structures, mrid) + return self.pricing_structures.get_by_mrid(mrid) + @deprecated("Use obj.pricing_structures.append(ps) instead.") def add_pricing_structure(self, ps: PricingStructure) -> CustomerAgreement: - """ - Associate `ps` with this `CustomerAgreement` - - `ps` the `PricingStructure` to associate with this `CustomerAgreement`. - Returns A reference to this `CustomerAgreement` to allow fluent use. - Raises `ValueError` if another `PricingStructure` with the same `mrid` already exists for this `CustomerAgreement` - """ - if self._validate_reference(ps, self.get_pricing_structure, "A PricingStructure"): - return self - - self._pricing_structures = list() if self._pricing_structures is None else self._pricing_structures - self._pricing_structures.append(ps) + self.pricing_structures.append(ps) return self + @deprecated("Use obj.pricing_structures.remove(ps) instead.") def remove_pricing_structure(self, ps: PricingStructure) -> CustomerAgreement: - """ - Disassociate `ps` from this `CustomerAgreement` - - `ps` the `PricingStructure` to disassociate from this `CustomerAgreement`. - Returns A reference to this `CustomerAgreement` to allow fluent use. - Raises `ValueError` if `ps` was not associated with this `CustomerAgreement`. - """ - self._pricing_structures = safe_remove(self._pricing_structures, ps) + self.pricing_structures.remove(ps) return self + @deprecated("Use obj.pricing_structures.clear() instead.") def clear_pricing_structures(self) -> CustomerAgreement: - """ - Clear all pricing structures. - Returns a reference to this `CustomerAgreement` to allow fluent use. - """ - self._pricing_structures = None + self.pricing_structures.clear() return self + + # endregion pricing_structures boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index 55070f585..d0c353800 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -7,11 +7,14 @@ __all__ = ["PricingStructure"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff @@ -29,68 +32,42 @@ class PricingStructure(Document): :var code: Unique user-allocated key for this pricing structure, used by company representatives to identify the correct price structure for allocating to a customer. For rate schedules it is often prefixed by a state code. """ - _tariffs: Optional[List[Tariff]] = None + _tariffs: Optional[List[Tariff]] = field(default=None) code: str | None = None - def __init__(self, *args, tariffs: List[Tariff] = None, **kwargs): - super(PricingStructure, self).__init__(*args, **kwargs) - if tariffs: - for tariff in tariffs: - self.add_tariff(tariff) + tariffs: MridCollection[Tariff] = LazyMridList( + _tariffs, + "A Tariff", + ) - @property - def tariffs(self) -> Generator[Tariff, None, None]: - """ - The `Tariff`s of this `PricingStructure`. - """ - return ngen(self._tariffs) + # region deprecated list boilerplate + # region tariffs boilerplate + + @deprecated("Use len(obj.tariffs) instead.") def num_tariffs(self): - """ - Returns The number of `Tariff`s associated with this `PricingStructure` - """ - return nlen(self._tariffs) + return len(self.tariffs) + @deprecated("Use obj.tariffs.get_by_mrid(mrid) instead.") def get_tariff(self, mrid: str) -> Tariff: - """ - Get the `Tariff` for this `PricingStructure` identified by `mrid` - - `mrid` the mRID of the required `Tariff` - Returns The `Tariff` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._tariffs, mrid) + return self.tariffs.get_by_mrid(mrid) + @deprecated("Use obj.tariffs.append(tariff) instead.") def add_tariff(self, tariff: Tariff) -> PricingStructure: - """ - Associate a `Tariff` with this `PricingStructure`. - - `tariff` the `Tariff` to associate with this `PricingStructure`. - Returns A reference to this `PricingStructure` to allow fluent use. - Raises `ValueError` if another `Tariff` with the same `mrid` already exists for this `PricingStructure`. - """ - if self._validate_reference(tariff, self.get_tariff, "A Tariff"): - return self - self._tariffs = list() if self._tariffs is None else self._tariffs - self._tariffs.append(tariff) + self.tariffs.append(tariff) return self + @deprecated("Use obj.tariffs.remove(tariff) instead.") def remove_tariff(self, tariff: Tariff) -> PricingStructure: - """ - Disassociate `tariff` from this `PricingStructure`. - - `tariff` the `Tariff` to disassociate from this `PricingStructure`. - Returns A reference to this `PricingStructure` to allow fluent use. - Raises `ValueError` if `tariff` was not associated with this `PricingStructure`. - """ - self._tariffs = safe_remove(self._tariffs, tariff) + self.tariffs.remove(tariff) return self + @deprecated("Use obj.tariffs.clear() instead.") def clear_tariffs(self) -> PricingStructure: - """ - Clear all tariffs. - Returns A reference to this `PricingStructure` to allow fluent use. - """ - self._tariffs = None + self.tariffs.clear() return self + + # endregion tariffs boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index 2a6b1c6b9..7c13ee54c 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -7,11 +7,14 @@ __all__ = ["Pole"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.structure import Structure -from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight @@ -24,66 +27,40 @@ class Pole(Structure): classification: Optional[str] = None """Pole class: 1, 2, 3, 4, 5, 6, 7, H1, H2, Other, Unknown.""" - _streetlights: Optional[List[Streetlight]] = None + _streetlights: Optional[List[Streetlight]] = field(default=None) - def __init__(self, *args, streetlights: List[Streetlight] = None, **kwargs): - super(Pole, self).__init__(*args, **kwargs) - if streetlights: - for light in streetlights: - self.add_streetlight(light) + streetlights: MridCollection[Streetlight] = LazyMridList( + _streetlights, + "A Streetlight", + ) - @property - def streetlights(self) -> Generator[Streetlight, None, None]: - """ - The `Streetlight`s of this `Pole`. - """ - return ngen(self._streetlights) + # region deprecated list boilerplate + # region streetlights boilerplate + + @deprecated("Use len(obj.streetlights) instead.") def num_streetlights(self) -> int: - """ - Get the number of `Streetlight`s associated with this `Pole`. - """ - return nlen(self._streetlights) + return len(self.streetlights) + @deprecated("Use obj.streetlights.get_by_mrid(mrid) instead.") def get_streetlight(self, mrid: str) -> Streetlight: - """ - Get the `Streetlight` for this asset identified by `mrid`. - - `mrid` the mRID of the required `Streetlight` - Returns The `Streetlight` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._streetlights, mrid) + return self.streetlights.get_by_mrid(mrid) + @deprecated("Use obj.streetlights.append(streetlight) instead.") def add_streetlight(self, streetlight: Streetlight) -> Pole: - """ - Associate a `Streetlight` with this `Pole` - - `streetlight` the `Streetlight` to associate with this `Pole`. - Returns A reference to this `Pole` to allow fluent use. - Raises `ValueError` if another `Streetlight` with the same `mrid` already exists in this `Pole` - """ - if self._validate_reference(streetlight, self.get_streetlight, "A Streetlight"): - return self - - self._streetlights = list() if self._streetlights is None else self._streetlights - self._streetlights.append(streetlight) + self.streetlights.append(streetlight) return self + @deprecated("Use obj.streetlights.remove(streetlight) instead.") def remove_streetlight(self, streetlight: Streetlight) -> Pole: - """ - Disassociate `streetlight` from this `Pole` - `streetlight` the `Streetlight` to disassociate from this `Pole`. - Raises `ValueError` if `streetlight` was not associated with this `Pole`. - Returns A reference to this `Pole` to allow fluent use. - """ - self._streetlights = safe_remove(self._streetlights, streetlight) + self.streetlights.remove(streetlight) return self + @deprecated("Use obj.streetlights.clear() instead.") def clear_streetlights(self) -> Pole: - """ - Clear all Streetlights. - Returns self - """ - self._streetlights = None + self.streetlights.clear() return self + + # endregion streetlights boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py index 943eaebf0..9bef5881f 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from enum import Enum -from typing import List, Union +from typing import List from zepben.ewb import unique @@ -69,7 +69,7 @@ class ControlledAppliance: _bitmask: int - def __init__(self, appliances: Union[int, Appliance, List[Appliance]]): + def __init__(self, appliances: int | Appliance | List[Appliance]): if isinstance(appliances, int): self._bitmask = appliances elif isinstance(appliances, Appliance): diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index 59bd836e1..ed4f559a2 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -7,12 +7,15 @@ __all__ = ["EndDevice"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location @@ -42,127 +45,74 @@ class EndDevice(AssetContainer, metaclass=ABCMeta): service_location: Optional[Location] = None """Service `zepben.ewb.model.cim.iec61968.common.location.Location` whose service delivery is measured by this `EndDevice`.""" - _usage_points: Optional[List[UsagePoint]] = None - - _functions: Optional[List[EndDeviceFunction]] = None - - def __init__(self, *args, usage_points: List[UsagePoint] = None, functions: List[EndDeviceFunction] = None, **kwargs): - super(EndDevice, self).__init__(*args, **kwargs) - if usage_points: - for up in usage_points: - self.add_usage_point(up) - if functions: - for edf in functions: - self.add_function(edf) - - @property - def usage_points(self) -> Generator[UsagePoint, None, None]: - """ - The `UsagePoint`s associated with this `EndDevice` - """ - return ngen(self._usage_points) - - @property - def functions(self) -> Generator[EndDeviceFunction, None, None]: - """ - The `EndDeviceFunction`s associated with this `EndDevice` - """ - return ngen(self._functions) + _usage_points: Optional[List[UsagePoint]] = field(default=None) + _functions: Optional[List[EndDeviceFunction]] = field(default=None) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + functions: MridCollection[EndDeviceFunction] = LazyMridList( + _functions, + "An EndDeviceFunction", + ) + + + # region deprecated list boilerplate + # region usage_points boilerplate + + @deprecated("Use len(obj.usage_points) instead.") def num_usage_points(self): - """ - Returns The number of `UsagePoint`s associated with this `EndDevice` - """ - return nlen(self._usage_points) + return len(self.usage_points) + @deprecated("Use obj.usage_points.get_by_mrid(mrid) instead.") def get_usage_point(self, mrid: str) -> UsagePoint: - """ - Get the `UsagePoint` for this `EndDevice` identified by `mrid` - - `mrid` the mRID of the required `UsagePoint` - Returns The `UsagePoint` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._usage_points, mrid) + return self.usage_points.get_by_mrid(mrid) + @deprecated("Use obj.usage_points.append(up) instead.") def add_usage_point(self, up: UsagePoint) -> EndDevice: - """ - Associate `up` to this `EndDevice`. - - `up` the `UsagePoint` to associate with this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if another `UsagePoint` with the same `mrid` already exists for this `EndDevice`. - """ - if self._validate_reference(up, self.get_usage_point, "A UsagePoint"): - return self - self._usage_points = list() if self._usage_points is None else self._usage_points - self._usage_points.append(up) + self.usage_points.append(up) return self + @deprecated("Use obj.usage_points.remove(up) instead.") def remove_usage_point(self, up: UsagePoint) -> EndDevice: - """ - Disassociate `up` from this `EndDevice` - - `up` the `UsagePoint` to disassociate from this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `EndDevice`. - """ - self._usage_points = safe_remove(self._usage_points, up) + self.usage_points.remove(up) return self + @deprecated("Use obj.usage_points.clear() instead.") def clear_usage_points(self) -> EndDevice: - """ - Clear all usage_points. - Returns A reference to this `EndDevice` to allow fluent use. - """ - self._usage_points = None + self.usage_points.clear() return self + # endregion usage_points boilerplate + + # region functions boilerplate + + @deprecated("Use len(obj.functions) instead.") def num_functions(self): - """ - Returns The number of `EndDeviceFunction`s associated with this `EndDevice` - """ - return nlen(self._functions) + return len(self.functions) + @deprecated("Use obj.functions.get_by_mrid(mrid) instead.") def get_function(self, mrid: str) -> EndDeviceFunction: - """ - Get the `EndDeviceFunction` for this `EndDevice` identified by `mrid` - - `mrid` the mRID of the required `EndDeviceFunction` - Returns The `EndDeviceFunction` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._functions, mrid) - - def add_function(self, edf: EndDeviceFunction) -> EndDevice: - """ - Associate `edf` to this `EndDevice`. - - `edf` the `EndDeviceFunction` to associate with this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if another `EndDeviceFunction` with the same `mrid` already exists for this `EndDevice`. - """ - if self._validate_reference(edf, self.get_function, "An EndDeviceFunction"): - return self - self._functions = list() if self._functions is None else self._functions - self._functions.append(edf) + return self.functions.get_by_mrid(mrid) + + @deprecated("Use obj.functions.append(edf) instead.") + def add_function(self, edf: 'EndDeviceFunction') -> 'EndDevice': + self.functions.append(edf) return self + @deprecated("Use obj.functions.remove(edf) instead.") def remove_function(self, edf: EndDeviceFunction) -> EndDevice: - """ - Disassociate `edf` from this `EndDevice` - - `up` the `EndDeviceFunction` to disassociate from this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `EndDevice`. - """ - self._functions = safe_remove(self._functions, edf) + self.functions.remove(edf) return self + @deprecated("Use obj.functions.clear() instead.") def clear_functions(self) -> EndDevice: - """ - Clear all end_device_functions. - Returns A reference to this `EndDevice` to allow fluent use. - """ - self._functions = None + self.functions.clear() return self + + # endregion functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 40a2c2cc7..5f6d3f612 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -7,13 +7,17 @@ __all__ = ["UsagePoint"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove +from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location @@ -56,172 +60,113 @@ class UsagePoint(IdentifiedObject): four-wire, s12n (splitSecondary12N) is single-phase, three-wire, and s1n and s2n are single-phase, two-wire. """ - _equipment: list[Equipment] | None = None - _end_devices: list[EndDevice] | None = None - _contacts: list[ContactDetails] | None = None - - def __init__(self, *args, equipment: List[Equipment] = None, end_devices: List[EndDevice] = None, contacts: List[ContactDetails] = None, **kwargs): - super(UsagePoint, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) - if end_devices: - for ed in end_devices: - self.add_end_device(ed) - if contacts: - for c in contacts: - self.add_contact(c) - - @property - def end_devices(self) -> Generator[EndDevice, None, None]: - """ - The `EndDevice`'s (Meter's) associated with this `UsagePoint`. - """ - return ngen(self._end_devices) + _equipment: list[Equipment] | None = field(default=None) + _end_devices: list[EndDevice] | None = field(default=None) + _contacts: list[ContactDetails] | None = field(default=None) - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `zepben.model.Equipment` associated with this `UsagePoint`. - """ - return ngen(self._equipment) - def is_metered(self): + end_devices: MridCollection[EndDevice] = LazyMridList( + _end_devices, + "An EndDevice", + ) + + equipment: MridCollection[Equipment] = LazyMridList( + _equipment, + "An Equipment", + ) + + contacts: MridCollection[ContactDetails] = LazyMridList( + _contacts, + "A ContactDetails" + ) + + def num_equipment(self): """ - Check whether this `UsagePoint` is metered. A `UsagePoint` is metered if it's associated with at least one `EndDevice`. - Returns True if this `UsagePoint` has an `EndDevice`, False otherwise. + Returns The number of `Equipment`s associated with this `UsagePoint` """ - return nlen(self._end_devices) > 0 + return nlen(self._equipment) - @property - def contacts(self) -> Generator[ContactDetails, None, None]: - """[ZBEX] All contact details for this `UsagePoint`""" - return ngen(self._contacts) + # region deprecated list boilerplate - def num_contacts(self): - """Get the number of entries in the `ContactDetails` collection""" - return nlen(self._contacts) + # region contacts boilerplate - def get_contact(self, _id: str) -> ContactDetails: - """All End devices at this usage point.""" # TODO: again, lol, also jvmsdk - try: - return next((it for it in self.contacts if it.id == _id)) - except StopIteration: - raise KeyError(_id) + @deprecated("Use len(contacts) instead.") + def num_contacts(self) -> int: + return len(self.contacts) - def add_contact(self, contact: ContactDetails) -> UsagePoint: - """Add a `ContactDetails` to this `UsagePoint`""" - if self._validate_reference(contact, self.get_contact, "A ContactDetails"): - return self + @deprecated("Use contacts.get_by_mrid(mrid) instead.") + def get_contact(self, mrid: str) -> ContactDetails: + return self.contacts.get_by_mrid(mrid) - if self._contacts is None: - self._contacts = list() - self._contacts.append(contact) + @deprecated("Use contacts.append(contact) instead.") + def add_contact(self, contact: ContactDetails) -> UsagePoint: + self.contacts.append(contact) return self + @deprecated("Use contacts.remove(contact) instead.") def remove_contact(self, contact: ContactDetails) -> UsagePoint: - self._contacts = safe_remove(self._contacts, contact) + self.contacts.remove(contact) return self + @deprecated("Use contacts.clear() instead.") def clear_contacts(self) -> UsagePoint: - self._contacts = None + self.contacts.clear() return self + # endregion + + # region end_devices boilerplate + + @deprecated("Use len(obj.end_devices) instead.") def num_end_devices(self): - """ - Returns The number of `EndDevice`s associated with this `UsagePoint` - """ - return nlen(self._end_devices) + return len(self.end_devices) + @deprecated("Use obj.end_devices.get_by_mrid(mrid) instead.") def get_end_device(self, mrid: str) -> EndDevice: - """ - Get the `EndDevice` for this `UsagePoint` identified by `mrid` - - `mrid` The mRID of the required `EndDevice` - Returns The `EndDevice` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_devices, mrid) + return self.end_devices.get_by_mrid(mrid) + @deprecated("Use obj.end_devices.append(end_device) instead.") def add_end_device(self, end_device: EndDevice) -> UsagePoint: - """ - Associate an `EndDevice` with this `UsagePoint` - - `end_device` The `EndDevice` to associate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if another `EndDevice` with the same `mrid` already exists for this `UsagePoint`. - """ - if self._validate_reference(end_device, self.get_end_device, "An EndDevice"): - return self - self._end_devices = list() if self._end_devices is None else self._end_devices - self._end_devices.append(end_device) + self.end_devices.append(end_device) return self + @deprecated("Use obj.end_devices.remove(end_device) instead.") def remove_end_device(self, end_device: EndDevice) -> UsagePoint: - """ - Disassociate `end_device` from this `UsagePoint`. - - `end_device` The `EndDevice` to disassociate from this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if `end_device` was not associated with this `UsagePoint`. - """ - self._end_devices = safe_remove(self._end_devices, end_device) + self.end_devices.remove(end_device) return self + @deprecated("Use obj.end_devices.clear() instead.") def clear_end_devices(self) -> UsagePoint: - """ - Clear all end_devices. - Returns A reference to this `UsagePoint` to allow fluent use. - """ - self._end_devices = None + self.end_devices.clear() return self + # endregion end_devices boilerplate + + # region equipment boilerplate + + @deprecated("Use len(obj.equipment) instead.") def num_equipment(self): - """ - Returns The number of `Equipment`s associated with this `UsagePoint` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use obj.equipment.get_by_mrid(mrid) instead.") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `UsagePoint` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment, mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use obj.equipment.append(equipment) instead.") def add_equipment(self, equipment: Equipment) -> UsagePoint: - """ - Associate an `Equipment` with this `UsagePoint` - - `equipment` The `Equipment` to associate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `UsagePoint`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - - self._equipment = list() if self._equipment is None else self._equipment - self._equipment.append(equipment) + self.equipment.append(equipment) return self + @deprecated("Use obj.equipment.remove(equipment) instead.") def remove_equipment(self, equipment: Equipment) -> UsagePoint: - """ - Disassociate an `Equipment` from this `UsagePoint` - - `equipment` The `Equipment` to disassociate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if `equipment` was not associated with this `UsagePoint`. - """ - self._equipment = safe_remove(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use obj.equipment.clear() instead.") def clear_equipment(self) -> UsagePoint: - """ - Clear all equipment. - Returns A reference to this `UsagePoint` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self + + # endregion equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index 4ce33da14..2d31b9262 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -7,11 +7,14 @@ __all__ = ["OperationalRestriction"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment @@ -29,66 +32,40 @@ class OperationalRestriction(Document): They then apply operational restrictions in the operational systems to warn operators of potential problems. After appropriate inspection and maintenance, the operational restrictions may be removed. """ - _equipment: Optional[List[Equipment]] = None + _equipment: Optional[List[Equipment]] = field(default=None) - def __init__(self, *args, equipment: List[Equipment] = None, **kwargs): - super(OperationalRestriction, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) + equipment: MridCollection[Equipment] = LazyMridList( + _equipment, + "An Equipment", + ) - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `Equipment` to which this `OperationalRestriction` applies. - """ - return ngen(self._equipment) + # region deprecated list boilerplate + # region equipment boilerplate + + @deprecated("Use len(obj.equipment) instead.") def num_equipment(self): - """ - Returns the number of `Equipment` associated with this `OperationalRestriction` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use obj.equipment.get_by_mrid(mrid) instead.") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `OperationalRestriction` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment, mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use obj.equipment.append(equipment) instead.") def add_equipment(self, equipment: Equipment) -> OperationalRestriction: - """ - Associate an `Equipment` with this `OperationalRestriction` - - `equipment` The `Equipment` to associate with this `OperationalRestriction`. - Returns A reference to this `OperationalRestriction` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `OperationalRestriction`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - self._equipment = list() if self._equipment is None else self._equipment - self._equipment.append(equipment) + self.equipment.append(equipment) return self + @deprecated("Use obj.equipment.remove(equipment) instead.") def remove_equipment(self, equipment: Equipment) -> OperationalRestriction: - """ - Disassociate `equipment` from this `OperationalRestriction`. - - `equipment` The `Equipment` to disassociate from this `OperationalRestriction`. - Returns A reference to this `OperationalRestriction` to allow fluent use. - Raises `ValueError` if `equipment` was not associated with this `OperationalRestriction`. - """ - self._equipment = safe_remove(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use obj.equipment.clear() instead.") def clear_equipment(self) -> OperationalRestriction: - """ - Clear all equipment. - Returns A reference to this `OperationalRestriction` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self + + # endregion equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 6299e06ae..03266cdb2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -7,12 +7,15 @@ __all__ = ["Sensor"] -from typing import Generator, Optional, List, TYPE_CHECKING, Iterable +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction @@ -25,71 +28,41 @@ class Sensor(AuxiliaryEquipment, metaclass=ABCMeta): used in control or be recorded. """ - _relay_functions: Optional[List[ProtectionRelayFunction]] = None + _relay_functions: Optional[List[ProtectionRelayFunction]] = field(default=None) """The relay functions influenced by this [Sensor].""" - def __init__(self, *args, relay_functions: Iterable[ProtectionRelayFunction] = None, **kwargs): - super(Sensor, self).__init__(*args, **kwargs) - if relay_functions is not None: - for relay_function in relay_functions: - self.add_relay_function(relay_function) + relay_functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _relay_functions, + "A ProtectionRelayFunction", + ) - @property - def relay_functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - Yields all the :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - :return: A generator that iterates over all ProtectionRelayFunction influenced by this Sensor. - """ - return ngen(self._relay_functions) + # region deprecated list boilerplate + # region relay_functions boilerplate + @deprecated("Use len(obj.relay_functions) instead.") def num_relay_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - - :return: The number of ProtectionRelayFunction influenced by this Sensor. - """ - return nlen(self._relay_functions) + return len(self.relay_functions) + @deprecated("Use obj.relay_functions.get_by_mrid(mrid) instead.") def get_relay_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - - :param mrid: The mRID of the desired ProtectionRelayFunction - :return: The ProtectionRelayFunction with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._relay_functions, mrid) + return self.relay_functions.get_by_mrid(mrid) + @deprecated("Use obj.relay_functions.append(protection_relay_function) instead.") def add_relay_function(self, protection_relay_function: ProtectionRelayFunction) -> Sensor: - """ - Associate this :class:`Sensor` with a :class:`ProtectionRelayFunction` it is influencing. - - :param protection_relay_function: The ProtectionRelayFunction to associate with this Sensor. - :return: A reference to this Sensor for fluent use. - """ - if self._validate_reference(protection_relay_function, self.get_relay_function, "A ProtectionRelayFunction"): - return self - - self._relay_functions = list() if self._relay_functions is None else self._relay_functions - self._relay_functions.append(protection_relay_function) + self.relay_functions.append(protection_relay_function) return self + @deprecated("Use obj.relay_functions.remove(protection_relay_function) instead.") def remove_relay_function(self, protection_relay_function: ProtectionRelayFunction) -> Sensor: - """ - Disassociate this :class:`Sensor` from a :class:`ProtectionRelayFunction` it is influencing. - - :param protection_relay_function: The ProtectionRelayFunction to disassociate from this Sensor. - :return: A reference to this Sensor for fluent use. - """ - self._relay_functions = safe_remove(self._relay_functions, protection_relay_function) + self.relay_functions.remove(protection_relay_function) return self + @deprecated("Use obj.relay_functions.clear() instead.") def clear_relay_function(self) -> Sensor: - """ - Disassociate all :class:`ProtectionRelayFunction` from this :class:`Sensor`. - - :return: A reference to this Sensor for fluent use. - """ - self._relay_functions = None + self.relay_functions.clear() return self + + # endregion relay_functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 0a6124c59..0cb3b5d35 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -8,17 +8,21 @@ __all__ = ['ConductingEquipment'] import sys -from typing import List, Optional, Generator, TYPE_CHECKING, Union +from typing import List, Optional, TYPE_CHECKING, Union from abc import ABCMeta from dataclasses import field +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.util import get_by_mrid, require, ngen +from zepben.ewb.boilerplate.relations.terminal_list import TerminalsList +from zepben.ewb.util import require from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage - from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal @zb_dataclass @@ -42,14 +46,6 @@ class ConductingEquipment(Equipment, metaclass=ABCMeta): _terminals: List[Terminal] = field(default_factory=list) max_terminals = int(sys.maxsize) - def __init__(self, *args, terminals: List[Terminal] = None, **kwargs): - super(ConductingEquipment, self).__init__(*args, **kwargs) - if terminals: - for term in terminals: - if term.conducting_equipment is None: - term.conducting_equipment = self - self.add_terminal(term) - # pylint: disable=unused-argument def get_base_voltage(self, terminal: Terminal = None): """ @@ -70,13 +66,13 @@ def base_voltage_value(self) -> int: """ return self.base_voltage.nominal_voltage if self.base_voltage and self.base_voltage.nominal_voltage else 0 - @property - def terminals(self) -> Generator[Terminal, None, None]: - """ - `ConductingEquipment` have `Terminal`s that may be connected to other `ConductingEquipment` - `Terminal`s via `ConnectivityNode`s. - """ - return ngen(self._terminals) + terminals: TerminalsList = TerminalsList( + _terminals, + "A Terminal", + backfill=Backfill(Terminal.conducting_equipment), + validate=lambda self, it: self._validate_terminal(it), + sort_by=lambda it: it.sequence_number + ) def __repr__(self): return (f"{super(ConductingEquipment, self).__repr__()}, in_service={self.in_service}, " @@ -92,111 +88,70 @@ def _validate_terminal(self, terminal: Terminal) -> bool: Raises `ValueError` if `Terminal`s `conducting_equipment` is not this `ConductingEquipment`, or if this `ConductingEquipment` has a different `Terminal` with the same mRID. """ - if self._validate_reference(terminal, self.get_terminal_by_mrid, "A Terminal"): - return True - if self._validate_reference_by_field(terminal, terminal.sequence_number, self.get_terminal_by_sn, "sequence_number"): return True - if not terminal.conducting_equipment: - terminal.conducting_equipment = self - - require(terminal.conducting_equipment is self, - lambda: f"Terminal {terminal} references another piece of conducting equipment {terminal.conducting_equipment}, expected {str(self)}.",) - return False - - def num_terminals(self): - """ - Get the number of `Terminal`s for this `ConductingEquipment`. - """ - return len(self._terminals) + require(self.num_terminals() < self.max_terminals, + lambda: f"Unable to add {terminal} to {str(self)}. This conducting equipment already has the maximum number of terminals ({self.max_terminals}).") - def get_terminal(self, identifier: Union[int, str]): - """ - Get the `Terminal` for this `ConductingEquipment` identified by `mrid` or `sequence_number` + if terminal.sequence_number == 0: + terminal.sequence_number = self.num_terminals() + 1 - :param identifier: the mRID of the required `Terminal`, or the `sequence_number` of the terminal in relation - to this `ConductingEquipment` - :return: The `Terminal` with the specified `mrid` if it exists + return False - Raises `KeyError` if `mrid` wasn't present. - Raises `TypeError` if the identifier wasn't a recognised type - """ - if isinstance(identifier, int): - return self.get_terminal_by_sn(identifier) - elif isinstance(identifier, str): - return self.get_terminal_by_mrid(identifier) - raise TypeError(f'`identifier` parameter not a recognised type: {type(identifier)}') - def get_terminal_by_mrid(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `ConductingEquipment` identified by `mrid` + # region deprecated list boilerplate - :param mrid: the mRID of the required `Terminal` + # region terminals boilerplate - :return: The `Terminal` with the specified `mrid` if it exists + @deprecated("Use len(terminals) instead.") + def num_terminals(self) -> int: + return len(self.terminals) - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._terminals, mrid) + @deprecated( + "Use terminals.get_by_sequence_number(identifier) for integer identifiers " + + "or terminals.get_by_mrid(identifier) for string identifiers instead." + ) + def get_terminal(self, identifier: Union[int, str]) -> Terminal: + if isinstance(identifier, int): + return self.terminals.get_by_sequence_number(identifier) - def get_terminal_by_sn(self, sequence_number: int): - """ - Get the `Terminal` on this `ConductingEquipment` by its `sequence_number`. + if isinstance(identifier, str): + return self.terminals.get_by_mrid(identifier) - :param sequence_number: The `sequence_number` of the `Terminal` in relation to this `ConductingEquipment`. + raise TypeError( + f"`identifier` parameter not a recognised type: {type(identifier)}" + ) - :return: The `Terminal` on this `ConductingEquipment` with sequence number `sequence_number` + @deprecated("Use terminals.get_by_mrid(mrid) instead.") + def get_terminal_by_mrid(self, mrid: str) -> Terminal: + return self.terminals.get_by_mrid(mrid) - Raises IndexError if no `Terminal` was found with sequence_number `sequence_number`. - """ - for term in self._terminals: - if term.sequence_number == sequence_number: - return term - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self)}") + @deprecated( + "Use terminals.get_by_sequence_number(sequence_number) instead." + ) + def get_terminal_by_sn(self, sequence_number: int) -> Terminal: + return self.terminals.get_by_sequence_number(sequence_number) - def __getitem__(self, item: int): - return self.get_terminal_by_sn(item) + @deprecated("Use terminals.get_by_sequence_number(item) instead.") + def __getitem__(self, item: int) -> Terminal: + return self.terminals.get_by_sequence_number(item) + @deprecated("Use terminals.append(terminal) instead.") def add_terminal(self, terminal: Terminal) -> ConductingEquipment: - """ - Associate `terminal` with this `ConductingEquipment`. If `terminal.sequence_number` == 0, the terminal will be assigned a sequence_number of - `self.num_terminals() + 1`. - - `terminal` The `Terminal` to associate with this `ConductingEquipment`. - Returns A reference to this `ConductingEquipment` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `ConductingEquipment`. - Raises `ValueError` if `max_terminals` has already been reached. - """ - if self._validate_terminal(terminal): - return self - - require(self.num_terminals() < self.max_terminals, - lambda: f"Unable to add {terminal} to {str(self)}. This conducting equipment already has the maximum number of terminals ({self.max_terminals}).") - - if terminal.sequence_number == 0: - terminal.sequence_number = self.num_terminals() + 1 - - self._terminals.append(terminal) - self._terminals.sort(key=lambda t: t.sequence_number) - + self.terminals.append(terminal) return self + @deprecated("Use terminals.remove(terminal) instead.") def remove_terminal(self, terminal: Terminal) -> ConductingEquipment: - """ - Disassociate `terminal` from this `ConductingEquipment` - - `terminal` the `Terminal` to disassociate from this `ConductingEquipment`. - Returns A reference to this `ConductingEquipment` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `ConductingEquipment`. - """ - self._terminals.remove(terminal) + self.terminals.remove(terminal) return self + @deprecated("Use terminals.clear() instead.") def clear_terminals(self) -> ConductingEquipment: - """ - Clear all terminals. - Returns A reference to this `ConductingEquipment` to allow fluent use. - """ - self._terminals.clear() + self.terminals.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index f900d551a..6fcda037d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -7,11 +7,15 @@ __all__ = ["ConnectivityNode"] -from typing import Generator, List, TYPE_CHECKING +from typing import List, TYPE_CHECKING from dataclasses import field +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.mrid_list import MridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: @@ -30,21 +34,14 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): """ _terminals: List[Terminal] = field(default_factory=list) - def __init__(self, *args, terminals: List[Terminal] = None, **kwargs): - super(ConnectivityNode, self).__init__(*args, **kwargs) - if terminals: - for term in terminals: - self.add_terminal(term) - def __iter__(self): return iter(self._terminals) - @property - def terminals(self) -> Generator[Terminal, None, None]: - """ - The `Terminal`s attached to this `ConnectivityNode` - """ - return ngen(self._terminals) + terminals: MridCollection[Terminal] = MridList( + _terminals, + "A Terminal" + ) + """The `Terminal`s attached to this `ConnectivityNode`""" def is_switched(self): return self.get_switch() is not None @@ -59,51 +56,33 @@ def get_switch(self): pass return None - def num_terminals(self): - """ - Get the number of `Terminal`s for this `ConnectivityNode`. - """ - return len(self._terminals) + # region deprecated list boilerplate - def get_terminal(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `ConnectivityNode` identified by `mrid` + # region terminals boilerplate - `mrid` The mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._terminals, mrid) - - def add_terminal(self, terminal: Terminal) -> ConnectivityNode: - """ - Associate a `terminal.Terminal` with this `ConnectivityNode` + @deprecated("Use len(terminals) instead.") + def num_terminals(self) -> int: + return len(self.terminals) - `terminal` The `Terminal` to add. Will only add to this object if it is not already associated. - Returns A reference to this `ConnectivityNode` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `ConnectivityNode`. - """ - if self._validate_reference(terminal, self.get_terminal, "A Terminal"): - return self + @deprecated("Use terminals.get_by_mrid(mrid) instead.") + def get_terminal(self, mrid: str) -> Terminal: + return self.terminals.get_by_mrid(mrid) - self._terminals.append(terminal) + @deprecated("Use terminals.append(terminal) instead.") + def add_terminal(self, terminal: Terminal) -> ConnectivityNode: + self.terminals.append(terminal) return self + @deprecated("Use terminals.remove(terminal) instead.") def remove_terminal(self, terminal: Terminal) -> ConnectivityNode: - """ - Disassociate `terminal` from this `ConnectivityNode`. - - `terminal` The `Terminal` to disassociate from this `ConnectivityNode`. - Returns A reference to this `ConnectivityNode` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `ConnectivityNode`. - """ - self._terminals.remove(terminal) + self.terminals.remove(terminal) return self + @deprecated("Use terminals.clear() instead.") def clear_terminals(self) -> ConnectivityNode: - """ - Clear all terminals. - Returns A reference to this `ConnectivityNode` to allow fluent use. - """ - self._terminals.clear() + self.terminals.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 175605f39..7e2a6765d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -5,13 +5,17 @@ __all__ = ["Curve"] -from typing import Optional, List, Generator +from dataclasses import field +from typing import Optional from abc import ABCMeta +from typing_extensions import deprecated + +from zepben.ewb import zb_dataclass from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import require, ngen, nlen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.relations.curve_data_list import CurveDataList +from zepben.ewb.util import require @zb_dataclass @@ -20,109 +24,70 @@ class Curve(IdentifiedObject, metaclass=ABCMeta): The Curve class is a multipurpose functional relationship between an independent variable (X-axis) and dependent (Y-axis) variables. """ - _data: Optional[List[CurveData]] = None + _data: list[CurveData] | None = field(default=None) + + data: CurveDataList[CurveData] = CurveDataList( + _data, + validate=lambda self, it: self._validate_data(it), + sort_by=lambda it: it.x_value + ) + + def _validate_data(self, curve_data: CurveData): + require(all([it.x_value != curve_data.x_value for it in self.data]), + lambda: f"Unable to add datapoint to {self}. x_value {curve_data.x_value} is invalid, as data with same x_value already exist in this Curve.") + - def __init__(self, *args, data: List[CurveData] = None, **kwargs): - """ - `data` A list of `CurveData`s to associate with this `Curve`. - """ - super(Curve, self).__init__(*args, **kwargs) - if data: - for curve_data in data: - self.add_curve_data(curve_data) + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - @property - def data(self) -> Generator[CurveData, None, None]: - """ - The point data values that define this curve, sorted by `x_value` in ascending order. - """ - return ngen(self._data) + # region data boilerplate + @deprecated("Use len(data) instead.") def num_data(self): - """Return the number of :class:`CurveData` associated with this :class:`Curve`.""" - return nlen(self._data) + return len(self.data) + @deprecated("Use data.get(x) instead.") def get_data(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` for this :class:`Curve` identified by its `x_value`. - - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises KeyError: When no `CurveData` was found with `x`. - """ - if self._data: - curve_data = next((it for it in self._data if it.x_value == x), None) - if curve_data: - return curve_data - raise KeyError(x) + return self.data.get(x) + @deprecated("Use data.get(x) instead.") def __getitem__(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` for this :class:`Curve` identified by its `x_value`. - - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises IndexError: When no `CurveData` was found with `x`. - """ - return self.get_data(x) - - def add_data(self, x: float, y1: float, y2: Optional[float], y3: Optional[float]) -> 'Curve': - """ - Add a data point to this :class:`Curve`. - - :param x: The data value of the X-axis variable, depending on the X-axis units. - :param y1: The data value of the first Y-axis variable, depending on the Y-axis units. - :param y2: The data value of the second Y-axis variable (if present), depending on the Y-axis units. - :param y3: The data value of the third Y-axis variable (if present), depending on the Y-axis units. - :raises ValueError: if a :class:`CurveData` for the provided `x` value already exists for this :class:`Curve`. - """ - require(all([it.x_value != x for it in self.data]), - lambda: f"Unable to add datapoint to {self}. x_value {x} is invalid, as data with same x_value already exist in this Curve.") - - if self._data is None: - self._data = [] - self._data.append(CurveData(x, y1, y2, y3)) - self._data.sort(key=lambda it: it.x_value) + return self.data.get(x) + + @deprecated("Use data.append(CurveData(x, y1, y2, y3)) instead.") + def add_data( + self, + x: float, + y1: float, + y2: Optional[float], + y3: Optional[float], + ) -> "Curve": + self.data.append(CurveData(x, y1, y2, y3)) + return self + @deprecated("Use data.append(curve_data) instead.") + def add_curve_data(self, curve_data: CurveData) -> "Curve": + self.data.append(curve_data) return self - def add_curve_data(self, curve_data: CurveData) -> 'Curve': - """ - Associate a :class:`CurveData` with this :class:`Curve`. - - :param curve_data: The :class:`CurveData` to associate with this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - :raises ValueError: If another :class:`CurveData` with the same `x_value` already exists for this :class:`Curve`. - """ - return self.add_data(curve_data.x_value, curve_data.y1_value, curve_data.y2_value, curve_data.y3_value) - - def remove_data(self, curve_data: CurveData) -> 'Curve': - """ - Disassociate a :class:`CurveData` from this :class:`Curve`. - - :param curve_data: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - :raises ValueError: If `curve_data` was not associated with this :class:`Curve`. - """ - self._data = safe_remove(self._data, curve_data) + @deprecated("Use data.remove(curve_data) instead.") + def remove_data(self, curve_data: CurveData) -> "Curve": + self.data.remove(curve_data) return self + @deprecated("Use data.remove_data_at(x) instead.") def remove_data_at(self, x: float) -> CurveData: - """ - Disassociate a :class:`CurveData` from this :class:`Curve` based on its `x_value`. - - :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to the removed :class:`CurveData`. - :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. - """ - data = self.get_data(x) - self._data = safe_remove(self._data, data) - return data - - def clear_data(self) -> 'Curve': - """ - Clear all :class:`CurveData` associated with this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - """ - self._data = None + return self.data.remove_data_at(x) + + @deprecated("Use data.clear() instead.") + def clear_data(self) -> "Curve": + self.data.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index a741c0752..679013e06 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -10,6 +10,8 @@ import datetime from typing import Optional, Generator, List, TYPE_CHECKING, TypeVar, Type from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation @@ -17,8 +19,10 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.core.substation import Substation from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove +from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint @@ -42,25 +46,15 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): commissioned_date: Optional[datetime.datetime] = None """The date this equipment was commissioned into service.""" - _usage_points: Optional[List[UsagePoint]] = None - _equipment_containers: Optional[List[EquipmentContainer]] = None - _operational_restrictions: Optional[List[OperationalRestriction]] = None - _current_containers: Optional[List[EquipmentContainer]] = None + _usage_points: Optional[List[UsagePoint]] = field(default=None) + _equipment_containers: Optional[List[EquipmentContainer]] = field(default=None) + _operational_restrictions: Optional[List[OperationalRestriction]] = field(default=None) - def __init__(self, *args, usage_points: List[UsagePoint] = None, equipment_containers: List[EquipmentContainer] = None, operational_restrictions: List[OperationalRestriction] = None, current_containers: List[EquipmentContainer] = None, **kwargs): + _current_containers: Optional[List[EquipmentContainer]] = field(default=None) + + def __init__(self, *args, equipment_containers=None, **kwargs): super(Equipment, self).__init__(*args, **kwargs) - if usage_points: - for up in usage_points: - self.add_usage_point(up) - if equipment_containers: - for container in equipment_containers: - self.add_container(container) - if operational_restrictions: - for restriction in operational_restrictions: - self.add_operational_restriction(restriction) - if current_containers: - for cf in current_containers: - self.add_current_container(cf) + self.containers.extend(equipment_containers) @property def sites(self) -> Generator['Site', None, None]: @@ -129,12 +123,10 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: """ return ngen(_of_type(self._current_containers, LvFeeder)) - @property - def containers(self) -> Generator[EquipmentContainer, None, None]: - """ - The `EquipmentContainer`s this equipment belongs to. - """ - return ngen(self._equipment_containers) + containers: MridCollection[EquipmentContainer] = LazyMridList( + _equipment_containers, + "An EquipmentContainer", + ) def num_sites(self) -> int: """ @@ -142,235 +134,140 @@ def num_sites(self) -> int: """ return len(list(self.sites)) - @property - def current_containers(self) -> Generator[EquipmentContainer, None, None]: - """ - The `EquipmentContainer`s this equipment belongs to in the current state of the network. - """ - return ngen(self._current_containers) + current_containers: MridCollection[EquipmentContainer] = LazyMridList( + _current_containers, + "A current EquipmentContainer", + ) - @property - def usage_points(self) -> Generator[UsagePoint, None, None]: - """ - The `UsagePoint`s for this equipment. - """ - return ngen(self._usage_points) + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) - @property - def operational_restrictions(self) -> Generator[OperationalRestriction, None, None]: - """ - The `OperationalRestriction`s that this equipment is associated with. - """ - return ngen(self._operational_restrictions) + operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( + _operational_restrictions, + "An OperationalRestriction", + ) + # region deprecated list boilerplate + # region containers boilerplate + + @deprecated("Use len(obj.containers) instead.") def num_containers(self) -> int: - """ - Returns The number of `EquipmentContainer`s associated with this `Equipment` - """ - return nlen(self._equipment_containers) + return len(self.containers) + @deprecated("Use len(obj.containers) instead.") def num_substations(self) -> int: - """ - Returns The number of `zepben.ewb.model.cim.iec61970.base.core.substation.Substation`s associated with this `Equipment` - """ - return len(list(_of_type(self._equipment_containers, Substation))) + return len(self.containers) + @deprecated("Use len(obj.containers) instead.") def num_normal_feeders(self) -> int: - """ - Returns The number of normal `Feeder`s associated with this `Equipment` - """ - return len(list(_of_type(self._equipment_containers, Feeder))) + return len(self.containers) + @deprecated("Use obj.containers.get_by_mrid(mrid) instead.") def get_container(self, mrid: str) -> EquipmentContainer: - """ - Get the `EquipmentContainer` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `EquipmentContainer` - Returns The `EquipmentContainer` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment_containers, mrid) + return self.containers.get_by_mrid(mrid) + @deprecated("Use obj.containers.append(ec) instead.") def add_container(self, ec: EquipmentContainer) -> Equipment: - """ - Associate an `EquipmentContainer` with this `Equipment` - - `ec` The `EquipmentContainer` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `EquipmentContainer` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(ec, self.get_container, "An EquipmentContainer"): - return self - self._equipment_containers = list() if self._equipment_containers is None else self._equipment_containers - self._equipment_containers.append(ec) + self.containers.append(ec) return self + @deprecated("Use obj.containers.remove(ec) instead.") def remove_container(self, ec: EquipmentContainer) -> Equipment: - """ - Disassociate `ec` from this `Equipment`. - - `ec` The `EquipmentContainer` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `ec` was not associated with this `Equipment`. - """ - self._equipment_containers = safe_remove(self._equipment_containers, ec) + self.containers.remove(ec) return self + @deprecated("Use obj.containers.clear() instead.") def clear_containers(self) -> Equipment: - """ - Clear all equipment. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._equipment_containers = None + self.containers.clear() return self + # endregion containers boilerplate + + # region current_containers boilerplate + + @deprecated("Use len(obj.current_containers) instead.") def num_current_containers(self) -> int: - """ - Returns The number of `EquipmentContainer`s associated with this `Equipment` - """ - return nlen(self._current_containers) + return len(self.current_containers) + @deprecated("Use obj.current_containers.get_by_mrid(mrid) instead.") def get_current_container(self, mrid: str) -> EquipmentContainer: - """ - Get the `EquipmentContainer` for this `Equipment` in the current state of the network, identified by `mrid` - - `mrid` The mRID of the required `EquipmentContainer` - Returns The `EquipmentContainer` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._current_containers, mrid) + return self.current_containers.get_by_mrid(mrid) + @deprecated("Use obj.current_containers.append(equipment_container) instead.") def add_current_container(self, equipment_container: EquipmentContainer) -> Equipment: - """ - Associate `equipment_container` with this `Equipment` in the current state of the network. - - `equipment_container` The `EquipmentContainer` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `EquipmentContainer` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(equipment_container, self.get_current_container, "A current EquipmentContainer"): - return self - self._current_containers = list() if self._current_containers is None else self._current_containers - self._current_containers.append(equipment_container) + self.current_containers.append(equipment_container) return self + @deprecated("Use obj.current_containers.remove(equipment_container) instead.") def remove_current_container(self, equipment_container: EquipmentContainer) -> Equipment: - """ - Disassociate `equipment_container` from this `Equipment` in the current state of the network. - - `equipment_container` The `EquipmentContainer` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `equipment_container` was not associated with this `Equipment`. - """ - self._current_containers = safe_remove(self._current_containers, equipment_container) + self.current_containers.remove(equipment_container) return self + @deprecated("Use obj.current_containers.clear() instead.") def clear_current_containers(self) -> Equipment: - """ - Clear all current `EquipmentContainer`s in the current state of the network. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._current_containers = None + self.current_containers.clear() return self + # endregion current_containers boilerplate + + # region usage_points boilerplate + + @deprecated("Use len(obj.usage_points) instead.") def num_usage_points(self) -> int: - """ - Returns The number of `UsagePoint`s associated with this `Equipment` - """ - return nlen(self._usage_points) + return len(self.usage_points) + @deprecated("Use obj.usage_points.get_by_mrid(mrid) instead.") def get_usage_point(self, mrid: str) -> UsagePoint: - """ - Get the `UsagePoint` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `UsagePoint` - Returns The `UsagePoint` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._usage_points, mrid) + return self.usage_points.get_by_mrid(mrid) + @deprecated("Use obj.usage_points.append(up) instead.") def add_usage_point(self, up: UsagePoint) -> Equipment: - """ - Associate `up` with this `Equipment`. - - `up` the `UsagePoint` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `UsagePoint` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(up, self.get_usage_point, "A UsagePoint"): - return self - self._usage_points = list() if self._usage_points is None else self._usage_points - self._usage_points.append(up) + self.usage_points.append(up) return self + @deprecated("Use obj.usage_points.remove(up) instead.") def remove_usage_point(self, up: UsagePoint) -> Equipment: - """ - Disassociate `up` from this `Equipment`. - - `up` The `UsagePoint` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `Equipment`. - """ - self._usage_points = safe_remove(self._usage_points, up) + self.usage_points.remove(up) return self + @deprecated("Use obj.usage_points.clear() instead.") def clear_usage_points(self) -> Equipment: - """ - Clear all usage_points. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._usage_points = None + self.usage_points.clear() return self + # endregion usage_points boilerplate + + # region operational_restrictions boilerplate + + @deprecated("Use len(obj.operational_restrictions) instead.") def num_operational_restrictions(self) -> int: - """ - Returns The number of `OperationalRestriction`s associated with this `Equipment` - """ - return nlen(self._operational_restrictions) + return len(self.operational_restrictions) + @deprecated("Use obj.operational_restrictions.get_by_mrid(mrid) instead.") def get_operational_restriction(self, mrid: str) -> OperationalRestriction: - """ - Get the `OperationalRestriction` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `OperationalRestriction` - Returns The `OperationalRestriction` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._operational_restrictions, mrid) + return self.operational_restrictions.get_by_mrid(mrid) + @deprecated("Use obj.operational_restrictions.append(op) instead.") def add_operational_restriction(self, op: OperationalRestriction) -> Equipment: - """ - Associate `op` with this `Equipment`. - - `op` The `OperationalRestriction` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `OperationalRestriction` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(op, self.get_operational_restriction, "An OperationalRestriction"): - return self - self._operational_restrictions = list() if self._operational_restrictions is None else self._operational_restrictions - self._operational_restrictions.append(op) + self.operational_restrictions.append(op) return self + @deprecated("Use obj.operational_restrictions.remove(op) instead.") def remove_operational_restriction(self, op: OperationalRestriction) -> Equipment: - """ - Disassociate `up` from this `Equipment`. - - `op` The `OperationalRestriction` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `op` was not associated with this `Equipment`. - """ - self._operational_restrictions = safe_remove(self._operational_restrictions, op) + self.operational_restrictions.remove(op) return self + @deprecated("Use obj.operational_restrictions.clear() instead.") def clear_operational_restrictions(self) -> Equipment: - """ - Clear all `OperationalRestrictions`. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._operational_restrictions = None + self.operational_restrictions.clear() return self + # endregion operational_restrictions boilerplate + + # endregion deprecated list boilerplate + def _of_type(containers: Optional[List[EquipmentContainer]], ectype: Type[TEquipmentContainer]) -> Generator[TEquipmentContainer, None, None]: yield from (ec for ec in containers if isinstance(ec, ectype)) if containers is not None else {} diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index 6f7290e57..e979566fe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -7,11 +7,16 @@ __all__ = ['EquipmentContainer'] -from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable, Type +from dataclasses import field +from typing import Dict, Generator, TYPE_CHECKING, TypeVar, Iterable, Type from abc import ABCMeta +from typing_extensions import deprecated + +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer -from zepben.ewb.util import nlen, ngen, safe_remove_by_id from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: @@ -32,28 +37,17 @@ class EquipmentContainer(ConnectivityNodeContainer, metaclass=ABCMeta): Unless overridden, all functions operating on currentEquipment simply operate on the equipment collection. i.e. currentEquipment = equipment """ - _equipment: Optional[Dict[str, Equipment]] = None + _equipment_by_id: Dict[str, Equipment] | None = field(default=None) """Map of Equipment in this EquipmentContainer by their mRID""" - def __init__(self, *args, equipment: List[Equipment] = None, **kwargs): - super(EquipmentContainer, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) + equipment: MridCollection[Equipment] = LazyMridMap( + _equipment_by_id, + "An Equipment", + ) + """The `Equipment` contained in this `EquipmentContainer`""" - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `Equipment` contained in this `EquipmentContainer` - """ - return ngen(self._equipment) - - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return self.equipment + current_equipment = Alias(equipment) + """Contained `Equipment` using the current state of the network.""" def current_feeders(self) -> Generator[Feeder, None, None]: """ @@ -61,7 +55,7 @@ def current_feeders(self) -> Generator[Feeder, None, None]: Returns the current feeders for all associated feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.current_feeders: if f not in seen: seen.add(f.mrid) @@ -73,7 +67,7 @@ def normal_feeders(self) -> Generator[Feeder, None, None]: Returns the normal feeders for all associated feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.normal_feeders: if f not in seen: seen.add(f.mrid) @@ -85,7 +79,7 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: Returns the normal LV feeders for all associated LV feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.current_lv_feeders: if f not in seen: seen.add(f.mrid) @@ -97,7 +91,7 @@ def normal_lv_feeders(self) -> Generator[LvFeeder, None, None]: Returns the normal LV feeders for all associated LV feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.normal_lv_feeders: if f not in seen: seen.add(f.mrid) @@ -146,103 +140,59 @@ def edge_terminals(self, state_operator: 'Type[NetworkStateOperators]' = None) - seen.add(ct.from_terminal) yield ct.from_terminal + + + # region deprecated list boilerplate + # region equipment boilerplate + + @deprecated("Use len(self.equipment) instead") def num_equipment(self): - """ - Returns The number of `Equipment` associated with this `EquipmentContainer` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use self.equipment.get_by_mrid(mrid) instead") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `EquipmentContainer` identified by `mrid` - - `mrid` the mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._equipment: - raise KeyError(mrid) - try: - return self._equipment[mrid] - except AttributeError: - raise KeyError(mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use equipment.append(equipment) instead") def add_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Associate `equipment` with this `EquipmentContainer`. - - `equipment` The `Equipment` to associate with this `EquipmentContainer`. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `EquipmentContainer`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - if self._equipment is None: - self._equipment = dict() - self._equipment[equipment.mrid] = equipment + self.equipment.append(equipment) return self + @deprecated("Use equipment.remove(equipment) instead") def remove_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Disassociate `equipment` from this `EquipmentContainer` - - `equipment` The `Equipment` to disassociate with this `EquipmentContainer`. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `EquipmentContainer`. - """ - self._equipment = safe_remove_by_id(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use equipment.clear() instead") def clear_equipment(self) -> EquipmentContainer: - """ - Clear all equipment. - Returns A reference to this `EquipmentContainer` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self - def num_current_equipment(self) -> int: - """ - Returns The number of `Equipment` contained in this `EquipmentContainer` in the current state of the network. - """ - return self.num_equipment() + # endregion + # region current_equipment boilerplate - def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` contained in this `EquipmentContainer` in the current state of the network, identified by `mrid` + @deprecated("Use len(current_equipment) instead") + def num_current_equipment(self): + return len(self.current_equipment) - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return self.get_equipment(mrid) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") + def get_current_equipment(self, mrid: str) -> Equipment: + return self.current_equipment.get_by_mrid(mrid) + @deprecated("Use current_equipment.append(equipment) instead") def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Associate `equipment` with this `EquipmentContainer` in the current state of the network. - - `equipment` the `Equipment` to associate with this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `EquipmentContainer`. - """ - self.add_equipment(equipment) + self.current_equipment.append(equipment) return self + @deprecated("Use current_equipment.remove(equipment) instead") def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Disassociate `equipment` from this `EquipmentContainer` in the current state of the network. - - `equipment` The `Equipment` to disassociate from this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `EquipmentContainer`. - """ - self.remove_equipment(equipment) + self.current_equipment.remove(equipment) return self + @deprecated("Use current_equipment.clear() instead") def clear_current_equipment(self) -> EquipmentContainer: - """ - Clear all `Equipment` from this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - """ - self.clear_equipment() + self.current_equipment.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 21f9dae7e..6415fec39 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -7,14 +7,15 @@ __all__ = ["Feeder"] -from typing import Optional, Dict, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, Dict, TYPE_CHECKING from typing_extensions import deprecated -from zepben.ewb import get_by_mrid -from zepben.ewb.model.cim.extensions.zbex import zbex +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import internal +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import ngen, nlen, safe_remove_by_id from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: @@ -35,52 +36,20 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - _normal_energizing_substation: Substation | None = None + _normal_energizing_substation: Substation | None = field(default=None) - _current_equipment: Dict[str, Equipment] | None = None + _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """The equipment contained in this feeder in the current state of the network.""" - _normal_energized_lv_feeders: Dict[str, LvFeeder] | None = None + _normal_energized_lv_feeders_by_id: Dict[str, LvFeeder] | None = field(default=None) """The LV feeders that are energized by this feeder in the normal state of the network.""" - _current_energized_lv_feeders: Dict[str, LvFeeder] | None = None + _current_energized_lv_feeders_by_id: Dict[str, LvFeeder] | None = field(default=None) """The LV feeders that are energized by this feeder in the current state of the network.""" - _normal_energized_lv_substations: Dict[str, 'LvSubstation'] | None = None - _current_energized_lv_substations: Dict[str, 'LvSubstation'] | None = None - - def __init__( - self, - *args, - normal_head_terminal: Terminal = None, - normal_energizing_substation: Substation = None, - current_equipment: List[Equipment] = None, - normal_energized_lv_feeders: List[LvFeeder] = None, - current_energized_lv_feeders: List[LvFeeder] = None, - normal_energized_lv_substations: List[LvSubstation] = None, - current_energized_lv_substations: List[LvSubstation] = None, - **kwargs, - ): - super(Feeder, self).__init__(*args, **kwargs) - if normal_head_terminal: - self.normal_head_terminal = normal_head_terminal - if normal_energizing_substation: - self.normal_energizing_substation = normal_energizing_substation - if normal_energized_lv_feeders: - for lv_feeder in normal_energized_lv_feeders: - self.add_normal_energized_lv_feeder(lv_feeder) - if current_equipment: - for eq in current_equipment: - self.add_current_equipment(eq) - if current_energized_lv_feeders: - for lv_feeder in current_energized_lv_feeders: - self.add_current_energized_lv_feeder(lv_feeder) - if normal_energized_lv_substations: - for lv_substation in normal_energized_lv_substations: - self.add_normal_energized_lv_substation(lv_substation) - if current_energized_lv_substations: - for lv_substation in current_energized_lv_substations: - self.add_current_energized_lv_substation(lv_substation) + _normal_energized_lv_substations_by_id: Dict[str, 'LvSubstation'] | None = field(default=None) + _current_energized_lv_substations_by_id: Dict[str, 'LvSubstation'] | None = field(default=None) + @property def normal_head_terminal(self) -> Optional[Terminal]: @@ -95,6 +64,7 @@ def normal_head_terminal(self, term: Optional[Terminal]): raise ValueError(f"Feeder {self.mrid} has equipment assigned to it. Cannot update normalHeadTerminal on a feeder with equipment assigned.") @property + @internal(_normal_energizing_substation) def normal_energizing_substation(self): """The substation that normally energizes the feeder. Also used for naming purposes.""" return self._normal_energizing_substation @@ -104,277 +74,164 @@ def normal_energizing_substation(self): def normal_energizing_substation(self, value): self._normal_energizing_substation = value - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return ngen(self._current_equipment) - - @property - def normal_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """ - The LV feeders that are normally energized by this feeder. - """ - return ngen(self._normal_energized_lv_feeders) - - @zbex - @property - def current_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """ - The LV feeders that are currently energized by this feeder. - """ - return ngen(self._current_energized_lv_feeders) - - @zbex - @property - def normal_energized_lv_substations(self) -> Generator['LvSubstation', None, None]: - return ngen(self._normal_energized_lv_substations) - - @zbex - @property - def current_energized_lv_substations(self) -> Generator['LvSubstation', None, None]: - return ngen(self._current_energized_lv_substations) - + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" + + normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _normal_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """The LV feeders that are normally energized by this feeder.""" + + current_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _current_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """[ZBEX] The LV feeders that are currently energized by this feeder.""" + + normal_energized_lv_substations: MridCollection[LvSubstation] = LazyMridMap( + _normal_energized_lv_substations_by_id, + "An LvSubstation", + ) + """[ZBEX]""" + + current_energized_lv_substations: MridCollection[LvSubstation] = LazyMridMap( + _current_energized_lv_substations_by_id, + "An LvSubstation", + ) + """[ZBEX]""" + # region deprecated list boilerplate + # region current_equipment boilerplate + + @deprecated("Use len(current_equipment) instead") def num_current_equipment(self): - """ - :returns: The number of `Equipment` associated with this `Feeder` - """ - return nlen(self._current_equipment) + return len(self.current_equipment) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `Feeder` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - :returns: The `Equipment` with the specified `mrid` if it exists - :raises: `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._current_equipment, mrid) - - def add_current_equipment(self, equipment: Equipment) -> Feeder: - """ - Associate `equipment` with this `Feeder`. - - `equipment` the `Equipment` to associate with this `Feeder`. - :returns: A reference to this `Feeder` to allow fluent use. - :raises: `ValueError` if another `Equipment` with the same `mrid` already exists for this `Feeder`. - """ - if self._validate_reference(equipment, self.get_current_equipment, "An Equipment"): - return self - self._current_equipment = dict() if self._current_equipment is None else self._current_equipment - self._current_equipment[equipment.mrid] = equipment - return self + return self.current_equipment.get_by_mrid(mrid) - def remove_current_equipment(self, equipment: Equipment) -> Feeder: - """ - Disassociate `equipment` from this `Feeder` + @deprecated("Use current_equipment.append(equipment) instead") + def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.append(equipment) + return self - `equipment` The `Equipment` to disassociate from this `Feeder`. - :returns: A reference to this `Feeder` to allow fluent use. - :raises: `KeyError` if `equipment` was not associated with this `Feeder`. - """ - self._current_equipment = safe_remove_by_id(self._current_equipment, equipment) + @deprecated("Use current_equipment.remove(equipment) instead") + def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.remove(equipment) return self - def clear_current_equipment(self) -> Feeder: - """ - Clear all equipment. - :returns: A reference to this `Feeder` to allow fluent use. - """ - self._current_equipment = None + @deprecated("Use current_equipment.clear() instead") + def clear_current_equipment(self) -> EquipmentContainer: + self.current_equipment.clear() return self - def num_normal_energized_lv_feeders(self) -> int: - """ - Get the number of LV feeders that are normally energized by this feeder. - """ - return nlen(self._normal_energized_lv_feeders) + # endregion + # region normal_energized_lv_feeders boilerplate - def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: - """ - Energized LvFeeder in the normal state of the network. + @deprecated("Use len(normal_energized_lv_feeders) instead") + def num_normal_energized_lv_feeders(self): + return len(self.normal_energized_lv_feeders) - :param mrid: The mrid of the `LvFeeder`. - :returns: A matching `LvFeeder` that is energized by this `Feeder` in the normal state of the network. - :raise sA `KeyError` if no matching `LvFeeder` was found. - """ - return get_by_mrid(self._normal_energized_lv_feeders, mrid) + @deprecated("Use normal_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.normal_energized_lv_feeders.get_by_mrid(mrid) + @deprecated("Use normal_energized_lv_feeders.append(lv_feeder) instead") def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Associate this `Feeder` with an `LvFeeder` in the normal state of the network. - - :param lv_feeder: the LV feeder to associate with this feeder in the normal state of the network. - :return: This `Feeder` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_normal_energized_lv_feeder, "An LvFeeder"): - return self - self._normal_energized_lv_feeders = dict() if self._normal_energized_lv_feeders is None else self._normal_energized_lv_feeders - self._normal_energized_lv_feeders[lv_feeder.mrid] = lv_feeder + self.normal_energized_lv_feeders.append(lv_feeder) return self + @deprecated("Use normal_energized_lv_feeders.remove(lv_feeder) instead") def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Disassociate this `Feeder` from an `LvFeeder` in the normal state of the network. - - :param lv_feeder: the LV feeder to disassociate from this feeder in the normal state of the network. - :return: This `Feeder` for fluent use. - :raises: A `ValueError` if `lv_feeder` is not found in the normal energized lv feeders collection. - """ - self._normal_energized_lv_feeders = safe_remove_by_id(self._normal_energized_lv_feeders, lv_feeder) + self.normal_energized_lv_feeders.remove(lv_feeder) return self + @deprecated("Use normal_energized_lv_feeders.clear() instead") def clear_normal_energized_lv_feeders(self) -> Feeder: - """ - Clear all `LvFeeder`s associated with `Feeder` in the normal state of the network. - - :return: This `Feeder` for fluent use. - """ - self._normal_energized_lv_feeders = None + self.normal_energized_lv_feeders.clear() return self - def num_current_energized_lv_feeders(self) -> int: - """ - Get the number of LV feeders that are currently energized by this feeder. - """ - return nlen(self._current_energized_lv_feeders) + # endregion + # region current_energized_lv_feeders boilerplate - def get_current_energized_lv_feeder(self, mrid: str) -> LvFeeder: - """ - Energized LvFeeder in the current state of the network. + @deprecated("Use len(current_energized_lv_feeders) instead") + def num_current_energized_lv_feeders(self): + return len(self.current_energized_lv_feeders) - :param mrid: The mrid of the `LvFeeder`. - :return: A matching `LvFeeder` that is energized by this `Feeder` in the current state of the network. - :raises: A `KeyError` if no matching `LvFeeder` was found. - """ - return get_by_mrid(self._current_energized_lv_feeders, mrid) + @deprecated("Use current_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_current_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.current_energized_lv_feeders.get_by_mrid(mrid) + @deprecated("Use current_energized_lv_feeders.append(lv_feeder) instead") def add_current_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Associate this `Feeder` with an `LvFeeder` in the current state of the network. - - :param lv_feeder: the LV feeder to associate with this feeder in the current state of the network. - :return: This `Feeder` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_current_energized_lv_feeder, "An LvFeeder"): - return self - self._current_energized_lv_feeders = dict() if self._current_energized_lv_feeders is None else self._current_energized_lv_feeders - self._current_energized_lv_feeders[lv_feeder.mrid] = lv_feeder + self.current_energized_lv_feeders.append(lv_feeder) return self + @deprecated("Use current_energized_lv_feeders.remove(lv_feeder) instead") def remove_current_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Disassociate this `Feeder` from an `LvFeeder` in the current state of the network. - - :param lv_feeder: the LV feeder to disassociate from this feeder in the current state of the network. - :return: This `Feeder` for fluent use. - :raises: A `ValueError` if `lv_feeder` is not found in the current energized lv feeders collection. - """ - self._current_energized_lv_feeders = safe_remove_by_id(self._current_energized_lv_feeders, lv_feeder) + self.current_energized_lv_feeders.remove(lv_feeder) return self + @deprecated("Use current_energized_lv_feeders.clear() instead") def clear_current_energized_lv_feeders(self) -> Feeder: - """ - Clear all `LvFeeder`s associated with `Feeder` in the current state of the network. - - :return: This `Feeder` for fluent use. - """ - self._current_energized_lv_feeders = None + self.current_energized_lv_feeders.clear() return self - def num_normal_energized_lv_substations(self) -> int: - """ - Get the number of entries in the normal [LvSubstation] collection. - """ - return nlen(self._normal_energized_lv_substations) - - def get_normal_energized_lv_substation(self, mrid: str) -> 'LvSubstation | None': - """ - Retrieve an energized LvSubstation using the normal state of the network. - - :param mrid: the mRID of the required normal [LvSubstation] - :returns: The [LvSubstation] with the specified [mRID] if it exists, otherwise null - """ - return get_by_mrid(self._normal_energized_lv_substations, mrid) - - def add_normal_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Associate this [Feeder] with a [LvSubstation] in the normal state of the network. - - :param lv_substation: the [LvSubstation] to associate with this LV feeder in the normal state of the network. - :returns: This [Feeder] for fluent use. - """ - if self._validate_reference(lv_substation, self.get_normal_energized_lv_substation, "An LvSubstation"): - return self - if self._normal_energized_lv_substations is None: - self._normal_energized_lv_substations = dict() - self._normal_energized_lv_substations[lv_substation.mrid] = lv_substation - return self + # endregion + # region normal_energized_lv_substations boilerplate - def remove_normal_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Disassociate this [Feeder] from a [LvSubstation] in the normal state of the network. + @deprecated("Use len(normal_energized_lv_substations) instead") + def num_normal_energized_lv_substations(self): + return len(self.normal_energized_lv_substations) - :param lv_substation: the [LvSubstation] to disassociate from this LV feeder in the normal state of the network. - :returns: true if a matching [LvSubstation] is removed from the collection. - """ - self._normal_energized_lv_substations = safe_remove_by_id(self._normal_energized_lv_substations, lv_substation) - return self + @deprecated("Use normal_energized_lv_substations.get_by_mrid(mrid) instead") + def get_normal_energized_lv_substation(self, mrid: str) -> LvSubstation: + return self.normal_energized_lv_substations.get_by_mrid(mrid) - def clear_normal_energized_lv_substations(self) -> "Feeder": - """ - Clear all [LvSubstation]'s associated with this [Feeder] in the normal state of the network. + @deprecated("Use normal_energized_lv_substations.append(lv_substation) instead") + def add_normal_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.normal_energized_lv_substations.append(lv_substation) + return self - :returns: This [Feeder] for fluent use. - """ - self._normal_energized_lv_substations = None + @deprecated("Use normal_energized_lv_substations.remove(lv_substation) instead") + def remove_normal_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.normal_energized_lv_substations.remove(lv_substation) return self - def num_current_energized_lv_substations(self) -> int: - """ - Get the number of entries in the current [LvSubstation] collection. - """ - return nlen(self._current_energized_lv_substations) - - def get_current_energized_lv_substation(self, mrid: str) -> 'LvSubstation | None': - """ - Retrieve an energized LvSubstation using the current state of the network. - - :param mrid: the mRID of the required current [LvSubstation] - :returns: The [LvSubstation] with the specified [mRID] if it exists, otherwise null - """ - return get_by_mrid(self._current_energized_lv_substations, mrid) - - def add_current_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Associate this [Feeder] with a [LvSubstation] in the current state of the network. - - :param lv_substation: the [LvSubstation] to associate with this LV feeder in the current state of the network. - :returns: This [Feeder] for fluent use. - """ - if self._validate_reference(lv_substation, self.get_current_energized_lv_substation, "An LvSubstation"): - return self - if self._current_energized_lv_substations is None: - self._current_energized_lv_substations = dict() - self._current_energized_lv_substations[lv_substation.mrid] = lv_substation + @deprecated("Use normal_energized_lv_substations.clear() instead") + def clear_normal_energized_lv_substations(self) -> Feeder: + self.normal_energized_lv_substations.clear() return self - def remove_current_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Disassociate this [Feeder] from a [LvSubstation] in the current state of the network. + # endregion + # region current_energized_lv_substations boilerplate + + @deprecated("Use len(current_energized_lv_substations) instead") + def num_current_energized_lv_substations(self): + return len(self.current_energized_lv_substations) - :param lv_substation: the [LvSubstation] to disassociate from this LV feeder in the current state of the network. - :returns: true if a matching [LvSubstation] is removed from the collection. - """ - self._current_energized_lv_substations = safe_remove_by_id(self._current_energized_lv_substations, lv_substation) + @deprecated("Use current_energized_lv_substations.get_by_mrid(mrid) instead") + def get_current_energized_lv_substation(self, mrid: str) -> LvSubstation: + return self.current_energized_lv_substations.get_by_mrid(mrid) + + @deprecated("Use current_energized_lv_substations.append(lv_substation) instead") + def add_current_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.current_energized_lv_substations.append(lv_substation) return self - def clear_current_energized_lv_substations(self) -> "Feeder": - """ - Clear all [LvSubstation]'s associated with this [Feeder] in the current state of the network. + @deprecated("Use current_energized_lv_substations.remove(lv_substation) instead") + def remove_current_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.current_energized_lv_substations.remove(lv_substation) + return self - :returns: This [Feeder] for fluent use. - """ - self._current_energized_lv_substations = None + @deprecated("Use current_energized_lv_substations.clear() instead") + def clear_current_energized_lv_substations(self) -> Feeder: + self.current_energized_lv_substations.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 801654734..3ab1de3f0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -7,12 +7,17 @@ __all__ = ["GeographicalRegion"] -from typing import Optional, List, Generator +from dataclasses import field +from typing import Optional, List +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass @@ -20,73 +25,41 @@ class GeographicalRegion(IdentifiedObject): """ A geographical region of a power system network phases. """ - _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = None + _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = field(default=None) - def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] = None, **kwargs): - super(GeographicalRegion, self).__init__(*args, **kwargs) - if sub_geographical_regions: - for sgr in sub_geographical_regions: - self.add_sub_geographical_region(sgr) + sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( + _sub_geographical_regions, + "A SubGeographicalRegion", + backfill=Backfill(SubGeographicalRegion.geographical_region) + ) - @property - def sub_geographical_regions(self) -> Generator[SubGeographicalRegion, None, None]: - """ - The `SubGeographicalRegion`s of this `GeographicalRegion`. - """ - return ngen(self._sub_geographical_regions) + # region deprecated list boilerplate + # region sub_geographical_regions boilerplate + + @deprecated("Use len(obj.sub_geographical_regions) instead.") def num_sub_geographical_regions(self) -> int: - """ - Returns The number of `SubGeographicalRegion`s associated with this `GeographicalRegion` - """ - return nlen(self._sub_geographical_regions) + return len(self.sub_geographical_regions) + @deprecated("Use obj.sub_geographical_regions.get_by_mrid(mrid) instead.") def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: - """ - Get the `SubGeographicalRegion` for this `GeographicalRegion` identified by `mrid` - - `mrid` The mRID of the required `SubGeographicalRegion` - Returns The `SubGeographicalRegion` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._sub_geographical_regions, mrid) + return self.sub_geographical_regions.get_by_mrid(mrid) + @deprecated("Use obj.sub_geographical_regions.append(sub_geographical_region) instead.") def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: - """ - Associate a `SubGeographicalRegion` with this `GeographicalRegion` - - `sub_geographical_region` The `SubGeographicalRegion` to associate with this `GeographicalRegion`. - Returns A reference to this `GeographicalRegion` to allow fluent use. - Raises `ValueError` if another `SubGeographicalRegion` with the same `mrid` already exists for this `GeographicalRegion`, or if - `sub_geographical_region.geographical_region` is not this `GeographicalRegion`. - """ - if self._validate_reference(sub_geographical_region, self.get_sub_geographical_region, "A SubGeographicalRegion"): - return self - - if sub_geographical_region.geographical_region is None: - sub_geographical_region.geographical_region = self - - require(sub_geographical_region.geographical_region is self, lambda: f"{sub_geographical_region} `geographical_region` property references " + - f"{sub_geographical_region.geographical_region}, expected {self}.") - - self._sub_geographical_regions = list() if self._sub_geographical_regions is None else self._sub_geographical_regions - self._sub_geographical_regions.append(sub_geographical_region) + self.sub_geographical_regions.append(sub_geographical_region) return self + @deprecated("Use obj.sub_geographical_regions.remove(sub_geographical_region) instead.") def remove_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: - """ - Disassociate `sub_geographical_region` from this `GeographicalRegion` - `sub_geographical_region` The `SubGeographicalRegion` to disassociate from this `GeographicalRegion`. - Returns A reference to this `GeographicalRegion` to allow fluent use. - Raises `ValueError` if `sub_geographical_region` was not associated with this `GeographicalRegion`. - """ - self._sub_geographical_regions = safe_remove(self._sub_geographical_regions, sub_geographical_region) + self.sub_geographical_regions.remove(sub_geographical_region) return self + @deprecated("Use obj.sub_geographical_regions.clear() instead.") def clear_sub_geographical_regions(self) -> GeographicalRegion: - """ - Clear all SubGeographicalRegions. - Returns A reference to this `GeographicalRegion` to allow fluent use. - """ - self._sub_geographical_regions = None + self.sub_geographical_regions.clear() return self + + # endregion sub_geographical_regions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py index 4c9b6c0a2..ef9b28821 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py @@ -62,7 +62,7 @@ def __init__(self, mrid: str, *args, names: Optional[List[Name]] = None, **kwarg def __str__(self): class_name = f'{self.__class__.__name__}' - if self.name: + if getattr(self, "name", None): return f'{class_name}{{{self.mrid}|{self.name}}}' return f'{class_name}{{{self.mrid}}}' diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name.py b/src/zepben/ewb/model/cim/iec61970/base/core/name.py index 05adcfda9..123b7cffc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Optional from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.backed_descriptor import remove_descriptor_annotations from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable if TYPE_CHECKING: @@ -19,7 +18,6 @@ @zb_dataclass -@remove_descriptor_annotations class Name(Identifiable): """ The Name class provides the means to define any number of human-readable names for an object. A name is **not** to be used for defining inter-object diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index 7e0231238..84cdd64ef 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor, remove_descriptor_annotations +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -21,7 +21,6 @@ @zb_dataclass -@remove_descriptor_annotations class NameType(Identifiable): """ Type of name. Possible values for attribute 'name' are implementation dependent but standard profiles may specify types. An enterprise may have multiple diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 6368c13ec..56a48491d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -7,12 +7,16 @@ __all__ = ['PowerSystemResource'] -from typing import Optional, TYPE_CHECKING, List, Generator, Iterable +from typing import Optional, TYPE_CHECKING, List from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove +from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset @@ -38,13 +42,7 @@ class PowerSystemResource(IdentifiedObject, metaclass=ABCMeta): num_controls: Optional[int] = None """Number of Control's known to associate with this [PowerSystemResource]""" - _assets: Optional[List[Asset]] = None - - def __init__(self, *args, assets: Iterable[Asset] = None, **kwargs): - super(PowerSystemResource, self).__init__(*args, **kwargs) - if assets: - for asset in assets: - self.add_asset(asset) + _assets: Optional[List[Asset]] = field(default=None) @property def has_controls(self) -> bool: @@ -53,57 +51,38 @@ def has_controls(self) -> bool: """ return nlen(self.num_controls) > 0 - @property - def assets(self) -> Generator[Asset, None, None]: - """ - The `Asset`s of this `PowerSystemResource`. - """ - return ngen(self._assets) + assets: MridCollection[Asset] = LazyMridList( + _assets, + "An Asset", + ) + + + # region deprecated list boilerplate + # region assets boilerplate + @deprecated("Use len(obj.assets) instead.") def num_assets(self) -> int: - """ - Get the number of `Asset`s associated with this `PowerSystemResource`. - """ - return nlen(self._assets) + return len(self.assets) + @deprecated("Use obj.assets.get_by_mrid(mrid) instead.") def get_asset(self, mrid: str) -> Asset: - """ - Get the `Asset` associated with this `PowerSystemResource` identified by `mrid`. - - `mrid` the mRID of the required `Asset` - Returns The `Asset` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._assets, mrid) + return self.assets.get_by_mrid(mrid) + @deprecated("Use obj.assets.append(asset) instead.") def add_asset(self, asset: Asset) -> PowerSystemResource: - """ - `asset` The `Asset` to associate with this `PowerSystemResource`. - Returns A reference to this `PowerSystemResource` to allow fluent use. - Raises `ValueError` if another `Asset` with the same `mrid` already exists in this `PowerSystemResource` - """ - if self._validate_reference(asset, self.get_asset, "An Asset"): - return self - - self._assets = list() if self._assets is None else self._assets - self._assets.append(asset) + self.assets.append(asset) return self + @deprecated("Use obj.assets.remove(asset) instead.") def remove_asset(self, asset: Asset) -> PowerSystemResource: - """ - Disassociate an `Asset` from this `PowerSystemResource`. - - `asset` the `Asset` to disassociate from this `PowerSystemResource`. - Raises `ValueError` if `asset` was not associated with this `PowerSystemResource`. - Returns A reference to this `PowerSystemResource` to allow fluent use. - """ - self._assets = safe_remove(self._assets, asset) + self.assets.remove(asset) return self + @deprecated("Use obj.assets.clear() instead.") def clear_assets(self) -> PowerSystemResource: - """ - Clear all assets. - Returns self - """ - self._assets = None + self.assets.clear() return self + + # endregion assets boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 869eef810..042cdcf2a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -7,17 +7,20 @@ __all__ = ["SubGeographicalRegion"] -from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, List, TYPE_CHECKING from typing_extensions import deprecated -from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill, internal +from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject +from zepben.ewb.model.cim.iec61970.base.core.substation import Substation if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.geographical_region import GeographicalRegion - from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @zb_dataclass @@ -26,88 +29,53 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - _geographical_region: Optional[GeographicalRegion] = None - - _substations: Optional[List[Substation]] = None - - def __init__(self, *args, substations: List[Substation] = None, **kwargs): - super(SubGeographicalRegion, self).__init__(*args, **kwargs) - if substations: - for sub in substations: - self.add_substation(sub) + _geographical_region: Optional[GeographicalRegion] = field(default=None) + _substations: Optional[List[Substation]] = field(default=None) @property + @internal(_geographical_region) def geographical_region(self): """The geographical region to which this sub-geographical region is within.""" return self._geographical_region @geographical_region.setter - @deprecated("geographical_region should never be set directly - it is automatically set when adding it to the `sub_geographical_regions` list") + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") def geographical_region(self, value): self._geographical_region = value - @property - def substations(self) -> Generator[Substation, None, None]: - """ - All substations belonging to this sub geographical region. - """ - return ngen(self._substations) + substations: MridCollection[Substation] = LazyMridList( + _substations, + "A Substation", + backfill=Backfill(Substation.sub_geographical_region) + ) + + # region deprecated list boilerplate + # region substations boilerplate + @deprecated("Use len(obj.substations) instead.") def num_substations(self) -> int: - """ - Returns The number of `Substation`s associated with this `SubGeographicalRegion` - """ - return nlen(self._substations) + return len(self.substations) + @deprecated("Use obj.substations.get_by_mrid(mrid) instead.") def get_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `SubGeographicalRegion` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._substations, mrid) + return self.substations.get_by_mrid(mrid) + @deprecated("Use obj.substations.append(substation) instead.") def add_substation(self, substation: Substation) -> SubGeographicalRegion: - """ - Associate a `Substation` with this `GeographicalRegion` - - `substation` the `Substation` to associate with this `SubGeographicalRegion`. - - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `SubGeographicalRegion`, or if - `substation.sub_geographical_region` is not this `SubGeographicalRegion`. - """ - if self._validate_reference(substation, self.get_substation, "A Substation"): - return self - - if substation.sub_geographical_region is None: - substation.sub_geographical_region = self - - require(substation.sub_geographical_region is self, lambda: f"{substation} `sub_geographical_region` property references {substation.sub_geographical_region}, expected {self}.") - - self._substations = list() if self._substations is None else self._substations - self._substations.append(substation) + self.substations.append(substation) return self + @deprecated("Use obj.substations.remove(substation) instead.") def remove_substation(self, substation: Substation) -> SubGeographicalRegion: - """ - Disassociate `substation` from this `GeographicalRegion` - - `substation` The `Substation` to disassociate from this `SubGeographicalRegion`. - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `SubGeographicalRegion`. - """ - self._substations = safe_remove(self._substations, substation) + self.substations.remove(substation) return self + @deprecated("Use obj.substations.clear() instead.") def clear_substations(self) -> SubGeographicalRegion: - """ - Clear all `Substations`. - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - """ - self._substations = None + self.substations.clear() return self + + # endregion substations boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index ed515fe95..56e68b7e3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -7,17 +7,19 @@ __all__ = ["Substation"] -from typing import Optional, Generator, List, TYPE_CHECKING - +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill, internal +from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop - from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion from zepben.ewb.model.cim.iec61970.infiec61970.feeder.circuit import Circuit @@ -29,269 +31,161 @@ class Substation(EquipmentContainer): is passed for the purposes of switching or modifying its characteristics. """ - _sub_geographical_region: Optional[SubGeographicalRegion] = None + _sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) - _normal_energized_feeders: Optional[List[Feeder]] = None + _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) - _loops: Optional[List[Loop]] = None + _loops: Optional[List[Loop]] = field(default=None) - _energized_loops: Optional[List[Loop]] = None + _energized_loops: Optional[List[Loop]] = field(default=None) - _circuits: Optional[List[Circuit]] = None + _circuits: Optional[List[Circuit]] = field(default=None) - def __init__(self, *args, normal_energized_feeders: List[Feeder] = None, loops: List[Loop] = None, energized_loops: List[Loop] = None, circuits: List[Circuit] = None, **kwargs): + def __init__(self, *args, normal_energized_feeders=None, **kwargs): super(Substation, self).__init__(*args, **kwargs) - if normal_energized_feeders: - for feeder in normal_energized_feeders: - self.add_feeder(feeder) - if loops: - for loop in loops: - self.add_loop(loop) - if energized_loops: - for loop in energized_loops: - self.add_energized_loop(loop) - if circuits: - for circuit in circuits: - self.add_circuit(circuit) - + self.feeders.extend(normal_energized_feeders) @property + @internal(_sub_geographical_region) def sub_geographical_region(self): """The SubGeographicalRegion containing the substation.""" return self._sub_geographical_region + + @sub_geographical_region.setter @deprecated("sub_geographical_region should never be set directly - it is automatically set when adding it to the `substations` list") def sub_geographical_region(self, value): self._sub_geographical_region = value - @property - def circuits(self) -> Generator[Circuit, None, None]: - """ - The `Circuit`s originating from this substation. - """ - return ngen(self._circuits) + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "A Circuit", + ) - @property - def loops(self) -> Generator[Loop, None, None]: - """ - The `Loop` originating from this substation. - """ - return ngen(self._loops) + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) - @property - def energized_loops(self) -> Generator[Loop, None, None]: - """ - The `Loop`s originating from this substation that are energised. - """ - return ngen(self._energized_loops) + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) - @property - def feeders(self) -> Generator[Feeder, None, None]: - """ - The normal energized feeders of the substation. Also used for naming purposes. - """ - return ngen(self._normal_energized_feeders) + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) + ) + + # region deprecated list boilerplate + # region circuits boilerplate + @deprecated("Use len(obj.circuits) instead.") def num_circuits(self): - """ - Returns The number of `Circuit`s associated with this `Substation` - """ - return nlen(self._circuits) + return len(self.circuits) + @deprecated("Use obj.circuits.get_by_mrid(mrid) instead.") def get_circuit(self, mrid: str) -> Circuit: - """ - Get the `Circuit` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Circuit` - Returns The `Circuit` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._circuits, mrid) + return self.circuits.get_by_mrid(mrid) + @deprecated("Use obj.circuits.append(circuit) instead.") def add_circuit(self, circuit: Circuit) -> Substation: - """ - Associate a `Circuit` with this `Substation` - - `circuit` The `Circuit` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Circuit` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(circuit, self.get_circuit, "A Circuit"): - return self - self._circuits = list() if self._circuits is None else self._circuits - self._circuits.append(circuit) + self.circuits.append(circuit) return self + @deprecated("Use obj.circuits.remove(circuit) instead.") def remove_circuit(self, circuit: Circuit) -> Substation: - """ - Disassociate `circuit` from this `Substation` - - `circuit` The `Circuit` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `circuit` was not associated with this `Substation`. - """ - self._circuits = safe_remove(self._circuits, circuit) + self.circuits.remove(circuit) return self + @deprecated("Use obj.circuits.clear() instead.") def clear_circuits(self) -> Substation: - """ - Clear all current `Circuit`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._circuits = None + self.circuits.clear() return self + # endregion circuits boilerplate + + # region loops boilerplate + + @deprecated("Use len(obj.loops) instead.") def num_loops(self): - """ - Returns The number of `Loop`s associated with this `Substation` - """ - return nlen(self._loops) + return len(self.loops) + @deprecated("Use obj.loops.get_by_mrid(mrid) instead.") def get_loop(self, mrid: str) -> Loop: - """ - Get the `Loop` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Loop` - Returns The `Loop` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._loops, mrid) + return self.loops.get_by_mrid(mrid) + @deprecated("Use obj.loops.append(loop) instead.") def add_loop(self, loop: Loop) -> Substation: - """ - Associate a `Loop` with this `Substation` - - `loop` The `Loop` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Loop` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(loop, self.get_loop, "A Loop"): - return self - self._loops = list() if self._loops is None else self._loops - self._loops.append(loop) + self.loops.append(loop) return self + @deprecated("Use obj.loops.remove(loop) instead.") def remove_loop(self, loop: Loop) -> Substation: - """ - Disassociate `loop` from this `Substation` - - `loop` The `Loop` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `loop` was not associated with this `Substation`. - """ - self._loops = safe_remove(self._loops, loop) + self.loops.remove(loop) return self + @deprecated("Use obj.loops.clear() instead.") def clear_loops(self) -> Substation: - """ - Clear all current `Loop`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._loops = None + self.loops.clear() return self + # endregion loops boilerplate + + # region energized_loops boilerplate + + @deprecated("Use len(obj.energized_loops) instead.") def num_energized_loops(self): - """ - Returns The number of `Loop`s associated with this `Substation` - """ - return nlen(self._energized_loops) + return len(self.energized_loops) + @deprecated("Use obj.energized_loops.get_by_mrid(mrid) instead.") def get_energized_loop(self, mrid: str) -> Loop: - """ - Get the `Loop` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Loop` - Returns The `Loop` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energized_loops, mrid) + return self.energized_loops.get_by_mrid(mrid) + @deprecated("Use obj.energized_loops.append(loop) instead.") def add_energized_loop(self, loop: Loop) -> Substation: - """ - Associate a `Loop` with this `Substation` - - `loop` The `Loop` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Loop` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(loop, self.get_energized_loop, "A Loop"): - return self - self._energized_loops = list() if self._energized_loops is None else self._energized_loops - self._energized_loops.append(loop) + self.energized_loops.append(loop) return self + @deprecated("Use obj.energized_loops.remove(loop) instead.") def remove_energized_loop(self, loop: Loop) -> Substation: - """ - Disassociate `loop` from this `Substation` - - `loop` The `Loop` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `loop` was not associated with this `Substation`. - """ - self._energized_loops = safe_remove(self._energized_loops, loop) + self.energized_loops.remove(loop) return self + @deprecated("Use obj.energized_loops.clear() instead.") def clear_energized_loops(self) -> Substation: - """ - Clear all current `Loop`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._energized_loops = None + self.energized_loops.clear() return self + # endregion energized_loops boilerplate + + # region feeders boilerplate + + @deprecated("Use len(obj.feeders) instead.") def num_feeders(self): - """ - Returns The number of `Feeder`s associated with this `Substation` - """ - return nlen(self._normal_energized_feeders) + return len(self.feeders) + @deprecated("Use obj.feeders.get_by_mrid(mrid) instead.") def get_feeder(self, mrid: str) -> Feeder: - """ - Get the `Feeder` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Feeder` - Returns The `Feeder` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._normal_energized_feeders, mrid) + return self.feeders.get_by_mrid(mrid) + @deprecated("Use obj.feeders.append(feeder) instead.") def add_feeder(self, feeder: Feeder) -> Substation: - """ - Associate a `Feeder` with this `Substation` - - `feeder` The `Feeder` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Feeder` with the same `mrid` already exists for this `Substation`, or if - `feeder.normal_energizing_substation` is not this `Substation`. - """ - if self._validate_reference(feeder, self.get_feeder, "A Feeder"): - return self - - if feeder.normal_energizing_substation is None: - feeder.normal_energizing_substation = self - - require(feeder.normal_energizing_substation is self, lambda: f"{feeder} `normal_energizing_substation` property references {feeder.normal_energizing_substation}, expected {self}.") - - self._normal_energized_feeders = list() if self._normal_energized_feeders is None else self._normal_energized_feeders - self._normal_energized_feeders.append(feeder) + self.feeders.append(feeder) return self + @deprecated("Use obj.feeders.remove(feeder) instead.") def remove_feeder(self, feeder: Feeder) -> Substation: - """ - Disassociate `feeder` from this `Substation` - - `feeder` The `Feeder` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `feeder` was not associated with this `Substation`. - """ - self._normal_energized_feeders = safe_remove(self._normal_energized_feeders, feeder) + self.feeders.remove(feeder) return self + @deprecated("Use obj.feeders.clear() instead.") def clear_feeders(self) -> Substation: - """ - Clear all current `Feeder`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._normal_energized_feeders = None + self.feeders.clear() return self + + # endregion feeders boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index 79668113d..ea1d8392a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -7,16 +7,17 @@ __all__ = ["Terminal"] +from dataclasses import field from typing import Optional, Generator from typing import TYPE_CHECKING from weakref import ref, ReferenceType from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode -from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection from zepben.ewb.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.ewb.services.network.tracing.phases.phase_status import PhaseStatus from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -32,7 +33,7 @@ class Terminal(AcDcTerminal): An AC electrical connection point to a piece of conducting equipment. Terminals are connected at physical connection points called connectivity nodes. """ - _conducting_equipment: Optional[ConductingEquipment] = None + _conducting_equipment: Optional['ConductingEquipment'] = field(default=None) """The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via connectivity nodes.""" @@ -58,21 +59,12 @@ class Terminal(AcDcTerminal): _normal_phases: PhaseStatus = None _current_phases: PhaseStatus = None - def __init__(self, *args, conducting_equipment: ConductingEquipment = None, connectivity_node: ConnectivityNode = None, **kwargs): + def __init__(self, *args, **kwargs): super(Terminal, self).__init__(*args, **kwargs) self._normal_phases = PhaseStatus(self) - self._current_phases = PhaseStatus(self) - if conducting_equipment: - self.conducting_equipment = conducting_equipment - - # We set the connectivity node to itself if the name parameter is not used to make sure the positional argument is wrapped in a reference. - if connectivity_node: - self.connectivity_node = connectivity_node - else: - self.connectivity_node = self._cn @property def normal_phases(self) -> PhaseStatus: @@ -85,6 +77,7 @@ def current_phases(self) -> PhaseStatus: return self._current_phases @property + @internal(_conducting_equipment) def conducting_equipment(self): """ The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via @@ -172,8 +165,10 @@ def is_feeder_head_terminal(self): for feeder in filter(lambda c: isinstance(c, Feeder), self.conducting_equipment.containers): if feeder.normal_head_terminal == self: return True + return False def has_connected_busbars(self): + from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection try: return any(it != self and isinstance(it.conducting_equipment, BusbarSection) for it in self.connectivity_node.terminals) except AttributeError: diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 9f48f929e..e627bae01 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -7,16 +7,19 @@ __all__ = ["Diagram"] -from typing import Optional, Dict, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Dict +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject +from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle from zepben.ewb.model.cim.iec61970.base.diagramlayout.orientation_kind import OrientationKind -from zepben.ewb.util import nlen, ngen, require, safe_remove_by_id -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject @zb_dataclass @@ -32,80 +35,41 @@ class Diagram(IdentifiedObject): orientation_kind: OrientationKind = OrientationKind.POSITIVE """Coordinate system orientation of the diagram.""" - _diagram_objects: Optional[Dict[str, DiagramObject]] = None + _diagram_objects: Dict[str, DiagramObject] | None = field(default=None) - def __init__(self, *args, diagram_objects: List[DiagramObject] = None, **kwargs): - super(Diagram, self).__init__(*args, **kwargs) - if diagram_objects: - for obj in diagram_objects: - self.add_diagram_object(obj) + diagram_objects: MridCollection[DiagramObject] = LazyMridMap( + _diagram_objects, + "A DiagramObject", + backfill=Backfill(DiagramObject.diagram) + ) + """The diagram objects belonging to this diagram.""" - @property - def diagram_objects(self) -> Generator[DiagramObject, None, None]: - """ - The diagram objects belonging to this diagram. - """ - return ngen(self._diagram_objects) + # region deprecated list boilerplate + # region cuts boilerplate + + @deprecated("Use len(diagram_objects) instead") def num_diagram_objects(self): - """ - Returns The number of `DiagramObject`s associated with this `Diagram` - """ - return nlen(self._diagram_objects) + return len(self.diagram_objects) + @deprecated("Use diagram_objects.get_by_mrid(mrid) instead") def get_diagram_object(self, mrid: str) -> DiagramObject: - """ - Get the `DiagramObject` for this `Diagram` identified by `mrid` - - `mrid` the mRID of the required `DiagramObject` - Returns The `DiagramObject` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._diagram_objects: - raise KeyError(mrid) - try: - return self._diagram_objects[mrid] - except AttributeError: - raise KeyError(mrid) + return self.diagram_objects.get_by_mrid(mrid) + @deprecated("Use diagram_objects.append(diagram_object) instead") def add_diagram_object(self, diagram_object: DiagramObject) -> Diagram: - """ - Associate a `DiagramObject` with this `Diagram`. - - `diagram_object` the `DiagramObject` to associate with this `Diagram`. - Returns The previous `DiagramObject` stored by `diagram_object`s mrid, otherwise `diagram_object` is returned - if there was no previous value. - Raises `ValueError` if another `DiagramObject` with the same `mrid` already exists for this `Diagram`, or if `diagram_object.diagram` is not this - `Diagram`. - """ - if not diagram_object.diagram: - diagram_object.diagram = self - require(diagram_object.diagram is self, lambda: f"{str(diagram_object)} `diagram` property references " - f"{str(diagram_object.diagram)}, expected {str(self)}.") - - if self._validate_reference(diagram_object, self.get_diagram_object, "A DiagramObject"): - return self - - self._diagram_objects = dict() if self._diagram_objects is None else self._diagram_objects - self._diagram_objects[diagram_object.mrid] = diagram_object - + self.diagram_objects.append(diagram_object) return self + @deprecated("Use diagram_objects.remove(diagram_object) instead") def remove_diagram_object(self, diagram_object: DiagramObject) -> Diagram: - """ - Disassociate `diagram_object` from this `Diagram` - - `diagram_object` the `DiagramObject` to disassociate with this `Diagram`. - Returns A reference to this `Diagram` to allow fluent use. - Raises `KeyError` if `diagram_object` was not associated with this `Diagram`. - """ - self._diagram_objects = safe_remove_by_id(self._diagram_objects, diagram_object) + self.diagram_objects.remove(diagram_object) return self + @deprecated("Use diagram_objects.clear() instead") def clear_diagram_objects(self) -> Diagram: - """ - Clear all `DiagramObject`s. - Returns A reference to this `Diagram` to allow fluent use. - """ - self._diagram_objects = None + self.diagram_objects.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index c4f5fdfe1..a3cb235cf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -7,13 +7,15 @@ __all__ = ["DiagramObject"] -from typing import Optional, List, Generator, Callable, TYPE_CHECKING, Any +from dataclasses import field +from typing import Optional, List, Callable, TYPE_CHECKING, Any from typing_extensions import deprecated +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint -from zepben.ewb.util import nlen, ngen, require, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: @@ -27,7 +29,7 @@ class DiagramObject(IdentifiedObject): analog values, breakers, disconnectors, power transformers, and transmission lines. """ - _diagram: Optional[Diagram] = None + _diagram: Optional[Diagram] = field(default=None) identified_object_mrid: Optional[str] = None """The domain object to which this diagram object is associated.""" @@ -38,17 +40,14 @@ class DiagramObject(IdentifiedObject): rotation: float = 0.0 """Sets the angle of rotation of the diagram object. Zero degrees is pointing to the top of the diagram. Rotation is clockwise.""" - _diagram_object_points: Optional[List[DiagramObjectPoint]] = None + _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - def __init__(self, *args, diagram: Diagram = None, diagram_object_points: List[DiagramObjectPoint] = None, **kwargs): + def __init__(self, *args, diagram_object_points=None, **kwargs): super(DiagramObject, self).__init__(*args, **kwargs) - if diagram: - self.diagram = diagram - if diagram_object_points: - for point in diagram_object_points: - self.add_point(point) + self.points.extend(diagram_object_points) @property + @internal(_diagram) def diagram(self): """A diagram object is part of a diagram.""" return self._diagram @@ -61,105 +60,80 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - @property - def points(self) -> Generator[DiagramObjectPoint, None, None]: - """ - The `DiagramObjectPoint`s for this `DiagramObject`. - """ - return ngen(self._diagram_object_points) + points: LazyIndexList[DiagramObjectPoint] = LazyIndexList( + _diagram_object_points, + "DiagramObjectPoint", + ) + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - def for_each_point(self, action: Callable[[int, DiagramObjectPoint], Any]): - """ - Call the `action` on each :class:`DiagramObjectPoint` in the `points` collection + # region points boilerplate - :param action: An action to apply to each :class:`DiagramObjectPoint` in the `points` collection, taking the index of the point, and the point itself. - """ - for index, point in enumerate(self.points): - action(index, point) + @deprecated("Use points.for_each_indexed(action) instead.") + def for_each_point( + self, + action: Callable[[int, DiagramObjectPoint], Any], + ): + self.points.for_each_indexed(action) + @deprecated("Use len(points) instead.") def num_points(self): - """ - Returns the number of `DiagramObjectPoint`s associated with this `DiagramObject` - """ - return nlen(self._diagram_object_points) + return len(self.points) + @deprecated("Use points[sequence_number] instead.") def get_point(self, sequence_number: int) -> DiagramObjectPoint: - """ - Get the `DiagramObjectPoint` for this `DiagramObject` represented by `sequence_number` . - A diagram object can have 0 or more points to reflect its layout position, routing (for polylines) or boundary (for polygons). - Index in the underlying points collection corresponds to the sequence number - - `sequence_number` The sequence number of the `DiagramObjectPoint` to get. - Returns The `DiagramObjectPoint` identified by `sequence_number` - Raises IndexError if this `DiagramObject` didn't contain `sequence_number` points. - """ - if self._diagram_object_points is not None: - return self._diagram_object_points[sequence_number] - else: - raise IndexError(sequence_number) + return self.points[sequence_number] + @deprecated("Use points[item] instead.") def __getitem__(self, item: int) -> DiagramObjectPoint: - return self.get_point(item) + return self.points[item] + @deprecated("Use points.append(point) instead.") def add_point(self, point: DiagramObjectPoint) -> DiagramObject: - """ - Associate a `DiagramObjectPoint` with this `DiagramObject`, assigning it a sequence_number of `num_points`. - `point` The `DiagramObjectPoint` to associate with this `DiagramObject`. - Returns A reference to this `DiagramObject` to allow fluent use. - """ - return self.insert_point(point) - - def insert_point(self, point: DiagramObjectPoint, sequence_number: int = None) -> DiagramObject: - """ - Associate a `DiagramObjectPoint` with this `DiagramObject` - - `point` The `DiagramObjectPoint` to associate with this `DiagramObject`. - `sequence_number` The sequence number of the `DiagramObjectPoint`. - Returns A reference to this `DiagramObject` to allow fluent use. - Raises `ValueError` if `sequence_number` < 0 or > `num_points()`. - """ - if sequence_number is None: - sequence_number = self.num_points() - require(0 <= sequence_number <= self.num_points(), - lambda: f"Unable to add DiagramObjectPoint to {str(self)}. Sequence number {sequence_number}" - f" is invalid. Expected a value between 0 and {self.num_points()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._diagram_object_points = list() if self._diagram_object_points is None else self._diagram_object_points - self._diagram_object_points.insert(sequence_number, point) + self.points.append(point) return self - def __setitem__(self, key, value): - self.insert_point(value, key) + @deprecated("Use points.insert(sequence_number, point)") + def insert_point( + self, + point: DiagramObjectPoint, + sequence_number: int | None = None, + ) -> DiagramObject: + if sequence_number is None: sequence_number = len(self.points) + self.points.insert(sequence_number, point) - def remove_point(self, point: DiagramObjectPoint) -> DiagramObject: - """ - Disassociate `point` from this `DiagramObject` - - `point` The `DiagramObjectPoint` to disassociate from this `DiagramObject`. - Returns A reference to this `DiagramObject` to allow fluent use. - Raises `ValueError` if `point` was not associated with this `DiagramObject`. - """ - self._diagram_object_points = safe_remove(self._diagram_object_points, point) return self - def remove_point_by_sequence_number(self, sequence_number: int) -> DiagramObjectPoint: - """ - Remove a :class:`DiagramObjectPoint` from this :class:`DiagramObject` by its sequence number. + @deprecated("Use points.insert(key, value) instead.") + def __setitem__( + self, + key: int, + value: DiagramObjectPoint, + ) -> None: + self.points.insert(key, value) - NOTE: This will update the sequence numbers of all items located after the removed sequence number. + @deprecated("Use points.remove(point) instead.") + def remove_point(self, point: DiagramObjectPoint) -> DiagramObject: + self.points.remove(point) + return self - :param sequence_number: The sequence number of the `DiagramObjectPoint` to remove. - :return: The :class:`DiagramObjectPoint` that was removed, or null if there was no :class:`DiagramObjectPoint` for the given `sequenceNumber`. - :raises IndexError: If no :class:`DiagramObjectPoint` with the specified `sequence_number` was not associated with this :class:`DiagramObject`. - """ - point = self.get_point(sequence_number) - self._diagram_object_points = safe_remove(self._diagram_object_points, point) - return point + @deprecated("Use points.pop(sequence_number) instead.") + def remove_point_by_sequence_number( + self, + sequence_number: int, + ) -> DiagramObjectPoint: + return self.points.pop(sequence_number) + @deprecated("Use points.clear() instead.") def clear_points(self) -> DiagramObject: - """ - Clear all points. - Returns A reference to this `DiagramObject` to allow fluent use. - """ - self._diagram_object_points = None + self.points.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index c31fe2691..8fde00398 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -3,15 +3,19 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ["BatteryUnit"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.relations.battery_control_list import BatteryControlList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl @@ -21,12 +25,6 @@ class BatteryUnit(PowerElectronicsUnit): """An electrochemical energy storage device.""" - def __init__(self, *args, controls: List['BatteryControl'] = None, **kwargs): - super(BatteryUnit, self).__init__(*args, **kwargs) - if controls: - for bc in controls: - self.add_control(bc) - battery_state: BatteryStateKind = BatteryStateKind.UNKNOWN """The current state of the battery (charging, full, etc.).""" @@ -36,75 +34,46 @@ def __init__(self, *args, controls: List['BatteryControl'] = None, **kwargs): stored_e: Optional[int] = None """Amount of energy currently stored in watt hours (Wh). The attribute shall be a positive value or zero and lower than `rated_e`.""" - _controls: Optional[List['BatteryControl']] = None + _controls: Optional[List['BatteryControl']] = field(default=None) # NOTE: This is called `num_battery_controls` because `num_controls` is already used by `PowerSystemResource`. - @property - def controls(self) -> Generator['BatteryControl', None, None]: - """ - [ZBEX] The `BatteryControl`s associated with this `BatteryUnit` - """ - return ngen(self._controls) + controls: BatteryControlList['BatteryControl'] = BatteryControlList( + _controls, + "A BatteryControl", + ) + + + # region deprecated list boilerplate + # region controls boilerplate + + @deprecated("Use len(obj.controls) instead.") def num_battery_controls(self): - """ - Returns The number of `BatteryControl`s associated with this `BatteryUnit` - """ - return nlen(self._controls) + return len(self.controls) + @deprecated("Use obj.controls.get_by_mrid(mrid) instead.") def get_control(self, mrid: str) -> 'BatteryControl': - """ - Get the `BatteryControl` for this `BatteryUnit` identified by `mrid` - - `mrid` the mRID of the required `BatteryControl` - Returns The `BatteryControl` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._controls, mrid) + return self.controls.get_by_mrid(mrid) + @deprecated("Use obj.controls.get_by_mode(control_mode) instead.") def get_control_by_mode(self, control_mode: BatteryControlMode) -> 'BatteryControl': - """ - Get the `BatteryControl` for this `BatteryUnit` identified by its `control_mode` - - `control_mode` the `BatteryControlMode` of the desired `BatteryControl` - Returns The `BatteryControl` with the specified `control_mode` if it exists - Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. - """ - if self._controls: - for control in self._controls: - if control.control_mode == control_mode: - return control - raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") + return self.controls.get_by_mode(control_mode) + @deprecated("Use obj.controls.append(bc) instead.") def add_control(self, bc: 'BatteryControl') -> 'BatteryUnit': - """ - Associate `bc` to this `BatteryUnit`. - - `bc` the `BatteryControl` to associate with this `BatteryUnit`. - Returns A reference to this `BatteryUnit` to allow fluent use. - Raises `ValueError` if another `BatteryControl` with the same `mrid` already exists for this `BatteryUnit`. - """ - if self._validate_reference(bc, self.get_control, "A BatteryControl"): - return self - self._controls = list() if self._controls is None else self._controls - self._controls.append(bc) + self.controls.append(bc) return self + @deprecated("Use obj.controls.remove(bc) instead.") def remove_control(self, bc: 'BatteryControl') -> 'BatteryUnit': - """ - Disassociate `bc` from this `BatteryUnit` - - `bc` the `BatteryControl` to disassociate from this `BatteryUnit`. - Returns A reference to this `BatteryUnit` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `BatteryUnit`. - """ - self._controls = safe_remove(self._controls, bc) + self.controls.remove(bc) return self + @deprecated("Use obj.controls.clear() instead.") def clear_controls(self) -> 'BatteryUnit': - """ - Clear all battery_controls. - Returns A reference to this `BatteryUnit` to allow fluent use. - """ - self._controls = None + self.controls.clear() return self + + # endregion controls boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index c6734028d..f88c7b2d0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -7,19 +7,23 @@ __all__ = ["AcLineSegment"] -from typing import Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase +from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor +from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.relations.ac_line_segment_phase_list import AcLineSegmentPhaseList if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo - from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp - from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut - from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_phase_impedance import PerLengthPhaseImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance @@ -43,9 +47,9 @@ class AcLineSegment(Conductor): per_length_impedance: 'PerLengthImpedance | None' = None """A `zepben.ewb.model.cim.iec61970.base.wires.PerLengthImpedance` describing this AcLineSegment""" - _cuts: list['Cut'] | None = None - _clamps: list['Clamp'] | None = None - _phases: list['AcLineSegmentPhase'] | None = None + _cuts: list[Cut] | None = field(default=None) + _clamps: list[Clamp] | None = field(default=None) + _phases: list[AcLineSegmentPhase] | None = field(default=None) @property def per_length_sequence_impedance(self) -> Optional['PerLengthSequenceImpedance']: @@ -79,62 +83,27 @@ def per_length_phase_impedance(self) -> Optional['PerLengthPhaseImpedance']: def per_length_phase_impedance(self, value: Optional['PerLengthPhaseImpedance']): self.per_length_impedance = value - @property - def cuts(self) -> Generator['Cut', None, None]: - """The `Cut`s for this `AcLineSegment`.""" - return ngen(self._cuts) - @property - def clamps(self) -> Generator['Clamp', None, None]: - """The `Clamp`s for this `AcLineSegment`.""" - return ngen(self._clamps) + cuts: MridCollection[Cut] = LazyMridList( + _cuts, + "A Cut", + backfill=Backfill(Cut.ac_line_segment) + ) - def _validate_cut(self, cut: 'Cut') -> bool: - """ - Validate a cut against this `AcLineSegment`'s `Cut`s. - - :param cut: The `Cut` to validate. - :return: True if `cut` is already associated with this `AcLineSegment`, otherwise False. - :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment`, or if this `AcLineSegment` has a different `Cut` with the same mRID. - """ - if self._validate_reference(cut, self.get_cut, "A Cut"): - return True - if not cut.ac_line_segment: - cut.ac_line_segment = self + clamps: MridCollection[Clamp] = LazyMridList( + _clamps, + "A Clamp", + backfill=Backfill(Clamp.ac_line_segment) + ) - require( - cut.ac_line_segment is self, - lambda: f"{cut} `ac_line_segment` property references {cut.ac_line_segment}, expected {str(self)}.", - ) - return False + phases: AcLineSegmentPhaseList = AcLineSegmentPhaseList( + _phases, + "An AcLineSegmentPhase", + backfill=Backfill(AcLineSegmentPhase.ac_line_segment), + sort_by=lambda it: it.sequence_number or 0 + ) - def _validate_clamp(self, clamp: 'Clamp') -> bool: - """ - Validate a clamp against this `AcLineSegment`'s `Clamp`s. - - :param clamp: The `Clamp` to validate. - :return: True if `clamp` is already associated with this `AcLineSegment`, otherwise False. - :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment`, or if this `AcLineSegment` has a different `Clamp` with the same mRID. - """ - if self._validate_reference(clamp, self.get_clamp, "A Clamp"): - return True - - if not clamp.ac_line_segment: - clamp.ac_line_segment = self - - require( - clamp.ac_line_segment is self, - lambda: f"{clamp} `ac_line_segment` property references {clamp.ac_line_segment}, expected {str(self)}.", - ) - return False - - @property - def phases(self) -> Generator['AcLineSegmentPhase', None, None]: - """ - The individual phase models for this AcLineSegment. The returned collection is read only. - """ - return ngen(self._phases) def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': """ @@ -150,162 +119,90 @@ def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': else: return self.asset_info - def num_cuts(self): - """ - Get the number of `Cut`s for this `AcLineSegment`. - """ - return nlen(self._cuts) - - def get_cut(self, mrid: str) -> 'Cut': - """ - Get the `Cut` for this `AcLineSegment` identified by `mrid` + # region deprecated list boilerplate + # region cuts boilerplate - :param mrid: The mRID of the required `Cut` - :return: The `Cut` with the specified `mrid` if it exists - :raise KeyError: If the `mrid` wasn't present. - """ - return get_by_mrid(self._cuts, mrid) + @deprecated("Use len(obj.cuts) instead.") + def num_cuts(self): + return len(self.cuts) - def add_cut(self, cut: 'Cut') -> 'AcLineSegment': - """ - Associate a `Cut` with this `AcLineSegment`. + @deprecated("Use obj.cuts.get_by_mrid(mrid) instead.") + def get_cut(self, mrid: str) -> Cut: + return self.cuts.get_by_mrid(mrid) - :param cut: the `Cut` to associate with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - :raise ValueError: If another `Cut` with the same `mrid` already exists for this `AcLineSegment`. - """ - if self._validate_cut(cut): - return self - - self._cuts = list() if self._cuts is None else self._cuts - self._cuts.append(cut) + @deprecated("Use obj.cuts.append(cut) instead.") + def add_cut(self, cut: Cut) -> 'AcLineSegment': + self.cuts.append(cut) return self - def remove_cut(self, cut: 'Cut') -> 'AcLineSegment': - """ - :param cut: The `Cut` to disassociate from this `AcLineSegment`. - :raise ValueError: If `cut` was not associated with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._cuts = safe_remove(self._cuts, cut) + @deprecated("Use obj.cuts.remove(cut) instead.") + def remove_cut(self, cut: Cut) -> 'AcLineSegment': + self.cuts.remove(cut) return self + @deprecated("Use obj.cuts.clear() instead.") def clear_cuts(self) -> 'AcLineSegment': - """ - Clear all `Cut`s. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._cuts.clear() + self.cuts.clear() return self - def num_clamps(self): - """ - Get the number of `Clamp`s for this `AcLineSegment`. - """ - return nlen(self._clamps) - - def get_clamp(self, mrid: str) -> 'Clamp': - """ - Get the `Clamp` for this `AcLineSegment` identified by `mrid` + # endregion cuts boilerplate - :param mrid: The mRID of the required `Clamp` - :return: The `Clamp` with the specified `mrid` if it exists - :raise KeyError: If the `mrid` wasn't present. - """ - return get_by_mrid(self._clamps, mrid) + # region clamps boilerplate - def add_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': - """ - Associate a `Clamp` with this `AcLineSegment`. + @deprecated("Use len(obj.clamps) instead.") + def num_clamps(self): + return len(self.clamps) - :param clamp: the `Clamp` to associate with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - :raise ValueError: If another `Clamp` with the same `mrid` already exists for this `AcLineSegment`. - """ - if self._validate_clamp(clamp): - return self + @deprecated("Use obj.clamps.get_by_mrid(mrid) instead.") + def get_clamp(self, mrid: str) -> Clamp: + return self.clamps.get_by_mrid(mrid) - self._clamps = list() if self._clamps is None else self._clamps - self._clamps.append(clamp) + @deprecated("Use obj.clamps.append(clamp) instead.") + def add_clamp(self, clamp: Clamp) -> 'AcLineSegment': + self.clamps.append(clamp) return self - def remove_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': - """ - :param clamp: The `Clamp` to disassociate from this `AcLineSegment`. - :raise ValueError: If `clamp` was not associated with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._clamps = safe_remove(self._clamps, clamp) + @deprecated("Use obj.clamps.remove(clamp) instead.") + def remove_clamp(self, clamp: Clamp) -> 'AcLineSegment': + self.clamps.remove(clamp) return self + @deprecated("Use obj.clamps.clear() instead.") def clear_clamps(self) -> 'AcLineSegment': - """ - Clear all `Clamp`s. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._clamps.clear() + self.clamps.clear() return self + # endregion clamps boilerplate + + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self) -> int: - """ - Get the number of entries in the [AcLineSegmentPhase] collection. - """ - return nlen(self._phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(identifier) or obj.phases.get_by_phase(identifier) instead.") def get_phase(self, identifier: 'str | SinglePhaseKind') -> 'AcLineSegmentPhase | None': - """ - The individual phase models for this AcLineSegment. - - :param identifier: the mRID or ``SinglePhaseKind`` of the required [AcLineSegmentPhase] - :returns: The [AcLineSegmentPhase] with the specified [mRID] if it exists, otherwise null - """ if isinstance(identifier, str): - if self._phases is not None: - return get_by_mrid(self._phases, identifier) - + return self.phases.get_by_mrid(identifier) elif isinstance(identifier, SinglePhaseKind): - for it in self._phases: - if it == identifier: - return it - - raise KeyError(identifier) + return self.phases.get_by_phase(identifier) + raise KeyError(identifier) # Wrong error, but consistent with previous functionality - deprecated regardless. + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: AcLineSegmentPhase) -> 'AcLineSegment': - """ - Add an [AcLineSegmentPhase] to this [AcLineSegment]. - - :param phase: The [AcLineSegmentPhase] to add. - :returns: This [AcLineSegment] for fluent use. - """ - if self._validate_reference(phase, self.get_phase, "An AcLineSegmentPhase"): - return self - - if phase.ac_line_segment is None: - phase.ac_line_segment = self - - require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") - - if self._phases is None: - self._phases = list() - self._phases.append(phase) - self._phases.sort(key=lambda it: it.sequence_number or 0) + self.phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: AcLineSegmentPhase) -> 'AcLineSegment': - """ - Remove an [AcLineSegmentPhase] from this [AcLineSegment]. - - :param phase: The [AcLineSegmentPhase] to remove. - :returns: true if [phase] is removed from the collection. - """ - self._phases = safe_remove(self._phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> 'AcLineSegment': - """ - Clear all [AcLineSegmentPhase]'s from this [AcLineSegment]. - - :returns: This [AcLineSegment] for fluent use. - """ - self._phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 297c5a50d..e5b0caedd 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -7,10 +7,12 @@ __all__ = ['AcLineSegmentPhase'] +from dataclasses import field from typing import TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo @@ -35,14 +37,10 @@ class AcLineSegmentPhase(PowerSystemResource): phase: SinglePhaseKind = SinglePhaseKind.X sequence_number: int | None = None - _ac_line_segment: AcLineSegment | None = None - - def __init__(self, *args, ac_line_segment: AcLineSegment = None, **kwargs): - super(AcLineSegmentPhase, self).__init__(*args, **kwargs) - if ac_line_segment is not None: - self.ac_line_segment = ac_line_segment + _ac_line_segment: AcLineSegment | None = field(default=None) @property + @internal(_ac_line_segment) def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment @@ -54,5 +52,4 @@ def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: else: raise ValueError(f"ac_line_segment has already been set to {self._ac_line_segment}. Cannot set this field again") - asset_info: WireInfo | None = None diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index e7067475a..afd6f37ae 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -5,10 +5,12 @@ __all__ = ["Clamp"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -26,11 +28,13 @@ class Clamp(ConductingEquipment): length_from_terminal_1: Optional[float] = None """The length to the place where the clamp is located starting from side one of the line segment, i.e. the line segment terminal with sequence number equal to 1.""" - _ac_line_segment: Optional['AcLineSegment'] = None + _ac_line_segment: Optional['AcLineSegment'] = field(default=None) + """The line segment to which the clamp is connected.""" max_terminals = 1 @property + @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the clamp is connected.""" return self._ac_line_segment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 19d9f08bf..d588a0e13 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -5,10 +5,12 @@ __all__ = ["Cut"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -32,9 +34,10 @@ class Cut(Switch): length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" - _ac_line_segment: Optional['AcLineSegment'] = None + _ac_line_segment: Optional['AcLineSegment'] = field(default=None) @property + @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the cut is applied.""" return self._ac_line_segment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 32247cf9c..70711e37b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -7,22 +7,25 @@ __all__ = ["EnergyConsumer"] -from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing import Optional, List +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection +from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase @zb_dataclass class EnergyConsumer(EnergyConnection): """Generic user of energy - a point of consumption on the power system phases. May also represent a pro-sumer with negative p/q values. """ - _energy_consumer_phases: Optional[List[EnergyConsumerPhase]] = None + _energy_consumer_phases: Optional[List[EnergyConsumerPhase]] = field(default=None) """The individual phase models for this energy consumer.""" customer_count: Optional[int] = None @@ -49,68 +52,43 @@ class EnergyConsumer(EnergyConnection): q_fixed: Optional[float] = None """Power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - def __init__(self, *args, energy_consumer_phases: List[EnergyConsumerPhase] = None, **kwargs): + def __init__(self, *args, energy_consumer_phases=None, **kwargs): super(EnergyConsumer, self).__init__(*args, **kwargs) - if energy_consumer_phases: - for phase in energy_consumer_phases: - self.add_phase(phase) + self.phases.extend(energy_consumer_phases) - @property - def phases(self) -> Generator[EnergyConsumerPhase, None, None]: - """The individual phase models for this energy consumer.""" - return ngen(self._energy_consumer_phases) + phases: MridCollection[EnergyConsumerPhase] = LazyMridList( + _energy_consumer_phases, + "An EnergyConsumerPhase", + backfill=Backfill(EnergyConsumerPhase.energy_consumer) + ) + + # region deprecated list boilerplate + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Get the number of `EnergySourcePhase`s for this `EnergyConsumer`.""" - return nlen(self._energy_consumer_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> EnergyConsumerPhase: - """ - Get the `EnergyConsumerPhase` for this `EnergyConsumer` identified by `mrid` - - `mrid` The mRID of the required `EnergyConsumerPhase` - Returns The `EnergyConsumerPhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energy_consumer_phases, mrid) + return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: - """ - Associate an `EnergyConsumerPhase` with this `EnergyConsumer` - - `phase` the `EnergyConsumerPhase` to associate with this `EnergyConsumer`. - Returns A reference to this `EnergyConsumer` to allow fluent use. - Raises `ValueError` if another `EnergyConsumerPhase` with the same `mrid` already exists for this `EnergyConsumer`, or if `phase.energy_consumer` is not - this `EnergyConsumer`. - """ - if self._validate_reference(phase, self.get_phase, "An EnergyConsumerPhase"): - return self - - if phase.energy_consumer is None: - phase.energy_consumer = self - - require(phase.energy_consumer is self, lambda: f"{phase} `energy_consumer` property references {phase.energy_consumer}, expected {self}.") - - self._energy_consumer_phases = list() if self._energy_consumer_phases is None else self._energy_consumer_phases - self._energy_consumer_phases.append(phase) + self.phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: - """ - Disassociate `phase` from this `OperationalRestriction`. - - `phase` the `EnergyConsumerPhase` to disassociate with this `EnergyConsumer`. - Raises `KeyError` if `phase` was not associated with this `EnergyConsumer`. - Returns A reference to this `EnergyConsumer` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `EnergyConsumer`. - """ - self._energy_consumer_phases = safe_remove(self._energy_consumer_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> EnergyConsumer: - """ - Clear all phases. - Returns A reference to this `EnergyConsumer` to allow fluent use. - """ - self._energy_consumer_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index a46682b4c..96985a9d8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -5,10 +5,12 @@ __all__ = ["EnergyConsumerPhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -21,7 +23,7 @@ class EnergyConsumerPhase(PowerSystemResource): """A single phase of an energy consumer.""" - _energy_consumer: Optional['EnergyConsumer'] = None + _energy_consumer: Optional['EnergyConsumer'] = field(default=None) phase: SinglePhaseKind = SinglePhaseKind.X """Phase of this energy consumer component. If the energy consumer is wye connected, the connection is from the indicated phase to the central ground or @@ -42,12 +44,8 @@ class EnergyConsumerPhase(PowerSystemResource): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - def __init__(self, *args, energy_consumer: 'EnergyConsumer' = None, **kwargs): - super(EnergyConsumerPhase, self).__init__(*args, **kwargs) - if energy_consumer: - self.energy_consumer = energy_consumer - @property + @internal(_energy_consumer) def energy_consumer(self): """The `EnergyConsumer` that has this phase.""" return self._energy_consumer diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index a544a2230..08f11d428 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -7,14 +7,17 @@ __all__ = ["EnergySource"] -from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing import List, Optional -from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from typing_extensions import deprecated -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection +from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase @zb_dataclass @@ -23,7 +26,7 @@ class EnergySource(EnergyConnection): A generic equivalent for an energy supplier on a transmission or distribution voltage level. """ - _energy_source_phases: Optional[List[EnergySourcePhase]] = None + _energy_source_phases: Optional[List[EnergySourcePhase]] = field(default=None) active_power: Optional[float] = None """ @@ -113,69 +116,42 @@ class EnergySource(EnergyConnection): x0_max: Optional[float] = None """Maximum zero sequence Thevenin reactance.""" - def __init__(self, *args, energy_source_phases: List[EnergySourcePhase] = None, **kwargs): + def __init__(self, *args, energy_source_phases=None, **kwargs): super(EnergySource, self).__init__(*args, **kwargs) - if energy_source_phases: - for phase in energy_source_phases: - self.add_phase(phase) + self.phases.extend(energy_source_phases) + + phases: MridCollection[EnergySourcePhase] = LazyMridList( + _energy_source_phases, + "An EnergySourcePhase", + backfill=Backfill(EnergySourcePhase.energy_source) + ) - @property - def phases(self) -> Generator[EnergySourcePhase, None, None]: - """ - The `EnergySourcePhase`s for this `EnergySource`. - """ - return ngen(self._energy_source_phases) + # region deprecated list boilerplate + # region phases boilerplate + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Return the number of `EnergySourcePhase`s associated with this `EnergySource`""" - return nlen(self._energy_source_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> EnergySourcePhase: - """ - Get the `EnergySourcePhase` for this `EnergySource` identified by `mrid` - - `mrid` the mRID of the required `EnergySourcePhase` - Returns The `EnergySourcePhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energy_source_phases, mrid) + return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: EnergySourcePhase) -> EnergySource: - """ - Associate an `EnergySourcePhase` with this `EnergySource` - - `phase` the `EnergySourcePhase` to associate with this `EnergySource`. - Returns A reference to this `EnergySource` to allow fluent use. - Raises `ValueError` if another `EnergySourcePhase` with the same `mrid` already exists for this `EnergySource`, or if `phase.energy_source` is not - this `EnergySource`. - """ - if self._validate_reference(phase, self.get_phase, "An EnergySourcePhase"): - return self - - if phase.energy_source is None: - phase.energy_source = self - - require(phase.energy_source is self, lambda: f"{phase} `energy_source` property references {phase.energy_source}, expected {self}.") - - self._energy_source_phases = list() if self._energy_source_phases is None else self._energy_source_phases - self._energy_source_phases.append(phase) + self.phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: EnergySourcePhase) -> EnergySource: - """ - Disassociate an `phase` from this `EnergySource` - - `phase` the `EnergySourcePhase` to disassociate from this `EnergySource`. - Returns A reference to this `EnergySource` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `EnergySource`. - """ - self._energy_source_phases = safe_remove(self._energy_source_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> EnergySource: - """ - Clear all phases. - Returns A reference to this `EnergySource` to allow fluent use. - """ - self._energy_source_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index a818d0eab..5a0f096df 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -5,10 +5,12 @@ __all__ = ["EnergySourcePhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -23,26 +25,21 @@ class EnergySourcePhase(PowerSystemResource): A single phase of an energy source. """ - _energy_source: Optional['EnergySource'] = None - """The `zepben.ewb.model.cim.iec61970.wires.EnergySource` with this `EnergySourcePhase`""" + _energy_source: Optional['EnergySource'] = field(default=None) phase: SinglePhaseKind = SinglePhaseKind.NONE """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy source connected from the indicated phase to the next logical non-neutral phase.""" - def __init__(self, *args, energy_source: 'EnergySource' = None, **kwargs): - super(EnergySourcePhase, self).__init__(*args, **kwargs) - if energy_source: - self.energy_source = energy_source - @property + @internal(_energy_source) def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" return self._energy_source @energy_source.setter - @deprecated("energy_sounrce should never be set directly - it is automatically set when adding it to the `phases` list") + @deprecated("energy_source should never be set directly - it is automatically set when adding it to the `phases` list") def energy_source(self, es): if self._energy_source is None or self._energy_source is es: self._energy_source = es diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index edb0557cb..906b58e87 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -3,15 +3,21 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ["PerLengthPhaseImpedance"] -from typing import Optional, List, Generator +from dataclasses import field +from typing import List, Generator + +from typing_extensions import deprecated +from zepben.ewb import zb_dataclass from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.util import require, ngen, nlen, safe_remove, none -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.relations.phase_impedance_data_list import PhaseImpedanceDataList +from zepben.ewb.util import require, none @zb_dataclass @@ -20,82 +26,49 @@ class PerLengthPhaseImpedance(PerLengthImpedance): Impedance and admittance parameters per unit length for n-wire unbalanced lines, in matrix form. """ - _data: Optional[List[PhaseImpedanceData]] = None + _data: List[PhaseImpedanceData] | None = field(default=None) - def __init__(self, *args, data: List[PhaseImpedanceData] = None, **kwargs): - """ - `data` A list of `PhaseImpedanceData`s to associate with this `PerLengthPhaseImpedance`. - """ - super(PerLengthPhaseImpedance, self).__init__(*args, **kwargs) - if data: - for phase_data in data: - self.add_data(phase_data) + data: PhaseImpedanceDataList[PhaseImpedanceData] = PhaseImpedanceDataList( + _data, + validate=lambda self, it: self._validate_data(it) + ) - @property - def data(self) -> Generator[PhaseImpedanceData, None, None]: - """ - The point data values that define this phase_impedance, sorted by `x_value` in ascending order. - """ - return ngen(self._data) + def _validate_data(self, phase_impedance_data: PhaseImpedanceData): + require(none([it.from_phase == phase_impedance_data.from_phase and it.to_phase == phase_impedance_data.to_phase for it in self.data]), + lambda: f"""Unable to add PhaseImpedanceData to {self}. A PhaseImpedanceData with from_phase {phase_impedance_data.from_phase} and to_phase {phase_impedance_data.to_phase} already exists in this PerLengthPhaseImpedance.""") + + + # region deprecated list boilerplate + # region data boilerplate @property + @deprecated("Use data.diagonal instead") def diagonal(self) -> Generator[PhaseImpedanceData, None, None]: - """ - Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. - """ - return ngen(pid for pid in self._data if pid.from_phase == pid.to_phase) + return self.data.diagonal + @deprecated("Use len(data) instead.") def num_data(self): - """Return the number of :class:`PhaseImpedanceData` associated with this :class:`PerLengthPhaseImpedance`.""" - return nlen(self._data) + return len(self.data) + @deprecated("Use data.get(from_phase, to_phase) instead.") def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: - """ - Get the matrix entry for the corresponding to and from phases. - - :param from_phase: The from_phase to lookup. - :param to_phase: The to_phase to lookup. - :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. - :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. - """ - if self._data: - phase_impedance_data = next((it for it in self._data if it.from_phase == from_phase and it.to_phase == to_phase), None) - if phase_impedance_data: - return phase_impedance_data - raise KeyError((from_phase, to_phase)) - - def add_data(self, phase_impedance_data: PhaseImpedanceData) -> 'PerLengthPhaseImpedance': - """ - Add a :class:`PhaseImpedanceData` to this :class:`PerLengthPhaseImpedance`. - - :param phase_impedance_data: The :class:`PhaseImpedanceData` to add. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - :raises ValueError: If another :class:`PhaseImpedanceData` with the same `from_phase` and `to_phase` already exists for this :class:`PerLengthPhaseImpedance`. - """ - - require(none([it.from_phase == phase_impedance_data.from_phase and it.to_phase == phase_impedance_data.to_phase for it in self.data]), - lambda: f"""Unable to add PhaseImpedanceData to {self}. A PhaseImpedanceData with from_phase {phase_impedance_data.from_phase} and to_phase {phase_impedance_data.to_phase} already exists in this PerLengthPhaseImpedance.""") - - self._data = self._data or [] - self._data.append(phase_impedance_data) + return self.data.get(from_phase, to_phase) + @deprecated("Use data.append(phase_impedance_data) instead.") + def add_data(self, phase_impedance_data: 'PhaseImpedanceData') -> PerLengthPhaseImpedance: + self.data.append(phase_impedance_data) return self - def remove_data(self, phase_impedance_data: PhaseImpedanceData) -> 'PerLengthPhaseImpedance': - """ - Remove a :class:`PhaseImpedanceData` from this :class:`PerLengthPhaseImpedance`. - - :param phase_impedance_data: The :class:`PhaseImpedanceData` to remove from this :class:`PerLengthPhaseImpedance`. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - :raises ValueError: If `phase_impedance_data` was not associated with this :class:`PerLengthPhaseImpedance`. - """ - self._data = safe_remove(self._data, phase_impedance_data) + @deprecated("Use data.remove(phase_impedance_data) instead.") + def remove_data(self, phase_impedance_data: 'PhaseImpedanceData') -> PerLengthPhaseImpedance: + self.data.remove(phase_impedance_data) return self - def clear_data(self) -> 'PerLengthPhaseImpedance': - """ - Clear all :class:`PhaseImpedanceData` associated with this :class:`PerLengthPhaseImpedance`. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - """ - self._data = None + @deprecated("Use data.clear() instead.") + def clear_data(self) -> PerLengthPhaseImpedance: + self.data.clear() return self + + # endregion data boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 7d680df12..caf4b3ebb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -7,15 +7,21 @@ __all__ = ["PowerElectronicsConnection"] -from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, List, TYPE_CHECKING + +from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove, require +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit - from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase @zb_dataclass @@ -136,91 +142,22 @@ class PowerElectronicsConnection(RegulatingCondEq): Permitted range is between -1.0 and 1.0 (inclusive), with a negative sign referring to “sink”. """ - _power_electronics_units: Optional[List[PowerElectronicsUnit]] = None + _power_electronics_units: Optional[List[PowerElectronicsUnit]] = field(default=None) """An AC network connection may have several power electronics units connecting through it.""" - _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = None + _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" def __init__( self, *args, - power_electronics_units: List[PowerElectronicsUnit] = None, - power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, - inv_watt_resp_v1=None, - inv_watt_resp_v2=None, - inv_watt_resp_v3=None, - inv_watt_resp_v4=None, - inv_watt_resp_p_at_v1=None, - inv_watt_resp_p_at_v2=None, - inv_watt_resp_p_at_v3=None, - inv_watt_resp_p_at_v4=None, - inv_var_resp_v1=None, - inv_var_resp_v2=None, - inv_var_resp_v3=None, - inv_var_resp_v4=None, - inv_var_resp_q_at_v1=None, - inv_var_resp_q_at_v2=None, - inv_var_resp_q_at_v3=None, - inv_var_resp_q_at_v4=None, - **kwargs, + power_electronics_units=None, + power_electronics_connection_phases=None, + **kwargs ): super(PowerElectronicsConnection, self).__init__(*args, **kwargs) - if power_electronics_units: - for unit in power_electronics_units: - self.add_unit(unit) - - if power_electronics_connection_phases: - for phase in power_electronics_connection_phases: - self.add_phase(phase) - - if inv_watt_resp_v1 is not None: - self.inv_watt_resp_v1 = inv_watt_resp_v1 - - if inv_watt_resp_v2 is not None: - self.inv_watt_resp_v2 = inv_watt_resp_v2 - - if inv_watt_resp_v3 is not None: - self.inv_watt_resp_v3 = inv_watt_resp_v3 - - if inv_watt_resp_v4 is not None: - self.inv_watt_resp_v4 = inv_watt_resp_v4 - - if inv_watt_resp_p_at_v1 is not None: - self.inv_watt_resp_p_at_v1 = inv_watt_resp_p_at_v1 - - if inv_watt_resp_p_at_v2 is not None: - self.inv_watt_resp_p_at_v2 = inv_watt_resp_p_at_v2 - - if inv_watt_resp_p_at_v3 is not None: - self.inv_watt_resp_p_at_v3 = inv_watt_resp_p_at_v3 - - if inv_watt_resp_p_at_v4 is not None: - self.inv_watt_resp_p_at_v4 = inv_watt_resp_p_at_v4 - - if inv_var_resp_v1 is not None: - self.inv_var_resp_v1 = inv_var_resp_v1 - - if inv_var_resp_v2 is not None: - self.inv_var_resp_v2 = inv_var_resp_v2 - - if inv_var_resp_v3 is not None: - self.inv_var_resp_v3 = inv_var_resp_v3 - - if inv_var_resp_v4 is not None: - self.inv_var_resp_v4 = inv_var_resp_v4 - - if inv_var_resp_q_at_v1 is not None: - self.inv_var_resp_q_at_v1 = inv_var_resp_q_at_v1 - - if inv_var_resp_q_at_v2 is not None: - self.inv_var_resp_q_at_v2 = inv_var_resp_q_at_v2 - - if inv_var_resp_q_at_v3 is not None: - self.inv_var_resp_q_at_v3 = inv_var_resp_q_at_v3 - - if inv_var_resp_q_at_v4 is not None: - self.inv_var_resp_q_at_v4 = inv_var_resp_q_at_v4 + self.units.extend(power_electronics_units) + self.phases.extend(power_electronics_connection_phases) @property def inv_watt_resp_v1(self): @@ -417,121 +354,70 @@ def inv_var_resp_q_at_v4(self, value): require(value is None or -0.6 <= value <= 0.0, lambda: f"inv_var_resp_q_at_v4 [{value}] must be between -0.6 and 0.0.") self._inv_var_resp_q_at_v4 = value - @property - def units(self) -> Generator[PowerElectronicsUnit, None, None]: - """ - The `PowerElectronicsUnit`s for this `PowerElectronicsConnection`. - """ - return ngen(self._power_electronics_units) + units: MridCollection[PowerElectronicsUnit] = LazyMridList( + _power_electronics_units, + "A PowerElectronicsUnit", + ) - @property - def phases(self) -> Generator[PowerElectronicsConnectionPhase, None, None]: - """ - The `PowerElectronicsConnectionPhase`s for this `PowerElectronicsConnection`. - """ - return ngen(self._power_electronics_connection_phases) + phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( + _power_electronics_connection_phases, + "A PowerElectronicsConnectionPhase", + backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) + ) + # region deprecated list boilerplate + # region units boilerplate + + @deprecated("Use len(obj.units) instead.") def num_units(self): - """Return the number of `PowerElectronicsUnit`s associated with this `PowerElectronicsConnection`""" - return nlen(self._power_electronics_units) + return len(self.units) + @deprecated("Use obj.units.get_by_mrid(mrid) instead.") def get_unit(self, mrid: str) -> PowerElectronicsUnit: - """ - Get the `PowerElectronicsUnit` for this - `PowerElectronicsConnection` identified by `mrid` - - `mrid` the mRID of the required `PowerElectronicsUnit` - Returns The `PowerElectronicsUnit` with the specified `mrid` - if it exists - - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_electronics_units, mrid) + return self.units.get_by_mrid(mrid) + @deprecated("Use obj.units.append(unit) instead.") def add_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: - """ - Associate an `PowerElectronicsUnit` with this - `PowerElectronicsConnection` - - `unit` the `PowerElectronicsUnit` to associate with this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if another `PowerElectronicsUnit` with the same `mrid` already exists for this `PowerElectronicsConnection`. - """ - if self._validate_reference(unit, self.get_unit, "A PowerElectronicsUnit"): - return self - self._power_electronics_units = list() if self._power_electronics_units is None else self._power_electronics_units - self._power_electronics_units.append(unit) + self.units.append(unit) return self + @deprecated("Use obj.units.remove(unit) instead.") def remove_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: - """ - Disassociate `unit` from this `PowerElectronicsConnection` - - `unit` the `PowerElectronicsUnit` to disassociate from this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if `unit` was not associated with this `PowerElectronicsConnection`. - """ - self._power_electronics_units = safe_remove(self._power_electronics_units, unit) + self.units.remove(unit) return self + @deprecated("Use obj.units.clear() instead.") def clear_units(self) -> PowerElectronicsConnection: - """ - Clear all units. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - """ - self._power_electronics_units = None + self.units.clear() return self + # endregion units boilerplate + + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Return the number of `PowerElectronicsConnectionPhase`s associated with this `PowerElectronicsConnection`""" - return nlen(self._power_electronics_connection_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: - """ - Get the `PowerElectronicsConnectionPhase` for this `PowerElectronicsConnection` identified by `mrid` - - `mrid` the mRID of the required `PowerElectronicsConnectionPhase` - Returns The `PowerElectronicsConnectionPhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_electronics_connection_phases, mrid) + return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: - """ - Associate a `PowerElectronicsConnectionPhase` with this `PowerElectronicsConnection` - - `phase` the `PowerElectronicsConnectionPhase` to associate with this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if another `PowerElectronicsConnectionPhase` with the same `mrid` already exists for this `PowerElectronicsConnection`, or if - `phase.power_electronics_connection` is not this `PowerElectronicsConnection`. - """ - if self._validate_reference(phase, self.get_phase, "A PowerElectronicsConnectionPhase"): - return self - - if phase.power_electronics_connection is None: - phase.power_electronics_connection = self - - require(phase.power_electronics_connection is self, lambda: f"{phase} `power_electronics_connection` property references {phase.power_electronics_connection}, expected {self}.") - - self._power_electronics_connection_phases = list() if self._power_electronics_connection_phases is None else self._power_electronics_connection_phases - self._power_electronics_connection_phases.append(phase) + self.phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: - """ - Disassociate `phase` from this `PowerElectronicsConnection` - - `phase` the `PowerElectronicsConnectionPhase` to disassociate from this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `PowerElectronicsConnection`. - """ - self._power_electronics_connection_phases = safe_remove(self._power_electronics_connection_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> PowerElectronicsConnection: - """ - Clear all phases. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - """ - self._power_electronics_connection_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 3345c6914..95a27a895 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -5,10 +5,12 @@ __all__ = ["PowerElectronicsConnectionPhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -21,7 +23,8 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): """A single phase of a power electronics connection.""" - _power_electronics_connection: Optional['PowerElectronicsConnection'] = None + _power_electronics_connection: Optional['PowerElectronicsConnection'] = field(default=None) + """The power electronics connection to which the phase belongs.""" p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" @@ -37,6 +40,7 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): """Reactive power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" @property + @internal(_power_electronics_connection) def power_electronics_connection(self): """The power electronics connection to which the phase belongs.""" return self._power_electronics_connection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index fc67591d6..dc59d9edf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -8,7 +8,12 @@ __all__ = ["PowerTransformer"] import sys -from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing import List, Optional, TYPE_CHECKING + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.relations.power_transformer_end_list import PowerTransformerEndList + if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -19,12 +24,11 @@ from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_construction_kind import TransformerConstructionKind from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_function_kind import TransformerFunctionKind from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.util import require, nlen, get_by_mrid, ngen, safe_remove +from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal - from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd @zb_dataclass @@ -68,7 +72,7 @@ class PowerTransformer(ConductingEquipment): numerical sequence if they are numbered: the phasors are assumed to rotate in a counter-clockwise sense. """ - _power_transformer_ends: Optional[List[PowerTransformerEnd]] = None + _power_transformer_ends: List[PowerTransformerEnd] | None = field(default=None) transformer_utilisation: Optional[float] = None """ @@ -86,18 +90,17 @@ class PowerTransformer(ConductingEquipment): The function of this transformer. """ - def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = None, **kwargs): + def __init__(self, *args, power_transformer_ends=None, **kwargs): super(PowerTransformer, self).__init__(*args, **kwargs) - if power_transformer_ends: - for end in power_transformer_ends: - if end.power_transformer is None: - end.power_transformer = self - self.add_end(end) + self.ends.extend(power_transformer_ends) - @property - def ends(self) -> Generator[PowerTransformerEnd, None, None]: - """The `PowerTransformerEnd`s for this `PowerTransformer`.""" - return ngen(self._power_transformer_ends) + ends: PowerTransformerEndList = PowerTransformerEndList( + _power_transformer_ends, + "A PowerTransformerEnd", + backfill=Backfill(PowerTransformerEnd.power_transformer), + validate=lambda self, it: self._validate_end(it), + sort_by=lambda it: it.end_number + ) @property @deprecated("use asset_info instead.") @@ -123,107 +126,53 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None - def _validate_end(self, end: PowerTransformerEnd) -> bool: - """ - Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. + def _validate_end(self, end: PowerTransformerEnd): + self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") - `end` The `PowerTransformerEnd` to validate. - Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. - Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` - with the same mRID. - """ - if self._validate_reference(end, self.get_end_by_mrid, "A PowerTransformerEnd"): - return True + if end.end_number == 0: + end.end_number = self.num_ends() + 1 - if self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number"): - return True - if not end.power_transformer: - end.power_transformer = self + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - require( - end.power_transformer is self, - lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", - ) - return False + # ends boilerplate + @deprecated("Use `len(power_transformer.ends)` instead.") def num_ends(self): - """ - Get the number of `PowerTransformerEnd`s for this `PowerTransformer`. - """ - return nlen(self._power_transformer_ends) + return len(self.ends) + @deprecated("Use `power_transformer.ends.get_by_mrid(mrid)` instead.") def get_end_by_mrid(self, mrid: str) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` for this `PowerTransformer` identified by `mrid` - - `mrid` the mRID of the required `PowerTransformerEnd` - Returns The `PowerTransformerEnd` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_transformer_ends, mrid) + return self.ends.get_by_mrid(mrid) + @deprecated("Use `power_transformer.ends.get_by_num(end_number)` instead.") def get_end_by_num(self, end_number: int) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. - - `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. - Returns The `PowerTransformerEnd` referred to by `end_number` - Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. - """ - if self._power_transformer_ends: - for end in self._power_transformer_ends: - if end.end_number == end_number: - return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self)}") + return self.ends.get_by_num(end_number) + @deprecated("Use `power_transformer.ends.get_by_terminal(terminal)` instead.") def get_end_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. - - `terminal` The `terminal` to find a `PowerTransformerEnd` for. - Returns The `PowerTransformerEnd` connected to the specified `terminal` - Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. - """ - if self._power_transformer_ends: - for end in self._power_transformer_ends: - if end.terminal is terminal: - return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self)}") + return self.ends.get_by_terminal(terminal) + @deprecated("Use `power_transformer.ends.append(end)` instead.") def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: - """ - Associate a `PowerTransformerEnd` with this `PowerTransformer`. If `end.end_number` == 0, the end will be assigned an end_number of - `self.num_ends() + 1`. - - `end` the `PowerTransformerEnd` to associate with this `PowerTransformer`. - Returns A reference to this `PowerTransformer` to allow fluent use. - Raises `ValueError` if another `PowerTransformerEnd` with the same `mrid` already exists for this `PowerTransformer`. - """ - if self._validate_end(end): - return self - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 - - self._power_transformer_ends = list() if self._power_transformer_ends is None else self._power_transformer_ends - self._power_transformer_ends.append(end) - self._power_transformer_ends.sort(key=lambda t: t.end_number) + self.ends.append(end) return self + @deprecated("Use `power_transformer.ends.remove(end)` instead.") def remove_end(self, end: PowerTransformerEnd) -> PowerTransformer: - """ - `end` the `PowerTransformerEnd` to disassociate from this `PowerTransformer`. - Raises `ValueError` if `end` was not associated with this `PowerTransformer`. - Returns A reference to this `PowerTransformer` to allow fluent use. - """ - self._power_transformer_ends = safe_remove(self._power_transformer_ends, end) + self.ends.remove(end) return self + @deprecated("Use `power_transformer.ends.clear()` instead.") def clear_ends(self) -> PowerTransformer: - """ - Clear all `PowerTransformerEnd`s. - Returns A reference to this `PowerTransformer` to allow fluent use. - """ - self._power_transformer_ends.clear() + self.ends.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index aa503e247..792790068 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -8,14 +8,18 @@ __all__ = ["PowerTransformerEnd"] import warnings -from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, List, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd from zepben.ewb.model.cim.iec61970.base.wires.winding_connection import WindingConnection +from zepben.ewb.boilerplate.relations.transformer_end_rated_s_list import TransformerEndRatedSList from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import ngen, nlen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: @@ -40,7 +44,7 @@ class PowerTransformerEnd(TransformerEnd): Instead use the TransformerMeshImpedance or split the transformer into multiple PowerTransformers. """ - _power_transformer: Optional[PowerTransformer] = None + _power_transformer: Optional[PowerTransformer] = field(default=None) """The power transformer of this power transformer end.""" _rated_s: Optional[int] = None @@ -80,17 +84,16 @@ class PowerTransformerEnd(TransformerEnd): secondary side end of a transformer with vector group code of 'Dyn11', specify the connection kind as wye with neutral and specify the phase angle of the clock as 11. The clock value of the transformer end number specified as 1, is assumed to be zero.""" - _s_ratings: Optional[List[TransformerEndRatedS]] = None + _s_ratings: Optional[List[TransformerEndRatedS]] = field(default=None) """ Backing list for storing transformer ratings. Placed here to not mess with __init__ param order. Must always be placed at the end. Should not be used directly, instead use add_rating and get_rating functions. """ - def __init__(self, *args, power_transformer: PowerTransformer = None, rated_s: int = None, ratings: list[TransformerEndRatedS] = None, **kwargs): + def __init__(self, *args, rated_s: int = None, ratings=None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) - if power_transformer: - self.power_transformer = power_transformer - if self._s_ratings: + self.s_ratings.extend(ratings) + if "_s_ratings" in kwargs: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: raise ValueError(f"Cannot specify both rated_s and _rated_s properties when constructing {self}. Check your constructor parameters.") @@ -104,11 +107,9 @@ def __init__(self, *args, power_transformer: PowerTransformer = None, rated_s: i if self._rated_s is not None: self.rated_s = self._rated_s self._rated_s = None - if ratings: - for rating in ratings: - self.add_rating(rating.rated_s, rating.cooling_type) @property + @internal(_power_transformer) def power_transformer(self): """The power transformer of this power transformer end.""" return self._power_transformer @@ -145,9 +146,15 @@ def rated_s(self, rated_s: Optional[int]): if rated_s is not None: self.add_transformer_end_rated_s(TransformerEndRatedS(TransformerCoolingType.UNKNOWN, rated_s)) - @property - def s_ratings(self) -> Generator[TransformerEndRatedS, None, None]: - return ngen(self._s_ratings) + s_ratings: TransformerEndRatedSList = TransformerEndRatedSList( + _s_ratings, + validate=lambda self, it: self._validate_rating(it), + sort_by=lambda it: -it.rated_s + ) + + def _validate_rating(self, rating: TransformerEndRatedS): + if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): + raise ValueError(f"A rating for coolingType {rating.cooling_type.name} already exists, please remove it first.") def resistance_reactance(self): """ @@ -166,48 +173,68 @@ def resistance_reactance(self): else None ) - def num_ratings(self) -> int: - return nlen(self._s_ratings) - - def get_rating(self, cooling_type: TransformerCoolingType) -> TransformerEndRatedS: - if self._s_ratings: - for s_rating in self._s_ratings: - if s_rating.cooling_type == cooling_type: - return s_rating - raise KeyError(cooling_type) - - def add_rating(self, rated_s: int, cooling_type: TransformerCoolingType = TransformerCoolingType.UNKNOWN) -> PowerTransformerEnd: - self._s_ratings = self._s_ratings if self._s_ratings else list() - - for s_rating in self._s_ratings: - if s_rating.cooling_type == cooling_type: - raise ValueError(f"A rating for coolingType {cooling_type.name} already exists, please remove it first.") + # region deprecated list methods - self._s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) + # region s_ratings boilerplate - def sort_by_rated_s(t: TransformerEndRatedS) -> int: - return t.rated_s + @deprecated("Use len(s_ratings) instead.") + def num_ratings(self) -> int: + return len(self.s_ratings) + + @deprecated("Use s_ratings.get_by_cooling_type(cooling_type) instead.") + def get_rating( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS: + rating = self.s_ratings.get_by_cooling_type(cooling_type) + + if rating is None: + raise KeyError(cooling_type) + + return rating + + @deprecated("Use s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) instead.") + def add_rating( + self, + rated_s: int, + cooling_type: TransformerCoolingType = TransformerCoolingType.UNKNOWN, + ) -> PowerTransformerEnd: + self.s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) + return self - self._s_ratings.sort(key=sort_by_rated_s, reverse=True) + @deprecated("Use s_ratings.append(transformer_end_rated_s) instead.") + def add_transformer_end_rated_s( + self, + transformer_end_rated_s: TransformerEndRatedS, + ) -> PowerTransformerEnd: + self.s_ratings.append(transformer_end_rated_s) + return self + @deprecated("Use s_ratings.remove(transformer_end_rated_s) instead.") + def remove_rating( + self, + transformer_end_rated_s: TransformerEndRatedS, + ) -> PowerTransformerEnd: + self.s_ratings.remove(transformer_end_rated_s) return self - def add_transformer_end_rated_s(self, transformer_end_rated_s: TransformerEndRatedS) -> PowerTransformerEnd: - return self.add_rating(transformer_end_rated_s.rated_s, transformer_end_rated_s.cooling_type) + @deprecated("Use s_ratings.remove_by_cooling_type(cooling_type) instead.") + def remove_rating_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS: + rating = self.s_ratings.remove_by_cooling_type(cooling_type) - def remove_rating(self, transformer_end_rated_s: TransformerEndRatedS) -> PowerTransformerEnd: - self._s_ratings = safe_remove(self._s_ratings, transformer_end_rated_s) - return self + if rating is None: + raise IndexError(cooling_type) - def remove_rating_by_cooling_type(self, cooling_type: TransformerCoolingType) -> TransformerEndRatedS: - if self._s_ratings: - for transformer_end_rated_s in self._s_ratings: - if transformer_end_rated_s.cooling_type == cooling_type: - self._s_ratings.remove(transformer_end_rated_s) - self._s_ratings = self._s_ratings if self._s_ratings else None - return transformer_end_rated_s - raise IndexError(cooling_type) + return rating + @deprecated("Use s_ratings.clear() instead.") def clear_ratings(self) -> PowerTransformerEnd: - self._s_ratings = None + self.s_ratings.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index 9f3eee533..03f914bac 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -7,12 +7,15 @@ __all__ = ["ProtectedSwitch"] -from typing import Optional, List, Generator, TYPE_CHECKING, Iterable +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction @@ -27,74 +30,40 @@ class ProtectedSwitch(Switch, metaclass=ABCMeta): breaking_capacity: Optional[int] = None """The maximum fault current in amps a breaking device can break safely under prescribed conditions of use.""" - _relay_functions: Optional[List[ProtectionRelayFunction]] = None + _relay_functions: Optional[List[ProtectionRelayFunction]] = field(default=None) - def __init__( - self, - *args, - relay_functions: Iterable[ProtectionRelayFunction] = None, - **kwargs - ): - super(ProtectedSwitch, self).__init__(*args, **kwargs) + relay_functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _relay_functions, + "A ProtectionRelayFunction", + ) - # breaking_capacity is handled via dataclassy. - if relay_functions is not None: - for relay_function in relay_functions: - self.add_relay_function(relay_function) - @property - def relay_functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - Yields all :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - - :return: A generator that iterates over all :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - """ - return ngen(self._relay_functions) + # region deprecated list boilerplate + # region relay_functions boilerplate + @deprecated("Use len(obj.relay_functions) instead.") def num_relay_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - - :return: The number of :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - """ - return nlen(self._relay_functions) + return len(self.relay_functions) + @deprecated("Use obj.relay_functions.get_by_mrid(mrid) instead.") def get_relay_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` operating this :class:`ProtectedSwitch` with the specified `mrid`. - - :param mrid: The mRID of the desired :class:`ProtectionRelayFunction` - :return: The :class:`ProtectionRelayFunction` with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._relay_functions, mrid) + return self.relay_functions.get_by_mrid(mrid) + @deprecated("Use obj.relay_functions.append(relay_function) instead.") def add_relay_function(self, relay_function: ProtectionRelayFunction) -> ProtectedSwitch: - """ - Associate this :class:`ProtectedSwitch` with a :class:`ProtectionRelayFunction` operating it. - :param relay_function: The :class:`ProtectionRelayFunction` to associate with this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - if self._validate_reference(relay_function, self.get_relay_function, "A ProtectionRelayFunction"): - return self - - self._relay_functions = list() if self._relay_functions is None else self._relay_functions - self._relay_functions.append(relay_function) + self.relay_functions.append(relay_function) return self + @deprecated("Use obj.relay_functions.remove(relay_function) instead.") def remove_relay_function(self, relay_function: Optional[ProtectionRelayFunction]) -> ProtectedSwitch: - """ - Disassociate this :class:`ProtectedSwitch` from a :class:`ProtectionRelayFunction`. - :param relay_function: The :class:`ProtectionRelayFunction` to disassociate from this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - self._relay_functions = safe_remove(self._relay_functions, relay_function) + self.relay_functions.remove(relay_function) return self + @deprecated("Use obj.relay_functions.clear() instead.") def clear_relay_functions(self) -> ProtectedSwitch: - """ - Disassociate all :class:`ProtectionRelayFunction` from this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - self._relay_functions = None + self.relay_functions.clear() return self + + # endregion relay_functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py index 21095033d..8554de6a6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py @@ -29,10 +29,6 @@ class RegulatingCondEq(EnergyConnection, metaclass=ABCMeta): _regulating_control: Optional[RegulatingControl] = None - def __init__(self, *args, regulating_control: Optional[RegulatingControl] = None, **kwargs): - super(RegulatingCondEq, self).__init__(*args, **kwargs) - if regulating_control: - self.regulating_control = regulating_control @property def regulating_control(self): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index ef87daaff..bc6eac26e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -7,14 +7,17 @@ __all__ = ["RegulatingControl"] -from typing import Optional, List, Generator, Iterable, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind -from zepben.ewb.util import nlen, get_by_mrid, safe_remove, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal @@ -107,70 +110,41 @@ class RegulatingControl(PowerSystemResource, metaclass=ABCMeta): regulators, shunt compensators, or battery units. """ - _regulating_cond_eq: Optional[List[RegulatingCondEq]] = None + _regulating_cond_eq: Optional[List[RegulatingCondEq]] = field(default=None) """The [RegulatingCondEq] that are controlled by this regulating control scheme.""" - def __init__(self, *args, regulating_conducting_equipment: Optional[Iterable[RegulatingCondEq]] = None, **kwargs): - super(RegulatingControl, self).__init__(*args, **kwargs) - if regulating_conducting_equipment is not None: - for eq in regulating_conducting_equipment: - self.add_regulating_cond_eq(eq) + regulating_conducting_equipment: MridCollection[RegulatingCondEq] = LazyMridList( + _regulating_cond_eq, + "A RegulatingCondEq", + ) - @property - def regulating_conducting_equipment(self) -> Generator[RegulatingCondEq, None, None]: - """ - Yields all the :class:`RegulatingCondEq` that are controlled by this :class:`RegulatingControl`. - :return: A generator that iterates over all RegulatingCondEq controlled by this RegulatingControl. - """ - return ngen(self._regulating_cond_eq) + # region deprecated list boilerplate + # region regulating_conducting_equipment boilerplate + @deprecated("Use len(obj.regulating_conducting_equipment) instead.") def num_regulating_cond_eq(self) -> int: - """ - Get the number of :class:`RegulatingCondEq` that are controlled by this :class:`RegulatingControl`. - - :return: The number of RegulatingCondEq that are controlled by this RegulatingControl. - """ - return nlen(self._regulating_cond_eq) + return len(self.regulating_conducting_equipment) + @deprecated("Use obj.regulating_conducting_equipment.get_by_mrid(mrid) instead.") def get_regulating_cond_eq(self, mrid: str) -> RegulatingCondEq: - """ - Get a :class:`RegulatingCondEq` controlled by this :class:`RegulatingControl`. - - :param mrid: The mRID of the desired RegulatingCondEq - :return: The RegulatingCondEq with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._regulating_cond_eq, mrid) + return self.regulating_conducting_equipment.get_by_mrid(mrid) + @deprecated("Use obj.regulating_conducting_equipment.append(regulating_cond_eq) instead.") def add_regulating_cond_eq(self, regulating_cond_eq: RegulatingCondEq) -> RegulatingControl: - """ - Associate this :class:`RegulatingControl` with a :class:`RegulatingCondEq` it is controlling. - - :param regulating_cond_eq: The RegulatingCondEq to associate with this RegulatingControl. - :return: A reference to this RegulatingControl for fluent use. - """ - if self._validate_reference(regulating_cond_eq, self.get_regulating_cond_eq, "A RegulatingCondEq"): - return self - - self._regulating_cond_eq = list() if self._regulating_cond_eq is None else self._regulating_cond_eq - self._regulating_cond_eq.append(regulating_cond_eq) + self.regulating_conducting_equipment.append(regulating_cond_eq) return self + @deprecated("Use obj.regulating_conducting_equipment.remove(regulating_cond_eq) instead.") def remove_regulating_cond_eq(self, regulating_cond_eq: Optional[RegulatingCondEq]) -> RegulatingControl: - """ - Disassociate this :class:`RegulatingControl` from a :class:`RegulatingCondEq`. - - :param regulating_cond_eq: The RegulatingCondEq to disassociate from this RegulatingControl. - :return: A reference to this RegulatingControl for fluent use. - """ - self._regulating_cond_eq = safe_remove(self._regulating_cond_eq, regulating_cond_eq) + self.regulating_conducting_equipment.remove(regulating_cond_eq) return self + @deprecated("Use obj.regulating_conducting_equipment.clear() instead.") def clear_regulating_cond_eq(self) -> RegulatingControl: - """ - Disassociate all :class:`RegulatingCondEq` from this :class:`RegulatingControl`. - :return: A reference to this RegulatingControl for fluent use. - """ - self._regulating_cond_eq = None + self.regulating_conducting_equipment.clear() return self + + # endregion regulating_conducting_equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py index 504ead31f..12e93c6c8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py @@ -57,11 +57,6 @@ class ShuntCompensator(RegulatingCondEq, metaclass=ABCMeta): _grounding_terminal: 'Terminal | None' = None sections: Optional[float] = None - def __init__(self, *args, grounding_terminal = None, **kwargs): - super(ShuntCompensator, self).__init__(*args, **kwargs) - if grounding_terminal is not None: - self.grounding_terminal = grounding_terminal - @property @deprecated("use asset_info instead.") def shunt_compensator_info(self) -> Optional['ShuntCompensatorInfo']: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index 0082bce55..5465514e1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -5,12 +5,15 @@ __all__ = ["SynchronousMachine"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve @@ -23,7 +26,7 @@ class SynchronousMachine(RotatingMachine): synchronous condenser or pump. """ - _reactive_capability_curves: Optional[List['ReactiveCapabilityCurve']] = None + _reactive_capability_curves: Optional[List['ReactiveCapabilityCurve']] = field(default=None) base_q: Optional[float] = None """Default base reactive power value in VAr. This value represents the initial reactive power that can be used by any application function.""" @@ -106,65 +109,42 @@ class SynchronousMachine(RotatingMachine): operating_mode: SynchronousMachineKind = SynchronousMachineKind.UNKNOWN """Current mode of operation.""" - def __init__(self, *args, curves: List['ReactiveCapabilityCurve'] = None, **kwargs): - """ - `reactive_capability_curves` A list of `ReactiveCapabilityCurve`s to associate with this `SynchronousMachine`. - """ - super(SynchronousMachine, self).__init__(*args, **kwargs) - if curves: - for rcc in curves: - self.add_curve(rcc) - - @property - def curves(self) -> Generator['ReactiveCapabilityCurve', None, None]: - """ - The available reactive capability curves for this synchronous machine. The first shall be the default for this :class:`SynchronousMachine`. - """ - return ngen(self._reactive_capability_curves) + curves: MridCollection['ReactiveCapabilityCurve'] = LazyMridList( + _reactive_capability_curves, + "A ReactiveCapabilityCurve", + ) + + + + + + # region deprecated list boilerplate + # region curves boilerplate + + @deprecated("Use len(obj.curves) instead.") def num_curves(self): - """Return the number of :class:`ReactiveCapabilityCurve`s associated with this :class:`SynchronousMachine`.""" - return nlen(self._reactive_capability_curves) + return len(self.curves) + @deprecated("Use obj.curves.get_by_mrid(mrid) instead.") def get_curve(self, mrid: str) -> 'ReactiveCapabilityCurve': - """ - Get the :class:`ReactiveCapabilityCurve` for this :class:`SynchronousMachine` identified by `mrid` - - :param mrid: The mRID of the required :class:`ReactiveCapabilityCurve`. - :returns: The :class:`ReactiveCapabilityCurve` with the specified `mrid` if it exists. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._reactive_capability_curves, mrid) + return self.curves.get_by_mrid(mrid) + @deprecated("Use obj.curves.append(curve) instead.") def add_curve(self, curve: 'ReactiveCapabilityCurve') -> 'SynchronousMachine': - """ - Associate a :class:`ReactiveCapabilityCurve` with this :class:`SynchronousMachine`. - - :param curve: The :class:`ReactiveCapabilityCurve` to associate with this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - :raises ValueError: If another :class:`ReactiveCapabilityCurve` with the same `mrid` already exists for this :class:`SynchronousMachine`. - """ - if self._validate_reference(curve, self.get_curve, "A ReactiveCapabilityCurve"): - return self - self._reactive_capability_curves = self._reactive_capability_curves or [] - self._reactive_capability_curves.append(curve) + self.curves.append(curve) return self + @deprecated("Use obj.curves.remove(curve) instead.") def remove_curve(self, curve: 'ReactiveCapabilityCurve') -> 'SynchronousMachine': - """ - Disassociate a :class:`ReactiveCapabilityCurve` from this :class:`SynchronousMachine`. - - :param curve: The :class:`ReactiveCapabilityCurve` to disassociate from this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - :raises ValueError: If `curve` was not associated with this :class:`SynchronousMachine`. - """ - self._reactive_capability_curves = safe_remove(self._reactive_capability_curves, curve) + self.curves.remove(curve) return self + @deprecated("Use obj.curves.clear() instead.") def clear_curves(self) -> 'SynchronousMachine': - """ - Clear all :class:`ReactiveCapabilityCurve` associated with this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - """ - self._reactive_capability_curves = None + self.curves.clear() return self + + # endregion curves boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py index 1efccef84..e0ebe95bc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py @@ -53,11 +53,6 @@ class TransformerEnd(IdentifiedObject, metaclass=ABCMeta): """(accurate for 2- or 3-winding transformers only) Pi-model impedances of this transformer end. By convention, for a two winding transformer, the full values of the transformer should be entered on the high voltage end (endNumber=1).""" - def __init__(self, *args, terminal: Optional['Terminal'] = None, **kwargs): - super(TransformerEnd, self).__init__(*args, **kwargs) - if terminal is not None: - self.terminal = terminal - @property def terminal(self) -> Optional['Terminal']: """ diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index b36289d3a..5fda60a1b 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -7,11 +7,14 @@ __all__ = ["Circuit"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.line import Line -from zepben.ewb.util import ngen, get_by_mrid, safe_remove, nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop @@ -24,123 +27,73 @@ class Circuit(Line): """Missing description""" loop: Optional[Loop] = None - _end_terminals: Optional[List[Terminal]] = None - _end_substations: Optional[List[Substation]] = None - - def __init__(self, *args, end_terminals: List[Terminal] = None, end_substations: List[Substation] = None, **kwargs): - super(Circuit, self).__init__(*args, **kwargs) - if end_terminals: - for term in end_terminals: - self.add_end_terminal(term) - - if end_substations: - for sub in end_substations: - self.add_end_substation(sub) - - @property - def end_terminals(self) -> Generator[Terminal, None, None]: - """ - The `Terminal`s representing the ends for this `Circuit`. - """ - return ngen(self._end_terminals) - - @property - def end_substations(self) -> Generator[Substation, None, None]: - """ - The `Substations`s representing the ends for this `Circuit`. - """ - return ngen(self._end_substations) + _end_terminals: Optional[List[Terminal]] = field(default=None) + _end_substations: Optional[List[Substation]] = field(default=None) + end_terminals: MridCollection[Terminal] = LazyMridList( + _end_terminals, + "An Terminal", + ) + + end_substations: MridCollection[Substation] = LazyMridList( + _end_substations, + "An Substation", + ) + + + # region deprecated list boilerplate + # region end_terminals boilerplate + + @deprecated("Use len(obj.end_terminals) instead.") def num_end_terminals(self): - """Return the number of end `Terminal`s associated with this `Circuit`""" - return nlen(self._end_terminals) + return len(self.end_terminals) + @deprecated("Use obj.end_terminals.get_by_mrid(mrid) instead.") def get_end_terminal(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `Circuit` identified by `mrid` - - `mrid` the mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_terminals, mrid) + return self.end_terminals.get_by_mrid(mrid) + @deprecated("Use obj.end_terminals.append(terminal) instead.") def add_end_terminal(self, terminal: Terminal) -> Circuit: - """ - Associate an `Terminal` with this `Circuit` - - `terminal` the `Terminal` to associate with this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `Circuit`. - """ - if self._validate_reference(terminal, self.get_end_terminal, "An Terminal"): - return self - self._end_terminals = list() if self._end_terminals is None else self._end_terminals - self._end_terminals.append(terminal) + self.end_terminals.append(terminal) return self + @deprecated("Use obj.end_terminals.remove(terminal) instead.") def remove_end_terminal(self, terminal: Terminal) -> Circuit: - """ - Disassociate `terminal` from this `Circuit` - - `terminal` the `Terminal` to disassociate from this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `Circuit`. - """ - self._end_terminals = safe_remove(self._end_terminals, terminal) + self.end_terminals.remove(terminal) return self + @deprecated("Use obj.end_terminals.clear() instead.") def clear_end_terminals(self) -> Circuit: - """ - Clear all end terminals. - Returns A reference to this `Circuit` to allow fluent use. - """ - self._end_terminals = None + self.end_terminals.clear() return self + # endregion end_terminals boilerplate + + # region end_substations boilerplate + + @deprecated("Use len(obj.end_substations) instead.") def num_end_substations(self): - """Return the number of end `Substation`s associated with this `Circuit`""" - return nlen(self._end_substations) + return len(self.end_substations) + @deprecated("Use obj.end_substations.get_by_mrid(mrid) instead.") def get_end_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Circuit` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_substations, mrid) + return self.end_substations.get_by_mrid(mrid) + @deprecated("Use obj.end_substations.append(substation) instead.") def add_end_substation(self, substation: Substation) -> Circuit: - """ - Associate an `Substation` with this `Circuit` - - `substation` the `Substation` to associate with this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Circuit`. - """ - if self._validate_reference(substation, self.get_end_substation, "An Substation"): - return self - self._end_substations = list() if self._end_substations is None else self._end_substations - self._end_substations.append(substation) + self.end_substations.append(substation) return self + @deprecated("Use obj.end_substations.remove(substation) instead.") def remove_end_substation(self, substation: Substation) -> Circuit: - """ - Disassociate `substation` from this `Circuit` - - `substation` the `Substation` to disassociate from this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Circuit`. - """ - self._end_substations = safe_remove(self._end_substations, substation) + self.end_substations.remove(substation) return self + @deprecated("Use obj.end_substations.clear() instead.") def clear_end_substations(self) -> Circuit: - """ - Clear all end substations. - Returns A reference to this `Circuit` to allow fluent use. - """ - self._end_substations = None + self.end_substations.clear() return self + + # endregion end_substations boilerplate + + # endregion deprecated list boilerplate diff --git a/test/boilerplate/test_abstract_backed_collection.py b/test/boilerplate/test_abstract_backed_collection.py new file mode 100644 index 000000000..35306120f --- /dev/null +++ b/test/boilerplate/test_abstract_backed_collection.py @@ -0,0 +1,109 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection + + +class BackedCollection(AbstractBackedCollection[str]): + def __init__(self, items: list[str] | None = None): + self.items = items or [] + + def _get_collection(self) -> list[str]: + return self.items + + def append(self, item: str) -> None: + self.items.append(item) + + def remove(self, item: str) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_extend_appends_each_item_in_order(): + collection = BackedCollection(["first"]) + + collection.extend(["second", "third"]) + + assert collection.items == ["first", "second", "third"] + + +def test_extend_accepts_single_pass_iterable(): + collection = BackedCollection() + + collection.extend(item for item in ["first", "second"]) + + assert collection.items == ["first", "second"] + + +def test_extend_empty_iterable_does_nothing(): + collection = BackedCollection(["item"]) + + collection.extend([]) + + assert collection.items == ["item"] + + +def test_extend_none_does_nothing(): + collection = BackedCollection(["item"]) + + collection.extend(None) + + assert collection.items == ["item"] + + +def test_len_returns_backing_collection_size(): + collection = BackedCollection(["first", "second"]) + + assert len(collection) == 2 + + +def test_len_returns_zero_for_empty_backing_collection(): + assert len(BackedCollection()) == 0 + + +def test_iter_returns_items_in_backing_collection_order(): + collection = BackedCollection(["first", "second"]) + + assert list(collection) == ["first", "second"] + + +def test_iter_returns_no_items_for_empty_backing_collection(): + assert list(BackedCollection()) == [] + + +def test_contains_finds_item_in_backing_collection(): + collection = BackedCollection(["first", "second"]) + + assert "second" in collection + + +def test_contains_rejects_item_missing_from_backing_collection(): + collection = BackedCollection(["first", "second"]) + + assert "missing" not in collection + + +def test_for_each_indexed_visits_each_item_with_its_index(): + collection = BackedCollection(["first", "second"]) + visited: list[tuple[int, str]] = [] + + collection.for_each_indexed( + lambda index, item: visited.append((index, item)) + ) + + assert visited == [(0, "first"), (1, "second")] + + +def test_for_each_indexed_does_not_call_action_for_empty_collection(): + collection = BackedCollection() + visited: list[tuple[int, str]] = [] + + collection.for_each_indexed( + lambda index, item: visited.append((index, item)) + ) + + assert visited == [] diff --git a/test/boilerplate/test_abstract_backed_list.py b/test/boilerplate/test_abstract_backed_list.py new file mode 100644 index 000000000..ea086979d --- /dev/null +++ b/test/boilerplate/test_abstract_backed_list.py @@ -0,0 +1,58 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import pytest + +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList + + +class BackedList(AbstractBackedList[str]): + def __init__(self, items: list[str]): + self.items = items + + def _get_collection(self) -> list[str]: + return self.items + + def append(self, item: str) -> None: + self.items.append(item) + + def remove(self, item: str) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_getitem_returns_item_at_index(): + collection = BackedList(["first", "second"]) + + assert collection[1] == "second" + + +def test_getitem_supports_negative_index(): + collection = BackedList(["first", "second"]) + + assert collection[-1] == "second" + + +def test_getitem_raises_for_index_out_of_range(): + collection = BackedList(["item"]) + + with pytest.raises(IndexError): + collection[1] + + +def test_getitem_returns_backing_list_slice(): + collection = BackedList(["first", "second", "third"]) + + assert collection[1:] == ["second", "third"] + + +def test_getitem_supports_slice_step(): + collection = BackedList( + ["first", "second", "third", "fourth"] + ) + + assert collection[::2] == ["first", "third"] diff --git a/test/boilerplate/test_backfill.py b/test/boilerplate/test_backfill.py new file mode 100644 index 000000000..a57820c5a --- /dev/null +++ b/test/boilerplate/test_backfill.py @@ -0,0 +1,92 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.backed_descriptor import Alias +from zepben.ewb.boilerplate.backfill import Backfill, internal + + +@dataclass +class Container: + """Container exposing each supported kind of internal back-reference.""" + + field_backing: object | None = field(default=None) + descriptor_backing: object | None = field(default=None) + property_backing: object | None = field(default=None) + + descriptor = Alias(descriptor_backing) + + @property + @internal(field_backing) + def field_parent(self): + return self.field_backing + + @property + @internal(descriptor) + def descriptor_parent(self): + return self.descriptor + + @property + def internal_parent(self): + return self.property_backing + + @internal_parent.setter + def internal_parent(self, value): + self.property_backing = value + + @property + @internal(internal_parent) + def property_parent(self): + return self.internal_parent + + +# @dataclass populates Field.name after the automatic __set_name__ call. +Container.descriptor.__set_name__(Container, "descriptor") + + +@pytest.mark.parametrize( + ("property_name", "backing_name"), + [ + ("field_parent", "field_backing"), + ("descriptor_parent", "descriptor_backing"), + ("property_parent", "property_backing"), + ], +) +def test_apply_sets_back_reference( + property_name: str, + backing_name: str, +): + container = Container() + owner = object() + + Backfill(getattr(Container, property_name)).apply(container, owner) + + assert getattr(container, property_name) is owner + assert getattr(container, backing_name) is owner + + +@pytest.mark.parametrize( + ("property_name", "backing_name"), + [ + ("field_parent", "field_backing"), + ("descriptor_parent", "descriptor_backing"), + ("property_parent", "property_backing"), + ], +) +def test_clear_nulls_back_reference( + property_name: str, + backing_name: str, +): + owner = object() + container = Container() + setattr(container, backing_name, owner) + + Backfill(getattr(Container, property_name)).clear(container) + + assert getattr(container, property_name) is None + assert getattr(container, backing_name) is None diff --git a/test/boilerplate/test_lazy_index_list.py b/test/boilerplate/test_lazy_index_list.py new file mode 100644 index 000000000..1cf12ba68 --- /dev/null +++ b/test/boilerplate/test_lazy_index_list.py @@ -0,0 +1,136 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList + + +def record_validation(owner: Owner, item: str): + owner.events.append(item) + + +def reject_validation(_owner: Owner, _item: str): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: list[str] | None = field(default=None) + events: list[str] = field(default_factory=list) + + items = LazyIndexList(backing_items, "Item") + validated_items = LazyIndexList( + backing_items, + "Item", + validate=record_validation, + ) + rejecting_items = LazyIndexList( + backing_items, + "Item", + validate=reject_validation, + ) + + +def test_insert_into_empty_creates_backing_list(): + owner = Owner() + + owner.items.insert(0, "item") + + assert owner.backing_items == ["item"] + + +def test_insert_places_item_at_requested_index(): + owner = Owner(["first", "third"]) + + owner.items.insert(1, "second") + + assert owner.backing_items == ["first", "second", "third"] + + +@pytest.mark.parametrize("index", [-1, 1]) +def test_insert_rejects_index_outside_valid_range(index: int): + owner = Owner() + + with pytest.raises( + ValueError, + match=rf"Sequence number {index} is invalid.*between 0 and 0", + ): + owner.items.insert(index, "item") + + assert owner.backing_items is None + + +def test_insert_validates_before_creating_backing_list(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_items.insert(0, "item") + + assert owner.backing_items is None + + +def test_insert_runs_validation(): + owner = Owner() + + owner.validated_items.insert(0, "item") + + assert owner.events == ["item"] + assert owner.backing_items == ["item"] + + +def test_append_inserts_at_end(): + owner = Owner(["first"]) + + owner.items.append("second") + + assert owner.backing_items == ["first", "second"] + + +def test_pop_returns_and_removes_requested_item(): + owner = Owner(["first", "second"]) + + popped = owner.items.pop(0) + + assert popped == "first" + assert owner.backing_items == ["second"] + + +def test_pop_uses_last_item_by_default_and_nulls_empty_backing(): + owner = Owner(["item"]) + + popped = owner.items.pop() + + assert popped == "item" + assert owner.backing_items is None + + +def test_pop_from_empty_raises(): + owner = Owner() + + with pytest.raises(IndexError, match="pop from empty list"): + owner.items.pop() + + assert owner.backing_items is None + + +def test_delete_removes_item_at_index(): + owner = Owner(["first", "second"]) + + del owner.items[0] + + assert owner.backing_items == ["second"] + + +def test_delete_last_item_nulls_backing_list(): + owner = Owner(["item"]) + + del owner.items[0] + + assert owner.backing_items is None diff --git a/test/boilerplate/test_lazy_list.py b/test/boilerplate/test_lazy_list.py new file mode 100644 index 000000000..a5c9c07e6 --- /dev/null +++ b/test/boilerplate/test_lazy_list.py @@ -0,0 +1,146 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList + + +def record_validation(owner: Owner, item: str): + owner.events.append(("validate", item)) + + +def reject_validation(_owner: Owner, _item: str): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: list[str] | None = field( + default=None, + repr=False, + ) + events: list[tuple[str, str]] = field( + default_factory=list, + repr=False, + ) + + items = LazyList(backing_items) + validated_items = LazyList( + backing_items, + validate=record_validation, + ) + rejecting_validation_items = LazyList( + backing_items, + validate=reject_validation, + ) + sorted_items = LazyList( + backing_items, + sort_by=lambda item: item, + ) + + +def test_append_to_empty_creates_owner_backing_list(): + owner = Owner() + + owner.items.append("item") + + assert owner.backing_items == ["item"] + + +def test_append_adds_item_to_existing_owner_backing_list(): + owner = Owner() + owner.backing_items = ["first"] + + owner.items.append("second") + + assert owner.backing_items == ["first", "second"] + + +def test_append_runs_validation(): + owner = Owner() + + owner.validated_items.append("item") + + assert owner.events == [("validate", "item")] + assert owner.backing_items == ["item"] + + +def test_failed_append_to_empty_does_not_create_backing_list(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append("item") + + assert owner.backing_items is None + + +def test_validation_failure_does_not_append_to_existing_list(): + owner = Owner() + owner.backing_items = ["existing"] + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append("item") + + assert owner.backing_items == ["existing"] + + +def test_append_sorts_owner_backing_list(): + owner = Owner() + + owner.sorted_items.append("second") + owner.sorted_items.append("first") + + assert owner.backing_items == ["first", "second"] + + +def test_remove_removes_item_from_owner_backing_list(): + owner = Owner() + owner.backing_items = ["retained", "removed"] + + owner.items.remove("removed") + + assert owner.backing_items == ["retained"] + + +def test_remove_last_item_sets_owner_backing_list_to_null(): + owner = Owner() + owner.backing_items = ["item"] + + owner.items.remove("item") + + assert owner.backing_items is None + + +def test_clear_sets_owner_backing_list_to_null(): + owner = Owner() + owner.backing_items = ["first", "second"] + + owner.items.clear() + + assert owner.backing_items is None + + +def test_repr_matches_owner_backing_list_repr(): + owner = Owner() + owner.backing_items = ["first", "second"] + + assert owner.backing_items is not None + assert repr(owner.items) == repr(owner.backing_items) + + +def test_empty_repr_matches_empty_list_repr(): + owner = Owner() + + assert repr(owner.items) == repr([]) + assert owner.backing_items is None + + +def test_class_field_repr_uses_descriptor_repr() -> None: + assert repr(Owner.items) == object.__repr__(Owner.items) diff --git a/test/boilerplate/test_lazy_mrid_list.py b/test/boilerplate/test_lazy_mrid_list.py new file mode 100644 index 000000000..3874eb15b --- /dev/null +++ b/test/boilerplate/test_lazy_mrid_list.py @@ -0,0 +1,88 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList + + +@dataclass(eq=False) +class Item: + mrid: str + + +class RecordingBackfill: + def apply(self, item: Item, owner: Owner): + owner.events.append(("backfill", item.mrid)) + + +class RejectingBackfill: + def apply(self, _item: Item, _owner: Owner): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Item): + owner.events.append(("validate", item.mrid)) + + +@dataclass +class Owner: + backing_items: list[Item] | None = field(default=None) + events: list[tuple[str, str]] = field(default_factory=list) + + items = LazyMridList(backing_items, "Item") + backfilled_items = LazyMridList( + backing_items, + "Item", + backfill=RecordingBackfill(), + validate=record_validation, + ) + rejecting_backfill_items = LazyMridList( + backing_items, + "Item", + backfill=RejectingBackfill(), + ) + + +def test_get_by_mrid_finds_item_in_backing_list(): + item = Item("item") + owner = Owner([item]) + + assert owner.items.get_by_mrid("item") is item + + +def test_get_by_mrid_treats_null_backing_as_empty(): + owner = Owner() + + with pytest.raises(KeyError, match="missing"): + owner.items.get_by_mrid("missing") + + assert owner.backing_items is None + + +def test_append_applies_backfill_before_superclass_validation(): + item = Item("item") + owner = Owner() + + owner.backfilled_items.append(item) + + assert owner.events == [ + ("backfill", "item"), + ("validate", "item"), + ] + assert owner.backing_items == [item] + + +def test_backfill_failure_does_not_call_superclass_append(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_items.append(Item("item")) + + assert owner.backing_items is None diff --git a/test/boilerplate/test_lazy_mrid_map.py b/test/boilerplate/test_lazy_mrid_map.py new file mode 100644 index 000000000..f6e073cfc --- /dev/null +++ b/test/boilerplate/test_lazy_mrid_map.py @@ -0,0 +1,175 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap + + +@dataclass(eq=False) +class Item: + mrid: str + + +class RecordingBackfill: + def apply(self, item: Item, owner: Owner): + owner.events.append(("backfill", item.mrid)) + + +class RejectingBackfill: + def apply(self, _item: Item, _owner: Owner): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Item): + owner.events.append(("validate", item.mrid)) + + +def reject_validation(_owner: Owner, _item: Item): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: dict[str, Item] | None = field(default=None) + events: list[tuple[str, str]] = field(default_factory=list) + + items = LazyMridMap(backing_items, "Item") + configured_items = LazyMridMap( + backing_items, + "Item", + backfill=RecordingBackfill(), + validate=record_validation, + ) + rejecting_backfill_items = LazyMridMap( + backing_items, + "Item", + backfill=RejectingBackfill(), + ) + rejecting_validation_items = LazyMridMap( + backing_items, + "Item", + validate=reject_validation, + ) + + +def test_append_to_empty_creates_map_keyed_by_mrid(): + item = Item("item") + owner = Owner() + + owner.items.append(item) + + assert owner.backing_items == {"item": item} + + +def test_append_to_existing_map_adds_mrid_entry(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first}) + + owner.items.append(second) + + assert owner.backing_items == { + "first": first, + "second": second, + } + + +def test_append_runs_backfill_before_validation(): + item = Item("item") + owner = Owner() + + owner.configured_items.append(item) + + assert owner.events == [ + ("backfill", "item"), + ("validate", "item"), + ] + assert owner.backing_items == {"item": item} + + +def test_backfill_failure_does_not_create_map(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_items.append(Item("item")) + + assert owner.backing_items is None + + +def test_validation_failure_does_not_create_map(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append(Item("item")) + + assert owner.backing_items is None + + +def test_lookup_and_indexing_use_mrid_keys(): + item = Item("item") + owner = Owner({"item": item}) + + assert owner.items.get_by_mrid("item") is item + assert owner.items["item"] is item + + with pytest.raises(KeyError, match="missing"): + owner.items.get_by_mrid("missing") + + with pytest.raises(KeyError, match="missing"): + owner.items["missing"] + + +def test_collection_view_exposes_map_values(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first, "second": second}) + + assert len(owner.items) == 2 + assert list(owner.items) == [first, second] + assert first in owner.items + assert Item("first") not in owner.items + + +def test_remove_deletes_item_by_mrid(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first, "second": second}) + + owner.items.remove(first) + + assert owner.backing_items == {"second": second} + + +def test_remove_last_item_nulls_backing_map(): + item = Item("item") + owner = Owner({"item": item}) + + owner.items.remove(item) + + assert owner.backing_items is None + + +def test_clear_nulls_backing_map(): + owner = Owner({"item": Item("item")}) + + owner.items.clear() + + assert owner.backing_items is None + + +def test_repr_matches_backing_map_repr(): + item = Item("item") + owner = Owner({"item": item}) + + assert repr(owner.items) == repr(owner.backing_items) + + +def test_class_field_repr_uses_descriptor_repr(): + assert repr(Owner.items) == object.__repr__(Owner.items) diff --git a/test/boilerplate/test_mrid_collection.py b/test/boilerplate/test_mrid_collection.py new file mode 100644 index 000000000..2ddd5d333 --- /dev/null +++ b/test/boilerplate/test_mrid_collection.py @@ -0,0 +1,86 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass + +import pytest + +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection + + +@dataclass +class Item: + mrid: str + + +class MridCollectionImpl(MridCollection[Item]): + def __init__(self, items: list[Item] | None = None): + self.items = items or [] + self.element_description = "Item" + self._instance = "owner" + + def _get_collection(self) -> list[Item]: + return self.items + + def _safe_get_by_mrid(self, mrid: str) -> Item | None: + return next( + (item for item in self.items if item.mrid == mrid), + None, + ) + + def append(self, item: Item) -> None: + if self._can_add_by_mrid(item): + self.items.append(item) + + def remove(self, item: Item) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_get_by_mrid_returns_matching_item(): + item = Item("item") + collection = MridCollectionImpl([item]) + + assert collection.get_by_mrid("item") is item + + +def test_get_by_mrid_raises_for_missing_item(): + collection = MridCollectionImpl() + + with pytest.raises(KeyError, match="missing"): + collection.get_by_mrid("missing") + + +def test_append_accepts_new_mrid(): + item = Item("item") + collection = MridCollectionImpl() + + collection.append(item) + + assert collection.items == [item] + + +def test_append_ignores_same_instance(): + item = Item("item") + collection = MridCollectionImpl([item]) + + collection.append(item) + + assert collection.items == [item] + + +def test_append_rejects_different_instance_with_same_mrid(): + existing = Item("duplicate") + collection = MridCollectionImpl([existing]) + + with pytest.raises( + ValueError, + match=r"Item with mRID duplicate already exists in owner", + ): + collection.append(Item("duplicate")) + + assert collection.items == [existing] diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py new file mode 100644 index 000000000..674bbe8b9 --- /dev/null +++ b/test/boilerplate/test_mrid_list.py @@ -0,0 +1,203 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Callable + +import pytest + +from zepben.ewb.boilerplate.collections.mrid_list import MridList +from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder + + +class CallbackBackfill: + def __init__( + self, + callback: Callable[[Owner, Feeder], None], + ): + self._callback = callback + + def apply(self, item: Feeder, owner: Owner): + self._callback(owner, item) + + +def record_backfill(owner: Owner, item: Feeder): + owner.events.append(("backfill", item.mrid)) + + +def reject_backfill(_owner: Owner, _item: Feeder): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Feeder): + owner.events.append(("validate", item.mrid)) + + +def reject_validation(_owner: Owner, _item: Feeder): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_feeders: list[Feeder] = field( + default_factory=list, + repr=False, + ) + events: list[tuple[str, str]] = field( + default_factory=list, + repr=False, + ) + + feeders = MridList( + backing_feeders, + "Feeder", + ) + backfilled_feeders = MridList( + backing_feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + ) + rejecting_backfill_feeders = MridList( + backing_feeders, + "Feeder", + backfill=CallbackBackfill(reject_backfill), + ) + validated_feeders = MridList( + backing_feeders, + "Feeder", + validate=record_validation, + ) + rejecting_validation_feeders = MridList( + backing_feeders, + "Feeder", + validate=reject_validation, + ) + backfilled_validated_feeders = MridList( + backing_feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + validate=record_validation, + ) + sorted_feeders = MridList( + backing_feeders, + "Feeder", + sort_by=lambda feeder: feeder.mrid, + ) + + +def test_append_adds_item_to_owner_backing_list(): + owner = Owner() + feeder = Feeder("feeder") + + owner.feeders.append(feeder) + + assert owner.backing_feeders == [feeder] + + +def test_append_applies_backfill(): + owner = Owner() + feeder = Feeder("feeder") + + owner.backfilled_feeders.append(feeder) + + assert owner.events == [("backfill", "feeder")] + assert owner.backing_feeders == [feeder] + + +def test_backfill_failure_does_not_append_item(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_feeders.append(Feeder("feeder")) + + assert owner.backing_feeders == [] + + +def test_append_runs_validation(): + owner = Owner() + feeder = Feeder("feeder") + + owner.validated_feeders.append(feeder) + + assert owner.events == [("validate", "feeder")] + assert owner.backing_feeders == [feeder] + + +def test_validation_failure_does_not_append_item(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_feeders.append(Feeder("feeder")) + + assert owner.backing_feeders == [] + + +def test_append_runs_backfill_before_validation(): + owner = Owner() + feeder = Feeder("feeder") + + owner.backfilled_validated_feeders.append(feeder) + + assert owner.events == [ + ("backfill", "feeder"), + ("validate", "feeder"), + ] + assert owner.backing_feeders == [feeder] + + +def test_append_sorts_owner_backing_list(): + owner = Owner() + second = Feeder("second") + first = Feeder("first") + + owner.sorted_feeders.append(second) + owner.sorted_feeders.append(first) + + assert owner.backing_feeders == [first, second] + + +def test_remove_removes_item_from_owner_backing_list(): + owner = Owner() + retained = Feeder("retained") + removed = Feeder("removed") + owner.backing_feeders.extend([retained, removed]) + + owner.feeders.remove(removed) + + assert owner.backing_feeders == [retained] + + +def test_clear_empties_owner_backing_list(): + owner = Owner() + + owner.backing_feeders.extend( + [ + Feeder("first"), + Feeder("second"), + ] + ) + + owner.feeders.clear() + + assert owner.backing_feeders == [] + + +def test_repr_matches_owner_backing_list_repr(): + owner = Owner() + + owner.backing_feeders.extend( + [ + Feeder("first"), + Feeder("second"), + ] + ) + + assert repr(owner.feeders) == repr(owner.backing_feeders) + + +def test_class_field_repr_uses_descriptor_repr() -> None: + assert repr(Owner.feeders) == object.__repr__(Owner.feeders) diff --git a/test/boilerplate/test_wrapper.py b/test/boilerplate/test_wrapper.py new file mode 100644 index 000000000..8fd3ba763 --- /dev/null +++ b/test/boilerplate/test_wrapper.py @@ -0,0 +1,116 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +# noinspection PyProtectedMember +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper, _Wrapper + + +class RecordingWrapper(_Wrapper): + def __init__(self, private_field, marker: str): + super().__init__(private_field) + self.marker = marker + + @property + def bound_instance(self): + return self._instance + + @property + def backing_name(self): + return self._backing_name + + +class ListWrapper(_IterableWrapper[str]): + def _get_collection(self) -> list[str]: + return getattr(self._instance, self._backing_name) + + def append(self, item: str) -> None: + self._get_collection().append(item) + + def remove(self, item: str) -> None: + self._get_collection().remove(item) + + def clear(self) -> None: + self._get_collection().clear() + + @property + def bound_instance(self): + return self._instance + + +@dataclass +class Owner: + backing_items: list[str] = field(default_factory=list) + + wrapped = RecordingWrapper(backing_items, marker="marker") + items = ListWrapper(backing_items) + + +# @dataclass populates Field.name after the automatic __set_name__ call. +Owner.wrapped.__set_name__(Owner, "wrapped") +Owner.items.__set_name__(Owner, "items") + + +def test_class_access_returns_shared_descriptor(): + assert isinstance(Owner.wrapped, RecordingWrapper) + + +def test_instance_access_returns_bound_copy_with_init_arguments(): + owner = Owner() + + first = owner.wrapped + second = owner.wrapped + + assert first is not Owner.wrapped + assert first is not second + assert first.marker == "marker" + assert first.bound_instance is owner + assert first.backing_name == "backing_items" + + +def test_fget_is_named_after_descriptor_and_returns_bound_wrapper(): + owner = Owner() + + wrapper = Owner.items.fget(owner) + + assert Owner.items.fget.__name__ == "items" + assert Owner.items.fget.__qualname__ == "Owner.items" + assert wrapper.bound_instance is owner + + +def test_assigning_iterable_to_empty_backing_extends_collection(): + owner = Owner() + + owner.items = ["first", "second"] + + assert owner.backing_items == ["first", "second"] + + +def test_assigning_none_to_empty_backing_keeps_it_empty(): + owner = Owner() + + owner.items = None + + assert owner.backing_items == [] + + +def test_assigning_to_non_empty_backing_raises(): + owner = Owner(["existing"]) + + with pytest.raises(ValueError, match="currently non-empty"): + owner.items = ["new"] + + assert owner.backing_items == ["existing"] + + +def test_assignment_resolves_missing_default_before_extending(): + owner = Owner.__new__(Owner) + + Owner.items.__set__(owner, ["item"]) + + assert owner.backing_items == ["item"] diff --git a/test/busbranch/test_bus_branch.py b/test/busbranch/test_bus_branch.py index cffa0cf6c..3e84e30c9 100644 --- a/test/busbranch/test_bus_branch.py +++ b/test/busbranch/test_bus_branch.py @@ -309,15 +309,15 @@ async def test_switches_excluded_when_getting_voltage(): def _get_expected(nb_network, line, pt, es, ec, pec, eb, ec_eb1, ec_eb2): # -- Bus - exp_bb0 = (create_terminal_based_id({next(es.terminals), get_term(pt, 1)}), + exp_bb0 = (create_terminal_based_id({next(iter(es.terminals)), get_term(pt, 1)}), (20000, frozenset(), frozenset({get_term(es, 1), get_term(pt, 1)}), frozenset(), nb_network)) exp_bb1 = (create_terminal_based_id({get_term(pt, 2), get_term(line, 1)}), (400, frozenset(), frozenset({get_term(line, 1), get_term(pt, 2)}), frozenset(), nb_network)) - exp_bb2 = (create_terminal_based_id({get_term(line, 2), next(ec.terminals), next(pec.terminals), get_term(eb, 1)}), + exp_bb2 = (create_terminal_based_id({get_term(line, 2), next(iter(ec.terminals)), next(iter(pec.terminals)), get_term(eb, 1)}), (400, frozenset(), frozenset({get_term(ec, 1), get_term(line, 2), get_term(pec, 1), get_term(eb, 1)}), frozenset(), nb_network)) - exp_bb3 = (create_terminal_based_id({get_term(eb, 2), next(ec_eb1.terminals), next(ec_eb2.terminals)}), - (400, frozenset(), frozenset({get_term(eb, 2), next(ec_eb1.terminals), next(ec_eb2.terminals)}), frozenset(), nb_network)) + exp_bb3 = (create_terminal_based_id({get_term(eb, 2), next(iter(ec_eb1.terminals)), next(iter(ec_eb2.terminals))}), + (400, frozenset(), frozenset({get_term(eb, 2), next(iter(ec_eb1.terminals)), next(iter(ec_eb2.terminals))}), frozenset(), nb_network)) # -- Branch exp_branch = (f"tb_{line.mrid}", ((exp_bb1[1], exp_bb2[1]), 100, frozenset({line}), frozenset({*line.terminals}), frozenset(), nb_network)) diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 121a04dec..a8166f7da 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -4,14 +4,14 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import field -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass, DataclassBase -from zepben.ewb.boilerplate.backed_descriptor import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb.boilerplate.dataclass_base import DataclassBase +from zepben.ewb import zb_dataclass +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor @zb_dataclass -@remove_descriptor_annotations class DescriptorTest(DataclassBase): - _x: int = field() + _x: int = field(default=0) x: int = BackedDescriptor(_x) diff --git a/test/dataclass_descriptors/test_dataclass_base.py b/test/dataclass_descriptors/test_dataclass_base.py index c6ae10269..ebc954a1b 100644 --- a/test/dataclass_descriptors/test_dataclass_base.py +++ b/test/dataclass_descriptors/test_dataclass_base.py @@ -7,13 +7,10 @@ import pytest -from zepben.ewb import Cut from zepben.ewb.boilerplate.dataclass_base import zb_dataclass, DataclassBase -from zepben.ewb.boilerplate.backed_descriptor import remove_descriptor_annotations @zb_dataclass -@remove_descriptor_annotations class Root(DataclassBase): mrid: str y: int @@ -26,15 +23,13 @@ def __init__(self, mrid, *_, **kwargs): self.mrid = mrid super(Root, self).__init__(**kwargs) - @zb_dataclass -@remove_descriptor_annotations class Child(Root): x: float = 42.0 z: str = "abc" dc_default: int = field(default=99) - dc_default_factory: List[int] = field(default_factory=lambda: [33]) + dc_default_factory: List[int] = field(default_factory=lambda : [33]) def test_dataclass_base(): @@ -48,8 +43,8 @@ def test_dataclass_base(): obj = Child(mrid, y=33, z="Hello there") # Memory layout correct # noinspection PyUnresolvedReferences - all_slots = set(obj.__slots__).union(set(Root.__slots__)) # Python 3.11+ stores parent slots only in parent - assert all_slots == {'mrid', 'y', 'x', 'z', 'dc_default', 'dc_default_factory'} + all_slots = set(obj.__slots__).union(set(Root.__slots__)) # Python 3.11+ stores parent slots only in parent + assert all_slots == {'mrid', 'y', 'x', 'z', 'dc_default', 'dc_default_factory'} # positional arg assert obj.mrid == mrid @@ -77,14 +72,3 @@ def test_dataclass_base(): other = Child("mrid2", y=42) other.dc_default_factory.append(24) assert obj.dc_default_factory == [33] - - -def test_identifiable_mrid(): - obj = Cut("it") - assert obj.mrid == "it" - - obj = Cut(mrid="it") - assert obj.mrid == "it" - - with pytest.raises(TypeError): - Cut("it", mrid="it") diff --git a/test/services/common/service_comparator_validator.py b/test/services/common/service_comparator_validator.py index b0530a169..c9fc79d0a 100644 --- a/test/services/common/service_comparator_validator.py +++ b/test/services/common/service_comparator_validator.py @@ -182,7 +182,7 @@ def validate_collection( in_source, target_empty, { - _prop_name(prop): CollectionDifference(missing_from_target=[next(_get_prop(in_source, prop))]) + _prop_name(prop): CollectionDifference(missing_from_target=[next(iter(_get_prop(in_source, prop)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -191,7 +191,7 @@ def validate_collection( source_empty, in_target, { - _prop_name(prop): CollectionDifference(missing_from_source=[next(_get_prop(in_target, prop))]) + _prop_name(prop): CollectionDifference(missing_from_source=[next(iter(_get_prop(in_target, prop)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -201,8 +201,8 @@ def validate_collection( in_target_difference, { _prop_name(prop): CollectionDifference( - missing_from_source=[next(_get_prop(in_target_difference, prop))], - missing_from_target=[next(_get_prop(in_source, prop))], + missing_from_source=[next(iter(_get_prop(in_target_difference, prop)))], + missing_from_target=[next(iter(_get_prop(in_source, prop)))], ) }, ) @@ -235,7 +235,7 @@ def validate_name_collection( in_source, target_empty, { - _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_target=[next(_get_prop(in_source, IdentifiedObject.names))]) + _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_target=[next(iter(_get_prop(in_source, IdentifiedObject.names)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -244,7 +244,7 @@ def validate_name_collection( source_empty, in_target, { - _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_source=[next(_get_prop(in_target, IdentifiedObject.names))]) + _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_source=[next(iter(_get_prop(in_target, IdentifiedObject.names)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -254,8 +254,8 @@ def validate_name_collection( in_target_difference, { _prop_name(IdentifiedObject.names): CollectionDifference( - missing_from_source=[next(_get_prop(in_target_difference, IdentifiedObject.names))], - missing_from_target=[next(_get_prop(in_source, IdentifiedObject.names))], + missing_from_source=[next(iter(_get_prop(in_target_difference, IdentifiedObject.names)))], + missing_from_target=[next(iter(_get_prop(in_source, IdentifiedObject.names)))], ) }, ) @@ -286,7 +286,7 @@ def validate_indexed_collection( self.validate_compare(in_source, in_target, options=options, options_stop_compare=options_stop_compare) def get_item(obj) -> Optional[R]: - return next(_get_prop(obj, prop), None) + return next(iter(_get_prop(obj, prop)), None) diff = ObjectDifference( in_source, diff --git a/test/services/network/tracing/test_assign_to_lv_feeders.py b/test/services/network/tracing/test_assign_to_lv_feeders.py index 94bcd234d..9ee758731 100644 --- a/test/services/network/tracing/test_assign_to_lv_feeders.py +++ b/test/services/network/tracing/test_assign_to_lv_feeders.py @@ -391,7 +391,7 @@ async def run_with_operators(operators: Type[NetworkStateOperators]): operators.associate_energizing_feeder(back_feed, lv_feeder) await Tracing.assign_equipment_to_lv_feeders().run( - next(b7.terminals), + next(iter(b7.terminals)), network.lv_feeder_start_points, terminal_to_aux_equipment = dict(), lv_feeders_to_assign=[lv_feeder],