Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,53 @@
* `ProtectionRelayFunction.relay_info`
* `ShuntCompensator.shunt_compensator_info`
* `Switch.switch_info`
* `NameType` and `Name` now require constructor args to be supplied as kwargs only.
* `GrpcClient` now requires a `stub` to be passed into its constructor as a kw only arg.
* `BaseService` functions `__contains__`, `get`, `add` and `remove` have replaced their `identified_object` parameters with
`identifiable: Identifiable`.
* `BaseService` members and functions that used to reference `IdentifiedObject` now use `Identifiable` instead.
* `MultiObjectResult.objects` is now a `dict[str, Identifiable]`. This will cause problems if you are explicitly expecting an `IdentifiedObject` to be
returned via a consumer client call without any further type narrowing.
* Private function `CimConsumerClient.extract_identified_objects` has been renamed to `extract_identifiables`.
* `ReferenceDifference` members `source` and `target_value` now reference `Identifiable`.
* Renamed the following gRPC messages and attributes to support identifiable objects that don't descend from `IdentifiedObject` (such as `DataSet`):
* In the `cc` protos:
* `CustomerConsumer.getIdentifiedObjects` -> `CustomerConsumer.getIdentifiables`
* `GetIdentifiedObjectsRequest` -> `GetIdentifiablesRequest`
* `GetIdentifiedObjectsResponse` -> `GetIdentifiablesResponse`
* `GetIdentifiablesResponse.identified_objects` -> `GetIdentifiablesResponse.identifiables`
* `CustomerIdentifiedObject` -> `CustomerIdentifiable`
* `CustomerIdentifiable.identified_object` -> `CustomerIdentifiable.identifiable`
* `GetCustomersForContainerResponse.identified_object` -> `GetCustomersForContainerResponse.identifiable`
* In the `dc` protos:
* `DiagramConsumer.getIdentifiedObjects` -> `DiagramConsumer.getIdentifiables`
* `GetIdentifiedObjectsRequest` -> `GetIdentifiablesRequest`
* `GetIdentifiedObjectsResponse` -> `GetIdentifiablesResponse`
* `GetIdentifiablesResponse.identified_objects` -> `GetIdentifiablesResponse.identifiables`
* `DiagramIdentifiedObject` -> `DiagramIdentifiable`
* `DiagramIdentifiable.identified_object` -> `DiagramIdentifiable.identifiable`
* `GetDiagramObjectsResponse.identified_object` -> `GetDiagramObjectsResponse.identifiable`
* In the `nc` protos:
* `NetworkConsumer.getIdentifiedObjects` -> `NetworkConsumer.getIdentifiables`
* `GetIdentifiedObjectsRequest` -> `GetIdentifiablesRequest`
* `GetIdentifiedObjectsResponse` -> `GetIdentifiablesResponse`
* `GetIdentifiablesResponse.identified_objects` -> `GetIdentifiablesResponse.identifiables`
* `NetworkIdentifiedObject` -> `NetworkIdentifiable`
* `NetworkIdentifiable.identified_object` -> `NetworkIdentifiable.identifiable`
* `GetEquipmentForContainersResponse.identified_object` -> `GetEquipmentForContainersResponse.identifiable`
* `GetEquipmentForRestrictionResponse.identified_object` -> `GetEquipmentForRestrictionResponse.identifiable`

### New Features
* None.
* Added `Identifiable` interface which defines `mrid`.
* Anything implementing `Identifiable` can now be added to a `BaseService`.
* `NameType` and `Name` now implement `Identifiable`. Their mRID will be set to `<name>` and `<name>-<type.name>-<identified_object.mrid>`.
* Promoted `BaseServiceReader`, `BaseServiceWriter`, `BaseCimReader`, `BaseCollectionReader`, `BaseCollectionWriter`, and `BaseServiceReader` to the public API.
* Added `customerIdentifiable` to replace `customer_identified_object` with support for `Identifiable` object types in the future.
* Added `diagramIdentifiable` to replace `diagram_identified_object` with support for `Identifiable` object types in the future.
* Added `networkIdentifiable` to replace `network_identified_object` with support for `Identifiable` object types in the future.
* Added the following functions to all `CimConsumerClient` descendants:
* `get_identifiable` which replaces the now deprecated `get_identified_object`.
* `get_identifiables` which replaces the now deprecated `get_identified_objects`.

### Enhancements
* Added sequence unpacking support for `UnresolvedReference` and `ObjectDifference`.
Expand All @@ -25,13 +69,21 @@
* You can now pass a list of `TransformerEndRatedS` to the `PowerTransformerEnd` constructor via the `ratings` argument.
* Updated all `Callable` type signatures for callables with unused return values to accept `Any` instead of `None`. The return is still unused, but requiring
`None` raises types errors if anything is actually returned.
* `IdentifiedObject`, `Name` and `NameType` now extends `Identifiable`.
* `BaseServiceComparator` will now compare all `Identifiable` objects that have been added, not just `IdentifiedObject` objects.

### Fixes
* Fixed the packing and unpacking of timestamps for `Agreement.validity_interval` in gRPC messages. Fix also ensures all other timestamps correctly support
`None` when optional.
* `BaseService.__contains__`` will now return `false` when passed an `Identifiable` that is not in the service, instead of raising a `KeyError`

### Notes
* None.
* Deprecated the `customer_identified_object` function, please use the replacement `customer_identifiable`.
* Deprecated the `diagram_identified_object` function, please use the replacement `diagram_identifiable`.
* Deprecated the `network_identified_object` function, please use the replacement `network_identifiable`.
* Deprecated the following functions on all `CimConsumerClient` descendants:
* `get_identified_object` which has been replaced with `get_identifiable`.
* `get_identified_objects` which has been replaced with `get_identifiables`.

## [1.2.0] - 2026-03-03
### Breaking Changes
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
{name = "Max Chesterfield", email = "max.chesterfield@zepben.com"}
]
dependencies = [
"zepben.protobuf==1.3.0",
"typing_extensions==4.14.1",
"zepben.protobuf==1.4.0b4",
"typing_extensions==4.15.0",
"requests==2.32.5",
"urllib3==2.5.0",
"PyJWT==2.10.1"
"PyJWT==2.10.1",
"typing-extensions==4.15.0",
]
classifiers = [
"Programming Language :: Python :: 3",
Expand Down
7 changes: 5 additions & 2 deletions src/zepben/ewb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
from zepben.ewb.model.cim.iec61970.base.core.equipment_container import *
from zepben.ewb.model.cim.iec61970.base.core.feeder import *
from zepben.ewb.model.cim.iec61970.base.core.geographical_region import *
from zepben.ewb.model.cim.iec61970.base.core.identifiable import *
from zepben.ewb.model.cim.iec61970.base.core.identified_object import *
from zepben.ewb.model.cim.iec61970.base.core.name import *
from zepben.ewb.model.cim.iec61970.base.core.name_type import *
Expand Down Expand Up @@ -579,8 +580,12 @@
from zepben.ewb.database.sqlite.network.network_database_tables import *
from zepben.ewb.database.sqlite.extensions.prepared_statement import *
from zepben.ewb.database.sqlite.tables.exceptions import *
from zepben.ewb.database.sqlite.common.base_cim_reader import *
from zepben.ewb.database.sqlite.common.base_cim_writer import *
from zepben.ewb.database.sqlite.common.base_collection_reader import *
from zepben.ewb.database.sqlite.common.base_collection_writer import *
from zepben.ewb.database.sqlite.common.base_entry_writer import *
from zepben.ewb.database.sqlite.common.base_service_reader import *
from zepben.ewb.database.sqlite.common.base_service_writer import *
from zepben.ewb.database.sqlite.common.metadata_collection_writer import *
from zepben.ewb.database.sqlite.common.metadata_entry_writer import *
Expand All @@ -598,8 +603,6 @@
from zepben.ewb.database.sqlite.network.network_database_writer import *
from zepben.ewb.database.sqlite.network.network_service_writer import *
from zepben.ewb.database.sqlite.extensions.result_set import ResultSet
from zepben.ewb.database.sqlite.common.base_cim_reader import *
from zepben.ewb.database.sqlite.common.base_service_reader import *
from zepben.ewb.database.sqlite.common.metadata_collection_reader import *
from zepben.ewb.database.sqlite.common.metadata_entry_reader import *
from zepben.ewb.database.sqlite.customer.customer_cim_reader import *
Expand Down
34 changes: 21 additions & 13 deletions src/zepben/ewb/database/sqlite/common/base_cim_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
# 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__ = ["BaseCimReader"]

import logging
from abc import ABC
from typing import Callable, Optional, Type
from typing import Callable, Optional, Type, TYPE_CHECKING

from zepben.ewb.database.sqlite.common.reader_exceptions import DuplicateMRIDException
from zepben.ewb.database.sqlite.extensions.result_set import ResultSet
Expand All @@ -20,10 +22,14 @@
from zepben.ewb.model.cim.iec61968.common.document import Document
from zepben.ewb.model.cim.iec61968.common.organisation import Organisation
from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole
from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject, TIdentifiedObject
from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable
from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject
from zepben.ewb.model.cim.iec61970.base.core.name_type import NameType
from zepben.ewb.services.common.base_service import BaseService

if TYPE_CHECKING:
from zepben.ewb.model.cim.iec61970.base.core.identifiable import TIdentifiable


class BaseCimReader(ABC):
"""
Expand Down Expand Up @@ -154,28 +160,30 @@ def load_name_types(self, table: TableNameTypes, result_set: ResultSet, set_last
:raises SQLException: For any errors encountered reading from the database.
"""
# noinspection PyArgumentList
name_type = NameType(set_last_name_type(result_set.get_string(table.name_.query_index)))
name_type = NameType(name=set_last_name_type(result_set.get_string(table.name_.query_index)))
name_type.description = result_set.get_string(table.description.query_index)

return self._add_or_throw_name_type(name_type)

def _add_or_throw(self, identified_object: IdentifiedObject) -> bool:
#############
# End Model #
#############

def _add_or_throw(self, identifiable: Identifiable) -> bool:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Not for here, but the schema tests weren't modified so here is where I am choosing to link it....

CimDatabaseSchemaCommonTests.create_identified_object and its overrides/usage haven't been updated. These probably don't really matter at this stage, but would be nice to keep them inline with the JVM side.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done?

"""
Try and add the `identified_object` to the `service`, and throw an `Exception` if unsuccessful.
Try and add the `identifiable` to the `service`, and throw an `Exception` if unsuccessful.

:param identified_object: The `IdentifiedObject` to add to the `service`.
:param identifiable: The `identifiable` to add to the `service`.

:return: True in all instances, otherwise it throws.
:raises DuplicateMRIDException: If the `IdentifiedObject.mRID` has already been used.
:raises UnsupportedIdentifiedObjectException: If the `IdentifiedObject` is not supported by the `service`. This is an indication of an internal coding
issue, rather than a problem with the data being read, and in a correctly configured system will never occur.
:raises DuplicateMRIDException: If the `identifiable.mRID` has already been used.
"""
if self._service.add(identified_object):
if self._service.add(identifiable):
return True
else:
duplicate = self._service.get(identified_object.mrid)
duplicate = self._service.get(identifiable.mrid)
raise DuplicateMRIDException(
f"Failed to load {identified_object}. " +
f"Failed to load {identifiable}. " +
f"Unable to add to service '{self._service.name}': duplicate MRID ({duplicate})"
)

Expand All @@ -196,7 +204,7 @@ def _add_or_throw_name_type(self, name_type: NameType) -> bool:
f"Unable to add to service '{self._service.name}': duplicate NameType)"
)

def _ensure_get(self, mrid: Optional[str], type_: Type[TIdentifiedObject] = IdentifiedObject) -> Optional[TIdentifiedObject]:
def _ensure_get(self, mrid: Optional[str], type_: Type[TIdentifiable] = Identifiable) -> Optional[TIdentifiable]:
"""
Optionally get an object associated with this service and throw if it is not found.

Expand Down
58 changes: 28 additions & 30 deletions src/zepben/ewb/database/sqlite/common/base_service_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
# 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__ = ["BaseServiceWriter"]

from abc import ABC, abstractmethod
from typing import Callable, Type

from zepben.ewb.database.sqlite.common.base_cim_writer import BaseCimWriter
from zepben.ewb.database.sqlite.common.base_collection_writer import BaseCollectionWriter
from zepben.ewb.model.cim.iec61970.base.core.identified_object import TIdentifiedObject
from zepben.ewb.model.cim.iec61970.base.core.name import Name
from zepben.ewb.model.cim.iec61970.base.core.name_type import NameType
from zepben.ewb.services.common.base_service import BaseService
from typing import Callable, Type, TYPE_CHECKING

if TYPE_CHECKING:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Although unlikely to be a problem, moving imports into the if TYPE_CHECKING: stops them being imported, which is a breaking change for anyone that imports this file and relies on these being available.

I assume this has been done as these types are only used for type checking. I also assume you missed the 99% of other cases where we don't if TYPE_CHECKING: guard the others across the code base 😁

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we <3 __all__

from zepben.ewb.database.sqlite.common.base_cim_writer import BaseCimWriter
from zepben.ewb.services.common.base_service import BaseService
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.model.cim.iec61970.base.core.name_type import NameType


class BaseServiceWriter(BaseCollectionWriter, ABC):
Expand Down Expand Up @@ -47,38 +50,33 @@ def _do_save(self) -> bool:
:return: True if the objects were successfully saved to the database, otherwise False
"""

"""
Save each object of the specified type using the provided `saver`.

@param T The type of object to save to the database.
@param saver The callback used to save the objects to the database. Will be called once for each object and should return True if the object is
successfully saved to the database.

:return: True if all objects are successfully saved to the database, otherwise False.
"""
def _save_each_object(self, type_: Type[Identifiable], saver: Callable[[Identifiable], bool]) -> bool:
"""
Save each object of the specified type using the provided `saver`.

@param T The type of object to save to the database.
@param saver The callback used to save the objects to the database. Will be called once for each object and should return True if the object is
successfully saved to the database.

def _save_each_object(self, type_: Type[TIdentifiedObject], saver: Callable[[TIdentifiedObject], bool]) -> bool:
:return: True if all objects are successfully saved to the database, otherwise False.
"""
status = True
for it in self._service.objects(type_):
status = status and self._validate_save_object(it, saver)

return status

"""
Validate that an object is actually saved to the database, logging an error if anything goes wrong.

@param T The type of object being saved.
@param it The object being saved.
@param saver The callback actually saving the object to the database.

:return: True if the object is successfully saved to the database, otherwise False.
"""
def _validate_save_object(self, it: Identifiable, saver: Callable[[Identifiable], bool]) -> bool:
"""
Validate that an object is actually saved to the database, logging an error if anything goes wrong.

def _validate_save_object(self, it: TIdentifiedObject, saver: Callable[[TIdentifiedObject], bool]) -> bool:
def log_error(e: Exception):
self._logger.error(f"Failed to save {it.__class__.__name__} {it.name} [{it.mrid}]: {e}")
@param T The type of object being saved.
@param it The object being saved.
@param saver The callback actually saving the object to the database.

return self._validate_save(it, saver, log_error)
:return: True if the object is successfully saved to the database, otherwise False.
"""
return self._validate_save(it, saver, lambda e: self._logger.error(f"Failed to save {it.__class__.__name__} {it.name} [{it.mrid}]: {e}"))

def _save_name_types(self) -> bool:
status = True
Expand Down
Loading
Loading