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())