From 7882715cefdd93c752675ab5d025b1720c48b39b Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 18 Dec 2025 13:02:33 -0500 Subject: [PATCH] [PNE-7661] Deprecate most top-level design spaces. As a simplification for the platform, all design spaces should be either a ProductDesignSpace or HierarchicalDesignSpace. The other types (namely, DataSourceDesignSpace and FormulationDesignSpace) should be subspaces of them. This is the only structure supported by the UI, so it will improve their experience of the platform, too. Additionally, EnumeratedDesignSpace is obsolete, and should be replaced by a DataSourceDesign space within a ProductDesignSpace. Note that we also emit a warning on retrieval. That's because when we go to v4.0, we will stop supporting such design spaces at all. This gives users a chance to update their design spaces as necessary, and ensures they will not be surprised when the type of design space they retrieve changes. --- src/citrine/__version__.py | 2 +- src/citrine/resources/design_space.py | 66 +++++++++++++++++++++++++-- tests/resources/test_design_space.py | 60 +++++++++++++++++------- 3 files changed, 108 insertions(+), 20 deletions(-) diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index 1e6bdc426..62bfee6c5 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "3.26.0" +__version__ = "3.27.0" diff --git a/src/citrine/resources/design_space.py b/src/citrine/resources/design_space.py index 6daf02927..fb1aa5f69 100644 --- a/src/citrine/resources/design_space.py +++ b/src/citrine/resources/design_space.py @@ -1,12 +1,14 @@ """Resources that represent collections of design spaces.""" +import warnings from functools import partial -from typing import Iterable, Optional, TypeVar, Union +from typing import Iterable, Iterator, Optional, TypeVar, Union from uuid import UUID from citrine._utils.functions import format_escaped_url -from citrine.informatics.design_spaces import DefaultDesignSpaceMode, DesignSpace, \ - DesignSpaceSettings, EnumeratedDesignSpace, HierarchicalDesignSpace +from citrine.informatics.design_spaces import DataSourceDesignSpace, DefaultDesignSpaceMode, \ + DesignSpace, DesignSpaceSettings, EnumeratedDesignSpace, FormulationDesignSpace, \ + HierarchicalDesignSpace from citrine._rest.collection import Collection from citrine._session import Session @@ -48,8 +50,16 @@ def _verify_write_request(self, design_space: DesignSpace): rather than let the POST or PUT call fail because the request body is too big. This validation is performed when the design space is sent to the platform in case a user creates a large intermediate design space but then filters it down before registering it. + + Additionally, checks for deprecated top-level design space types, and emits deprecation + warnings as appropriate. """ if isinstance(design_space, EnumeratedDesignSpace): + warnings.warn("As of 3.27.0, EnumeratedDesignSpace is deprecated in favor of a " + "ProductDesignSpace containing a DataSourceDesignSpace subspace. " + "Support for EnumeratedDesignSpace will be dropped in 4.0.", + DeprecationWarning) + width = len(design_space.descriptors) length = len(design_space.data) if width * length > self._enumerated_cell_limit: @@ -57,6 +67,31 @@ def _verify_write_request(self, design_space: DesignSpace): "but {} were given. Please reduce the number of descriptors or candidates " \ "in this EnumeratedDesignSpace" raise ValueError(msg.format(self._enumerated_cell_limit, width * length)) + elif isinstance(design_space, (DataSourceDesignSpace, FormulationDesignSpace)): + typ = type(design_space).__name__ + warnings.warn(f"As of 3.27.0, saving a top-level {typ} is deprecated. Support " + "will be removed in 4.0. Wrap it in a ProductDesignSpace instead: " + f"ProductDesignSpace('name', 'description', subspaces=[{typ}(...)])", + DeprecationWarning) + + def _verify_read_request(self, design_space: DesignSpace): + """Perform read-time validations of the design space. + + Checks for deprecated top-level design space types, and emits deprecation warnings as + appropriate. + """ + if isinstance(design_space, EnumeratedDesignSpace): + warnings.warn("As of 3.27.0, EnumeratedDesignSpace is deprecated in favor of a " + "ProductDesignSpace containing a DataSourceDesignSpace subspace. " + "Support for EnumeratedDesignSpace will be dropped in 4.0.", + DeprecationWarning) + elif isinstance(design_space, (DataSourceDesignSpace, FormulationDesignSpace)): + typ = type(design_space).__name__ + warnings.warn(f"As of 3.27.0, top-level {typ}s are deprecated. Any that remain when " + "SDK 4.0 are released will be wrapped in a ProductDesignSpace. You " + "can wrap it yourself to get rid of this warning now: " + f"ProductDesignSpace('name', 'description', subspaces=[{typ}(...)])", + DeprecationWarning) def register(self, design_space: DesignSpace) -> DesignSpace: """Create a new design space.""" @@ -116,6 +151,31 @@ def restore(self, uid: Union[UUID, str]) -> DesignSpace: entity = self.session.put_resource(url, {}, version=self._api_version) return self.build(entity) + def get(self, uid: Union[UUID, str]) -> DesignSpace: + """Get a particular element of the collection.""" + design_space = super().get(uid) + self._verify_read_request(design_space) + return design_space + + def _build_collection_elements(self, collection: Iterable[dict]) -> Iterator[DesignSpace]: + """ + For each element in the collection, build the appropriate resource type. + + Parameters + --------- + collection: Iterable[dict] + collection containing the elements to be built + + Returns + ------- + Iterator[DesignSpace] + Resources in this collection. + + """ + for design_space in super()._build_collection_elements(collection=collection): + self._verify_read_request(design_space) + yield design_space + def _list_base(self, *, per_page: int = 100, archived: Optional[bool] = None): filters = {} if archived is not None: diff --git a/tests/resources/test_design_space.py b/tests/resources/test_design_space.py index 3bcde2e14..f56f3ea85 100644 --- a/tests/resources/test_design_space.py +++ b/tests/resources/test_design_space.py @@ -169,18 +169,21 @@ def test_design_space_limits(): session.responses.append(mock_response) # Then - with pytest.raises(ValueError) as excinfo: - collection.register(too_big) + with pytest.deprecated_call(): + with pytest.raises(ValueError) as excinfo: + collection.register(too_big) assert "only supports" in str(excinfo.value) # test register - collection.register(just_right) + with pytest.deprecated_call(): + collection.register(just_right) # add back the response for the next test session.responses.append(mock_response) # test update - collection.update(just_right) + with pytest.deprecated_call(): + collection.update(just_right) @pytest.mark.parametrize("predictor_version", (2, "1", "latest", None)) @@ -312,12 +315,12 @@ def test_create_default_with_config(valid_product_design_space, ingredient_fract assert default_design_space.dump() == expected_response -def test_list_design_spaces(valid_formulation_design_space_data, valid_enumerated_design_space_data): +def test_list_design_spaces(valid_product_design_space_data, valid_hierarchical_design_space_data): # Given session = FakeSession() collection = DesignSpaceCollection(uuid.uuid4(), session) session.set_response({ - 'response': [valid_formulation_design_space_data, valid_enumerated_design_space_data] + 'response': [valid_product_design_space_data, valid_hierarchical_design_space_data] }) # When @@ -331,12 +334,12 @@ def test_list_design_spaces(valid_formulation_design_space_data, valid_enumerate assert len(design_spaces) == 2 -def test_list_all_design_spaces(valid_formulation_design_space_data, valid_enumerated_design_space_data): +def test_list_all_design_spaces(valid_product_design_space_data, valid_hierarchical_design_space_data): # Given session = FakeSession() collection = DesignSpaceCollection(uuid.uuid4(), session) session.set_response({ - 'response': [valid_formulation_design_space_data, valid_enumerated_design_space_data] + 'response': [valid_product_design_space_data, valid_hierarchical_design_space_data] }) # When @@ -350,12 +353,12 @@ def test_list_all_design_spaces(valid_formulation_design_space_data, valid_enume assert len(design_spaces) == 2 -def test_list_archived_design_spaces(valid_formulation_design_space_data, valid_enumerated_design_space_data): +def test_list_archived_design_spaces(valid_product_design_space_data, valid_hierarchical_design_space_data): # Given session = FakeSession() collection = DesignSpaceCollection(uuid.uuid4(), session) session.set_response({ - 'response': [valid_formulation_design_space_data, valid_enumerated_design_space_data] + 'response': [valid_product_design_space_data, valid_hierarchical_design_space_data] }) # When @@ -369,13 +372,13 @@ def test_list_archived_design_spaces(valid_formulation_design_space_data, valid_ assert len(design_spaces) == 2 -def test_archive(valid_formulation_design_space_data): +def test_archive(valid_product_design_space_data): session = FakeSession() dsc = DesignSpaceCollection(uuid.uuid4(), session) base_path = DesignSpaceCollection._path_template.format(project_id=dsc.project_id) - ds_id = valid_formulation_design_space_data["id"] + ds_id = valid_product_design_space_data["id"] - response = deepcopy(valid_formulation_design_space_data) + response = deepcopy(valid_product_design_space_data) response["metadata"]["archived"] = response["metadata"]["created"] session.set_response(response) @@ -387,13 +390,13 @@ def test_archive(valid_formulation_design_space_data): ] -def test_restore(valid_formulation_design_space_data): +def test_restore(valid_product_design_space_data): session = FakeSession() dsc = DesignSpaceCollection(uuid.uuid4(), session) base_path = DesignSpaceCollection._path_template.format(project_id=dsc.project_id) - ds_id = valid_formulation_design_space_data["id"] + ds_id = valid_product_design_space_data["id"] - response = deepcopy(valid_formulation_design_space_data) + response = deepcopy(valid_product_design_space_data) if "archived" in response["metadata"]: del response["metadata"]["archived"] session.set_response(deepcopy(response)) @@ -563,3 +566,28 @@ def test_locked(valid_product_design_space_data): assert ds.is_locked assert ds.locked_by == lock_user assert ds.lock_time == lock_time + + +@pytest.mark.parametrize("ds_data_fixture_name", ("valid_formulation_design_space_data", + "valid_enumerated_design_space_data", + "valid_data_source_design_space_dict")) +def test_deprecated_top_level_design_spaces(request, ds_data_fixture_name): + ds_data = request.getfixturevalue(ds_data_fixture_name) + + session = FakeSession() + session.set_response(ds_data) + dc = DesignSpaceCollection(uuid.uuid4(), session) + + with pytest.deprecated_call(): + ds = dc.get(uuid.uuid4()) + + with pytest.deprecated_call(): + dc.register(ds) + + with pytest.deprecated_call(): + dc.update(ds) + + session.set_response({"response": [ds_data]}) + + with pytest.deprecated_call(): + next(dc.list())