diff --git a/CHANGELOG.md b/CHANGELOG.md index 56eb2a23cd..f4702e18ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Changes are grouped as follows ## [Unreleased] ### Added +- Support for the `/simulators/models` and `/simulators/models/revisions` API endpoints. - Support for the `/simulators` and `/simulators/integration` API endpoints. ## [7.73.3] - 2025-02-07 diff --git a/cognite/client/_api/simulators/__init__.py b/cognite/client/_api/simulators/__init__.py index 091b910427..849fe088b1 100644 --- a/cognite/client/_api/simulators/__init__.py +++ b/cognite/client/_api/simulators/__init__.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, overload from cognite.client._api.simulators.integrations import SimulatorIntegrationsAPI +from cognite.client._api.simulators.models import SimulatorModelsAPI from cognite.client._api_client import APIClient from cognite.client._constants import DEFAULT_LIMIT_READ from cognite.client.data_classes.simulators.simulators import Simulator, SimulatorList @@ -20,6 +21,7 @@ class SimulatorsAPI(APIClient): def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: super().__init__(config, api_version, cognite_client) self.integrations = SimulatorIntegrationsAPI(config, api_version, cognite_client) + self.models = SimulatorModelsAPI(config, api_version, cognite_client) self._warning = FeaturePreviewWarning( api_maturity="General Availability", sdk_maturity="alpha", feature_name="Simulators" ) diff --git a/cognite/client/_api/simulators/integrations.py b/cognite/client/_api/simulators/integrations.py index 1c8699e83b..40b179c1a0 100644 --- a/cognite/client/_api/simulators/integrations.py +++ b/cognite/client/_api/simulators/integrations.py @@ -93,7 +93,7 @@ def list( >>> res = client.simulators.integrations.list() Filter integrations by active status: - >>> from cognite.client.data_classes.simulators.filters import SimulatorIntegrationFilter + >>> from cognite.client.data_classes.simulators import SimulatorIntegrationFilter >>> res = client.simulators.integrations.list( ... filter=SimulatorIntegrationFilter(active=True)) """ @@ -110,13 +110,13 @@ def list( def delete( self, id: int | Sequence[int] | None = None, - external_ids: str | SequenceNotStr[str] | SequenceNotStr[str] | None = None, + external_id: str | SequenceNotStr[str] | None = None, ) -> None: """`Delete one or more integrations `_ Args: id (int | Sequence[int] | None): Id or list of ids - external_ids (str | SequenceNotStr[str] | SequenceNotStr[str] | None): external_ids of simulator integrations to delete. + external_id (str | SequenceNotStr[str] | None): External_id(s) of simulator integrations to delete Examples: @@ -127,6 +127,6 @@ def delete( >>> client.simulators.integrations.delete(id=[1,2,3], external_id="foo") """ self._delete_multiple( - identifiers=IdentifierSequence.load(ids=id, external_ids=external_ids), + identifiers=IdentifierSequence.load(ids=id, external_ids=external_id), wrap_ids=True, ) diff --git a/cognite/client/_api/simulators/models.py b/cognite/client/_api/simulators/models.py new file mode 100644 index 0000000000..cf3c1d68a9 --- /dev/null +++ b/cognite/client/_api/simulators/models.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from typing import TYPE_CHECKING, Any, NoReturn, overload + +from cognite.client._api.simulators.models_revisions import SimulatorModelRevisionsAPI +from cognite.client._api_client import APIClient +from cognite.client._constants import DEFAULT_LIMIT_READ +from cognite.client.data_classes._base import CogniteFilter +from cognite.client.data_classes.simulators.filters import SimulatorModelsFilter +from cognite.client.data_classes.simulators.models import ( + CreatedTimeSort, + SimulatorModel, + SimulatorModelList, + SimulatorModelUpdate, + SimulatorModelWrite, +) +from cognite.client.utils._experimental import FeaturePreviewWarning +from cognite.client.utils._identifier import IdentifierSequence +from cognite.client.utils._validation import assert_type +from cognite.client.utils.useful_types import SequenceNotStr + +if TYPE_CHECKING: + from cognite.client import ClientConfig, CogniteClient + + +class SimulatorModelsAPI(APIClient): + _RESOURCE_PATH = "/simulators/models" + + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self.revisions = SimulatorModelRevisionsAPI(config, api_version, cognite_client) + self._warning = FeaturePreviewWarning( + api_maturity="General Availability", sdk_maturity="alpha", feature_name="Simulators" + ) + self._RETRIEVE_LIMIT = 1 + self._CREATE_LIMIT = 1 + self._DELETE_LIMIT = 1 + + def list( + self, + limit: int = DEFAULT_LIMIT_READ, + filter: SimulatorModelsFilter | dict[str, Any] | None = None, + sort: CreatedTimeSort | None = None, + ) -> SimulatorModelList: + """`Filter simulator models `_ + Retrieves a list of simulator models that match the given criteria + Args: + limit (int): Maximum number of results to return. Defaults to 25. Set to -1, float(“inf”) or None to return all items. + filter (SimulatorModelsFilter | dict[str, Any] | None): Filter to apply. + sort (CreatedTimeSort | None): The criteria to sort by. + Returns: + SimulatorModelList: List of simulator models + + Examples: + List simulator models: + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.simulators.models.list() + + Specify filter and sort order: + >>> from cognite.client.data_classes.simulators import SimulatorModelsFilter, CreatedTimeSort + >>> res = client.simulators.models.list( + ... filter=SimulatorModelsFilter(simulator_external_ids=["simulator_external_id"]), + ... sort=CreatedTimeSort(order="asc") + ... ) + + """ + self._warning.warn() + return self._list( + method="POST", + limit=limit, + resource_cls=SimulatorModel, + list_cls=SimulatorModelList, + sort=[CreatedTimeSort.load(sort).dump()] if sort else None, + filter=filter.dump(camel_case=True) if isinstance(filter, CogniteFilter) else filter, + ) + + @overload + def retrieve(self, id: None = None, external_id: None = None) -> NoReturn: ... + + @overload + def retrieve(self, id: int, external_id: None = None) -> SimulatorModel | None: ... + + @overload + def retrieve( + self, + id: None, + external_id: str, + ) -> SimulatorModel | None: ... + + @overload + def retrieve( + self, + id: Sequence[int] | None = None, + external_id: SequenceNotStr[str] | None = None, + ) -> SimulatorModelList | None: ... + + def retrieve( + self, + id: int | Sequence[int] | None = None, + external_id: str | SequenceNotStr[str] | None = None, + ) -> SimulatorModel | SimulatorModelList | None: + """`Retrieve simulator model(s) `_ + Retrieve one or more simulator models by ID(s) or external ID(s) + Args: + id (int | Sequence[int] | None): The id of the simulator model. + external_id (str | SequenceNotStr[str] | None): The external id of the simulator model. + Returns: + SimulatorModel | SimulatorModelList | None: Requested simulator model(s) + Examples: + Get simulator model by id: + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.simulators.models.retrieve(id=1) + + Get simulator model by external id: + >>> res = client.simulators.models.retrieve(external_id="model_external_id") + + Get multiple simulator models by ids: + >>> res = client.simulators.models.retrieve(id=[1,2]) + + Get multiple simulator models by external ids: + >>> res = client.simulators.models.retrieve(external_id=["model_external_id", "model_external_id2"]) + """ + self._warning.warn() + + return self._retrieve_multiple( + list_cls=SimulatorModelList, + resource_cls=SimulatorModel, + identifiers=IdentifierSequence.load(ids=id, external_ids=external_id), + ) + + def __iter__(self) -> Iterator[SimulatorModel]: + """Iterate over simulator models + + Fetches simulator models as they are iterated over, so you keep a limited number of simulator models in memory. + + Returns: + Iterator[SimulatorModel]: yields Simulator model one by one. + """ + return self() + + @overload + def __call__( + self, chunk_size: None = None, filter: SimulatorModelsFilter | None = None, limit: int | None = None + ) -> Iterator[SimulatorModel]: ... + + @overload + def __call__( + self, chunk_size: int, filter: SimulatorModelsFilter | None = None, limit: int | None = None + ) -> Iterator[SimulatorModelList]: ... + + def __call__( + self, chunk_size: int | None = None, filter: SimulatorModelsFilter | None = None, limit: int | None = None + ) -> Iterator[SimulatorModel] | Iterator[SimulatorModelList]: + """Iterate over simulator simulator models + + Fetches simulator models as they are iterated over, so you keep a limited number of simulator models in memory. + + Args: + chunk_size (int | None): Number of simulator models to return in each chunk. Defaults to yielding one simulator model a time. + filter (SimulatorModelsFilter | None): Filter to apply on the model revisions list. + limit (int | None): Maximum number of simulator models to return. Defaults to return all items. + + Returns: + Iterator[SimulatorModel] | Iterator[SimulatorModelList]: yields Simulator one by one if chunk is not specified, else SimulatorList objects. + """ + return self._list_generator( + list_cls=SimulatorModelList, + resource_cls=SimulatorModel, + method="POST", + filter=filter.dump() if isinstance(filter, CogniteFilter) else filter, + chunk_size=chunk_size, + limit=limit, + ) + + @overload + def create(self, model: SimulatorModelWrite) -> SimulatorModel: ... + + @overload + def create(self, model: Sequence[SimulatorModelWrite]) -> SimulatorModelList: ... + + def create(self, model: SimulatorModelWrite | Sequence[SimulatorModelWrite]) -> SimulatorModel | SimulatorModelList: + """`Create simulator models `_ + Args: + model (SimulatorModelWrite | Sequence[SimulatorModelWrite]): The model to create. + Returns: + SimulatorModel | SimulatorModelList: Created simulator model(s) + Examples: + Create new simulator models: + >>> from cognite.client import CogniteClient + >>> from cognite.client.data_classes.simulators import SimulatorModelWrite + >>> client = CogniteClient() + >>> models = [ + ... SimulatorModelWrite( + ... name="model1", simulator_external_id="sim1", type="SteadyState", + ... data_set_id=1, external_id="model_external_id" + ... ), + ... SimulatorModelWrite( + ... name="model2", simulator_external_id="sim2", type="SteadyState", + ... data_set_id=2, external_id="model_external_id2" + ... ) + ... ] + >>> res = client.simulators.models.create(models) + """ + assert_type(model, "simulator_model", [SimulatorModelWrite, Sequence]) + + return self._create_multiple( + list_cls=SimulatorModelList, + resource_cls=SimulatorModel, + items=model, + input_resource_cls=SimulatorModelWrite, + resource_path=self._RESOURCE_PATH, + ) + + def delete( + self, + id: int | Sequence[int] | None = None, + external_id: str | SequenceNotStr[str] | None = None, + ) -> None: + """`Delete simulator models `_ + Args: + id (int | Sequence[int] | None): id (or sequence of ids) for the model(s) to delete. + external_id (str | SequenceNotStr[str] | None): external id (or sequence of external ids) for the model(s) to delete. + Examples: + Delete models by id or external id: + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> client.simulators.models.delete(id=[1,2,3], external_id="model_external_id") + """ + self._delete_multiple( + identifiers=IdentifierSequence.load(ids=id, external_ids=external_id), + wrap_ids=True, + resource_path=self._RESOURCE_PATH, + ) + + @overload + def update( + self, + item: Sequence[SimulatorModel | SimulatorModelWrite | SimulatorModelUpdate], + ) -> SimulatorModelList: ... + + @overload + def update( + self, + item: SimulatorModel | SimulatorModelWrite | SimulatorModelUpdate, + ) -> SimulatorModel: ... + + def update( + self, + item: SimulatorModel + | SimulatorModelWrite + | SimulatorModelUpdate + | Sequence[SimulatorModel | SimulatorModelWrite | SimulatorModelUpdate], + ) -> SimulatorModel | SimulatorModelList: + return self._update_multiple( + list_cls=SimulatorModelList, resource_cls=SimulatorModel, update_cls=SimulatorModelUpdate, items=item + ) diff --git a/cognite/client/_api/simulators/models_revisions.py b/cognite/client/_api/simulators/models_revisions.py new file mode 100644 index 0000000000..266c8efb32 --- /dev/null +++ b/cognite/client/_api/simulators/models_revisions.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from typing import TYPE_CHECKING, Any, NoReturn, overload + +from cognite.client._api_client import APIClient +from cognite.client._constants import DEFAULT_LIMIT_READ +from cognite.client.data_classes._base import CogniteFilter +from cognite.client.data_classes.simulators.filters import SimulatorModelRevisionsFilter +from cognite.client.data_classes.simulators.models import ( + CreatedTimeSort, + SimulatorModelRevision, + SimulatorModelRevisionList, + SimulatorModelRevisionWrite, +) +from cognite.client.utils._experimental import FeaturePreviewWarning +from cognite.client.utils._identifier import IdentifierSequence +from cognite.client.utils._validation import assert_type +from cognite.client.utils.useful_types import SequenceNotStr + +if TYPE_CHECKING: + from cognite.client import ClientConfig, CogniteClient + + +class SimulatorModelRevisionsAPI(APIClient): + _RESOURCE_PATH = "/simulators/models/revisions" + + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self._warning = FeaturePreviewWarning( + api_maturity="General Availability", sdk_maturity="alpha", feature_name="Simulators" + ) + self._CREATE_LIMIT = 1 + self._RETRIEVE_LIMIT = 1 + + def list( + self, + limit: int = DEFAULT_LIMIT_READ, + sort: CreatedTimeSort | None = None, + filter: SimulatorModelRevisionsFilter | dict[str, Any] | None = None, + ) -> SimulatorModelRevisionList: + """`Filter simulator model revisions `_ + Retrieves a list of simulator model revisions that match the given criteria + Args: + limit (int): Maximum number of results to return. Defaults to 25. Set to -1, float(“inf”) or None to return all items. + sort (CreatedTimeSort | None): The criteria to sort by. + filter (SimulatorModelRevisionsFilter | dict[str, Any] | None): Filter to apply. + Returns: + SimulatorModelRevisionList: List of simulator model revisions + Examples: + List simulator model revisions: + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.simulators.models.revisions.list() + + Specify filter and sort order: + >>> from cognite.client.data_classes.simulators import SimulatorModelRevisionsFilter, CreatedTimeSort + >>> res = client.simulators.models.revisions.list( + ... filter=SimulatorModelRevisionsFilter(model_external_ids=["model_external_id"]), + ... sort=CreatedTimeSort(order="asc") + ... ) + """ + self._warning.warn() + return self._list( + method="POST", + limit=limit, + resource_cls=SimulatorModelRevision, + list_cls=SimulatorModelRevisionList, + sort=[CreatedTimeSort.load(sort).dump()] if sort else None, + filter=filter.dump(camel_case=True) if isinstance(filter, CogniteFilter) else filter, + ) + + @overload + def retrieve(self, id: None = None, external_id: None = None) -> NoReturn: ... + + @overload + def retrieve(self, id: int, external_id: None = None) -> SimulatorModelRevision | None: ... + + @overload + def retrieve( + self, + id: None, + external_id: str, + ) -> SimulatorModelRevision | None: ... + + @overload + def retrieve( + self, + id: int | Sequence[int] | None = None, + external_id: str | SequenceNotStr[str] | None = None, + ) -> SimulatorModelRevision | SimulatorModelRevisionList | None: ... + + def retrieve( + self, + id: int | Sequence[int] | None = None, + external_id: str | SequenceNotStr[str] | None = None, + ) -> SimulatorModelRevision | SimulatorModelRevisionList | None: + """`Retrieve simulator model revision(s) `_ + Retrieve one or more simulator model revisions by ID(s) or external ID(s) + Args: + id (int | Sequence[int] | None): The ids of the simulator model revisions. + external_id (str | SequenceNotStr[str] | None): The external ids of the simulator model revisions. + Returns: + SimulatorModelRevision | SimulatorModelRevisionList | None: Requested simulator model revision(s) + Examples: + Get simulator model revision by id: + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.simulators.models.revisions.retrieve(id=1) + + Get simulator model revision by external id: + >>> res = client.simulators.models.revisions.retrieve(external_id="revision_external_id") + + Get multiple simulator model revisions by ids: + >>> res = client.simulators.models.revisions.retrieve(id=[1,2]) + + Get multiple simulator model revisions by external ids: + >>> res = client.simulators.models.revisions.retrieve(external_id=["revision1", "revision2"]) + """ + self._warning.warn() + + return self._retrieve_multiple( + list_cls=SimulatorModelRevisionList, + resource_cls=SimulatorModelRevision, + identifiers=IdentifierSequence.load(ids=id, external_ids=external_id), + ) + + def __iter__(self) -> Iterator[SimulatorModelRevision]: + """Iterate over simulator model revisions + + Fetches simulator model revisions as they are iterated over, so you keep a limited number of simulator model revisions in memory. + + Returns: + Iterator[SimulatorModelRevision]: yields Simulator model revisions one by one. + """ + return self() + + @overload + def __call__( + self, chunk_size: int, filter: SimulatorModelRevisionsFilter | None = None, limit: int | None = None + ) -> Iterator[SimulatorModelRevisionList]: ... + + @overload + def __call__( + self, chunk_size: None = None, filter: SimulatorModelRevisionsFilter | None = None, limit: int | None = None + ) -> Iterator[SimulatorModelRevision]: ... + + def __call__( + self, + chunk_size: int | None = None, + filter: SimulatorModelRevisionsFilter | None = None, + limit: int | None = None, + ) -> Iterator[SimulatorModelRevision] | Iterator[SimulatorModelRevisionList]: + """Iterate over simulator simulator model revisions + + Fetches simulator model revisions as they are iterated over, so you keep a limited number of simulator model revisions in memory. + + Args: + chunk_size (int | None): Number of simulator model revisions to return in each chunk. Defaults to yielding one simulator model revision a time. + filter (SimulatorModelRevisionsFilter | None): Filter to apply on the model revisions list. + limit (int | None): Maximum number of simulator model revisions to return. Defaults to return all items. + + Returns: + Iterator[SimulatorModelRevision] | Iterator[SimulatorModelRevisionList]: yields Simulator one by one if chunk is not specified, else SimulatorList objects. + """ + return self._list_generator( + list_cls=SimulatorModelRevisionList, + resource_cls=SimulatorModelRevision, + method="POST", + filter=filter.dump() if isinstance(filter, CogniteFilter) else filter, + chunk_size=chunk_size, + limit=limit, + ) + + @overload + def create(self, revision: SimulatorModelRevisionWrite) -> SimulatorModelRevision: ... + + @overload + def create(self, revision: Sequence[SimulatorModelRevisionWrite]) -> SimulatorModelRevisionList: ... + + def create( + self, revision: SimulatorModelRevisionWrite | Sequence[SimulatorModelRevisionWrite] + ) -> SimulatorModelRevision | SimulatorModelRevisionList: + """`Create one or more simulator model revisions. `_ + You can create an arbitrary number of simulator model revisions. + Args: + revision (SimulatorModelRevisionWrite | Sequence[SimulatorModelRevisionWrite]): The model revision to create. + Returns: + SimulatorModelRevision | SimulatorModelRevisionList: Created simulator model(s) + Examples: + Create new simulator models: + >>> from cognite.client import CogniteClient + >>> from cognite.client.data_classes.simulators import SimulatorModelRevisionWrite + >>> client = CogniteClient() + >>> models = [ + ... SimulatorModelRevisionWrite(external_id="model1", file_id=1, model_external_id="a_1"), + ... SimulatorModelRevisionWrite(external_id="model2", file_id=2, model_external_id="a_2") + ... ] + >>> res = client.simulators.models.revisions.create(models) + """ + assert_type(revision, "simulator_model_revision", [SimulatorModelRevisionWrite, Sequence]) + + return self._create_multiple( + list_cls=SimulatorModelRevisionList, + resource_cls=SimulatorModelRevision, + items=revision, + input_resource_cls=SimulatorModelRevisionWrite, + ) diff --git a/cognite/client/_cognite_client.py b/cognite/client/_cognite_client.py index b673feb3d3..7a35b152d5 100644 --- a/cognite/client/_cognite_client.py +++ b/cognite/client/_cognite_client.py @@ -87,7 +87,6 @@ def __init__(self, config: ClientConfig | None = None) -> None: self.workflows = WorkflowAPI(self._config, self._API_VERSION, self) self.units = UnitAPI(self._config, self._API_VERSION, self) self.simulators = SimulatorsAPI(self._config, self._API_VERSION, self) - # APIs just using base_url: self._api_client = APIClient(self._config, api_version=None, cognite_client=self) diff --git a/cognite/client/data_classes/simulators/__init__.py b/cognite/client/data_classes/simulators/__init__.py index 6c3fc96dc0..fb0d7f88cc 100644 --- a/cognite/client/data_classes/simulators/__init__.py +++ b/cognite/client/data_classes/simulators/__init__.py @@ -1,5 +1,20 @@ from __future__ import annotations +from cognite.client.data_classes.simulators.filters import ( + SimulatorIntegrationFilter, + SimulatorModelRevisionsFilter, + SimulatorModelsFilter, +) +from cognite.client.data_classes.simulators.models import ( + CreatedTimeSort, + PropertySort, + SimulatorModel, + SimulatorModelList, + SimulatorModelRevision, + SimulatorModelRevisionList, + SimulatorModelRevisionWrite, + SimulatorModelWrite, +) from cognite.client.data_classes.simulators.simulators import ( Simulator, SimulatorIntegration, @@ -12,10 +27,21 @@ ) __all__ = [ + "CreatedTimeSort", + "PropertySort", "Simulator", "SimulatorIntegration", + "SimulatorIntegrationFilter", "SimulatorIntegrationList", "SimulatorList", + "SimulatorModel", + "SimulatorModelList", + "SimulatorModelRevision", + "SimulatorModelRevisionList", + "SimulatorModelRevisionWrite", + "SimulatorModelRevisionsFilter", + "SimulatorModelWrite", + "SimulatorModelsFilter", "SimulatorStep", "SimulatorStepField", "SimulatorStepOption", diff --git a/cognite/client/data_classes/simulators/filters.py b/cognite/client/data_classes/simulators/filters.py index e912b961a6..8f40433869 100644 --- a/cognite/client/data_classes/simulators/filters.py +++ b/cognite/client/data_classes/simulators/filters.py @@ -1,5 +1,7 @@ from __future__ import annotations +from collections.abc import Sequence + from cognite.client.data_classes._base import CogniteFilter from cognite.client.utils.useful_types import SequenceNotStr @@ -12,3 +14,22 @@ def __init__( ) -> None: self.simulator_external_ids = simulator_external_ids self.active = active + + +class SimulatorModelsFilter(CogniteFilter): + def __init__( + self, + simulator_external_ids: Sequence[str] | None = None, + ) -> None: + self.simulator_external_ids = ( + [simulator_external_ids] if isinstance(simulator_external_ids, str) else simulator_external_ids + ) + + +class SimulatorModelRevisionsFilter(CogniteFilter): + def __init__( + self, + model_external_ids: Sequence[str] | None = None, + all_versions: bool | None = None, + ) -> None: + self.model_external_ids = [model_external_ids] if isinstance(model_external_ids, str) else model_external_ids diff --git a/cognite/client/data_classes/simulators/models.py b/cognite/client/data_classes/simulators/models.py new file mode 100644 index 0000000000..b2eb14486d --- /dev/null +++ b/cognite/client/data_classes/simulators/models.py @@ -0,0 +1,334 @@ +from __future__ import annotations + +from abc import ABC +from typing import TYPE_CHECKING, Any, Literal + +from typing_extensions import Self + +from cognite.client.data_classes._base import ( + CognitePrimitiveUpdate, + CogniteResource, + CogniteResourceList, + CogniteSort, + CogniteUpdate, + ExternalIDTransformerMixin, + IdTransformerMixin, + PropertySpec, + WriteableCogniteResource, + WriteableCogniteResourceList, +) + +if TYPE_CHECKING: + from cognite.client import CogniteClient + + +class PropertySort(CogniteSort): + def dump(self, camel_case: bool = True) -> dict[str, Any]: + dumped = super().dump(camel_case=camel_case) + dumped["property"] = self.property + return dumped + + +class CreatedTimeSort(PropertySort): + def __init__( + self, + property: Literal["createdTime"] = "createdTime", + order: Literal["asc", "desc"] = "asc", + ): + super().__init__(property, order) + + +class SimulatorModelRevisionCore(WriteableCogniteResource["SimulatorModelRevisionWrite"], ABC): + def __init__( + self, + external_id: str, + model_external_id: str, + file_id: int, + description: str | None = None, + ) -> None: + self.external_id = external_id + self.model_external_id = model_external_id + self.file_id = file_id + self.description = description + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + external_id=resource["externalId"], + model_external_id=resource["modelExternalId"], + file_id=resource["fileId"], + description=resource.get("description"), + ) + + +class SimulatorModelRevisionWrite(SimulatorModelRevisionCore): + def as_write(self) -> SimulatorModelRevisionWrite: + """Returns a writeable version of this resource""" + return self + + @classmethod + def _load( + cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None + ) -> SimulatorModelRevisionWrite: + return cls( + external_id=resource["externalId"], + model_external_id=resource["modelExternalId"], + file_id=resource["fileId"], + description=resource.get("description"), + ) + + +class SimulatorModelRevision(SimulatorModelRevisionCore): + """ + Simulator model revisions track changes and updates to a simulator model over time. + Each revision ensures that modifications to models are traceable and allows users to understand the evolution of a given model. + Args: + id (int): Internal id of the simulator model revision + external_id (str): External id of the simulator model revision + model_external_id (str): External id of the associated simulator model + file_id (int): The id of the file associated with the simulator model revision + created_time (int): The time when the simulator model revision was created + last_updated_time (int): The time when the simulator model revision was last updated + simulator_external_id (str): External id of the simulator associated with the simulator model revision + data_set_id (int): The id of the dataset associated with the simulator model revision + created_by_user_id (str): The id of the user who created the simulator model revision + status (str): The status of the simulator model revision + version_number (int): The version number of the simulator model revision + log_id (int): The id of the log associated with the simulator model revision + description (str | None): The description of the simulator model revision + status_message (str | None): The current status message of the simulator model revision + """ + + def __init__( + self, + id: int, + external_id: str, + model_external_id: str, + file_id: int, + created_time: int, + last_updated_time: int, + simulator_external_id: str, + data_set_id: int, + created_by_user_id: str, + status: str, + version_number: int, + log_id: int, + description: str | None = None, + status_message: str | None = None, + ) -> None: + super().__init__( + external_id=external_id, + model_external_id=model_external_id, + file_id=file_id, + description=description, + ) + self.id = id + self.created_time = created_time + self.last_updated_time = last_updated_time + self.data_set_id = data_set_id + self.created_by_user_id = created_by_user_id + self.status = status + self.version_number = version_number + self.log_id = log_id + self.status_message = status_message + self.simulator_external_id = simulator_external_id + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + id=resource["id"], + external_id=resource["externalId"], + simulator_external_id=resource["simulatorExternalId"], + model_external_id=resource["modelExternalId"], + data_set_id=resource["dataSetId"], + file_id=resource["fileId"], + created_by_user_id=resource["createdByUserId"], + status=resource["status"], + created_time=resource["createdTime"], + last_updated_time=resource["lastUpdatedTime"], + version_number=resource["versionNumber"], + log_id=resource["logId"], + description=resource.get("description"), + status_message=resource.get("statusMessage"), + ) + + def as_write(self) -> SimulatorModelRevisionWrite: + """Returns this SimulatorModelRevision in its writing version.""" + return SimulatorModelRevisionWrite( + external_id=self.external_id, + model_external_id=self.model_external_id, + file_id=self.file_id, + description=self.description, + ) + + +class SimulatorModelCore(WriteableCogniteResource["SimulatorModelWrite"], ABC): + """ + The simulator model resource represents an asset modeled in a simulator. + This asset could range from a pump or well to a complete processing facility or refinery. + The simulator model is the root of its associated revisions, routines, runs, and results. + The dataset assigned to a model is inherited by its children. Deleting a model also deletes all its children, thereby + maintaining the integrity and hierarchy of the simulation data. + Simulator model revisions track changes and updates to a simulator model over time. + Each revision ensures that modifications to models are traceable and allows users to understand the evolution of a given model. + This is the read/response format of a simulator model. + Args: + external_id (str): External id of the simulator model + simulator_external_id (str): External id of the associated simulator + data_set_id (int): The id of the dataset associated with the simulator model + name (str): The name of the simulator model + type (str): The type key of the simulator model + description (str | None): The description of the simulator model + """ + + def __init__( + self, + external_id: str, + simulator_external_id: str, + data_set_id: int, + name: str, + type: str, + description: str | None = None, + ) -> None: + self.external_id = external_id + self.simulator_external_id = simulator_external_id + self.data_set_id = data_set_id + self.name = name + self.type = type + self.description = description + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + external_id=resource["externalId"], + simulator_external_id=resource["simulatorExternalId"], + data_set_id=resource["dataSetId"], + name=resource["name"], + type=resource["type"], + description=resource.get("description"), + ) + + +class SimulatorModelWrite(SimulatorModelCore): + def as_write(self) -> SimulatorModelWrite: + return self + + +class SimulatorModel(SimulatorModelCore): + """ + The simulator model resource represents an asset modeled in a simulator. + This asset could range from a pump or well to a complete processing facility or refinery. + The simulator model is the root of its associated revisions, routines, runs, and results. + The dataset assigned to a model is inherited by its children. Deleting a model also deletes all its children, thereby + maintaining the integrity and hierarchy of the simulation data. + Simulator model revisions track changes and updates to a simulator model over time. + Each revision ensures that modifications to models are traceable and allows users to understand the evolution of a given model. + This is the read/response format of a simulator model. + Args: + id (int): A unique id of a simulator model + external_id (str): External id of the simulator model + simulator_external_id (str): External id of the associated simulator + data_set_id (int): The id of the dataset associated with the simulator model + name (str): The name of the simulator model + type (str): The type key of the simulator model + created_time (int): The time when the simulator model was created + last_updated_time (int): The time when the simulator model was last updated + description (str | None): The description of the simulator model + """ + + def __init__( + self, + id: int, + external_id: str, + simulator_external_id: str, + data_set_id: int, + name: str, + type: str, + created_time: int, + last_updated_time: int, + description: str | None = None, + ) -> None: + super().__init__( + external_id=external_id, + simulator_external_id=simulator_external_id, + data_set_id=data_set_id, + name=name, + type=type, + description=description, + ) + + self.id = id + self.created_time = created_time + self.last_updated_time = last_updated_time + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + id=resource["id"], + external_id=resource["externalId"], + simulator_external_id=resource["simulatorExternalId"], + data_set_id=resource["dataSetId"], + name=resource["name"], + type=resource["type"], + description=resource.get("description"), + created_time=resource["createdTime"], + last_updated_time=resource["lastUpdatedTime"], + ) + + def as_write(self) -> SimulatorModelWrite: + """Returns this SimulatorModel in its writing version.""" + return SimulatorModelWrite( + external_id=self.external_id, + simulator_external_id=self.simulator_external_id, + data_set_id=self.data_set_id, + name=self.name, + type=self.type, + description=self.description, + ) + + +class SimulatorModelWriteList(CogniteResourceList[SimulatorModelWrite], ExternalIDTransformerMixin): + _RESOURCE = SimulatorModelWrite + + +class SimulatorModelList(WriteableCogniteResourceList[SimulatorModelWrite, SimulatorModel], IdTransformerMixin): + _RESOURCE = SimulatorModel + + def as_write(self) -> SimulatorModelWriteList: + return SimulatorModelWriteList([a.as_write() for a in self.data], cognite_client=self._get_cognite_client()) + + +class SimulatorModelRevisionWriteList(CogniteResourceList[SimulatorModelRevisionWrite], ExternalIDTransformerMixin): + _RESOURCE = SimulatorModelRevisionWrite + + +class SimulatorModelRevisionList( + WriteableCogniteResourceList[SimulatorModelRevisionWrite, SimulatorModelRevision], IdTransformerMixin +): + _RESOURCE = SimulatorModelRevision + + def as_write(self) -> SimulatorModelRevisionWriteList: + return SimulatorModelRevisionWriteList( + [a.as_write() for a in self.data], cognite_client=self._get_cognite_client() + ) + + +class SimulatorModelUpdate(CogniteUpdate): + class _PrimitiveModelUpdate(CognitePrimitiveUpdate): + def set(self, value: Any) -> None: + self._set(value) + + @property + def name(self) -> _PrimitiveModelUpdate: + return SimulatorModelUpdate._PrimitiveModelUpdate(self, "name") + + @property + def description(self) -> _PrimitiveModelUpdate: + return SimulatorModelUpdate._PrimitiveModelUpdate(self, "description") + + @classmethod + def _get_update_properties(cls, item: CogniteResource | None = None) -> list[PropertySpec]: + return [ + PropertySpec("name"), + PropertySpec("description"), + ] diff --git a/cognite/client/testing.py b/cognite/client/testing.py index 2a56ea0e55..b5828d9692 100644 --- a/cognite/client/testing.py +++ b/cognite/client/testing.py @@ -48,6 +48,8 @@ from cognite.client._api.sequences import SequencesAPI, SequencesDataAPI from cognite.client._api.simulators import SimulatorsAPI from cognite.client._api.simulators.integrations import SimulatorIntegrationsAPI +from cognite.client._api.simulators.models import SimulatorModelsAPI +from cognite.client._api.simulators.models_revisions import SimulatorModelRevisionsAPI from cognite.client._api.synthetic_time_series import SyntheticDatapointsAPI from cognite.client._api.templates import ( TemplateGroupsAPI, @@ -153,6 +155,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.simulators = MagicMock(spec=SimulatorsAPI) self.simulators.integrations = MagicMock(spec_set=SimulatorIntegrationsAPI) + self.simulators.models = MagicMock(spec=SimulatorModelsAPI) + self.simulators.models.revisions = MagicMock(spec_set=SimulatorModelRevisionsAPI) self.sequences = MagicMock(spec=SequencesAPI) self.sequences.rows = MagicMock(spec_set=SequencesDataAPI) diff --git a/tests/tests_integration/test_api/test_simulators/conftest.py b/tests/tests_integration/test_api/test_simulators/conftest.py index cd93be94ff..26dd3dc1e5 100644 --- a/tests/tests_integration/test_api/test_simulators/conftest.py +++ b/tests/tests_integration/test_api/test_simulators/conftest.py @@ -1,12 +1,21 @@ from __future__ import annotations import time +from collections.abc import Iterator import pytest from cognite.client._cognite_client import CogniteClient from cognite.client.data_classes.data_sets import DataSetWrite -from tests.tests_integration.test_api.test_simulators.seed.data import resource_names, simulator, simulator_integration +from cognite.client.data_classes.files import FileMetadata +from cognite.client.data_classes.simulators.filters import SimulatorModelRevisionsFilter +from cognite.client.data_classes.simulators.models import SimulatorModelWrite +from tests.tests_integration.test_api.test_simulators.seed.data import ( + resource_names, + simulator, + simulator_integration, + simulator_model, +) @pytest.fixture(scope="session") @@ -23,7 +32,21 @@ def seed_resource_names(cognite_client: CogniteClient) -> dict[str, str]: @pytest.fixture(scope="session") -def seed_simulator(cognite_client: CogniteClient, seed_resource_names) -> None: +def seed_file(cognite_client: CogniteClient, seed_resource_names) -> FileMetadata | None: + data_set_id = seed_resource_names["simulator_test_data_set_id"] + file = cognite_client.files.retrieve(external_id=seed_resource_names["simulator_model_file_external_id"]) + if not file: + file = cognite_client.files.upload( + path="tests/tests_integration/test_api/test_simulators/seed/ShowerMixer.txt", + external_id=seed_resource_names["simulator_model_file_external_id"], + name="ShowerMixer.txt", + data_set_id=data_set_id, + ) + yield file + + +@pytest.fixture(scope="session") +def seed_simulator(cognite_client: CogniteClient, seed_resource_names) -> Iterator[None]: simulator_external_id = seed_resource_names["simulator_external_id"] simulators = cognite_client.simulators.list(limit=None) if not simulators.get(external_id=simulator_external_id): @@ -42,8 +65,57 @@ def seed_simulator_integration(cognite_client: CogniteClient, seed_simulator, se ) else: integration = simulator_integrations.get(external_id=simulator_integration["externalId"]) - # update hearbeat instead cognite_client.simulators.integrations._post( "/simulators/integrations/update", json={"items": [{"id": integration.id, "update": {"heartbeat": {"set": int(time.time() * 1000)}}}]}, ) + + +@pytest.fixture(scope="session") +def seed_simulator_models(cognite_client: CogniteClient, seed_simulator_integration, seed_resource_names) -> None: + model_unique_external_id = seed_resource_names["simulator_model_external_id"] + models = cognite_client.simulators.models.list(limit=None) + model_exists = models.get(external_id=model_unique_external_id) + + if model_exists: + return + + simulator_model["dataSetId"] = seed_resource_names["simulator_test_data_set_id"] + simulator_model["externalId"] = model_unique_external_id + + model = SimulatorModelWrite._load( + { + "externalId": simulator_model["externalId"], + "simulatorExternalId": simulator_model["simulatorExternalId"], + "dataSetId": simulator_model["dataSetId"], + "name": simulator_model["name"], + "type": simulator_model["type"], + "description": simulator_model["description"], + } + ) + cognite_client.simulators.models.create(model) + + +@pytest.fixture(scope="session") +def seed_simulator_model_revisions(cognite_client: CogniteClient, seed_simulator_models, seed_file) -> None: + model_unique_external_id = resource_names["simulator_model_external_id"] + model_revision_unique_external_id = resource_names["simulator_model_revision_external_id"] + model_revisions = cognite_client.simulators.models.revisions.list( + filter=SimulatorModelRevisionsFilter(model_external_ids=[model_unique_external_id]) + ) + model_revision_not_exists = not model_revisions.get(external_id=model_revision_unique_external_id) + + if model_revision_not_exists: + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators/models/revisions", + json={ + "items": [ + { + "description": "test sim model revision description", + "fileId": seed_file.id, + "modelExternalId": model_unique_external_id, + "externalId": model_revision_unique_external_id, + } + ] + }, + ) diff --git a/tests/tests_integration/test_api/test_simulators/seed/ShowerMixer.txt b/tests/tests_integration/test_api/test_simulators/seed/ShowerMixer.txt new file mode 100644 index 0000000000..c130588885 --- /dev/null +++ b/tests/tests_integration/test_api/test_simulators/seed/ShowerMixer.txt @@ -0,0 +1 @@ +test model \ No newline at end of file diff --git a/tests/tests_integration/test_api/test_simulators/seed/data.py b/tests/tests_integration/test_api/test_simulators/seed/data.py index 757d286444..562fad9fa4 100644 --- a/tests/tests_integration/test_api/test_simulators/seed/data.py +++ b/tests/tests_integration/test_api/test_simulators/seed/data.py @@ -6,7 +6,7 @@ "simulator_integration_external_id": "py_sdk_integration_tests_connector", "simulator_model_external_id": "py_sdk_integration_tests_model", "simulator_model_revision_external_id": "pysdk_model_revision", - "simulator_model_file_external_id": "ShowerMixer_simulator_model_file_3", + "simulator_model_file_external_id": "ShowerMixer_simulator_model_file_5", "simulator_routine_external_id": "pysdk_routine", "simulator_routine_revision_external_id": "pysdk_routine_revision", "simulator_test_data_set_id": None, @@ -16,7 +16,7 @@ simulator = { "name": resource_names["simulator_external_id"], "externalId": resource_names["simulator_external_id"], - "fileExtensionTypes": ["dwxmz"], + "fileExtensionTypes": ["txt"], "modelTypes": [{"name": "Steady State", "key": "SteadyState"}], "stepFields": [ { @@ -214,3 +214,12 @@ "connectorStatus": "IDLE", "connectorStatusUpdatedTime": 0, } + +simulator_model = { + "externalId": resource_names["simulator_model_external_id"], + "simulatorExternalId": resource_names["simulator_external_id"], + "name": "Test Simulator Model", + "description": "Test Simulator Model Desc", + "dataSetId": resource_names["simulator_test_data_set_id"], + "type": "SteadyState", +} diff --git a/tests/tests_integration/test_api/test_simulators/test_integrations.py b/tests/tests_integration/test_api/test_simulators/test_integrations.py index 46b532da6f..d2b8af216d 100644 --- a/tests/tests_integration/test_api/test_simulators/test_integrations.py +++ b/tests/tests_integration/test_api/test_simulators/test_integrations.py @@ -16,7 +16,6 @@ def test_list_integrations(self, cognite_client: CogniteClient) -> None: assert len(integrations) > 0 def test_filter_integrations(self, cognite_client: CogniteClient, seed_resource_names) -> None: - # quick test of the iterator for integration in cognite_client.simulators.integrations(filter=SimulatorIntegrationFilter(active=True)): assert integration.active is True @@ -52,7 +51,7 @@ def test_delete_integrations(self, cognite_client: CogniteClient, seed_resource_ all_integrations = cognite_client.simulators.integrations.list(limit=None) assert all_integrations.get(external_id=simulator_integration["externalId"]) is not None - cognite_client.simulators.integrations.delete(external_ids=simulator_integration["externalId"]) + cognite_client.simulators.integrations.delete(external_id=simulator_integration["externalId"]) all_integrations = cognite_client.simulators.integrations.list(limit=None) assert all_integrations.get(external_id=simulator_integration["externalId"]) is None diff --git a/tests/tests_integration/test_api/test_simulators/test_models.py b/tests/tests_integration/test_api/test_simulators/test_models.py new file mode 100644 index 0000000000..39188e88ec --- /dev/null +++ b/tests/tests_integration/test_api/test_simulators/test_models.py @@ -0,0 +1,146 @@ +import pytest + +from cognite.client._cognite_client import CogniteClient +from cognite.client.data_classes.files import FileMetadata +from cognite.client.data_classes.simulators.filters import SimulatorModelRevisionsFilter, SimulatorModelsFilter +from cognite.client.data_classes.simulators.models import ( + SimulatorModelRevisionWrite, + SimulatorModelWrite, +) +from cognite.client.utils._text import random_string + + +@pytest.mark.usefixtures( + "seed_resource_names", + "seed_simulator_model_revisions", +) +class TestSimulatorModels: + def test_list_models(self, cognite_client: CogniteClient, seed_resource_names) -> None: + models = cognite_client.simulators.models.list( + limit=5, filter=SimulatorModelsFilter(simulator_external_ids=[seed_resource_names["simulator_external_id"]]) + ) + + model_ids = [] + for model in cognite_client.simulators.models(limit=2): + assert model.created_time is not None + model_ids.append(model.id) + + found_models = cognite_client.simulators.models.retrieve(id=model_ids) + + assert len(found_models) == len(model_ids) + + assert len(models) > 0 + + def test_retrieve_model(self, cognite_client: CogniteClient, seed_resource_names) -> None: + model_external_id = seed_resource_names["simulator_model_external_id"] + model = cognite_client.simulators.models.retrieve(external_id=model_external_id) + assert model is not None + assert model.external_id == model_external_id + assert model.created_time > 0 + assert model.last_updated_time >= model.created_time + assert model.type == "SteadyState" + assert model.data_set_id == seed_resource_names["simulator_test_data_set_id"] + assert model.name == "Test Simulator Model" + + def test_list_model_revisions(self, cognite_client: CogniteClient, seed_resource_names) -> None: + model_external_id = seed_resource_names["simulator_model_external_id"] + + revisions = cognite_client.simulators.models.revisions.list( + limit=5, + filter=SimulatorModelRevisionsFilter(model_external_ids=[model_external_id]), + ) + + model_revision_ids = [] + for revision in cognite_client.simulators.models.revisions(limit=2): + assert revision.created_time is not None + model_revision_ids.append(revision.id) + + found_revisions = cognite_client.simulators.models.revisions.retrieve(id=model_revision_ids) + assert len(found_revisions) == len(model_revision_ids) + + assert len(revisions) > 0 + + def test_retrieve_model_revision(self, cognite_client: CogniteClient, seed_resource_names) -> None: + model_revision_external_id = seed_resource_names["simulator_model_revision_external_id"] + model_revision = cognite_client.simulators.models.revisions.retrieve(external_id=model_revision_external_id) + assert model_revision is not None + assert model_revision.model_external_id == seed_resource_names["simulator_model_external_id"] + + @pytest.mark.usefixtures("seed_file", "seed_resource_names") + def test_create_model_and_revisions( + self, cognite_client: CogniteClient, seed_file: FileMetadata, seed_resource_names + ) -> None: + model_external_id_1 = random_string(10) + model_external_id_2 = random_string(10) + models_to_create = [ + SimulatorModelWrite( + name="sdk-test-model1", + simulator_external_id=seed_resource_names["simulator_external_id"], + external_id=model_external_id_1, + data_set_id=seed_resource_names["simulator_test_data_set_id"], + type="SteadyState", + ), + SimulatorModelWrite( + name="sdk-test-model2", + simulator_external_id=seed_resource_names["simulator_external_id"], + external_id=model_external_id_2, + data_set_id=seed_resource_names["simulator_test_data_set_id"], + type="SteadyState", + ), + ] + + models_created = cognite_client.simulators.models.create(models_to_create) + + assert models_created is not None + assert len(models_created) == 2 + model_revision_external_id = model_external_id_1 + "revision" + model_revision_to_create = SimulatorModelRevisionWrite( + external_id=model_revision_external_id, + model_external_id=model_external_id_1, + file_id=seed_file.id, + description="Test revision", + ) + multiple_model_revisions_to_create = [ + SimulatorModelRevisionWrite( + external_id=model_revision_external_id + "1", + model_external_id=model_external_id_1, + file_id=seed_file.id, + description="Test revision", + ), + SimulatorModelRevisionWrite( + external_id=model_revision_external_id + "2", + model_external_id=model_external_id_2, + file_id=seed_file.id, + description="Test revision", + ), + ] + + multiple_model_revisions_created = cognite_client.simulators.models.revisions.create( + multiple_model_revisions_to_create + ) + model_revision_created = cognite_client.simulators.models.revisions.create(model_revision_to_create) + + assert model_revision_created is not None + assert model_revision_created.external_id == model_revision_external_id + assert len(multiple_model_revisions_created) == 2 + cognite_client.simulators.models.delete(external_id=[model_external_id_1, model_external_id_2]) + + def test_update_model(self, cognite_client: CogniteClient, seed_resource_names) -> None: + model_external_id = random_string(10) + models_to_create = SimulatorModelWrite( + name="sdk-test-model1", + simulator_external_id=seed_resource_names["simulator_external_id"], + external_id=model_external_id, + data_set_id=seed_resource_names["simulator_test_data_set_id"], + type="SteadyState", + ) + + models_created = cognite_client.simulators.models.create(models_to_create) + assert models_created is not None + assert models_created.external_id == model_external_id + models_created.description = "updated description" + models_created.name = "updated name" + model_updated = cognite_client.simulators.models.update(models_created) + assert model_updated is not None + assert model_updated.description == "updated description" + assert model_updated.name == "updated name" diff --git a/tests/tests_unit/test_docstring_examples.py b/tests/tests_unit/test_docstring_examples.py index 3890aa526e..0ac0ac325a 100644 --- a/tests/tests_unit/test_docstring_examples.py +++ b/tests/tests_unit/test_docstring_examples.py @@ -20,6 +20,7 @@ raw, relationships, sequences, + simulators, three_d, time_series, units, @@ -141,3 +142,9 @@ def test_ai(self): run_docstring_tests(ai) run_docstring_tests(ai.tools) run_docstring_tests(ai.tools.documents) + + def test_simulators(self): + run_docstring_tests(simulators) + run_docstring_tests(simulators.models) + run_docstring_tests(simulators.models_revisions) + run_docstring_tests(simulators.integrations) diff --git a/tests/tests_unit/test_meta.py b/tests/tests_unit/test_meta.py index a066ff2379..831d66dfc3 100644 --- a/tests/tests_unit/test_meta.py +++ b/tests/tests_unit/test_meta.py @@ -36,7 +36,7 @@ def keep(path): err_msg = "File: '{}' is missing 'from __future__ import annotations' at line=0" for filepath in filter(keep, ALL_FILEPATHS): - with filepath.open("r") as file: + with filepath.open("r", encoding="utf-8") as file: # We just read the first line from each file: assert file.readline() == "from __future__ import annotations\n", err_msg.format(filepath)