Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/citrine/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.26.0"
__version__ = "3.27.0"
66 changes: 63 additions & 3 deletions src/citrine/resources/design_space.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -48,15 +50,48 @@ 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:
msg = "EnumeratedDesignSpace only supports up to {} descriptor-values, " \
"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."""
Expand Down Expand Up @@ -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:
Expand Down
60 changes: 44 additions & 16 deletions tests/resources/test_design_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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))
Expand Down Expand Up @@ -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())
Loading