diff --git a/doc/changelog.d/633.documentation.md b/doc/changelog.d/633.documentation.md new file mode 100644 index 000000000..6c525bcb2 --- /dev/null +++ b/doc/changelog.d/633.documentation.md @@ -0,0 +1 @@ +MAINT: Bump pytest from 8.4.1 to 8.4.2 diff --git a/doc/changelog.d/641.added.md b/doc/changelog.d/641.added.md new file mode 100644 index 000000000..d7502683e --- /dev/null +++ b/doc/changelog.d/641.added.md @@ -0,0 +1 @@ +Feat: Added APIs saveHarmonicProfile(), saveRandomVibeProfile(), saveShockPulseProfile() and saveThermalProfile() diff --git a/pyproject.toml b/pyproject.toml index 6beb6cadc..e27f475e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-sherlock==0.1.49", + "ansys-api-sherlock==0.1.50", "grpcio>=1.17, <1.68.0", "protobuf>=3.20", "pydantic>=2.9.2", @@ -34,7 +34,7 @@ dependencies = [ tests = [ "grpcio>=1.17, <1.68.0", "protobuf==5.28.0", - "pytest==8.4.1", + "pytest==8.4.2", "pytest-cov==6.2.1", ] doc = [ diff --git a/src/ansys/sherlock/core/errors.py b/src/ansys/sherlock/core/errors.py index 7c8835370..dceff4aa1 100644 --- a/src/ansys/sherlock/core/errors.py +++ b/src/ansys/sherlock/core/errors.py @@ -1292,3 +1292,15 @@ def __init__(self, message): def __str__(self): """Format error message.""" return f"Export layer image error: {self.message}" + + +class SherlockSaveProfileError(Exception): + """Contains the errors raised when a profile for an existing event cannot be saved.""" + + def __init__(self, message): + """Initialize error message.""" + self.message = message + + def __str__(self): + """Format error message.""" + return f"Save profile error: {self.message}" diff --git a/src/ansys/sherlock/core/lifecycle.py b/src/ansys/sherlock/core/lifecycle.py index ac985bb09..a4bfa1c68 100644 --- a/src/ansys/sherlock/core/lifecycle.py +++ b/src/ansys/sherlock/core/lifecycle.py @@ -35,9 +35,16 @@ SherlockLoadShockProfilePulsesError, SherlockLoadThermalProfileError, SherlockNoGrpcConnectionException, + SherlockSaveProfileError, ) from ansys.sherlock.core.grpc_stub import GrpcStub -from ansys.sherlock.core.types.lifecycle_types import ImportThermalSignalRequest +from ansys.sherlock.core.types.lifecycle_types import ( + ImportThermalSignalRequest, + SaveHarmonicProfileRequest, + SaveRandomVibeProfileRequest, + SaveShockPulseProfileRequest, + SaveThermalProfileRequest, +) from ansys.sherlock.core.utils.version_check import require_version @@ -2167,3 +2174,182 @@ def import_thermal_signal( raise SherlockNoGrpcConnectionException() return self.stub.importThermalSignal(import_thermal_signal_request) + + @require_version(261) + def save_harmonic_profile( + self, request: SaveHarmonicProfileRequest + ) -> SherlockCommonService_pb2.ReturnCode: + """Save a harmonic life cycle event profile to a .dat or .csv file. + + Available Since: 2026R1 + + Parameters + ---------- + request : SaveHarmonicProfileRequest + Request object containing the information needed to save a harmonic profile. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Status code of the response. 0 for success. + + Examples + -------- + >>> from ansys.sherlock.core.types.lifecycle_types import SaveHarmonicProfileRequest + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> sherlock = launch_sherlock() + >>> response = sherlock.lifecycle.save_harmonic_profile( + >>> SaveHarmonicProfileRequest( + >>> project="MyProject", + >>> phase_name="DurabilityPhase", + >>> event_name="Harmonic_100Hz", + >>> triaxial_axis="x", + >>> file_path="/tmp/Harmonic_100Hz.csv", + >>> ) + >>> ) + >>> assert response.value == 0 + """ + grpc_request = request._convert_to_grpc() + + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + response = self.stub.saveHarmonicProfile(grpc_request) + + # Raise error if save failed + if response.value != 0: + raise SherlockSaveProfileError(response.message) + + return response + + @require_version(261) + def save_random_vibe_profile( + self, request: SaveRandomVibeProfileRequest + ) -> SherlockCommonService_pb2.ReturnCode: + """Save a random vibe life cycle event profile to a .dat or .csv file. + + Available Since: 2026R1 + + Parameters + ---------- + request : SaveRandomVibeProfileRequest + Request object containing the information needed to save a random vibe profile. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Status code of the response. 0 for success. + + Examples + -------- + >>> from ansys.sherlock.core.types.lifecycle_types import SaveRandomVibeProfileRequest + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> sherlock = launch_sherlock() + >>> response = sherlock.lifecycle.save_random_vibe_profile( + >>> SaveRandomVibeProfileRequest( + >>> project="MyProject", + >>> phase_name="RandomVibePhase", + >>> event_name="RV_Event_01", + >>> file_path="/tmp/RV_Event_01.dat", + >>> ) + >>> ) + >>> assert response.value == 0 + """ + grpc_request = request._convert_to_grpc() + + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + response = self.stub.saveRandomVibeProfile(grpc_request) + + # Raise error if save failed + if response.value != 0: + raise SherlockSaveProfileError(response.message) + + @require_version(261) + def save_shock_pulse_profile( + self, request: SaveShockPulseProfileRequest + ) -> SherlockCommonService_pb2.ReturnCode: + """Save a shock pulse life cycle event profile to a .dat or .csv file. + + Available Since: 2026R1 + + Parameters + ---------- + request : SaveShockPulseProfileRequest + Request object containing the information needed to save a shock pulse profile. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Status code of the response. 0 for success. + + Examples + -------- + >>> from ansys.sherlock.core.types.lifecycle_types import SaveShockPulseProfileRequest + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> sherlock = launch_sherlock() + >>> response = sherlock.lifecycle.save_shock_pulse_profile( + >>> SaveShockPulseProfileRequest( + >>> project="MyProject", + >>> phase_name="ShockPhase", + >>> event_name="Pulse_200g", + >>> file_path="/tmp/Pulse_200g.csv", + >>> ) + >>> ) + >>> assert response.value == 0 + """ + grpc_request = request._convert_to_grpc() + + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + response = self.stub.saveShockPulseProfile(grpc_request) + + # Raise error if save failed + if response.value != 0: + raise SherlockSaveProfileError(response.message) + + @require_version(261) + def save_thermal_profile( + self, request: SaveThermalProfileRequest + ) -> SherlockCommonService_pb2.ReturnCode: + """Save a thermal life cycle event profile to a .dat or .csv file. + + Available Since: 2026R1 + + Parameters + ---------- + request : SaveThermalProfileRequest + Request object containing the information needed to save a thermal profile. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Status code of the response. 0 for success. + + Examples + -------- + >>> from ansys.sherlock.core.types.lifecycle_types import SaveThermalProfileRequest + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> sherlock = launch_sherlock() + >>> response = sherlock.lifecycle.save_thermal_profile( + >>> SaveThermalProfileRequest( + >>> project="MyProject", + >>> phase_name="ThermalPhase", + >>> event_name="ThermalCycle_A", + >>> file_path="/tmp/ThermalCycle_A.dat", + >>> ) + >>> ) + >>> assert response.value == 0 + """ + grpc_request = request._convert_to_grpc() + + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + response = self.stub.saveThermalProfile(grpc_request) + + # Raise error if save failed + if response.value != 0: + raise SherlockSaveProfileError(response.message) diff --git a/src/ansys/sherlock/core/types/lifecycle_types.py b/src/ansys/sherlock/core/types/lifecycle_types.py index 8be3ff486..4aefc4776 100644 --- a/src/ansys/sherlock/core/types/lifecycle_types.py +++ b/src/ansys/sherlock/core/types/lifecycle_types.py @@ -122,3 +122,133 @@ def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.ImportThermalSignalRe timeFilteringLimitUnits=self.time_filtering_limit_units, generatedCyclesLabel=self.generated_cycles_label, ) + + +class SaveHarmonicProfileRequest(BaseModel): + """Request to save a harmonic life cycle event profile to a .dat or .csv file.""" + + project: str + """Sherlock project name.""" + + phase_name: str + """The name of the life cycle phase this event is associated with.""" + + event_name: str + """Harmonic event name.""" + + triaxial_axis: str | None = None + """If the harmonic profile type is 'Triaxial', the axis this profile should be assigned to. + Valid values are: x, y, z. + """ + + file_path: str + """Full destination path for the .dat or .csv file.""" + + @field_validator("project", "phase_name", "event_name", "file_path", "triaxial_axis") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.SaveHarmonicProfileRequest: + """Convert to gRPC SaveHarmonicProfileRequest.""" + return SherlockLifeCycleService_pb2.SaveHarmonicProfileRequest( + project=self.project, + phaseName=self.phase_name, + eventName=self.event_name, + triaxialAxis=self.triaxial_axis or "", + filePath=self.file_path, + ) + + +class SaveRandomVibeProfileRequest(BaseModel): + """Request to save a random vibe life cycle event profile to a .dat or .csv file.""" + + project: str + """Sherlock project name.""" + + phase_name: str + """The name of the life cycle phase this event is associated with.""" + + event_name: str + """Random vibe event name.""" + + file_path: str + """Full destination path for the .dat or .csv file.""" + + @field_validator("project", "phase_name", "event_name", "file_path") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.SaveRandomVibeProfileRequest: + """Convert to gRPC SaveRandomVibeProfileRequest.""" + return SherlockLifeCycleService_pb2.SaveRandomVibeProfileRequest( + project=self.project, + phaseName=self.phase_name, + eventName=self.event_name, + filePath=self.file_path, + ) + + +class SaveShockPulseProfileRequest(BaseModel): + """Request to save a shock pulse life cycle event profile to a .dat or .csv file.""" + + project: str + """Sherlock project name.""" + + phase_name: str + """The name of the life cycle phase this event is associated with.""" + + event_name: str + """Shock event name.""" + + file_path: str + """Full destination path for the .dat or .csv file.""" + + @field_validator("project", "phase_name", "event_name", "file_path") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.SaveShockPulseProfileRequest: + """Convert to gRPC SaveShockPulseProfileRequest.""" + return SherlockLifeCycleService_pb2.SaveShockPulseProfileRequest( + project=self.project, + phaseName=self.phase_name, + eventName=self.event_name, + filePath=self.file_path, + ) + + +class SaveThermalProfileRequest(BaseModel): + """Request to save a thermal life cycle event profile to a .dat or .csv file.""" + + project: str + """Sherlock project name.""" + + phase_name: str + """The name of the life cycle phase this event is associated with.""" + + event_name: str + """Thermal event name.""" + + file_path: str + """Full destination path for the .dat or .csv file.""" + + @field_validator("project", "phase_name", "event_name", "file_path") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.SaveThermalProfileRequest: + """Convert to gRPC SaveThermalProfileRequest.""" + return SherlockLifeCycleService_pb2.SaveThermalProfileRequest( + project=self.project, + phaseName=self.phase_name, + eventName=self.event_name, + filePath=self.file_path, + ) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index 7fc62228b..822d1c432 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -21,10 +21,15 @@ SherlockLoadShockProfileDatasetError, SherlockLoadShockProfilePulsesError, SherlockLoadThermalProfileError, + SherlockSaveProfileError, ) from ansys.sherlock.core.lifecycle import Lifecycle from ansys.sherlock.core.types.lifecycle_types import ( ImportThermalSignalRequest, + SaveHarmonicProfileRequest, + SaveRandomVibeProfileRequest, + SaveShockPulseProfileRequest, + SaveThermalProfileRequest, ThermalSignalFileProperties, ) from ansys.sherlock.core.utils.version_check import SKIP_VERSION_CHECK @@ -52,6 +57,11 @@ def test_all(): helper_test_load_shock_profile_pulses(lifecycle) helper_test_import_thermal_signal(lifecycle) + helper_test_save_harmonic_profile(lifecycle) + helper_test_save_random_vibe_profile(lifecycle) + helper_test_save_shock_pulse_profile(lifecycle) + helper_test_save_thermal_profile(lifecycle) + def helper_test_create_life_phase(lifecycle: Lifecycle): """Test create_life_phase API""" @@ -2328,5 +2338,285 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): ) +def helper_test_save_harmonic_profile(lifecycle: Lifecycle): + # project missing + try: + lifecycle.save_harmonic_profile( + SaveHarmonicProfileRequest( + project="", + phase_name="On The Road", + event_name="5 - Harmonic Event", + triaxial_axis="x", + file_path="C:/Temp/Harmonic.dat", + ) + ) + pytest.fail("No exception raised when using a missing project parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, project is invalid because it is None or empty." + ) + + # phase_name missing + try: + lifecycle.save_harmonic_profile( + SaveHarmonicProfileRequest( + project="Tutorial Project", + phase_name="", + event_name="5 - Harmonic Event", + triaxial_axis="x", + file_path="C:/Temp/Harmonic.dat", + ) + ) + pytest.fail("No exception raised when using a missing phase_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, phase_name is invalid because it is None or empty." + ) + + # event_name missing + try: + lifecycle.save_harmonic_profile( + SaveHarmonicProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="", + triaxial_axis="x", + file_path="C:/Temp/Harmonic.dat", + ) + ) + pytest.fail("No exception raised when using a missing event_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, event_name is invalid because it is None or empty." + ) + + if lifecycle._is_connection_up(): + + # invalid triaxial_axis + try: + lifecycle.save_harmonic_profile( + SaveHarmonicProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="5 - Harmonic Event", + triaxial_axis="a", + file_path="C:/Temp/Harmonic.dat", + ) + ) + pytest.fail("No exception raised when using a missing triaxial_axis parameter") + except Exception as e: + assert type(e) == SherlockSaveProfileError + + +def helper_test_save_random_vibe_profile(lifecycle: Lifecycle): + # project missing + try: + lifecycle.save_random_vibe_profile( + SaveRandomVibeProfileRequest( + project="", + phase_name="On The Road", + event_name="RV_Event_01", + file_path="C:/Temp/RV_Event_01.dat", + ) + ) + pytest.fail("No exception raised when using a missing project parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, project is invalid because it is None or empty." + ) + + # phase_name missing + try: + lifecycle.save_random_vibe_profile( + SaveRandomVibeProfileRequest( + project="Tutorial Project", + phase_name="", + event_name="1 - Vibration", + file_path="C:/Temp/1 - Vibration.dat", + ) + ) + pytest.fail("No exception raised when using a missing phase_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, phase_name is invalid because it is None or empty." + ) + + # event_name missing + try: + lifecycle.save_random_vibe_profile( + SaveRandomVibeProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="", + file_path="C:/Temp/1 - Vibration.dat", + ) + ) + pytest.fail("No exception raised when using a missing event_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, event_name is invalid because it is None or empty." + ) + + if lifecycle._is_connection_up(): + + # invalid file_path + try: + lifecycle.save_random_vibe_profile( + SaveRandomVibeProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="1 - Vibration", + file_path="C:/Temp/RV_Event_01.txt", + ) + ) + pytest.fail("No exception raised when using an invalid file_path parameter") + except Exception as e: + assert type(e) == SherlockSaveProfileError + + +def helper_test_save_shock_pulse_profile(lifecycle: Lifecycle): + # project missing + try: + lifecycle.save_shock_pulse_profile( + SaveShockPulseProfileRequest( + project="", + phase_name="On The Road", + event_name="3 - Collision", + file_path="C:/Temp/3 - Collision.dat", + ) + ) + pytest.fail("No exception raised when using a missing project parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, project is invalid because it is None or empty." + ) + + # phase_name missing + try: + lifecycle.save_shock_pulse_profile( + SaveShockPulseProfileRequest( + project="Tutorial Project", + phase_name="", + event_name="3 - Collision", + file_path="C:/Temp/3 - Collision.dat", + ) + ) + pytest.fail("No exception raised when using a missing phase_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, phase_name is invalid because it is None or empty." + ) + + # event_name missing + try: + lifecycle.save_shock_pulse_profile( + SaveShockPulseProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="", + file_path="C:/Temp/3 - Collision.dat", + ) + ) + pytest.fail("No exception raised when using a missing event_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, event_name is invalid because it is None or empty." + ) + + if lifecycle._is_connection_up(): + + # invalid file_path + try: + lifecycle.save_shock_pulse_profile( + SaveShockPulseProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="3 - Collision", + file_path="C:/Temp/3 - Collision.txt", + ) + ) + pytest.fail("No exception raised when using an invalid file_path parameter") + except Exception as e: + assert type(e) == SherlockSaveProfileError + + +def helper_test_save_thermal_profile(lifecycle: Lifecycle): + # project missing + try: + lifecycle.save_thermal_profile( + SaveThermalProfileRequest( + project="", + phase_name="On The Road", + event_name="ThermalCycle_A", + file_path="C:/Temp/ThermalCycle_A.dat", + ) + ) + pytest.fail("No exception raised when using a missing project parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, project is invalid because it is None or empty." + ) + + # phase_name missing + try: + lifecycle.save_thermal_profile( + SaveThermalProfileRequest( + project="Tutorial Project", + phase_name="", + event_name="ThermalCycle_A", + file_path="C:/Temp/ThermalCycle_A.dat", + ) + ) + pytest.fail("No exception raised when using a missing phase_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, phase_name is invalid because it is None or empty." + ) + + # event_name missing + try: + lifecycle.save_thermal_profile( + SaveThermalProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="", + file_path="C:/Temp/ThermalCycle_A.dat", + ) + ) + pytest.fail("No exception raised when using a missing event_name parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert str(e.errors()[0]["msg"]) == ( + "Value error, event_name is invalid because it is None or empty." + ) + + if lifecycle._is_connection_up(): + + # invalid file_path + try: + lifecycle.save_thermal_profile( + SaveThermalProfileRequest( + project="Tutorial Project", + phase_name="On The Road", + event_name="ThermalCycle_A", + file_path="C:/Temp/ThermalCycle_A.txt", + ) + ) + pytest.fail("No exception raised when using an invalid file_path parameter") + except Exception as e: + assert type(e) == SherlockSaveProfileError + + if __name__ == "__main__": test_all()