From e129af8404d3a09807166d46df669bee80fd8fa7 Mon Sep 17 00:00:00 2001 From: Nayane Fernandes <143632290+ansnfernand@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:47:02 +0200 Subject: [PATCH 01/13] feat: Added API import_parts_to_avl() --- src/ansys/sherlock/core/parts.py | 41 ++++++++++++++++++++ src/ansys/sherlock/core/types/parts_types.py | 27 +++++++++++++ tests/test_parts.py | 32 +++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/ansys/sherlock/core/parts.py b/src/ansys/sherlock/core/parts.py index f69903684..595822bcc 100644 --- a/src/ansys/sherlock/core/parts.py +++ b/src/ansys/sherlock/core/parts.py @@ -2,9 +2,11 @@ """Module containing all parts management capabilities.""" try: + import SherlockCommonService_pb2 import SherlockPartsService_pb2 import SherlockPartsService_pb2_grpc except ModuleNotFoundError: + from ansys.api.sherlock.v0 import SherlockCommonService_pb2 from ansys.api.sherlock.v0 import SherlockPartsService_pb2 from ansys.api.sherlock.v0 import SherlockPartsService_pb2_grpc @@ -29,6 +31,7 @@ AVLPartNum, DeletePartsFromPartsListRequest, GetPartsListPropertiesRequest, + ImportPartsToAVLRequest, PartLocation, PartsListSearchDuplicationMode, UpdatePadPropertiesRequest, @@ -1191,3 +1194,41 @@ def delete_parts_from_parts_list( delete_request = request._convert_to_grpc() return list(self.stub.deletePartsFromPartsList(delete_request)) + + @require_version(261) + def import_parts_to_avl( + self, + request: ImportPartsToAVLRequest, + ) -> SherlockCommonService_pb2.ReturnCode: + """Import a parts list into the Approved Vendor List (AVL). + + Available Since: 2026R1 + + Parameters + ---------- + request : ImportPartsToAVLRequest + Contains the file path and import mode to use for the AVL parts import. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Return code indicating the result of the AVL parts import. + + Examples + -------- + >>> from ansys.sherlock.core.types.project_types import ImportPartsToAVLRequest + >>> from ansys.api.sherlock.v0 import SherlockPartsService_pb2 + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> + >>> sherlock = launch_sherlock() + >>> return_code = sherlock.project.import_parts_to_avl( + >>> ImportPartsToAVLRequest( + >>> import_file="C:/path/to/AVL_parts_file.xls", + >>> import_type=SherlockPartsService_pb2.AVLImportType.Update + >>> ) + >>> ) + """ + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + return self.stub.importPartsToAVL(request._convert_to_grpc()) diff --git a/src/ansys/sherlock/core/types/parts_types.py b/src/ansys/sherlock/core/types/parts_types.py index 782ca312e..019bf0e3c 100644 --- a/src/ansys/sherlock/core/types/parts_types.py +++ b/src/ansys/sherlock/core/types/parts_types.py @@ -162,3 +162,30 @@ def _convert_to_grpc(self) -> parts_service.DeletePartsFromPartsListRequest: ccaName=self.cca_name, refDes=self.reference_designators, ) + + +class ImportPartsToAVLRequest(BaseModel): + """Request to import parts into the Approved Vendor List (AVL).""" + + import_file: str + """Full file path to the AVL file.""" + + import_type: parts_service.AVLImportType + """Import mode to use for AVL data.""" + + """Allow non-standard types like Protobuf enums in Pydantic models.""" + model_config = {"arbitrary_types_allowed": True} + + @field_validator("import_file") + @classmethod + def import_file_validator(cls, value: str, info): + """Validate that the import file path is not empty.""" + if value.strip() == "": + raise ValueError(f"{info.field_name} cannot be empty.") + return basic_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> parts_service.ImportPartsToAVLRequest: + request = parts_service.ImportPartsToAVLRequest() + request.importFile = self.import_file + request.importType = self.import_type + return request diff --git a/tests/test_parts.py b/tests/test_parts.py index ce5118f88..a570f92d7 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -3,6 +3,7 @@ import os import platform +from ansys.api.sherlock.v0 import SherlockPartsService_pb2 import grpc import pydantic import pytest @@ -27,11 +28,14 @@ AVLPartNum, DeletePartsFromPartsListRequest, GetPartsListPropertiesRequest, + ImportPartsToAVLRequest, PartsListSearchDuplicationMode, UpdatePadPropertiesRequest, ) from ansys.sherlock.core.utils.version_check import SKIP_VERSION_CHECK +parts_service = SherlockPartsService_pb2 + def test_all(): """Test all parts APIs""" @@ -1071,5 +1075,33 @@ def helper_test_delete_parts_from_parts_list(parts: Parts): pytest.fail(f"Unexpected exception raised: {e}") +def helper_test_import_parts_to_avl(parts: Parts): + """Test import parts to AVL API.""" + try: + # Test with an empty import_file path + ImportPartsToAVLRequest(import_file="", import_type=parts_service.AVLImportType.Replace) + pytest.fail("No exception raised when using an empty import_file parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert e.errors()[0]["msg"] == "Value error, import_file cannot be empty." + + try: + # Test with a invalid AVL file path + request = ImportPartsToAVLRequest( + import_file="invalid/path/parts_list.csv", + import_type=parts_service.AVLImportType.Add, + ) + + if parts._is_connection_up(): + return_code = parts.import_parts_to_avl(request) + + # Check that an invalid import_file returns an error + assert return_code.value == -1 + assert return_code.message == "File Not Found" + + except Exception as e: + pytest.fail(f"Unexpected exception raised: {e}") + + if __name__ == "__main__": test_all() From b9cbc563e7be7db6a6eff585608cde92c46c0265 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:50:44 +0000 Subject: [PATCH 02/13] chore: adding changelog file 617.miscellaneous.md [dependabot-skip] --- doc/changelog.d/617.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/617.miscellaneous.md diff --git a/doc/changelog.d/617.miscellaneous.md b/doc/changelog.d/617.miscellaneous.md new file mode 100644 index 000000000..a386e7461 --- /dev/null +++ b/doc/changelog.d/617.miscellaneous.md @@ -0,0 +1 @@ +Feat: Added API import_parts_to_avl() \ No newline at end of file From f2c3b8444425411e5648aa8478255b9ef5f258f8 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:51:44 +0000 Subject: [PATCH 03/13] chore: adding changelog file 617.added.md [dependabot-skip] --- doc/changelog.d/{617.miscellaneous.md => 617.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{617.miscellaneous.md => 617.added.md} (100%) diff --git a/doc/changelog.d/617.miscellaneous.md b/doc/changelog.d/617.added.md similarity index 100% rename from doc/changelog.d/617.miscellaneous.md rename to doc/changelog.d/617.added.md From df9762764bd0eb6d5cd81248e7e057e3f869d127 Mon Sep 17 00:00:00 2001 From: Nayane Fernandes <143632290+ansnfernand@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:13:39 +0200 Subject: [PATCH 04/13] Bump ansys-api-sherlock version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1488c817c..7cf362dde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-sherlock==0.1.48", + "ansys-api-sherlock==0.1.49", "grpcio>=1.17, <1.68.0", "protobuf>=3.20", "pydantic>=2.9.2", From 76171e6f811953e1738612e7fd6c16708c7df6f6 Mon Sep 17 00:00:00 2001 From: ansjdudkows <173175544+ansjdudkows@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:04:45 -0400 Subject: [PATCH 05/13] feat: added new PySherlock updateTestPoints API (#621) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/621.added.md | 1 + doc/source/api/layer_types.rst | 2 + pyproject.toml | 2 +- src/ansys/sherlock/core/layer.py | 47 ++++++++ src/ansys/sherlock/core/types/layer_types.py | 72 ++++++++++++ tests/test_layer.py | 116 ++++++++++++++++++- 6 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 doc/changelog.d/621.added.md diff --git a/doc/changelog.d/621.added.md b/doc/changelog.d/621.added.md new file mode 100644 index 000000000..5b1adf703 --- /dev/null +++ b/doc/changelog.d/621.added.md @@ -0,0 +1 @@ +Feat: added new PySherlock updateTestPoints API diff --git a/doc/source/api/layer_types.rst b/doc/source/api/layer_types.rst index 1e13cbc93..f9003ea20 100644 --- a/doc/source/api/layer_types.rst +++ b/doc/source/api/layer_types.rst @@ -24,7 +24,9 @@ Classes used for the Layer API. PottingRegionCopyData PottingRegionDeleteData PottingRegionUpdateData + TestPointProperties UpdatePottingRegionRequest + UpdateTestPointsRequest diff --git a/pyproject.toml b/pyproject.toml index 7cf362dde..88c98e9a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "ansys-sherlock-core" -version = "0.10.dev0" +version = "0.11.dev0" description = "A python wrapper for Ansys Sherlock" readme = "README.rst" requires-python = ">=3.10,<4" diff --git a/src/ansys/sherlock/core/layer.py b/src/ansys/sherlock/core/layer.py index bf615d298..2cb6dbc3c 100644 --- a/src/ansys/sherlock/core/layer.py +++ b/src/ansys/sherlock/core/layer.py @@ -14,6 +14,7 @@ RectangularShape, SlotShape, UpdatePottingRegionRequest, + UpdateTestPointsRequest, ) try: @@ -2134,3 +2135,49 @@ def get_ict_fixtures_props( response = self.stub.getICTFixturesProperties(get_ict_fixture_props_request) return response + + @require_version(261) + def update_test_points( + self, request: UpdateTestPointsRequest + ) -> SherlockLayerService_pb2.UpdateTestPointsResponse: + """Update test point properties of a CCA from input parameters. + + Available Since: 2026R1 + + Parameters + ---------- + request: UpdateTestPointsRequest + Contains all the information needed to update the properties for one or more + test points. + + Returns + ------- + SherlockCommonService_pb2.UpdateTestPointsResponse + A status code and message for the update test points request. + + Examples + -------- + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> from ansys.sherlock.core.types.layer_types import UpdateTestPointsRequest, + >>> TestPointProperties + >>> sherlock = connect() + >>> test_point = TestPointProperties( + >>> id="TP1", + >>> side="BOTTOM", + >>> units="in", + >>> center_x=1.0, + >>> center_y=0.5, + >>> radius=0.2, + >>> load_type="Force", + >>> load_value=3.0, + >>> load_units="ozf", + >>> ) + >>> response = sherlock.layer.update_test_points(UpdateTestPointsRequest( + >>> project="Tutorial Project", + >>> cca_name="Main Board", + >>> update_test_points=[test_point], + >>> )) + """ + update_request = request._convert_to_grpc() + response = self.stub.updateTestPoints(update_request) + return response diff --git a/src/ansys/sherlock/core/types/layer_types.py b/src/ansys/sherlock/core/types/layer_types.py index 8f3757d64..8d51c7fa7 100644 --- a/src/ansys/sherlock/core/types/layer_types.py +++ b/src/ansys/sherlock/core/types/layer_types.py @@ -402,3 +402,75 @@ def _convert_to_grpc(self) -> SherlockLayerService_pb2.GetICTFixturesPropertiesR if self.ict_fixtures_ids is not None: request.ICTFixtureIDs = self.ict_fixtures_ids return request + + +class TestPointProperties(BaseModel): + """Contains the properties of a test point.""" + + __test__ = False # This line is to notify pytest that this is not a test class. + + id: str + """ID""" + side: str + """Side""" + units: str + """Units""" + center_x: float + """Center x-value""" + center_y: float + """Center y-value""" + radius: float + """Radius""" + load_type: str + """Load type""" + load_value: float + """Load value""" + load_units: str + """Load units""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.TestPointProperties: + grpc_test_point_data = SherlockLayerService_pb2.TestPointProperties() + + grpc_test_point_data.ID = self.id + grpc_test_point_data.side = self.side + grpc_test_point_data.units = self.units + grpc_test_point_data.centerX = self.center_x + grpc_test_point_data.centerY = self.center_y + grpc_test_point_data.radius = self.radius + grpc_test_point_data.loadType = self.load_type + grpc_test_point_data.loadValue = self.load_value + grpc_test_point_data.loadUnits = self.load_units + + return grpc_test_point_data + + @field_validator("side", "units", "load_type") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + +class UpdateTestPointsRequest(BaseModel): + """Contains the properties of a test points update per project.""" + + project: str + """Name of the Sherlock project.""" + cca_name: str + """Name of the Sherlock CCA.""" + update_test_points: list[TestPointProperties] + """List of test points with their properties to update""" + + @field_validator("project", "cca_name") + @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) -> SherlockLayerService_pb2.UpdateTestPointsRequest: + request = SherlockLayerService_pb2.UpdateTestPointsRequest() + request.project = self.project + request.ccaName = self.cca_name + if self.update_test_points is not None: + for update_test_point in self.update_test_points: + request.testPointProperties.append(update_test_point._convert_to_grpc()) + return request diff --git a/tests/test_layer.py b/tests/test_layer.py index fd35a2bf7..8d69fdc4b 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -42,7 +42,9 @@ PottingRegionUpdateData, RectangularShape, SlotShape, + TestPointProperties, UpdatePottingRegionRequest, + UpdateTestPointsRequest, ) from ansys.sherlock.core.utils.version_check import SKIP_VERSION_CHECK from tests.test_utils import assert_float_equals @@ -55,14 +57,12 @@ def test_all(): layer = Layer(channel, SKIP_VERSION_CHECK) helper_test_update_mount_points_by_file(layer) - helper_test_delete_all_ict_fixtures(layer) - helper_test_delete_all_mount_points(layer) - helper_test_delete_all_test_points(layer) helper_test_add_potting_region(layer) helper_test_update_potting_region(layer) helper_test_copy_potting_regions(layer) helper_test_delete_potting_regions(layer) helper_test_update_test_fixtures_by_file(layer) + helper_test_update_test_points(layer) helper_test_update_test_points_by_file(layer) helper_test_export_all_mount_points(layer) helper_test_export_all_test_fixtures(layer) @@ -75,6 +75,10 @@ def test_all(): helper_test_get_ict_fixtures_props(layer) helper_test_get_test_point_props(layer) helper_test_export_layer_image(layer) + # Delete APIs must be called last so that tests for update/properties APIs pass + helper_test_delete_all_ict_fixtures(layer) + helper_test_delete_all_mount_points(layer) + helper_test_delete_all_test_points(layer) def helper_test_add_potting_region(layer: Layer): @@ -1847,6 +1851,112 @@ def helper_test_get_ict_fixtures_props(layer): assert response.ICTFixtureProperties[0].chassisMaterial == "ALUMINUM" +def helper_test_update_test_points(layer): + """Test update_test_points API""" + + project = "Tutorial Project" + cca_name = "Main Board" + + test_point_1 = TestPointProperties( + id="TP1", + side="BOTTOM", + units="in", + center_x=1.0, + center_y=0.5, + radius=0.2, + load_type="Force", + load_value=3.0, + load_units="ozf", + ) + + test_point_2 = TestPointProperties( + id="", + side="TOP", + units="mm", + center_x=-30, + center_y=-10, + radius=5, + load_type="Displacement", + load_value=0, + load_units="in", + ) + + invalid_test_point = TestPointProperties( + id="TP2", + side="invalid", + units="mm", + center_x=60, + center_y=-40, + radius=4, + load_type="Force", + load_value=5, + load_units="N", + ) + + # Missing Project Name + try: + UpdateTestPointsRequest(project="", cca_name=cca_name, update_test_points=[test_point_1]) + pytest.fail("No exception thrown when using an invalid parameter") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + + # Missing CCA Name + try: + UpdateTestPointsRequest(project=project, cca_name="", update_test_points=[test_point_1]) + pytest.fail("No exception thrown when using an invalid parameter") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + + if layer._is_connection_up(): + + # Invalid test point test + invalid_request = UpdateTestPointsRequest( + project=project, + cca_name=cca_name, + update_test_points=[invalid_test_point], + ) + invalid_response = layer.update_test_points(invalid_request) + assert invalid_response.returnCode.value == -1 + + # Successful test point test + successful_request = UpdateTestPointsRequest( + project=project, + cca_name=cca_name, + update_test_points=[test_point_1, test_point_2], + ) + successful_response = layer.update_test_points(successful_request) + assert successful_response.returnCode.value == 0 + + properties_request = GetTestPointPropertiesRequest( + project=project, + cca_name=cca_name, + test_point_ids="TP1, TP5", + ) + properties_responses = layer.get_test_point_props(properties_request) + + # Tests updated properties for TP1 + assert properties_responses[0].testPointProperties.ID == "TP1" + assert properties_responses[0].testPointProperties.side == "BOTTOM" + assert properties_responses[0].testPointProperties.units == "in" + assert_float_equals(1.0, properties_responses[0].testPointProperties.centerX) + assert_float_equals(0.5, properties_responses[0].testPointProperties.centerY) + assert properties_responses[0].testPointProperties.radius == 0.2 + assert properties_responses[0].testPointProperties.loadType == 0 + assert properties_responses[0].testPointProperties.loadValue == 3.0 + assert properties_responses[0].testPointProperties.loadUnits == "ozf" + + # Tests updated properties for TP5 + assert properties_responses[1].testPointProperties.ID == "TP5" + assert properties_responses[1].testPointProperties.side == "TOP" + assert properties_responses[1].testPointProperties.units == "mm" + assert_float_equals(-30.0, properties_responses[1].testPointProperties.centerX) + assert_float_equals(-10.0, properties_responses[1].testPointProperties.centerY) + assert properties_responses[1].testPointProperties.radius == 5.0 + assert properties_responses[1].testPointProperties.loadType == 1 + assert properties_responses[1].testPointProperties.loadValue == 0.0 + assert properties_responses[1].testPointProperties.loadUnits == "in" + + def helper_test_export_layer_image(layer): """Test export_layer_image API""" From 4f53c24435db185711fac7a759ab3c2ecd373702 Mon Sep 17 00:00:00 2001 From: ansys-pwalters <119450156+ansys-pwalters@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:17:26 -0400 Subject: [PATCH 06/13] feat: Added time filtering to importThermalSignal API. (#619) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Jeff Moody <110494049+ansjmoody@users.noreply.github.com> --- doc/changelog.d/619.miscellaneous.md | 1 + src/ansys/sherlock/core/lifecycle.py | 4 +- .../sherlock/core/types/lifecycle_types.py | 20 ++++--- tests/test_lifecycle.py | 52 +++++++++++++++++-- 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 doc/changelog.d/619.miscellaneous.md diff --git a/doc/changelog.d/619.miscellaneous.md b/doc/changelog.d/619.miscellaneous.md new file mode 100644 index 000000000..3b84c146b --- /dev/null +++ b/doc/changelog.d/619.miscellaneous.md @@ -0,0 +1 @@ +Feat: Added time filtering to importThermalSignal API. diff --git a/src/ansys/sherlock/core/lifecycle.py b/src/ansys/sherlock/core/lifecycle.py index 0e351a569..290891ea2 100644 --- a/src/ansys/sherlock/core/lifecycle.py +++ b/src/ansys/sherlock/core/lifecycle.py @@ -2152,7 +2152,9 @@ def import_thermal_signal( >>> time_removal= False, >>> load_range_percentage=0.25, >>> number_of_bins=0, - >>> filtering_limit=0.0, + >>> temperature_range_filtering_limit=0.0, + >>> time_filtering_limit=72.0, + >>> time_filtering_limit_units="hr", >>> generated_cycles_label="Second Generated Cycles from Python", >>> ) >>> ) diff --git a/src/ansys/sherlock/core/types/lifecycle_types.py b/src/ansys/sherlock/core/types/lifecycle_types.py index fd65f4f1d..66c9d569d 100644 --- a/src/ansys/sherlock/core/types/lifecycle_types.py +++ b/src/ansys/sherlock/core/types/lifecycle_types.py @@ -73,15 +73,21 @@ class ImportThermalSignalRequest(BaseModel): time_removal: bool """Option to indicate that time results with shorter half-cycle durations are removed.""" load_range_percentage: float - """Defines the fraction of the range near peaks and valleys considered as a dwell region""" + """Defines the fraction of the range near peaks and valleys considered as a dwell region.""" number_of_bins: int - """Number of bins for binning cycles, 0 for no binning""" - filtering_limit: float - """Minimum cycle range to include in results, 0 for not filtering""" + """Number of bins for binning cycles, 0 for no binning.""" + temperature_range_filtering_limit: float + """Minimum cycle range to include in results, 0 for not filtering.""" + time_filtering_limit: float + """Maximum cycle time to include in results, default is 72 hours.""" + time_filtering_limit_units: str + """Units of the time filtering limit.""" generated_cycles_label: str """Label used to define the name of all generated thermal events.""" - @field_validator("file_name", "project", "phase_name", "generated_cycles_label") + @field_validator( + "file_name", "project", "phase_name", "time_filtering_limit_units", "generated_cycles_label" + ) @classmethod def str_validation(cls, value: str, info: ValidationInfo): """Validate string fields listed.""" @@ -105,6 +111,8 @@ def _convert_to_grpc(self) -> SherlockLifeCycleService_pb2.ImportThermalSignalRe timeRemoval=self.time_removal, loadRangePercentage=self.load_range_percentage, numberOfBins=self.number_of_bins, - filteringLimit=self.filtering_limit, + temperatureRangeFilteringLimit=self.temperature_range_filtering_limit, + timeFilteringLimit=self.time_filtering_limit, + timeFilteringLimitUnits=self.time_filtering_limit_units, generatedCyclesLabel=self.generated_cycles_label, ) diff --git a/tests/test_lifecycle.py b/tests/test_lifecycle.py index fd8493edf..058fe784f 100644 --- a/tests/test_lifecycle.py +++ b/tests/test_lifecycle.py @@ -2073,7 +2073,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2103,7 +2105,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2133,7 +2137,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2163,7 +2169,9 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=-1, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="Generated Cycles from pySherlock", ) ) @@ -2193,7 +2201,41 @@ def helper_test_import_thermal_signal(lifecycle: Lifecycle): time_removal=False, load_range_percentage=0.25, number_of_bins=0, - filtering_limit=0.0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="", + generated_cycles_label="Generated Cycles from pySherlock", + ) + ) + pytest.fail("No exception raised when using a missing time_filtering_limit_units parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert ( + str(e.errors()[0]["msg"]) + == "Value error, time_filtering_limit_units is invalid because it is None or empty." + ) + + try: + lifecycle.import_thermal_signal( + ImportThermalSignalRequest( + file_name="C:/Temp/ThermalSignalMissing.csv", + project="Tutorial Project", + thermal_signal_file_properties=ThermalSignalFileProperties( + header_row_count=0, + numeric_format="English", + column_delimiter=",", + time_column="Time", + time_units="sec", + temperature_column="Temperature", + temperature_units="C", + ), + phase_name="Environmental", + time_removal=False, + load_range_percentage=0.25, + number_of_bins=0, + temperature_range_filtering_limit=0.0, + time_filtering_limit=72.0, + time_filtering_limit_units="hr", generated_cycles_label="", ) ) From 65dd6aaaeb377b766523fd70fa6646f05af699c3 Mon Sep 17 00:00:00 2001 From: ansjdudkows <173175544+ansjdudkows@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:05:07 -0400 Subject: [PATCH 07/13] feat: added new Pysherlock updateICTFixtures API (#622) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/622.added.md | 1 + doc/source/api/layer_types.rst | 2 + pyproject.toml | 2 +- src/ansys/sherlock/core/layer.py | 57 ++++++ src/ansys/sherlock/core/types/layer_types.py | 100 +++++++++++ tests/test_layer.py | 177 ++++++++++++++++++- 6 files changed, 329 insertions(+), 10 deletions(-) create mode 100644 doc/changelog.d/622.added.md diff --git a/doc/changelog.d/622.added.md b/doc/changelog.d/622.added.md new file mode 100644 index 000000000..dce948b95 --- /dev/null +++ b/doc/changelog.d/622.added.md @@ -0,0 +1 @@ +Feat: added new Pysherlock updateICTFixtures API diff --git a/doc/source/api/layer_types.rst b/doc/source/api/layer_types.rst index f9003ea20..323d874df 100644 --- a/doc/source/api/layer_types.rst +++ b/doc/source/api/layer_types.rst @@ -15,6 +15,7 @@ Classes used for the Layer API. DeletePottingRegionRequest GetICTFixturesPropertiesRequest GetTestPointPropertiesRequest + ICTFixtureProperties PolygonalShape RectangularShape SlotShape @@ -25,6 +26,7 @@ Classes used for the Layer API. PottingRegionDeleteData PottingRegionUpdateData TestPointProperties + UpdateICTFixturesRequest UpdatePottingRegionRequest UpdateTestPointsRequest diff --git a/pyproject.toml b/pyproject.toml index 88c98e9a0..4556fa6c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "ansys-sherlock-core" -version = "0.11.dev0" +version = "0.12.dev0" description = "A python wrapper for Ansys Sherlock" readme = "README.rst" requires-python = ">=3.10,<4" diff --git a/src/ansys/sherlock/core/layer.py b/src/ansys/sherlock/core/layer.py index 2cb6dbc3c..2d102fc1c 100644 --- a/src/ansys/sherlock/core/layer.py +++ b/src/ansys/sherlock/core/layer.py @@ -13,6 +13,7 @@ PolygonalShape, RectangularShape, SlotShape, + UpdateICTFixturesRequest, UpdatePottingRegionRequest, UpdateTestPointsRequest, ) @@ -2181,3 +2182,59 @@ def update_test_points( update_request = request._convert_to_grpc() response = self.stub.updateTestPoints(update_request) return response + + @require_version(261) + def update_ict_fixtures( + self, request: UpdateICTFixturesRequest + ) -> SherlockLayerService_pb2.UpdateICTFixturesResponse: + """Update ict fixture properties of a CCA from input parameters. + + Available Since: 2026R1 + + Parameters + ---------- + request: UpdateICTFixturesRequest + Contains all the information needed to update the properties for one or more + ict fixtures. + + Returns + ------- + SherlockCommonService_pb2.UpdateICTFixturesResponse + A status code and message for the update ict fixtures request. + + Examples + -------- + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> from ansys.sherlock.core.types.layer_types import UpdateICTFixturesRequest, + >>> ICTFixtureProperties + >>> sherlock = connect() + >>> fixture = ICTFixtureProperties( + >>> id="F1", + >>> type="Mount Hole", + >>> units="in", + >>> side="TOP", + >>> height="0.0", + >>> material="GOLD", + >>> state="DISABLED", + >>> shape="Slot", + >>> x="0.3", + >>> y="-0.4", + >>> length="1.0", + >>> width="0.2", + >>> diameter="0.0", + >>> nodes="10", + >>> rotation="15", + >>> polygon="", + >>> boundary="Outline", + >>> constraints="X-axis translation|Z-axis translation", + >>> chassis_material="SILVER", + >>> ) + >>> response = sherlock.layer.update_ict_fixtures(UpdateICTFixturesRequest( + >>> project="Tutorial Project", + >>> cca_name="Main Board", + >>> update_fixtures=[fixture], + >>> )) + """ + update_request = request._convert_to_grpc() + response = self.stub.updateICTFixtures(update_request) + return response diff --git a/src/ansys/sherlock/core/types/layer_types.py b/src/ansys/sherlock/core/types/layer_types.py index 8d51c7fa7..47089e75f 100644 --- a/src/ansys/sherlock/core/types/layer_types.py +++ b/src/ansys/sherlock/core/types/layer_types.py @@ -474,3 +474,103 @@ def _convert_to_grpc(self) -> SherlockLayerService_pb2.UpdateTestPointsRequest: for update_test_point in self.update_test_points: request.testPointProperties.append(update_test_point._convert_to_grpc()) return request + + +class ICTFixtureProperties(BaseModel): + """Contains the properties of an ict fixture.""" + + id: str + """ID""" + type: str + """Type""" + units: str + """Units""" + side: str + """Side""" + height: str + """Height""" + material: str + """Material""" + state: str + """State""" + shape: str + """Shape type""" + x: str + """Center X""" + y: str + """Center Y""" + length: str + """Length""" + width: str + """Width""" + diameter: str + """Diameter""" + nodes: str + """Number of nodes""" + rotation: str + """Degrees of rotation""" + polygon: str + """Coordinates of points""" + boundary: str + """Boundary point(s)""" + constraints: str + """FEA constraints""" + chassis_material: str + """Chassis material""" + + def _convert_to_grpc(self) -> SherlockLayerService_pb2.ICTFixtureProperties: + grpc_fixture_data = SherlockLayerService_pb2.ICTFixtureProperties() + + grpc_fixture_data.ID = self.id + grpc_fixture_data.type = self.type + grpc_fixture_data.units = self.units + grpc_fixture_data.side = self.side + grpc_fixture_data.height = self.height + grpc_fixture_data.material = self.material + grpc_fixture_data.state = self.state + grpc_fixture_data.shape = self.shape + grpc_fixture_data.x = self.x + grpc_fixture_data.y = self.y + grpc_fixture_data.length = self.length + grpc_fixture_data.width = self.width + grpc_fixture_data.diameter = self.diameter + grpc_fixture_data.nodes = self.nodes + grpc_fixture_data.rotation = self.rotation + grpc_fixture_data.polygon = self.polygon + grpc_fixture_data.boundary = self.boundary + grpc_fixture_data.constraints = self.constraints + grpc_fixture_data.chassisMaterial = self.chassis_material + + return grpc_fixture_data + + @field_validator("type", "units", "side", "state", "shape") + @classmethod + def str_validation(cls, value: str, info: ValidationInfo): + """Validate string fields listed.""" + return basic_str_validator(value, info.field_name) + + +class UpdateICTFixturesRequest(BaseModel): + """Contains the properties of an ict fixtures update per project.""" + + project: str + """Name of the Sherlock project.""" + cca_name: str + """Name of the Sherlock CCA.""" + update_fixtures: list[ICTFixtureProperties] + """List of ict fixtures with their properties to update""" + + @field_validator("project", "cca_name") + @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) -> SherlockLayerService_pb2.UpdateICTFixturesRequest: + request = SherlockLayerService_pb2.UpdateICTFixturesRequest() + request.project = self.project + request.ccaName = self.cca_name + if self.update_fixtures is not None: + for update_fixture in self.update_fixtures: + request.ICTFixtureProperties.append(update_fixture._convert_to_grpc()) + return request diff --git a/tests/test_layer.py b/tests/test_layer.py index 8d69fdc4b..597a301a4 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -34,6 +34,7 @@ DeletePottingRegionRequest, GetICTFixturesPropertiesRequest, GetTestPointPropertiesRequest, + ICTFixtureProperties, PCBShape, PolygonalShape, PottingRegion, @@ -43,6 +44,7 @@ RectangularShape, SlotShape, TestPointProperties, + UpdateICTFixturesRequest, UpdatePottingRegionRequest, UpdateTestPointsRequest, ) @@ -56,29 +58,33 @@ def test_all(): channel = grpc.insecure_channel(channel_param) layer = Layer(channel, SKIP_VERSION_CHECK) - helper_test_update_mount_points_by_file(layer) helper_test_add_potting_region(layer) - helper_test_update_potting_region(layer) helper_test_copy_potting_regions(layer) - helper_test_delete_potting_regions(layer) - helper_test_update_test_fixtures_by_file(layer) - helper_test_update_test_points(layer) - helper_test_update_test_points_by_file(layer) helper_test_export_all_mount_points(layer) helper_test_export_all_test_fixtures(layer) helper_test_export_all_test_points(layer) region_id = helper_test_add_modeling_region(layer) - region_id = helper_test_update_modeling_region(layer, region_id) - helper_test_copy_modeling_region(layer, region_id) - helper_test_delete_modeling_region(layer, region_id) helper_test_list_layers(layer) helper_test_get_ict_fixtures_props(layer) helper_test_get_test_point_props(layer) helper_test_export_layer_image(layer) + + # Update APIs must be called after properties APIs so all pass + helper_test_update_ict_fixtures(layer) + helper_test_update_mount_points_by_file(layer) + helper_test_update_potting_region(layer) + helper_test_update_test_fixtures_by_file(layer) + helper_test_update_test_points(layer) + helper_test_update_test_points_by_file(layer) + region_id = helper_test_update_modeling_region(layer, region_id) + helper_test_copy_modeling_region(layer, region_id) + # Delete APIs must be called last so that tests for update/properties APIs pass helper_test_delete_all_ict_fixtures(layer) helper_test_delete_all_mount_points(layer) helper_test_delete_all_test_points(layer) + helper_test_delete_modeling_region(layer, region_id) + helper_test_delete_potting_regions(layer) def helper_test_add_potting_region(layer: Layer): @@ -1957,6 +1963,159 @@ def helper_test_update_test_points(layer): assert properties_responses[1].testPointProperties.loadUnits == "in" +def helper_test_update_ict_fixtures(layer): + """Test update_ict_fixtures API""" + + project = "Tutorial Project" + cca_name = "Main Board" + + fixture_1 = ICTFixtureProperties( + id="F1", + type="Mount Hole", + units="in", + side="TOP", + height="0.0", + material="GOLD", + state="DISABLED", + shape="Slot", + x="0.3", + y="-0.4", + length="1.0", + width="0.2", + diameter="0.0", + nodes="10", + rotation="15", + polygon="", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + chassis_material="SILVER", + ) + + fixture_2 = ICTFixtureProperties( + id="", + type="Standoff", + units="mil", + side="BOTTOM", + height="10", + material="FERRITE", + state="ENABLED", + shape="Circular", + x="100", + y="50", + length="20", + width="20", + diameter="150", + nodes="6", + rotation="0", + polygon="", + boundary="Center", + constraints="Y-axis translation", + chassis_material="NYLON", + ) + + invalid_fixture = ICTFixtureProperties( + id="F1", + type="Mount Hole", + units="in", + side="TOP", + height="0.0", + material="GOLD", + state="DISABLED", + shape="Slot", + x="invalid", + y="-0.4", + length="1.0", + width="0.2", + diameter="0.0", + nodes="10", + rotation="15", + polygon="", + boundary="Outline", + constraints="X-axis translation|Z-axis translation", + chassis_material="SILVER", + ) + + # Missing Project Name + try: + UpdateICTFixturesRequest(project="", cca_name=cca_name, update_fixtures=[fixture_1]) + pytest.fail("No exception thrown when using an invalid parameter") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + + # Missing CCA Name + try: + UpdateICTFixturesRequest(project=project, cca_name="", update_fixtures=[fixture_1]) + pytest.fail("No exception thrown when using an invalid parameter") + except pydantic.ValidationError as e: + assert isinstance(e, pydantic.ValidationError) + + if layer._is_connection_up(): + # Invalid ict fixture test + invalid_request = UpdateICTFixturesRequest( + project=project, + cca_name=cca_name, + update_fixtures=[invalid_fixture], + ) + invalid_response = layer.update_ict_fixtures(invalid_request) + assert invalid_response.returnCode.value == -1 + + # Successful ict fixture test + successful_request = UpdateICTFixturesRequest( + project=project, + cca_name=cca_name, + update_fixtures=[fixture_1, fixture_2], + ) + successful_response = layer.update_ict_fixtures(successful_request) + assert successful_response.returnCode.value == 0 + + properties_request = GetICTFixturesPropertiesRequest( + project=project, + cca_name=cca_name, + ict_fixtures_ids="F1, F2", + ) + + properties_response = layer.get_ict_fixtures_props(properties_request) + + # Tests updated properties for F1 + assert properties_response.ICTFixtureProperties[0].ID == "F1" + assert properties_response.ICTFixtureProperties[0].type == "Mount Hole" + assert properties_response.ICTFixtureProperties[0].units == "in" + assert properties_response.ICTFixtureProperties[0].side == "TOP" + assert properties_response.ICTFixtureProperties[0].material == "GOLD" + assert properties_response.ICTFixtureProperties[0].state == "DISABLED" + assert properties_response.ICTFixtureProperties[0].shape == "Slot" + assert properties_response.ICTFixtureProperties[0].x == "0.3" + assert properties_response.ICTFixtureProperties[0].y == "-0.4" + assert properties_response.ICTFixtureProperties[0].length == "1" + assert properties_response.ICTFixtureProperties[0].width == "0.2" + assert properties_response.ICTFixtureProperties[0].nodes == "10" + assert properties_response.ICTFixtureProperties[0].rotation == "15.0" + assert properties_response.ICTFixtureProperties[0].boundary == "Outline" + assert properties_response.ICTFixtureProperties[0].constraints == ( + "X-axis translation|" "Z-axis translation" + ) + assert properties_response.ICTFixtureProperties[0].chassisMaterial == "SILVER" + + # Tests updated properties for F2 + assert properties_response.ICTFixtureProperties[1].ID == "F2" + assert properties_response.ICTFixtureProperties[1].type == "Standoff" + assert properties_response.ICTFixtureProperties[1].units == "mil" + assert properties_response.ICTFixtureProperties[1].side == "BOTTOM" + assert properties_response.ICTFixtureProperties[1].height == "-10.0" + assert properties_response.ICTFixtureProperties[1].material == "FERRITE" + assert properties_response.ICTFixtureProperties[1].state == "ENABLED" + assert properties_response.ICTFixtureProperties[1].shape == "Circular" + assert properties_response.ICTFixtureProperties[1].x == "100" + assert properties_response.ICTFixtureProperties[1].y == "50" + assert properties_response.ICTFixtureProperties[1].length == "150" + assert properties_response.ICTFixtureProperties[1].width == "150" + assert properties_response.ICTFixtureProperties[1].diameter == "150" + assert properties_response.ICTFixtureProperties[1].nodes == "6" + assert properties_response.ICTFixtureProperties[1].boundary == "Center" + assert properties_response.ICTFixtureProperties[1].constraints == "Y-axis translation" + assert properties_response.ICTFixtureProperties[1].chassisMaterial == "NYLON" + + def helper_test_export_layer_image(layer): """Test export_layer_image API""" From 0a82376f6106d4c913e99903dddf6a2bab97328f Mon Sep 17 00:00:00 2001 From: Keith Hanson <122566160+anskhanson@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:08:27 -0400 Subject: [PATCH 08/13] Updated test assertion and test point enums to fix failing unit tests (#624) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/624.miscellaneous.md | 1 + src/ansys/sherlock/core/layer.py | 3 ++- src/ansys/sherlock/core/types/layer_types.py | 2 +- tests/test_layer.py | 7 ++++--- tests/test_utils.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 doc/changelog.d/624.miscellaneous.md diff --git a/doc/changelog.d/624.miscellaneous.md b/doc/changelog.d/624.miscellaneous.md new file mode 100644 index 000000000..07d7fb811 --- /dev/null +++ b/doc/changelog.d/624.miscellaneous.md @@ -0,0 +1 @@ +Updated test assertion and test point enums to fix failing unit tests diff --git a/src/ansys/sherlock/core/layer.py b/src/ansys/sherlock/core/layer.py index 2d102fc1c..83abf416e 100644 --- a/src/ansys/sherlock/core/layer.py +++ b/src/ansys/sherlock/core/layer.py @@ -2161,6 +2161,7 @@ def update_test_points( >>> from ansys.sherlock.core.launcher import launch_sherlock >>> from ansys.sherlock.core.types.layer_types import UpdateTestPointsRequest, >>> TestPointProperties + >>> from ansys.api.sherlock.v0 import SherlockLayerService_pb2 >>> sherlock = connect() >>> test_point = TestPointProperties( >>> id="TP1", @@ -2169,7 +2170,7 @@ def update_test_points( >>> center_x=1.0, >>> center_y=0.5, >>> radius=0.2, - >>> load_type="Force", + >>> load_type=SherlockLayerService_pb2.TestPointProperties.LoadType.Force, >>> load_value=3.0, >>> load_units="ozf", >>> ) diff --git a/src/ansys/sherlock/core/types/layer_types.py b/src/ansys/sherlock/core/types/layer_types.py index 47089e75f..ea9547d3a 100644 --- a/src/ansys/sherlock/core/types/layer_types.py +++ b/src/ansys/sherlock/core/types/layer_types.py @@ -421,7 +421,7 @@ class TestPointProperties(BaseModel): """Center y-value""" radius: float """Radius""" - load_type: str + load_type: SherlockLayerService_pb2.TestPointProperties.LoadType.ValueType """Load type""" load_value: float """Load value""" diff --git a/tests/test_layer.py b/tests/test_layer.py index 597a301a4..56c8dee33 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -5,6 +5,7 @@ import platform import uuid +from ansys.api.sherlock.v0 import SherlockLayerService_pb2 import grpc import pydantic import pytest @@ -1870,7 +1871,7 @@ def helper_test_update_test_points(layer): center_x=1.0, center_y=0.5, radius=0.2, - load_type="Force", + load_type=SherlockLayerService_pb2.TestPointProperties.LoadType.Force, load_value=3.0, load_units="ozf", ) @@ -1882,7 +1883,7 @@ def helper_test_update_test_points(layer): center_x=-30, center_y=-10, radius=5, - load_type="Displacement", + load_type=SherlockLayerService_pb2.TestPointProperties.LoadType.Displacement, load_value=0, load_units="in", ) @@ -1894,7 +1895,7 @@ def helper_test_update_test_points(layer): center_x=60, center_y=-40, radius=4, - load_type="Force", + load_type=SherlockLayerService_pb2.TestPointProperties.LoadType.Force, load_value=5, load_units="N", ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 490afb186..ab0411b36 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -27,7 +27,7 @@ def test_version_check(): def assert_float_equals(expected, actual): - assert pytest.approx(expected, abs=1e-14) == pytest.approx(actual, abs=1e-14) + assert pytest.approx(actual, abs=1e-14) == pytest.approx(expected, abs=1e-14) if __name__ == "__main__": From d033415271a453ad829d6dee1eeffed1f9b16027 Mon Sep 17 00:00:00 2001 From: Nayane Fernandes <143632290+ansnfernand@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:47:02 +0200 Subject: [PATCH 09/13] feat: Added API import_parts_to_avl() --- src/ansys/sherlock/core/parts.py | 41 ++++++++++++++++++++ src/ansys/sherlock/core/types/parts_types.py | 27 +++++++++++++ tests/test_parts.py | 32 +++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/ansys/sherlock/core/parts.py b/src/ansys/sherlock/core/parts.py index f69903684..595822bcc 100644 --- a/src/ansys/sherlock/core/parts.py +++ b/src/ansys/sherlock/core/parts.py @@ -2,9 +2,11 @@ """Module containing all parts management capabilities.""" try: + import SherlockCommonService_pb2 import SherlockPartsService_pb2 import SherlockPartsService_pb2_grpc except ModuleNotFoundError: + from ansys.api.sherlock.v0 import SherlockCommonService_pb2 from ansys.api.sherlock.v0 import SherlockPartsService_pb2 from ansys.api.sherlock.v0 import SherlockPartsService_pb2_grpc @@ -29,6 +31,7 @@ AVLPartNum, DeletePartsFromPartsListRequest, GetPartsListPropertiesRequest, + ImportPartsToAVLRequest, PartLocation, PartsListSearchDuplicationMode, UpdatePadPropertiesRequest, @@ -1191,3 +1194,41 @@ def delete_parts_from_parts_list( delete_request = request._convert_to_grpc() return list(self.stub.deletePartsFromPartsList(delete_request)) + + @require_version(261) + def import_parts_to_avl( + self, + request: ImportPartsToAVLRequest, + ) -> SherlockCommonService_pb2.ReturnCode: + """Import a parts list into the Approved Vendor List (AVL). + + Available Since: 2026R1 + + Parameters + ---------- + request : ImportPartsToAVLRequest + Contains the file path and import mode to use for the AVL parts import. + + Returns + ------- + SherlockCommonService_pb2.ReturnCode + Return code indicating the result of the AVL parts import. + + Examples + -------- + >>> from ansys.sherlock.core.types.project_types import ImportPartsToAVLRequest + >>> from ansys.api.sherlock.v0 import SherlockPartsService_pb2 + >>> from ansys.sherlock.core.launcher import launch_sherlock + >>> + >>> sherlock = launch_sherlock() + >>> return_code = sherlock.project.import_parts_to_avl( + >>> ImportPartsToAVLRequest( + >>> import_file="C:/path/to/AVL_parts_file.xls", + >>> import_type=SherlockPartsService_pb2.AVLImportType.Update + >>> ) + >>> ) + """ + if not self._is_connection_up(): + raise SherlockNoGrpcConnectionException() + + return self.stub.importPartsToAVL(request._convert_to_grpc()) diff --git a/src/ansys/sherlock/core/types/parts_types.py b/src/ansys/sherlock/core/types/parts_types.py index 782ca312e..019bf0e3c 100644 --- a/src/ansys/sherlock/core/types/parts_types.py +++ b/src/ansys/sherlock/core/types/parts_types.py @@ -162,3 +162,30 @@ def _convert_to_grpc(self) -> parts_service.DeletePartsFromPartsListRequest: ccaName=self.cca_name, refDes=self.reference_designators, ) + + +class ImportPartsToAVLRequest(BaseModel): + """Request to import parts into the Approved Vendor List (AVL).""" + + import_file: str + """Full file path to the AVL file.""" + + import_type: parts_service.AVLImportType + """Import mode to use for AVL data.""" + + """Allow non-standard types like Protobuf enums in Pydantic models.""" + model_config = {"arbitrary_types_allowed": True} + + @field_validator("import_file") + @classmethod + def import_file_validator(cls, value: str, info): + """Validate that the import file path is not empty.""" + if value.strip() == "": + raise ValueError(f"{info.field_name} cannot be empty.") + return basic_str_validator(value, info.field_name) + + def _convert_to_grpc(self) -> parts_service.ImportPartsToAVLRequest: + request = parts_service.ImportPartsToAVLRequest() + request.importFile = self.import_file + request.importType = self.import_type + return request diff --git a/tests/test_parts.py b/tests/test_parts.py index ce5118f88..a570f92d7 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -3,6 +3,7 @@ import os import platform +from ansys.api.sherlock.v0 import SherlockPartsService_pb2 import grpc import pydantic import pytest @@ -27,11 +28,14 @@ AVLPartNum, DeletePartsFromPartsListRequest, GetPartsListPropertiesRequest, + ImportPartsToAVLRequest, PartsListSearchDuplicationMode, UpdatePadPropertiesRequest, ) from ansys.sherlock.core.utils.version_check import SKIP_VERSION_CHECK +parts_service = SherlockPartsService_pb2 + def test_all(): """Test all parts APIs""" @@ -1071,5 +1075,33 @@ def helper_test_delete_parts_from_parts_list(parts: Parts): pytest.fail(f"Unexpected exception raised: {e}") +def helper_test_import_parts_to_avl(parts: Parts): + """Test import parts to AVL API.""" + try: + # Test with an empty import_file path + ImportPartsToAVLRequest(import_file="", import_type=parts_service.AVLImportType.Replace) + pytest.fail("No exception raised when using an empty import_file parameter") + except Exception as e: + assert isinstance(e, pydantic.ValidationError) + assert e.errors()[0]["msg"] == "Value error, import_file cannot be empty." + + try: + # Test with a invalid AVL file path + request = ImportPartsToAVLRequest( + import_file="invalid/path/parts_list.csv", + import_type=parts_service.AVLImportType.Add, + ) + + if parts._is_connection_up(): + return_code = parts.import_parts_to_avl(request) + + # Check that an invalid import_file returns an error + assert return_code.value == -1 + assert return_code.message == "File Not Found" + + except Exception as e: + pytest.fail(f"Unexpected exception raised: {e}") + + if __name__ == "__main__": test_all() From 49d31731a7d509fc0592bc7af091e60ef6312fa6 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:50:44 +0000 Subject: [PATCH 10/13] chore: adding changelog file 617.miscellaneous.md [dependabot-skip] --- doc/changelog.d/617.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/617.miscellaneous.md diff --git a/doc/changelog.d/617.miscellaneous.md b/doc/changelog.d/617.miscellaneous.md new file mode 100644 index 000000000..a386e7461 --- /dev/null +++ b/doc/changelog.d/617.miscellaneous.md @@ -0,0 +1 @@ +Feat: Added API import_parts_to_avl() \ No newline at end of file From e7b6d39ce67c5a7a061d96334b01d81c6355e1e3 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:51:44 +0000 Subject: [PATCH 11/13] chore: adding changelog file 617.added.md [dependabot-skip] --- doc/changelog.d/{617.miscellaneous.md => 617.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{617.miscellaneous.md => 617.added.md} (100%) diff --git a/doc/changelog.d/617.miscellaneous.md b/doc/changelog.d/617.added.md similarity index 100% rename from doc/changelog.d/617.miscellaneous.md rename to doc/changelog.d/617.added.md From 2218b61ada1f6df2865dacb700e5208884783aae Mon Sep 17 00:00:00 2001 From: Nayane Fernandes <143632290+ansnfernand@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:49:09 +0200 Subject: [PATCH 12/13] Update after feedback --- src/ansys/sherlock/core/parts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/sherlock/core/parts.py b/src/ansys/sherlock/core/parts.py index 595822bcc..60a4783d7 100644 --- a/src/ansys/sherlock/core/parts.py +++ b/src/ansys/sherlock/core/parts.py @@ -1216,7 +1216,7 @@ def import_parts_to_avl( Examples -------- - >>> from ansys.sherlock.core.types.project_types import ImportPartsToAVLRequest + >>> from ansys.sherlock.core.types.part_types import ImportPartsToAVLRequest >>> from ansys.api.sherlock.v0 import SherlockPartsService_pb2 >>> from ansys.sherlock.core.launcher import launch_sherlock >>> From fc75d183a3c470e6716a4f52d043e1a9eb7de305 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:51:25 +0000 Subject: [PATCH 13/13] chore: adding changelog file 617.added.md [dependabot-skip] --- doc/changelog.d/617.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.d/617.added.md b/doc/changelog.d/617.added.md index a386e7461..67b618aa9 100644 --- a/doc/changelog.d/617.added.md +++ b/doc/changelog.d/617.added.md @@ -1 +1 @@ -Feat: Added API import_parts_to_avl() \ No newline at end of file +Feat: Added API import_parts_to_avl()