diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index c7a18d13e..703970876 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "4.0.3" +__version__ = "4.1.0" diff --git a/src/citrine/informatics/data_sources.py b/src/citrine/informatics/data_sources.py index c9f957cbf..9d26ffe75 100644 --- a/src/citrine/informatics/data_sources.py +++ b/src/citrine/informatics/data_sources.py @@ -2,6 +2,8 @@ from abc import abstractmethod from uuid import UUID +from deprecation import deprecated + from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable @@ -115,7 +117,7 @@ def from_gemtable(cls, table: GemTable) -> "GemTableDataSource": class ExperimentDataSourceRef(Serializable['ExperimentDataSourceRef'], DataSource): - """A reference to a data source based on an experiment result hosted on the data platform. + """[DEPRECATED] A reference to a data source based on an experiment result on the platform. Parameters ---------- @@ -129,6 +131,10 @@ class ExperimentDataSourceRef(Serializable['ExperimentDataSourceRef'], DataSourc _data_source_type = "experiments" + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates on the platform. " + "Alternatively, you may convert the candidate into a collection of GEMD " + "objects manually.") def __init__(self, *, datasource_id: UUID): self.datasource_id: UUID = datasource_id diff --git a/src/citrine/informatics/experiment_values.py b/src/citrine/informatics/experiment_values.py index 3a0f90bff..e78f7b8b7 100644 --- a/src/citrine/informatics/experiment_values.py +++ b/src/citrine/informatics/experiment_values.py @@ -1,3 +1,5 @@ +from deprecation import deprecated + from citrine._serialization.serializable import Serializable from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties @@ -14,11 +16,18 @@ class ExperimentValue(PolymorphicSerializable['ExperimentValue']): - """An container for experiment values. + """[DEPRECATED] An container for experiment values. Abstract type that returns the proper type given a serialized dict. """ + @classmethod + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") + def build(cls, data: dict) -> 'ExperimentValue': + """Build the underlying type.""" + return super().build(data) + @classmethod def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" @@ -67,62 +76,74 @@ def _equals(self, other, attrs) -> bool: class RealExperimentValue(Serializable['RealExperimentValue'], ExperimentValue): - """A floating point experiment result.""" + """[DEPRECATED] A floating point experiment result.""" value = properties.Float('value') typ = properties.String('type', default='RealValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: float): self.value = value class IntegerExperimentValue(Serializable['IntegerExperimentValue'], ExperimentValue): - """An integer value experiment result.""" + """[DEPRECATED] An integer value experiment result.""" value = properties.Integer('value') typ = properties.String('type', default='IntegerValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: int): self.value = value class CategoricalExperimentValue(Serializable['CategoricalExperimentValue'], ExperimentValue): - """An experiment result with a categorical value.""" + """[DEPRECATED] An experiment result with a categorical value.""" value = properties.String('value') typ = properties.String('type', default='CategoricalValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: str): self.value = value class MixtureExperimentValue(Serializable['MixtureExperimentValue'], ExperimentValue): - """An experiment result mapping ingredients and labels to real values.""" + """[DEPRECATED] An experiment result mapping ingredients and labels to real values.""" value = properties.Mapping(properties.String, properties.Float, 'value') typ = properties.String('type', default='MixtureValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: dict[str, float]): self.value = value class ChemicalFormulaExperimentValue(Serializable['ChemicalFormulaExperimentValue'], ExperimentValue): - """Experiment value for a chemical formula.""" + """[DEPRECATED] Experiment value for a chemical formula.""" value = properties.String('value') typ = properties.String('type', default='InorganicValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: str): self.value = value class MolecularStructureExperimentValue(Serializable['MolecularStructureExperimentValue'], ExperimentValue): - """Experiment value for a molecular structure.""" + """[DEPRECATED] Experiment value for a molecular structure.""" value = properties.String('value') typ = properties.String('type', default='OrganicValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: str): self.value = value diff --git a/src/citrine/informatics/predictors/graph_predictor.py b/src/citrine/informatics/predictors/graph_predictor.py index a636e1b7b..3ebb4c6bf 100644 --- a/src/citrine/informatics/predictors/graph_predictor.py +++ b/src/citrine/informatics/predictors/graph_predictor.py @@ -48,8 +48,6 @@ class GraphPredictor(VersionedEngineResource['GraphPredictor'], AsynchronousObje description = properties.Optional(properties.String(), 'data.description') predictors = properties.List(properties.Object(PredictorNode), 'data.instance.predictors') - # the default seems to be defined in instances, not the class itself - # this is tested in test_graph_default_training_data training_data = properties.List( properties.Object(DataSource), 'data.instance.training_data', default=[] ) diff --git a/src/citrine/resources/experiment_datasource.py b/src/citrine/resources/experiment_datasource.py index 5171c111f..e78ab0fbd 100644 --- a/src/citrine/resources/experiment_datasource.py +++ b/src/citrine/resources/experiment_datasource.py @@ -5,6 +5,8 @@ from io import StringIO from uuid import UUID +from deprecation import deprecated + from citrine._rest.collection import Collection from citrine._serialization import properties from citrine._serialization.serializable import Serializable @@ -32,6 +34,8 @@ class CandidateExperimentSnapshot(Serializable['CandidateExperimentSnapshot']): 'overrides') """:dict[str, ExperimentValue]: dictionary of candidate material variable overrides""" + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, *args, **kwargs): """Candidate experiment snapshots are not directly instantiated by the user.""" pass # pragma: no cover @@ -58,6 +62,8 @@ class ExperimentDataSource(Serializable['ExperimentDataSource']): create_time = properties.Datetime('metadata.created.time', serializable=False) """:datetime: date and time at which this data source was created""" + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, *args, **kwargs): """Experiment data sources are not directly instantiated by the user.""" pass # pragma: no cover @@ -86,13 +92,17 @@ def read(self) -> str: class ExperimentDataSourceCollection(Collection[ExperimentDataSource]): - """Represents the collection of all experiment data sources associated with a project.""" + """[DEPRECATED] The collection of all experiment data sources associated with a project.""" _path_template = 'projects/{project_id}/candidate-experiment-datasources' _individual_key = None _resource = ExperimentDataSource _collection_key = 'response' + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates on the platform. " + "Alternatively, you may convert the candidate into a collection of GEMD " + "objects manually.") def __init__(self, project_id: UUID, session: Session): self.project_id = project_id self.session: Session = session diff --git a/src/citrine/resources/predictor.py b/src/citrine/resources/predictor.py index 4125f052c..fad6dd839 100644 --- a/src/citrine/resources/predictor.py +++ b/src/citrine/resources/predictor.py @@ -1,4 +1,5 @@ """Resources that represent collections of predictors.""" +import warnings from collections.abc import Iterable from functools import partial from typing import Any @@ -11,7 +12,7 @@ from citrine._rest.paginator import Paginator from citrine._serialization import properties from citrine._session import Session -from citrine.informatics.data_sources import DataSource +from citrine.informatics.data_sources import DataSource, ExperimentDataSourceRef from citrine.informatics.design_candidate import HierarchicalDesignMaterial from citrine.informatics.predictors import GraphPredictor from citrine.resources.status_detail import StatusDetail @@ -107,6 +108,16 @@ def _page_fetcher(self, *, uid: UUID | str, **additional_params): } return partial(self._fetch_page, **fetcher_params) + def _check_data_sources(self, predictor: GraphPredictor): + for data_source in predictor.training_data: + print(data_source) + if isinstance(data_source, ExperimentDataSourceRef): + warnings.warn("This predictor contains an experiment result, which is being " + "replaced by creating materials from candidates on the platform. " + "Alternatively, you may convert the candidate into a collection of " + "GEMD objects manually.", + DeprecationWarning) + def build(self, data: dict) -> GraphPredictor: """Build an individual Predictor.""" predictor: GraphPredictor = GraphPredictor.build(data) @@ -120,7 +131,9 @@ def get(self, version: int | str = MOST_RECENT_VER) -> GraphPredictor: path = self._construct_path(uid, version) entity = self.session.get_resource(path, version=self._api_version) - return self.build(entity) + predictor = self.build(entity) + self._check_data_sources(predictor) + return predictor def get_featurized_training_data( self, diff --git a/tests/informatics/test_data_source.py b/tests/informatics/test_data_source.py index 7ca003371..9bc8f04a2 100644 --- a/tests/informatics/test_data_source.py +++ b/tests/informatics/test_data_source.py @@ -15,12 +15,16 @@ @pytest.fixture(params=[ GemTableDataSource(table_id=uuid.uuid4(), table_version=1), GemTableDataSource(table_id=uuid.uuid4(), table_version="2"), - ExperimentDataSourceRef(datasource_id=uuid.uuid4()), SnapshotDataSource(snapshot_id=uuid.uuid4()) ]) def data_source(request): return request.param +@pytest.fixture +def deprecated_data_source(): + with pytest.deprecated_call(): + return ExperimentDataSourceRef(datasource_id=uuid.uuid4()) + def test_deser_from_parent(data_source): # Serialize and deserialize the descriptors, making sure they are round-trip serializable @@ -29,6 +33,13 @@ def test_deser_from_parent(data_source): assert data_source == data_source_deserialized +def test_deser_from_parent_deprecated(deprecated_data_source): + # Serialize and deserialize the descriptors, making sure they are round-trip serializable + data = deprecated_data_source.dump() + data_source_deserialized = DataSource.build(data) + assert deprecated_data_source == data_source_deserialized + + def test_invalid_eq(data_source): other = None assert not data_source == other @@ -42,6 +53,10 @@ def test_invalid_deser(): DataSource.build({"type": "foo"}) +def test_deprecated_data_source_id(deprecated_data_source): + with pytest.deprecated_call(): + assert deprecated_data_source == DataSource.from_data_source_id(deprecated_data_source.to_data_source_id()) + def test_data_source_id(data_source): assert data_source == DataSource.from_data_source_id(data_source.to_data_source_id()) diff --git a/tests/informatics/test_experiment_values.py b/tests/informatics/test_experiment_values.py index fc2887a8e..0c6427803 100644 --- a/tests/informatics/test_experiment_values.py +++ b/tests/informatics/test_experiment_values.py @@ -12,21 +12,24 @@ @pytest.fixture(params=[ - CategoricalExperimentValue("categorical"), - ChemicalFormulaExperimentValue("(Ca)1(O)3(Si)1"), - IntegerExperimentValue(7), - MixtureExperimentValue({"ingredient1": 0.3, "ingredient2": 0.7}), - MolecularStructureExperimentValue("CC1(CC(CC(N1)(C)C)NCCCCCCNC2CC(NC(C2)(C)C)(C)C)C.C1COCCN1C2=NC(=NC(=N2)Cl)Cl"), - RealExperimentValue(3.5) + (CategoricalExperimentValue, ("categorical", )), + (ChemicalFormulaExperimentValue, ("(Ca)1(O)3(Si)1",)), + (IntegerExperimentValue, (7,)), + (MixtureExperimentValue, ({"ingredient1": 0.3, "ingredient2": 0.7},)), + (MolecularStructureExperimentValue, ("CC1(CC(CC(N1)(C)C)NCCCCCCNC2CC(NC(C2)(C)C)(C)C)C.C1COCCN1C2=NC(=NC(=N2)Cl)Cl",)), + (RealExperimentValue, (3.5,)) ]) def experiment_value(request): - return request.param + cls, args = request.param + with pytest.deprecated_call(): + return cls(*args) def test_deser_from_parent(experiment_value): # Serialize and deserialize the experiment values, making sure they are round-trip serializable data = experiment_value.dump() - experiment_value_deserialized = ExperimentValue.build(data) + with pytest.deprecated_call(): + experiment_value_deserialized = ExperimentValue.build(data) assert experiment_value == experiment_value_deserialized diff --git a/tests/resources/test_branch.py b/tests/resources/test_branch.py index c0feea86b..bd1581861 100644 --- a/tests/resources/test_branch.py +++ b/tests/resources/test_branch.py @@ -537,7 +537,9 @@ def test_experiment_datasource(session, collection): session.set_response({'response': [erds]}) # When / Then - assert branch.experiment_datasource is not None + with pytest.deprecated_call(): + assert branch.experiment_datasource is not None + assert session.calls == [ FakeCall(method='GET', path=erds_path, params={'branch': str(branch.uid), 'version': LATEST_VER, 'per_page': 100, 'page': 1}) ] @@ -550,7 +552,9 @@ def test_no_experiment_datasource(session, collection): session.set_response({'response': []}) # When / Then - assert branch.experiment_datasource is None + with pytest.deprecated_call(): + assert branch.experiment_datasource is None + assert session.calls == [ FakeCall(method='GET', path=erds_path, params={'branch': str(branch.uid), 'version': LATEST_VER, 'per_page': 100, 'page': 1}) ] diff --git a/tests/resources/test_experiment_datasource.py b/tests/resources/test_experiment_datasource.py index b7db06011..4e4e1bf4d 100644 --- a/tests/resources/test_experiment_datasource.py +++ b/tests/resources/test_experiment_datasource.py @@ -23,7 +23,8 @@ def session(): @pytest.fixture def collection(session) -> ExperimentDataSourceCollection: - return ExperimentDataSourceCollection(uuid.uuid4(), session) + with pytest.deprecated_call(): + return ExperimentDataSourceCollection(uuid.uuid4(), session) @pytest.fixture diff --git a/tests/resources/test_predictor.py b/tests/resources/test_predictor.py index efce269ec..9eaf206ec 100644 --- a/tests/resources/test_predictor.py +++ b/tests/resources/test_predictor.py @@ -5,7 +5,7 @@ from copy import deepcopy from citrine.exceptions import BadRequest, Conflict, ModuleRegistrationFailedException, NotFound -from citrine.informatics.data_sources import GemTableDataSource +from citrine.informatics.data_sources import ExperimentDataSourceRef, GemTableDataSource from citrine.informatics.descriptors import RealDescriptor from citrine.informatics.predictors import ( AutoMLPredictor, @@ -754,3 +754,20 @@ def test_rename_description_only(valid_graph_predictor_data): versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) expected_payload = {"name": None, "description": new_description} assert session.calls == [FakeCall(method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload)] + + +def test_get_predictor_with_experiment_data_source_deprecated(valid_graph_predictor_data): + # Given + session = FakeSession() + pc = PredictorCollection(uuid.uuid4(), session) + + with pytest.deprecated_call(): + erds = ExperimentDataSourceRef(datasource_id=uuid.uuid4()) + entity = deepcopy(valid_graph_predictor_data) + entity["data"]["instance"]["training_data"] = [erds.dump()] + + session.set_responses(entity) + + # When + with pytest.deprecated_call(): + pc.get(uuid.uuid4())