From 8ecd51b2f3a030fc26b3845d2f77ff08a9648af6 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 21 Dec 2023 15:16:37 -0500 Subject: [PATCH 01/22] Bump to 3.0 --- src/citrine/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index 03e94c21f..528787cfc 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "2.42.1" +__version__ = "3.0.0" From 644b3e715751404b5cadc53d9df84488cd4946dd Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 21 Dec 2023 15:33:25 -0500 Subject: [PATCH 02/22] [PLA-13767] Remove all deprecated code. Cleans out all deprecated code, be it through the deprecated package, a DeprecationWarning, or any other form. --- src/citrine/_rest/ai_resource_metadata.py | 10 - src/citrine/_rest/engine_resource.py | 37 +-- src/citrine/_utils/functions.py | 11 - .../ingredient_ratio_constraint.py | 15 +- src/citrine/informatics/data_sources.py | 15 +- src/citrine/informatics/descriptors.py | 11 - .../design_spaces/data_source_design_space.py | 4 +- .../informatics/design_spaces/design_space.py | 14 - .../design_spaces/enumerated_design_space.py | 4 +- .../design_spaces/formulation_design_space.py | 4 +- .../hierarchical_design_space.py | 25 +- .../design_spaces/product_design_space.py | 4 +- .../informatics/executions/execution.py | 11 +- .../predictors/auto_ml_predictor.py | 3 - .../informatics/predictors/graph_predictor.py | 19 +- .../ingredients_to_formulation_predictor.py | 14 +- .../predictors/mean_property_predictor.py | 3 - src/citrine/informatics/predictors/node.py | 157 +-------- .../predictors/simple_mixture_predictor.py | 24 +- .../informatics/workflows/design_workflow.py | 20 +- src/citrine/resources/branch.py | 192 +++-------- src/citrine/resources/descriptors.py | 8 - src/citrine/resources/design_execution.py | 15 - src/citrine/resources/design_space.py | 35 +- src/citrine/resources/design_workflow.py | 71 ++--- .../resources/experiment_datasource.py | 11 - src/citrine/resources/file_link.py | 186 +---------- src/citrine/resources/job.py | 45 --- .../predictor_evaluation_execution.py | 14 +- src/citrine/resources/project.py | 9 - .../sample_design_space_execution.py | 3 - tests/_util/test_functions.py | 27 +- tests/informatics/test_data_source.py | 9 - tests/informatics/test_descriptors.py | 6 - tests/informatics/test_design_spaces.py | 19 -- tests/informatics/test_predictors.py | 92 ------ tests/resources/test_branch.py | 301 +----------------- tests/resources/test_descriptors.py | 42 --- tests/resources/test_design_executions.py | 8 - tests/resources/test_design_space.py | 129 -------- tests/resources/test_design_workflows.py | 99 ------ tests/resources/test_experiment_datasource.py | 25 -- tests/resources/test_file_link.py | 85 +---- .../test_generative_design_execution.py | 2 - tests/resources/test_job_client.py | 102 ------ .../test_predictor_evaluation_executions.py | 8 - tests/resources/test_project.py | 5 - tests/resources/test_workflow.py | 11 - tests/utils/fakes/fake_workflow_collection.py | 5 +- 49 files changed, 100 insertions(+), 1869 deletions(-) delete mode 100644 src/citrine/resources/job.py delete mode 100644 tests/resources/test_job_client.py diff --git a/src/citrine/_rest/ai_resource_metadata.py b/src/citrine/_rest/ai_resource_metadata.py index 627a4864b..4b3175ed7 100644 --- a/src/citrine/_rest/ai_resource_metadata.py +++ b/src/citrine/_rest/ai_resource_metadata.py @@ -1,10 +1,6 @@ -from typing import List - from citrine.resources.status_detail import StatusDetail from citrine._serialization import properties -from deprecation import deprecated - class AIResourceMetadata(): """Abstract class for representing common metadata for Resources.""" @@ -34,9 +30,3 @@ class AIResourceMetadata(): status_detail = properties.List(properties.Object(StatusDetail), 'status_detail', default=[], serializable=False) """:List[StatusDetail]: a list of structured status info, containing the message and level""" - - @property - @deprecated(deprecated_in="2.2.0", removed_in="3.0.0", details="Use status_detail instead.") - def status_info(self) -> List[str]: - """:List[str]: human-readable explanations of the status.""" - return [detail.msg for detail in self.status_detail] diff --git a/src/citrine/_rest/engine_resource.py b/src/citrine/_rest/engine_resource.py index 2d5deeec0..09c956ae9 100644 --- a/src/citrine/_rest/engine_resource.py +++ b/src/citrine/_rest/engine_resource.py @@ -1,12 +1,10 @@ -from typing import List, TypeVar +from typing import TypeVar from citrine._rest.resource import Resource, ResourceTypeEnum from citrine._serialization import properties from citrine._serialization.include_parent_properties import IncludeParentProperties from citrine.resources.status_detail import StatusDetail -from deprecation import deprecated - Self = TypeVar('Self', bound='Resource') @@ -75,36 +73,3 @@ class VersionedEngineResource(EngineResource[Self], IncludeParentProperties[Self def build(cls, data: dict): """Build an instance of this object from given data.""" return super().build_with_parent(data, __class__) - - -class ModuleEngineResource(EngineResource[Self], IncludeParentProperties[Self]): - """Base resource for metadata from stand-alone AI Engine modules with deprecated fields.""" - - # Due to the way object construction is done at present, __init__ is not executed on Resource - # objects, so initializing _archived doesn't work. - _archived = properties.Optional(properties.Boolean(), '', default=None, serializable=False, - deserializable=False) - - @property - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", - details="Please use the 'is_archived' property instead.'") - def archived(self): - """[DEPRECATED] whether the design space is archived.""" - return self.is_archived - - @archived.setter - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", - details="Please use archive() and restore() on DesignSpaceCollection instead.") - def archived(self, value): - self._archived = value - - @property - @deprecated(deprecated_in="2.2.0", removed_in="3.0.0", details="Use status_detail instead.") - def status_info(self) -> List[str]: - """:List[str]: human-readable explanations of the status.""" - return [detail.msg for detail in self.status_detail] - - @classmethod - def build(cls, data: dict): - """Build an instance of this object from given data.""" - return super().build_with_parent(data, __class__) diff --git a/src/citrine/_utils/functions.py b/src/citrine/_utils/functions.py index 03af2684c..e22826cf9 100644 --- a/src/citrine/_utils/functions.py +++ b/src/citrine/_utils/functions.py @@ -1,6 +1,4 @@ from abc import ABCMeta -from deprecation import deprecated -import inspect import os from pathlib import Path from typing import Any, Dict, Optional, Sequence, Union @@ -223,15 +221,6 @@ class _CustomMeta(MigratedClassMeta, type(target)): return _CustomMeta -@deprecated(deprecated_in="2.22.1", removed_in="3.0.0", - details="Use MigratedClassMeta to explicitly deprecate migrated classes.") -def shadow_classes_in_module(source_module, target_module): - """Shadow classes from a source to a target module, for backwards compatibility purposes.""" - for c in [cls for _, cls in inspect.getmembers(source_module, inspect.isclass) if - cls.__module__ == source_module.__name__]: - setattr(target_module, c.__qualname__, c) - - def migrate_deprecated_argument( new_arg: Optional[Any], new_arg_name: str, diff --git a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py index 0f7b0163c..d86dcd7f3 100644 --- a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py @@ -102,14 +102,8 @@ def basis_ingredients(self) -> Mapping[str, float]: return self._basis_ingredients @basis_ingredients.setter - def basis_ingredients(self, value: Union[Set[str], Mapping[str, float]]): + def basis_ingredients(self, value: Set[str]): """Set the ingredients in the denominator of the ratio.""" - if isinstance(value, dict): - warnings.warn("As of version 2.13.0, multipliers for all basis ingredients are " - "ignored, so basis_ingredients should be a list of ingredient names.", - DeprecationWarning) - value = set(value.keys()) - self.basis_ingredient_names = value @property @@ -131,13 +125,8 @@ def basis_labels(self) -> Mapping[str, float]: return self._basis_labels @basis_labels.setter - def basis_labels(self, value: Union[Set[str], Mapping[str, float]]): + def basis_labels(self, value: Set[str]): """Set the labels in the denominator of the ratio.""" - if isinstance(value, dict): - warnings.warn("As of version 2.13.0, multipliers for all basis labels are ignored, so " - "basis_labels should be a list of label names.", DeprecationWarning) - value = set(value.keys()) - self.basis_label_names = value @property diff --git a/src/citrine/informatics/data_sources.py b/src/citrine/informatics/data_sources.py index a96282df1..39b5d4ecf 100644 --- a/src/citrine/informatics/data_sources.py +++ b/src/citrine/informatics/data_sources.py @@ -1,5 +1,4 @@ """Tools for working with Descriptors.""" -import warnings from abc import abstractmethod from typing import Type, List, Mapping, Optional, Union from uuid import UUID @@ -7,7 +6,7 @@ from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable -from citrine.informatics.descriptors import Descriptor, FormulationDescriptor +from citrine.informatics.descriptors import Descriptor from citrine.resources.file_link import FileLink __all__ = ['DataSource', @@ -109,20 +108,10 @@ def _attrs(self) -> List[str]: def __init__(self, *, table_id: UUID, - table_version: Union[int, str], - formulation_descriptor: Optional[FormulationDescriptor] = None): + table_version: Union[int, str]): self.table_id: UUID = table_id self.table_version: Union[int, str] = table_version - if formulation_descriptor is not None: - warnings.warn( - "The field `formulation_descriptor` on a GemTableDataSource is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - "FormulationDescriptor with key 'Formulation' for tables containing formulations.", - DeprecationWarning - ) - self.formulation_descriptor = None - class ExperimentDataSourceRef(Serializable['ExperimentDataSourceRef'], DataSource): """A reference to a data source based on an experiment result hosted on the data platform. diff --git a/src/citrine/informatics/descriptors.py b/src/citrine/informatics/descriptors.py index a509a6d69..692dfe91e 100644 --- a/src/citrine/informatics/descriptors.py +++ b/src/citrine/informatics/descriptors.py @@ -1,7 +1,6 @@ """Tools for working with Descriptors.""" from typing import Type, Set, Union -from deprecation import deprecated from gemd.enumeration.base_enumeration import BaseEnumeration from citrine._serialization.serializable import Serializable @@ -154,16 +153,6 @@ def __str__(self): def __repr__(self): return "IntegerDescriptor({}, {}, {})".format(self.key, self.lower_bound, self.upper_bound) - @property - @deprecated( - deprecated_in="2.27.0", - removed_in="3.0.0", - details="Integer descriptors are always dimensionless." - ) - def units(self) -> str: - """Return 'dimensionless' for the units of an integer descriptor.""" - return "dimensionless" - class ChemicalFormulaDescriptor(Serializable['ChemicalFormulaDescriptor'], Descriptor): """Captures domain-specific context about a stoichiometric chemical formula. diff --git a/src/citrine/informatics/design_spaces/data_source_design_space.py b/src/citrine/informatics/design_spaces/data_source_design_space.py index c87d3cb4a..dc8ef4409 100644 --- a/src/citrine/informatics/design_spaces/data_source_design_space.py +++ b/src/citrine/informatics/design_spaces/data_source_design_space.py @@ -1,4 +1,4 @@ -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.data_sources import DataSource from citrine.informatics.design_spaces.design_space import DesignSpace @@ -6,7 +6,7 @@ __all__ = ['DataSourceDesignSpace'] -class DataSourceDesignSpace(ModuleEngineResource['DataSourceDesignSpace'], DesignSpace): +class DataSourceDesignSpace(EngineResource['DataSourceDesignSpace'], DesignSpace): """An enumeration of candidates stored in a data source. Parameters diff --git a/src/citrine/informatics/design_spaces/design_space.py b/src/citrine/informatics/design_spaces/design_space.py index 4b09c387d..77c17dfb5 100644 --- a/src/citrine/informatics/design_spaces/design_space.py +++ b/src/citrine/informatics/design_spaces/design_space.py @@ -10,8 +10,6 @@ from citrine.resources.sample_design_space_execution import \ SampleDesignSpaceExecutionCollection -from deprecation import deprecated - __all__ = ['DesignSpace'] @@ -23,18 +21,6 @@ class DesignSpace(PolymorphicSerializable['DesignSpace'], AsynchronousObject): """ - @property - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", - details="Please use `isinstance` or `issubclass` instead.") - def module_type(self): - """The type of module.""" - return "DESIGN_SPACE" - - @module_type.setter - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0") - def module_type(self, value): - pass - uid = properties.Optional(properties.UUID, 'id', serializable=False) """:Optional[UUID]: Citrine Platform unique identifier""" name = properties.String('data.name') diff --git a/src/citrine/informatics/design_spaces/enumerated_design_space.py b/src/citrine/informatics/design_spaces/enumerated_design_space.py index 891e57a3a..8d1043216 100644 --- a/src/citrine/informatics/design_spaces/enumerated_design_space.py +++ b/src/citrine/informatics/design_spaces/enumerated_design_space.py @@ -1,6 +1,6 @@ from typing import List, Mapping, Any -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.descriptors import Descriptor from citrine.informatics.design_spaces.design_space import DesignSpace @@ -8,7 +8,7 @@ __all__ = ['EnumeratedDesignSpace'] -class EnumeratedDesignSpace(ModuleEngineResource['EnumeratedDesignSpace'], DesignSpace): +class EnumeratedDesignSpace(EngineResource['EnumeratedDesignSpace'], DesignSpace): """An explicit enumeration of candidate materials to score. Enumerated design spaces are intended to capture small spaces with fewer than diff --git a/src/citrine/informatics/design_spaces/formulation_design_space.py b/src/citrine/informatics/design_spaces/formulation_design_space.py index 4e8df7761..a77e65b2d 100644 --- a/src/citrine/informatics/design_spaces/formulation_design_space.py +++ b/src/citrine/informatics/design_spaces/formulation_design_space.py @@ -1,6 +1,6 @@ from typing import Mapping, Optional, Set -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.constraints import Constraint from citrine.informatics.descriptors import FormulationDescriptor @@ -9,7 +9,7 @@ __all__ = ['FormulationDesignSpace'] -class FormulationDesignSpace(ModuleEngineResource['FormulationDesignSpace'], DesignSpace): +class FormulationDesignSpace(EngineResource['FormulationDesignSpace'], DesignSpace): """Design space composed of mixtures of ingredients. Parameters diff --git a/src/citrine/informatics/design_spaces/hierarchical_design_space.py b/src/citrine/informatics/design_spaces/hierarchical_design_space.py index f8518218c..457a35227 100644 --- a/src/citrine/informatics/design_spaces/hierarchical_design_space.py +++ b/src/citrine/informatics/design_spaces/hierarchical_design_space.py @@ -1,10 +1,7 @@ -import warnings from typing import Optional, List from uuid import UUID -from deprecation import deprecated - -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine._serialization.serializable import Serializable from citrine.informatics.data_sources import DataSource @@ -46,29 +43,13 @@ def __init__( material_template: UUID, process_template: UUID, material_template_name: Optional[str] = None, - process_template_name: Optional[str] = None, - template_name: Optional[str] = None + process_template_name: Optional[str] = None ): self.material_template: UUID = material_template self.process_template: UUID = process_template self.material_template_name: Optional[str] = material_template_name self.process_template_name: Optional[str] = process_template_name - if template_name is not None: - warnings.warn( - "The field 'template_name' has been deprecated in v2.36.0 and will be removed " - "in v3.0.0. Please use the field 'material_template_name' instead.", - DeprecationWarning - ) - - @property - @deprecated( - deprecated_in="2.36.0", removed_in="3.0.0", details="Use material_template_name instead." - ) - def template_name(self) -> str: - """Return the name of the material template.""" - return self.material_template_name - class MaterialNodeDefinition(Serializable["MaterialNodeDefinition"]): """A single node in a material history design space. @@ -126,7 +107,7 @@ def __repr__(self): return f"" -class HierarchicalDesignSpace(ModuleEngineResource["HierarchicalDesignSpace"], DesignSpace): +class HierarchicalDesignSpace(EngineResource["HierarchicalDesignSpace"], DesignSpace): """A design space that produces hierarchical candidates representing a material history. A hierarchical design space always contains a root node that defines the diff --git a/src/citrine/informatics/design_spaces/product_design_space.py b/src/citrine/informatics/design_spaces/product_design_space.py index 13e7f60f9..bd7ca704d 100644 --- a/src/citrine/informatics/design_spaces/product_design_space.py +++ b/src/citrine/informatics/design_spaces/product_design_space.py @@ -1,7 +1,7 @@ from typing import List, Union, Optional from uuid import UUID -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.design_spaces.design_space import DesignSpace from citrine.informatics.dimensions import Dimension @@ -9,7 +9,7 @@ __all__ = ['ProductDesignSpace'] -class ProductDesignSpace(ModuleEngineResource['ProductDesignSpace'], DesignSpace): +class ProductDesignSpace(EngineResource['ProductDesignSpace'], DesignSpace): """A Cartesian product of design spaces. Factors can be other design spaces and/or univariate dimensions. diff --git a/src/citrine/informatics/executions/execution.py b/src/citrine/informatics/executions/execution.py index 079617667..ea873f9eb 100644 --- a/src/citrine/informatics/executions/execution.py +++ b/src/citrine/informatics/executions/execution.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import List, Optional +from typing import Optional from uuid import UUID from citrine._rest.asynchronous_object import AsynchronousObject @@ -10,9 +10,6 @@ from citrine.resources.status_detail import StatusDetail -from deprecation import deprecated - - class Execution(Pageable, AsynchronousObject, ABC): """A base class for execution resources. @@ -49,12 +46,6 @@ class Execution(Pageable, AsynchronousObject, ABC): """:Optional[datetime]: date and time at which the resource was most recently updated, if it has been updated""" - @property - @deprecated(deprecated_in="2.2.0", removed_in="3.0.0", details="Use status_detail instead.") - def status_info(self) -> List[str]: - """:List[str]: human-readable explanations of the status.""" - return [detail.msg for detail in self.status_detail] - def __str__(self): return f'<{self.__class__.__name__} {str(self.uid)!r}>' diff --git a/src/citrine/informatics/predictors/auto_ml_predictor.py b/src/citrine/informatics/predictors/auto_ml_predictor.py index 81b80ad2f..c636e70e1 100644 --- a/src/citrine/informatics/predictors/auto_ml_predictor.py +++ b/src/citrine/informatics/predictors/auto_ml_predictor.py @@ -7,7 +7,6 @@ from citrine.informatics.data_sources import DataSource from citrine.informatics.descriptors import Descriptor from citrine.informatics.predictors import PredictorNode -from citrine.informatics.predictors.node import _check_deprecated_training_data __all__ = ['AutoMLPredictor', 'AutoMLEstimator'] @@ -91,8 +90,6 @@ def __init__(self, self.inputs: List[Descriptor] = inputs self.estimators: Set[AutoMLEstimator] = estimators or {AutoMLEstimator.RANDOM_FOREST} self.outputs = outputs - - _check_deprecated_training_data(training_data) self.training_data: List[DataSource] = training_data or [] def __str__(self): diff --git a/src/citrine/informatics/predictors/graph_predictor.py b/src/citrine/informatics/predictors/graph_predictor.py index 2577bd53e..2e2cebdca 100644 --- a/src/citrine/informatics/predictors/graph_predictor.py +++ b/src/citrine/informatics/predictors/graph_predictor.py @@ -1,5 +1,4 @@ -import warnings -from typing import List, Optional, Union +from typing import List, Optional from uuid import UUID from citrine._rest.asynchronous_object import AsynchronousObject @@ -72,24 +71,12 @@ def __init__(self, name: str, *, description: str, - predictors: List[Union[UUID, PredictorNode]], + predictors: List[PredictorNode], training_data: Optional[List[DataSource]] = None): self.name: str = name self.description: str = description self.training_data: List[DataSource] = training_data or [] - - uid_predictors = [x for x in predictors if isinstance(x, UUID)] - if len(uid_predictors) > 0: - warnings.warn( - "Referencing predictors by a UUID inside a GraphPredictor is no longer supported " - "on the Citrine Platform. Please remove all references to predictors " - "and add only PredictorNode objects to the `predictors` field.", - DeprecationWarning - ) - - self.predictors: List[PredictorNode] = [ - x for x in predictors if isinstance(x, PredictorNode) - ] + self.predictors: List[PredictorNode] = predictors def __str__(self): return ''.format(self.name) diff --git a/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py b/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py index 5d3d9585e..e068ee3b7 100644 --- a/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py +++ b/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py @@ -1,9 +1,8 @@ -import warnings -from typing import Set, Mapping, Optional +from typing import Set, Mapping from citrine._rest.resource import Resource from citrine._serialization import properties -from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor, FormulationKey +from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor from citrine.informatics.predictors import PredictorNode __all__ = ['IngredientsToFormulationPredictor'] @@ -40,7 +39,6 @@ def __init__(self, name: str, *, description: str, - output: Optional[FormulationDescriptor] = None, id_to_quantity: Mapping[str, RealDescriptor], labels: Mapping[str, Set[str]]): self.name: str = name @@ -48,14 +46,6 @@ def __init__(self, self.id_to_quantity: Mapping[str, RealDescriptor] = id_to_quantity self.labels: Mapping[str, Set[str]] = labels - if output is not None: - warnings.warn( - "The field `output` on an IngredientsToFormulationPredictor is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - f"FormulationDescriptor with key '{FormulationKey.HIERARCHICAL.value}' as output.", - DeprecationWarning - ) - def __str__(self): return ''.format(self.name) diff --git a/src/citrine/informatics/predictors/mean_property_predictor.py b/src/citrine/informatics/predictors/mean_property_predictor.py index f60fe0858..69c568c73 100644 --- a/src/citrine/informatics/predictors/mean_property_predictor.py +++ b/src/citrine/informatics/predictors/mean_property_predictor.py @@ -7,7 +7,6 @@ FormulationDescriptor, RealDescriptor, CategoricalDescriptor ) from citrine.informatics.predictors import PredictorNode -from citrine.informatics.predictors.node import _check_deprecated_training_data __all__ = ['MeanPropertyPredictor'] @@ -105,8 +104,6 @@ def __init__(self, self.impute_properties: bool = impute_properties self.label: Optional[str] = label self.default_properties: Optional[Mapping[str, Union[str, float]]] = default_properties - - _check_deprecated_training_data(training_data) self.training_data: List[DataSource] = training_data or [] def __str__(self): diff --git a/src/citrine/informatics/predictors/node.py b/src/citrine/informatics/predictors/node.py index 2415bbec4..637b43400 100644 --- a/src/citrine/informatics/predictors/node.py +++ b/src/citrine/informatics/predictors/node.py @@ -1,26 +1,8 @@ -import warnings -from datetime import datetime -from typing import Type, Optional, List -from uuid import UUID - -from deprecation import deprecated +from typing import Type from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable -from citrine.informatics.data_sources import DataSource from citrine.informatics.predictors import Predictor -from citrine.resources.status_detail import StatusDetail - - -def _check_deprecated_training_data(training_data: Optional[List[DataSource]]) -> None: - if training_data is not None: - warnings.warn( - f"The field `training_data` on single predictor nodes is deprecated " - "and will be removed in version 3.0.0. Include training data for all " - "sub-predictors on the parent GraphPredictor. Existing training data " - "on this predictor will be moved to the parent GraphPredictor upon registration.", - DeprecationWarning - ) class PredictorNode(PolymorphicSerializable["PredictorNode"], Predictor): @@ -65,140 +47,3 @@ def get_type(cls, data) -> Type['PredictorNode']: '{} is not a valid predictor node type. ' 'Must be in {}.'.format(data['type'], type_dict.keys()) ) - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `uid` on parent GraphPredictor." - ) - def uid(self) -> Optional[UUID]: - """Optional Citrine Platform unique identifier.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `version` on parent GraphPredictor." - ) - def version(self) -> Optional[int]: - """The version number of the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `draft` on parent GraphPredictor." - ) - def draft(self) -> Optional[bool]: - """The draft status of the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `created_by` on parent GraphPredictor." - ) - def created_by(self) -> Optional[UUID]: - """The id of the user who created the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `updated_by` on parent GraphPredictor." - ) - def updated_by(self) -> Optional[UUID]: - """The id of the user who most recently updated the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `archived_by` on parent GraphPredictor." - ) - def archived_by(self) -> Optional[UUID]: - """The id of the user who most recently archived the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `create_time` on parent GraphPredictor." - ) - def create_time(self) -> Optional[datetime]: - """The date and time at which the resource was created.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `update_time` on parent GraphPredictor." - ) - def update_time(self) -> Optional[datetime]: - """The date and time at which the resource was most recently updated.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `archive_time` on parent GraphPredictor." - ) - def archive_time(self) -> Optional[datetime]: - """The date and time at which the resource was archived.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `status` on parent GraphPredictor." - ) - def status(self) -> Optional[str]: - """Short description of the resource's status.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `status_detail` on parent GraphPredictor." - ) - def status_detail(self) -> List[StatusDetail]: - """A list of structured status info, containing the message and level.""" - return [] - - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `in_progress` on parent GraphPredictor." - ) - def in_progress(self) -> bool: - """Whether the backend process is in progress.""" - return False - - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `succeeded` on parent GraphPredictor." - ) - def succeeded(self) -> bool: - """Whether the backend process has completed successfully.""" - return False - - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `failed` on parent GraphPredictor." - ) - def failed(self) -> bool: - """Whether the backend process has completed unsuccessfully.""" - return False diff --git a/src/citrine/informatics/predictors/simple_mixture_predictor.py b/src/citrine/informatics/predictors/simple_mixture_predictor.py index 1f59be38c..5f803abe5 100644 --- a/src/citrine/informatics/predictors/simple_mixture_predictor.py +++ b/src/citrine/informatics/predictors/simple_mixture_predictor.py @@ -1,12 +1,10 @@ -import warnings from typing import List, Optional from citrine._rest.resource import Resource from citrine._serialization import properties from citrine.informatics.data_sources import DataSource -from citrine.informatics.descriptors import FormulationDescriptor, FormulationKey +from citrine.informatics.descriptors import FormulationDescriptor from citrine.informatics.predictors import PredictorNode -from citrine.informatics.predictors.node import _check_deprecated_training_data __all__ = ['SimpleMixturePredictor'] @@ -38,31 +36,11 @@ def __init__(self, name: str, *, description: str, - input_descriptor: Optional[FormulationDescriptor] = None, - output_descriptor: Optional[FormulationDescriptor] = None, training_data: Optional[List[DataSource]] = None): self.name: str = name self.description: str = description - - _check_deprecated_training_data(training_data) self.training_data: List[DataSource] = training_data or [] - if input_descriptor is not None: - warnings.warn( - "The field `input_descriptor` on a SimpleMixturePredictor is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - f"FormulationDescriptor with key '{FormulationKey.HIERARCHICAL.value}' as input.", - DeprecationWarning - ) - - if output_descriptor is not None: - warnings.warn( - "The field `output_descriptor` on a SimpleMixturePredictor is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - f"FormulationDescriptor with key '{FormulationKey.FLAT.value}' as output.", - DeprecationWarning - ) - def __str__(self): return ''.format(self.name) diff --git a/src/citrine/informatics/workflows/design_workflow.py b/src/citrine/informatics/workflows/design_workflow.py index 6b81057fa..6b869e2e0 100644 --- a/src/citrine/informatics/workflows/design_workflow.py +++ b/src/citrine/informatics/workflows/design_workflow.py @@ -1,8 +1,6 @@ from typing import Optional, Union from uuid import UUID -from deprecation import deprecated - from citrine._rest.resource import Resource from citrine._serialization import properties from citrine.informatics.workflows.workflow import Workflow @@ -35,7 +33,6 @@ class DesignWorkflow(Resource['DesignWorkflow'], Workflow, AIResourceMetadata): predictor_version = properties.Optional( properties.Union([properties.Integer(), properties.String()]), 'predictor_version') _branch_id: Optional[UUID] = properties.Optional(properties.UUID, 'branch_id') - """:Optional[UUID]: Unique ID of the branch that contains this workflow.""" status_description = properties.String('status_description', serializable=False) """:str: more detailed description of the workflow's status""" @@ -72,21 +69,6 @@ def design_executions(self) -> DesignExecutionCollection: return DesignExecutionCollection( project_id=self.project_id, session=self._session, workflow_id=self.uid) - @property - @deprecated(deprecated_in="2.42.0", removed_in="3.0.0", - details="Please use the branch_root_id and branch_version instead.") - def branch_id(self): - """[deprecated] Retrieve the version ID of the branch this workflow is on.""" - return self._branch_id - - @branch_id.setter - @deprecated(deprecated_in="2.42.0", removed_in="3.0.0", - details="Please set the branch_root_id and branch_version instead.") - def branch_id(self, value): - self._branch_id = value - self._branch_root_id = None - self._branch_version = None - @property def branch_root_id(self): """Retrieve the root ID of the branch this workflow is on.""" @@ -94,6 +76,7 @@ def branch_root_id(self): @branch_root_id.setter def branch_root_id(self, value): + """Set the root ID of the branch this workflow is on.""" self._branch_root_id = value self._branch_id = None @@ -104,5 +87,6 @@ def branch_version(self): @branch_version.setter def branch_version(self, value): + """Set the version of the branch this workflow is on.""" self._branch_version = value self._branch_id = None diff --git a/src/citrine/resources/branch.py b/src/citrine/resources/branch.py index 91c2510fe..4d9099304 100644 --- a/src/citrine/resources/branch.py +++ b/src/citrine/resources/branch.py @@ -1,15 +1,11 @@ import functools -import warnings from typing import Iterator, Optional, Union from uuid import UUID -from deprecation import deprecated - from citrine._rest.collection import Collection from citrine._rest.resource import Resource from citrine._serialization import properties from citrine._session import Session -from citrine._utils.functions import migrate_deprecated_argument from citrine.exceptions import NotFound from citrine.resources.data_version_update import BranchDataUpdate, NextBranchVersionRequest from citrine.resources.design_workflow import DesignWorkflowCollection @@ -108,51 +104,18 @@ def build(self, data: dict) -> Branch: branch.project_id = self.project_id return branch - @deprecated(deprecated_in="2.42.0", removed_in="3.0.0", details="Use .get() instead.") - def get_by_root_id(self, - *, - branch_root_id: Union[UUID, str], - version: Optional[Union[int, str]] = LATEST_VER) -> Branch: - """ - Given a branch root ID and (optionally) the version, retrieve the specific branch version. - - Parameters - --------- - branch_root_id: Union[UUID, str] - Unique identifier of the branch root - - version: Union[int, str], optional - The version of the branch to retrieve. If provided, must either be a positive integer, - or "latest". Defaults to "latest". - - Returns - ------- - Branch - The requested version of the branch. - - """ - return self.get(root_id=branch_root_id, version=version) - def get(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER) -> Branch: """ - Retrieve a branch using either the version ID, or the root ID and version number. - - Providing both the version ID and the root ID, or neither, will result in an error. - - Providing the root ID and no version number will retrieve the latest version. + Retrieve a branch by its root ID and, optionally, its version number. - Using the version ID with this method is deprecated in favor of .get_by_version_id(). + Omitting the version number will retrieve the latest version. Parameters --------- - uid: Union[UUID, str], optional - [deprecated] Unqiue ID of the branch version. - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique identifier of the branch root version: Union[int, str], optional @@ -165,30 +128,18 @@ def get(self, The requested version of the branch. """ - if uid: - warnings.warn("Retrieving a branch by its version ID using .get() is deprecated. " - "Please use .get_by_version_id().", - DeprecationWarning) - - if root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - return self.get_by_version_id(version_id=uid) - elif root_id: - version = version or LATEST_VER - params = {"root": str(root_id), "version": version} - branch = next(self._list_with_params(per_page=1, **params), None) - if branch: - return branch - else: - raise NotFound.build( - message=f"Branch root '{root_id}', version {version} not found", - method="GET", - path=self._get_path(), - params=params - ) - + version = version or LATEST_VER + params = {"root": str(root_id), "version": version} + branch = next(self._list_with_params(per_page=1, **params), None) + if branch: + return branch else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + raise NotFound.build( + message=f"Branch root '{root_id}', version {version} not found", + method="GET", + path=self._get_path(), + params=params + ) def get_by_version_id(self, *, version_id: Union[UUID, str]) -> Branch: """ @@ -209,9 +160,9 @@ def get_by_version_id(self, *, version_id: Union[UUID, str]) -> Branch: def list(self, *, per_page: int = 20) -> Iterator[Branch]: """ - List all branches using pagination. + List all active branches using pagination. - Yields all branches, regardless of archive status, paginating over all available pages. + Yields all active branches paginating over all available pages. Parameters --------- @@ -225,10 +176,7 @@ def list(self, *, per_page: int = 20) -> Iterator[Branch]: All branches in this collection. """ - warnings.warn("Beginning in the 3.0 release, this method will only list unarchived " - "branches. To list all branches, use .list_all().", - DeprecationWarning) - return super().list(per_page=per_page) + return self._list_with_params(per_page=per_page, archived=False) def list_archived(self, *, per_page: int = 20) -> Iterator[Branch]: """ @@ -277,19 +225,15 @@ def _list_with_params(self, *, per_page, **kwargs): per_page=per_page) def archive(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER): """ Archive a branch. Parameters ---------- - uid: Union[UUID, str], optional - [deprecated] Unique identifier of the branch - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -297,35 +241,24 @@ def archive(self, Defaults to "latest". """ - if uid: - warnings.warn("Archiving a branch by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - elif root_id: - version = version or LATEST_VER - # The backend API at present expects a branch version ID, so we must look it up. - uid = self.get(root_id=root_id, version=version).uid - else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + version = version or LATEST_VER + # The backend API at present expects a branch version ID, so we must look it up. + uid = self.get(root_id=root_id, version=version).uid url = self._get_path(uid, action="archive") data = self.session.put_resource(url, {}, version=self._api_version) return self.build(data) def restore(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, - version: Optional[int] = None): + root_id: Union[UUID, str], + version: Optional[Union[int, str]] = LATEST_VER): """ Restore an archived branch. Parameters ---------- - uid: Union[UUID, str], optional - [deprecated] Unique identifier of the branch - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -333,25 +266,17 @@ def restore(self, Defaults to "latest". """ - if uid: - warnings.warn("Restoring a branch by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - elif root_id: - version = version or LATEST_VER - # The backend API at present expects a branch version ID, so we must look it up. - uid = self.get(root_id=root_id, version=version).uid - else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + version = version or LATEST_VER + # The backend API at present expects a branch version ID, so we must look it up. + uid = self.get(root_id=root_id, version=version).uid url = self._get_path(uid, action="restore") data = self.session.put_resource(url, {}, version=self._api_version) return self.build(data) def update_data(self, - branch: Optional[Union[UUID, str, Branch]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER, use_existing: bool = True, retrain_models: bool = False) -> Optional[Branch]: @@ -363,10 +288,7 @@ def update_data(self, Parameters ---------- - branch: Union[UUID, str, Branch], optional - [deprecated] Branch Identifier or Branch object - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -387,24 +309,6 @@ def update_data(self, The new branch record after version update or None if no update """ - if branch: - if root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - - if isinstance(branch, Branch): - warnings.warn("Updating a branch's data by its object is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - else: - warnings.warn("Updating a branch's data by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - branch = self.get_by_version_id(version_id=branch) - root_id = branch.root_id - version = branch.version - elif not root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - version = version or LATEST_VER version_updates = self.data_updates(root_id=root_id, version=version) # If no new data sources, then exit, nothing to do @@ -423,19 +327,15 @@ def update_data(self, return branch def data_updates(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER) -> BranchDataUpdate: """ Get data updates for a branch. Parameters ---------- - uid: Union[UUID, str], optional - [deprecated] Unqiue ID of the branch version. - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -448,26 +348,16 @@ def data_updates(self, A list of data updates and compatible predictors """ - if uid: - warnings.warn("Retrieving data updates for a branch by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - if root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - elif root_id: - version = version or LATEST_VER - # The backend API at present expects a branch version ID, so we must look it up. - uid = self.get(root_id=root_id, version=version).uid - else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + version = version or LATEST_VER + # The backend API at present expects a branch version ID, so we must look it up. + uid = self.get(root_id=root_id, version=version).uid path = self._get_path(uid=uid, action="data-version-updates-predictor") data = self.session.get_resource(path, version=self._api_version) return BranchDataUpdate.build(data) def next_version(self, - branch_root_id: Optional[Union[UUID, str]] = None, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], *, branch_instructions: NextBranchVersionRequest, retrain_models: bool = True): @@ -476,11 +366,7 @@ def next_version(self, Parameters ---------- - branch_root_id: Union[UUID, str], optional - [deprecated] Unique identifier of the branch root to advance to next version. - Deprecated in favor of root_id. - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique identifier of the branch root to advance to next version branch_instructions: NextBranchVersionRequest @@ -501,8 +387,6 @@ def next_version(self, The new branch record after version update """ - root_id = migrate_deprecated_argument(root_id, "root_id", branch_root_id, "branch_root_id") - path = self._get_path(action="next-version-predictor") data = self.session.post_resource(path, branch_instructions.dump(), version=self._api_version, diff --git a/src/citrine/resources/descriptors.py b/src/citrine/resources/descriptors.py index b0d95ad8e..13c3441e8 100644 --- a/src/citrine/resources/descriptors.py +++ b/src/citrine/resources/descriptors.py @@ -1,8 +1,6 @@ from typing import List, Union from uuid import UUID -from deprecation import deprecated - from citrine._session import Session from citrine._utils.functions import format_escaped_url from citrine.informatics.data_sources import DataSource @@ -76,9 +74,3 @@ def from_data_source(self, *, data_source: DataSource) -> List[Descriptor]: } ) return [Descriptor.build(r) for r in response['descriptors']] - - @deprecated(deprecated_in="2.31.0", removed_in="3.0.0", - details="Use from_data_source instead.") - def descriptors_from_data_source(self, *, data_source: DataSource) -> List[Descriptor]: - """[DEPRECATED] See from_data_source.""" - return self.from_data_source(data_source=data_source) diff --git a/src/citrine/resources/design_execution.py b/src/citrine/resources/design_execution.py index 081325e0f..7bf87fa77 100644 --- a/src/citrine/resources/design_execution.py +++ b/src/citrine/resources/design_execution.py @@ -3,27 +3,12 @@ from uuid import UUID from citrine._rest.collection import Collection -from citrine._utils.functions import MigratedClassMeta from citrine._session import Session from citrine.informatics import executions from citrine.informatics.scores import Score from citrine.resources.response import Response -class DesignExecution(executions.DesignExecution, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """The execution of a DesignWorkflow. - - Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. - Design executions also have a ``status_description`` field with more information. - - DesignExecution should be imported from citrine.informatics.executions. - - """ - - class DesignExecutionCollection(Collection["DesignExecution"]): """A collection of DesignExecutions.""" diff --git a/src/citrine/resources/design_space.py b/src/citrine/resources/design_space.py index 588b6043a..65eeaf5e0 100644 --- a/src/citrine/resources/design_space.py +++ b/src/citrine/resources/design_space.py @@ -1,6 +1,5 @@ """Resources that represent collections of design spaces.""" -import warnings -from typing import List, Optional, TypeVar, Union +from typing import Optional, TypeVar, Union from uuid import UUID from gemd.enumeration.base_enumeration import BaseEnumeration @@ -70,28 +69,11 @@ def _verify_write_request(self, design_space: DesignSpace): "in this EnumeratedDesignSpace" raise ValueError(msg.format(self._enumerated_cell_limit, width * length)) - def _hydrate_design_space(self, design_space: DesignSpace) -> List[DesignSpace]: - if design_space.typ != "ProductDesignSpace": - return design_space - - subspaces = [] - for subspace in design_space.subspaces: - if isinstance(subspace, (str, UUID)): - warnings.warn("Support for UUIDs in subspaces is deprecated as of 2.16.0, and " - "will be dropped in 3.0. Please use DesignSpace objects instead.", - DeprecationWarning) - subspaces.append(self.get(subspace)) - else: - subspaces.append(subspace) - design_space.subspaces = subspaces - return design_space - def register(self, design_space: DesignSpace) -> DesignSpace: """Create a new design space.""" self._verify_write_request(design_space) - hydrated_ds = self._hydrate_design_space(design_space) - registered_ds = super().register(hydrated_ds) + registered_ds = super().register(design_space) # If the initial response is invalid, just return it. # If not, kick off validation, since we never exposed saving a design space without @@ -104,18 +86,7 @@ def register(self, design_space: DesignSpace) -> DesignSpace: def update(self, design_space: DesignSpace) -> DesignSpace: """Update and validate an existing DesignSpace.""" self._verify_write_request(design_space) - hydrated_ds = self._hydrate_design_space(design_space) - updated_ds = super().update(hydrated_ds) - - # The /api/v3/design-spaces endpoint switched archiving from a field on the update payload - # to their own endpoints. To maintain backwards compatibility, all design spaces have an - # _archived field set by the archived property. It will be archived if True, and restored - # if False. It defaults to None, which does nothing. The value is reset afterwards. - if design_space._archived is True: - self.archive(design_space.uid) - elif design_space._archived is False: - self.restore(design_space.uid) - design_space._archived = None + updated_ds = super().update(design_space) # If the initial response is invalid, just return it. # If not, kick off validation, since we never exposed saving a design space without diff --git a/src/citrine/resources/design_workflow.py b/src/citrine/resources/design_workflow.py index da25e4774..48952874e 100644 --- a/src/citrine/resources/design_workflow.py +++ b/src/citrine/resources/design_workflow.py @@ -1,4 +1,3 @@ -import warnings from copy import deepcopy from typing import Callable, Union, Iterable, Optional, Tuple from uuid import UUID @@ -23,21 +22,12 @@ class DesignWorkflowCollection(Collection[DesignWorkflow]): def __init__(self, project_id: UUID, session: Session, - branch_id: Optional[UUID] = None, *, branch_root_id: Optional[UUID] = None, branch_version: Optional[int] = None): self.project_id: UUID = project_id self.session: Session = session - if branch_id: - warnings.warn("Constructing the design workflow interface by its parent branch's " - "version ID is deprecated. Please use its root ID and version number.", - DeprecationWarning) - if branch_root_id: - raise ValueError("Please provide at most one: the version ID or root ID.") - - self.branch_id = branch_id self.branch_root_id = branch_root_id self.branch_version = branch_version @@ -64,9 +54,9 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: """ Upload a new design workflow. - The model's branch ID is ignored in favor of this collection's. If this - collection has a null branch ID, then a branch is first created, then - the design workflow is created on it. + The model's branch root ID and branch version are ignored in favor of this collection's. + If this collection has a null branch root ID or branch version, an exception is raised + directing the caller to use one. Parameters ---------- @@ -80,18 +70,12 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: """ if self.branch_root_id is None or self.branch_version is None: - if self.branch_id is None: - # There are a number of contexts in which hitting design workflow endpoints without - # a branch ID is valid, so only this particular usage is deprecated. - msg = ('A design workflow must be created with a branch. Please use ' - 'branch.design_workflows.register() instead of ' - 'project.design_workflows.register().') - raise RuntimeError(msg) - else: - warnings.warn("Registering a design workflow to a branch by its parent's version " - "ID is deprecated. Please use its root ID and version number.", - DeprecationWarning) - branch_id = self.branch_id + # There are a number of contexts in which hitting design workflow endpoints without + # a branch ID is valid, so only this particular usage is disallowed. + msg = ('A design workflow must be created with a branch. Please use ' + 'branch.design_workflows.register() instead of ' + 'project.design_workflows.register().') + raise RuntimeError(msg) else: # branch_id is in the body of design workflow endpoints, so it must be serialized. # This means the collection branch_id might not match the workflow branch_id. The @@ -99,10 +83,10 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: # represented by this collection. # To avoid modifying the parameter, and to ensure the only change is the branch_id, we # deepcopy, modify, then register it. - branch_id = self._resolve_branch_id(self.branch_root_id, self.branch_version) - model_copy = deepcopy(model) - model_copy._branch_id = branch_id - return super().register(model_copy) + model_copy = deepcopy(model) + model_copy._branch_id = self._resolve_branch_id(self.branch_root_id, + self.branch_version) + return super().register(model_copy) def build(self, data: dict) -> DesignWorkflow: """ @@ -128,9 +112,9 @@ def build(self, data: dict) -> DesignWorkflow: def update(self, model: DesignWorkflow) -> DesignWorkflow: """Update a design workflow. - Identifies the workflow by the model's uid. It must have a branch_id, and if this - collection also has a branch_id, they must match. Prefer updating a workflow through - Project.design_workflows.update. + Identifies the workflow by its uid. It must have a branch_root_id and branch_version, and + if this collection also has a branch_root_id and branch_version, they must match. Prefer + updating a workflow through Project.design_workflows.update. Parameters ---------- @@ -148,25 +132,18 @@ def update(self, model: DesignWorkflow) -> DesignWorkflow: self.branch_version != model.branch_version: raise ValueError('To move a design workflow to another branch, please use ' 'Project.design_workflows.update') - elif self.branch_id is not None: - warnings.warn("Updating a design workflow by its parent's branch version ID is " - "deprecated. Please use its root ID and version number.", - DeprecationWarning) - if self.branch_id != model._branch_id: - raise ValueError('To move a design workflow to another branch, please use ' - 'Project.design_workflows.update') - if model.branch_root_id is not None and model.branch_version is not None: - try: - model._branch_id = self._resolve_branch_id(model.branch_root_id, - model.branch_version) - except NotFound: - raise ValueError('Cannot update a design workflow unless its branch_root_id and ' - 'branch_version exists.') - elif model._branch_id is None: + if model.branch_root_id is None or model.branch_version is None: raise ValueError('Cannot update a design workflow unless its branch_root_id and ' 'branch_version are set.') + try: + model._branch_id = self._resolve_branch_id(model.branch_root_id, + model.branch_version) + except NotFound: + raise ValueError('Cannot update a design workflow unless its branch_root_id and ' + 'branch_version exists.') + # If executions have already been done, warn about future behavior change executions = model.design_executions.list() if next(executions, None) is not None: diff --git a/src/citrine/resources/experiment_datasource.py b/src/citrine/resources/experiment_datasource.py index 39159c312..a0934c034 100644 --- a/src/citrine/resources/experiment_datasource.py +++ b/src/citrine/resources/experiment_datasource.py @@ -9,7 +9,6 @@ from citrine._serialization import properties from citrine._serialization.serializable import Serializable from citrine._session import Session -from citrine._utils.functions import migrate_deprecated_argument from citrine.informatics.experiment_values import ExperimentValue @@ -107,7 +106,6 @@ def build(self, data: dict) -> ExperimentDataSource: def list(self, *, per_page: int = 100, - branch_id: Optional[Union[UUID, str]] = None, branch_version_id: Optional[Union[UUID, str]] = None, version: Optional[Union[int, str]] = None) -> Iterator[ExperimentDataSource]: """Paginate over the experiment data sources. @@ -118,8 +116,6 @@ def list(self, *, Max number of results to return per page. Default is 100. This parameter is used when making requests to the backend service. If the page parameter is specified it limits the maximum number of elements in the response. - branch_id: UUID, optional - [deprecated] Filter the list by the branch version ID. branch_version_id: UUID, optional Filter the list by the branch version ID. version: Union[int, str], optional @@ -131,13 +127,6 @@ def list(self, *, An iterator that can be used to loop over all matching experiment data sources. """ - # migrate_deprecated_argument requires one argument be provided, but this method does not. - if branch_version_id or branch_id: - branch_version_id = migrate_deprecated_argument(branch_version_id, - "branch_version_id", - branch_id, - "branch_id") - params = {} if branch_version_id: params["branch"] = str(branch_version_id) diff --git a/src/citrine/resources/file_link.py b/src/citrine/resources/file_link.py index 9c06cf2fe..7185bd489 100644 --- a/src/citrine/resources/file_link.py +++ b/src/citrine/resources/file_link.py @@ -1,11 +1,10 @@ """A collection of FileLink objects.""" -from deprecation import deprecated import mimetypes import os from pathlib import Path from logging import getLogger from tempfile import TemporaryDirectory -from typing import Optional, Tuple, Union, List, Dict, Iterable, Sequence +from typing import Optional, Tuple, Union, Dict, Iterable, Sequence from urllib.parse import urlparse, unquote_plus from uuid import UUID @@ -21,7 +20,6 @@ from citrine._utils.functions import rewrite_s3_links_locally from citrine._utils.functions import write_file_locally -from citrine.jobs.job import JobSubmissionResponse, _poll_for_job_completion from citrine.resources.response import Response from gemd.entity.dict_serializable import DictSerializableMeta from gemd.entity.bounds.base_bounds import BaseBounds @@ -67,18 +65,6 @@ def __init__(self): self.s3_addressing_style = 'auto' -class FileProcessingType(BaseEnumeration): - """The supported File Processing Types.""" - - VALIDATE_CSV = "VALIDATE_CSV" - - -class FileProcessingData: - """The base class of all File Processing related data implementations.""" - - pass - - class CsvColumnInfo(Serializable): """The info for a CSV Column, contains the name, recommended and exact bounds.""" @@ -96,35 +82,6 @@ def __init__(self, name: str, bounds: BaseBounds, self.exact_range_bounds = exact_range_bounds -class CsvValidationData(FileProcessingData, Serializable): - """The resulting data from the processed CSV file.""" - - columns = properties.Optional(properties.List(properties.Object(CsvColumnInfo)), - 'columns') - """:Optional[List[CsvColumnInfo]]: all of the columns in the CSV""" - record_count = properties.Integer('record_count') - """:int: the number of rows in the CSV""" - - def __init__(self, columns: List[CsvColumnInfo], - record_count: int): # pragma: no cover - self.columns = columns - self.record_count = record_count - - -class FileProcessingResult: - """ - The results of a successful file processing operation. - - The type of the actual data depends on the specific processing type. - """ - - def __init__(self, - processing_type: FileProcessingType, - data: Union[Dict, FileProcessingData]): - self.processing_type = processing_type - self.data = data - - class FileLinkMeta(DictSerializableMeta): """Metaclass for FileLink deserialization.""" @@ -701,147 +658,6 @@ def read(self, *, file_link: Union[str, UUID, FileLink]) -> bytes: download_response = requests.get(final_url) return download_response.content - @deprecated(deprecated_in="2.4.0", - removed_in="3.0.0", - details="The process file protocol is deprecated " - "in favor of ingest()") - def process(self, *, file_link: Union[FileLink, str, UUID], - processing_type: FileProcessingType, - wait_for_response: bool = True, - timeout: float = 2 * 60, - polling_delay: float = 1.0) -> Union[JobSubmissionResponse, - Dict[FileProcessingType, - FileProcessingResult]]: - """ - Start a File Processing async job, returning a pollable job response. - - Parameters - ---------- - file_link: FileLink, str, or UUID - The file to process. - processing_type: FileProcessingType - The type of file processing to invoke. - wait_for_response: bool - Whether to wait for a result, vs. just return a job handle. Default: True - timeout: float - How long to poll for the result before giving up. This is expressed in - (fractional) seconds. Default: 120 seconds. - polling_delay: float - How long to delay between each polling retry attempt. Default: 1 second. - - Returns - ------- - JobSubmissionResponse - A JobSubmissionResponse which can be used to poll for the result. - - """ - if self._is_external_url(file_link.url): - raise ValueError(f"Only on-platform resources can be processed. " - f"Passed URL {file_link.url}.") - file_link = self._resolve_file_link(file_link) - - params = {"processing_type": processing_type.value} - response = self.session.put_resource( - self._get_path_from_file_link(file_link, action="processed"), - json={}, - params=params - ) - job = JobSubmissionResponse.build(response) - logger.info('Build job submitted with job ID {}.'.format(job.job_id)) - - if wait_for_response: - return self.poll_file_processing_job(file_link=file_link, - processing_type=processing_type, - job_id=job.job_id, timeout=timeout, - polling_delay=polling_delay) - else: - return job - - @deprecated(deprecated_in="2.4.0", - removed_in="3.0.0", - details="The process file protocol is deprecated " - "in favor of ingest()") - def poll_file_processing_job(self, *, file_link: FileLink, - processing_type: FileProcessingType, - job_id: UUID, - timeout: float = 2 * 60, - polling_delay: float = 1.0) -> Dict[FileProcessingType, - FileProcessingResult]: - """ - Poll for the result of the file processing task. - - Parameters - ---------- - file_link: FileLink - The file to process. - processing_type: FileProcessingType - The processing algorithm to apply. - job_id: UUID - The background job ID to poll for. - timeout: - How long to poll for the result before giving up. This is expressed in - (fractional) seconds. - polling_delay: - How long to delay between each polling retry attempt. - - Returns - ------- - None - This method will raise an appropriate exception if the job failed, else - it will return None to indicate the job was successful. - - """ - # Poll for job completion - this will raise an error if the job failed - _poll_for_job_completion(self.session, self.project_id, job_id, timeout=timeout, - polling_delay=polling_delay) - - return self.file_processing_result(file_link=file_link, processing_types=[processing_type]) - - @deprecated(deprecated_in="2.4.0", - removed_in="3.0.0", - details="The process file protocol is deprecated " - "in favor of ingest()") - def file_processing_result(self, *, - file_link: FileLink, - processing_types: List[FileProcessingType]) -> \ - Dict[FileProcessingType, FileProcessingResult]: - """ - Return the file processing result for the given file link and processing type. - - Parameters - ---------- - file_link: FileLink - The file to process - processing_types: FileProcessingType - A list of the particular file processing types to retrieve - - Returns - ------- - Map[FileProcessingType, FileProcessingResult] - The file processing results, mapped by processing type. - - """ - processed_results_path = self._get_path_from_file_link(file_link, action="processed") - - params = [] - for proc_type in processing_types: - params.append(('processing_type', proc_type.value)) - - response = self.session.get_resource(processed_results_path, params=params) - results_json = response['results'] - results = {} - for result_json in results_json: - processing_type = FileProcessingType[result_json['processing_type']] - data = result_json['data'] - - if processing_type == FileProcessingType.VALIDATE_CSV: - data = CsvValidationData.build(data) - - result = FileProcessingResult(processing_type, data) - results[processing_type] = result - - return results - def ingest(self, files: Iterable[Union[FileLink, Path, str]], *, diff --git a/src/citrine/resources/job.py b/src/citrine/resources/job.py deleted file mode 100644 index 34d731f6e..000000000 --- a/src/citrine/resources/job.py +++ /dev/null @@ -1,45 +0,0 @@ -import citrine.jobs.job -from citrine.jobs.job import _poll_for_job_completion # noqa -from citrine._utils.functions import MigratedClassMeta - - -class JobSubmissionResponse(citrine.jobs.job.JobSubmissionResponse, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """ - A response to a submit-job request for the job submission framework. - - This is returned as a successful response from the remote service. - - Importing from this package is deprecated; import from citrine.jobs.job instead. - - """ - - -class TaskNode(citrine.jobs.job.TaskNode, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """ - Individual task status. - - The TaskNode describes a component of an overall job. - - Importing from this package is deprecated; import from citrine.jobs.job instead. - - """ - - -class JobStatusResponse(citrine.jobs.job.JobStatusResponse, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """ - A response to a job status check. - - The JobStatusResponse summarizes the status for the entire job. - - Importing from this package is deprecated; import from citrine.jobs.job instead. - - """ diff --git a/src/citrine/resources/predictor_evaluation_execution.py b/src/citrine/resources/predictor_evaluation_execution.py index a045853e5..51bc91d5e 100644 --- a/src/citrine/resources/predictor_evaluation_execution.py +++ b/src/citrine/resources/predictor_evaluation_execution.py @@ -6,23 +6,11 @@ from citrine._rest.collection import Collection from citrine._rest.resource import PredictorRef from citrine._session import Session -from citrine._utils.functions import MigratedClassMeta, format_escaped_url +from citrine._utils.functions import format_escaped_url from citrine.informatics.executions import predictor_evaluation_execution from citrine.resources.response import Response -class PredictorEvaluationExecution(predictor_evaluation_execution.PredictorEvaluationExecution, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """The execution of a PredictorEvaluationWorkflow. - - Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. - Predictor evaluation executions also have a ``status_description`` field with more information. - - """ - - class PredictorEvaluationExecutionCollection(Collection["PredictorEvaluationExecution"]): """A collection of PredictorEvaluationExecutions.""" diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index a2b4bbc4e..f08cab49d 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -3,8 +3,6 @@ from typing import Optional, Dict, List, Union, Iterable, Tuple, Iterator from uuid import UUID -from deprecation import deprecated - from gemd.entity.base_entity import BaseEntity from gemd.entity.link_by_uid import LinkByUID @@ -32,7 +30,6 @@ from citrine.resources.measurement_run import MeasurementRunCollection from citrine.resources.measurement_spec import MeasurementSpecCollection from citrine.resources.measurement_template import MeasurementTemplateCollection -from citrine.resources.module import ModuleCollection from citrine.resources.parameter_template import ParameterTemplateCollection from citrine.resources.predictor import PredictorCollection from citrine.resources.predictor_evaluation_execution import \ @@ -107,12 +104,6 @@ def __str__(self): def _path(self): return format_escaped_url('/projects/{project_id}', project_id=self.uid) - @property - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", details="Use design_spaces instead.") - def modules(self) -> ModuleCollection: - """Return a resource representing all visible design spaces.""" - return ModuleCollection(self.uid, self.session) - @property def branches(self) -> BranchCollection: """Return a resource representing all visible branches.""" diff --git a/src/citrine/resources/sample_design_space_execution.py b/src/citrine/resources/sample_design_space_execution.py index e21db5150..9c5d0f953 100644 --- a/src/citrine/resources/sample_design_space_execution.py +++ b/src/citrine/resources/sample_design_space_execution.py @@ -59,9 +59,6 @@ def list(self, *, Parameters --------- - page: int, optional - The "page" of results to list. Default is to read all pages and yield - all results. This option is deprecated. per_page: int, optional Max number of results to return per page. Default is 100. This parameter is used when making requests to the backend service. If the page parameter diff --git a/tests/_util/test_functions.py b/tests/_util/test_functions.py index 8d899b558..803c99484 100644 --- a/tests/_util/test_functions.py +++ b/tests/_util/test_functions.py @@ -7,8 +7,8 @@ from gemd.entity.link_by_uid import LinkByUID from citrine._utils.functions import get_object_id, validate_type, object_to_link_by_uid, \ - rewrite_s3_links_locally, write_file_locally, shadow_classes_in_module, migrate_deprecated_argument, \ - format_escaped_url, MigratedClassMeta, generate_shared_meta + rewrite_s3_links_locally, write_file_locally, migrate_deprecated_argument, format_escaped_url, \ + MigratedClassMeta, generate_shared_meta from gemd.entity.attribute.property import Property from citrine.resources.condition_template import ConditionTemplate @@ -165,29 +165,6 @@ class MigratedProperty(Simple, assert not issubclass(dict, Simple) -def test_shadow_classes_in_module(): - - # Given - from tests._util import source_mod, target_mod - assert getattr(target_mod, 'ExampleClass', None) == None - - # When - with pytest.deprecated_call(): - shadow_classes_in_module(source_mod, target_mod) - - # Then (ensure the class is copied over) - copied_class = getattr(target_mod, 'ExampleClass', None) # Do this vs a direct ref so IJ doesn't warn us - assert copied_class == source_mod.ExampleClass - - # Python also considers the classes equivalent - assert issubclass(copied_class, source_mod.ExampleClass) - assert issubclass(source_mod.ExampleClass, copied_class) - - # Reset target_mod status - for attr in dir(target_mod): - delattr(target_mod, attr) - - def test_migrate_deprecated_argument(): with pytest.raises(ValueError): # ValueError if neither argument is specified diff --git a/tests/informatics/test_data_source.py b/tests/informatics/test_data_source.py index 46d5f4290..c5819247b 100644 --- a/tests/informatics/test_data_source.py +++ b/tests/informatics/test_data_source.py @@ -39,12 +39,3 @@ def test_invalid_deser(): with pytest.raises(ValueError): DataSource.build({"type": "foo"}) - - -def test_deprecated_formulation_option(): - with pytest.warns(DeprecationWarning): - GemTableDataSource( - table_id=uuid.uuid4(), - table_version=1, - formulation_descriptor=FormulationDescriptor.hierarchical() - ) diff --git a/tests/informatics/test_descriptors.py b/tests/informatics/test_descriptors.py index 4ba6c8795..8c1744756 100644 --- a/tests/informatics/test_descriptors.py +++ b/tests/informatics/test_descriptors.py @@ -67,9 +67,3 @@ def test_to_json(descriptor): def test_formulation_from_string_key(): descriptor = FormulationDescriptor(FormulationKey.HIERARCHICAL.value) assert descriptor.key == FormulationKey.HIERARCHICAL.value - - -def test_integer_units_deprecation(): - descriptor = IntegerDescriptor("integer", lower_bound=5, upper_bound=10) - with pytest.deprecated_call(): - assert descriptor.units == "dimensionless" diff --git a/tests/informatics/test_design_spaces.py b/tests/informatics/test_design_spaces.py index d7b4b8d26..734a59dc2 100644 --- a/tests/informatics/test_design_spaces.py +++ b/tests/informatics/test_design_spaces.py @@ -121,17 +121,6 @@ def test_hierarchical_initialization(hierarchical_design_space): assert "formulation_descriptor" in fds_data -def test_template_link_deprecations(): - with pytest.warns(DeprecationWarning): - link = TemplateLink( - material_template=uuid.uuid4(), - process_template=uuid.uuid4(), - template_name="I am deprecated", - material_template_name="I am not deprecated" - ) - assert link.template_name == link.material_template_name - - def test_data_source_build(valid_data_source_design_space_dict): ds = DesignSpace.build(valid_data_source_design_space_dict) assert ds.name == valid_data_source_design_space_dict["data"]["instance"]["name"] @@ -146,11 +135,3 @@ def test_data_source_create(valid_data_source_design_space_dict): assert ds["data"]["description"] == round_robin.description assert ds["data"]["instance"]["data_source"] == round_robin.data_source.dump() assert "DataSource" in str(ds) - - -def test_deprecated_module_type(product_design_space): - with pytest.deprecated_call(): - product_design_space.module_type = "foo" - - with pytest.deprecated_call(): - assert product_design_space.module_type == "DESIGN_SPACE" diff --git a/tests/informatics/test_predictors.py b/tests/informatics/test_predictors.py index 67b59efc1..b4250ece6 100644 --- a/tests/informatics/test_predictors.py +++ b/tests/informatics/test_predictors.py @@ -420,95 +420,3 @@ def test_single_predict(graph_predictor): prediction_out = graph_predictor.predict(request) assert prediction_out.dump() == prediction_in.dump() assert session.post_resource.call_count == 1 - - -def test_deprecated_training_data(): - with pytest.warns(DeprecationWarning): - AutoMLPredictor( - name="AutoML", - description="", - inputs=[x, y], - outputs=[z], - training_data=[data_source] - ) - - with pytest.warns(DeprecationWarning): - MeanPropertyPredictor( - name="SimpleMixture", - description="", - input_descriptor=flat_formulation, - properties=[x, y, z], - p=1.0, - impute_properties=True, - training_data=[data_source] - ) - - with pytest.warns(DeprecationWarning): - SimpleMixturePredictor( - name="Warning", - description="Description", - training_data=[data_source] - ) - - -def test_formulation_deprecations(): - with pytest.warns(DeprecationWarning): - SimpleMixturePredictor( - name="Warning", - description="Description", - input_descriptor=FormulationDescriptor.hierarchical(), - output_descriptor=FormulationDescriptor.flat() - ) - with pytest.warns(DeprecationWarning): - IngredientsToFormulationPredictor( - name="Warning", - description="Description", - output=FormulationDescriptor.hierarchical(), - id_to_quantity={}, - labels={} - ) - - -def test_deprecated_node_fields(valid_auto_ml_predictor_data): - # Just testing for coverage of methods - aml = AutoMLPredictor.build(valid_auto_ml_predictor_data) - with pytest.deprecated_call(): - assert aml.uid is None - with pytest.deprecated_call(): - assert aml.version is None - with pytest.deprecated_call(): - assert aml.draft is None - with pytest.deprecated_call(): - assert aml.created_by is None - with pytest.deprecated_call(): - assert aml.updated_by is None - with pytest.deprecated_call(): - assert aml.archived_by is None - with pytest.deprecated_call(): - assert aml.create_time is None - with pytest.deprecated_call(): - assert aml.update_time is None - with pytest.deprecated_call(): - assert aml.archive_time is None - with pytest.deprecated_call(): - assert aml.status is None - with pytest.deprecated_call(): - assert len(aml.status_detail) == 0 - with pytest.deprecated_call(): - assert aml.in_progress() is False - with pytest.deprecated_call(): - assert aml.succeeded() is False - with pytest.deprecated_call(): - assert aml.failed() is False - - -def test_unhydrated_graph_deprecation(): - good = SimpleMixturePredictor(name="Warning", description="Description") - bad = uuid.uuid4() - with pytest.warns(DeprecationWarning): - graph = GraphPredictor( - name="Warning", - description="Hydrate me!", - predictors=[good, bad] - ) - assert len(graph.predictors) == 1 diff --git a/tests/resources/test_branch.py b/tests/resources/test_branch.py index 76b7d1b99..c1e8408c4 100644 --- a/tests/resources/test_branch.py +++ b/tests/resources/test_branch.py @@ -117,12 +117,11 @@ def test_branch_list(session, collection, branch_path): session.set_response({'response': branches_data}) # When - with pytest.deprecated_call(): - branches = list(collection.list()) + branches = list(collection.list()) # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=branch_path, params={'page': 1, 'per_page': 20}) + assert session.last_call == FakeCall(method='GET', path=branch_path, params={'archived': False, 'page': 1, 'per_page': 20}) assert len(branches) == branch_count @@ -140,8 +139,6 @@ def test_branch_list_all(session, collection, branch_path): assert session.last_call == FakeCall(method='GET', path=branch_path, params={'per_page': 20, 'page': 1}) - - def test_branch_delete(session, collection, branch_path): # Given branch_id = uuid.uuid4() @@ -546,297 +543,3 @@ def test_experiment_data_source_no_project_id(session): branch.experiment_datasource assert not session.calls - - -def test_get_by_root_id_deprecated(session, collection, branch_path): - # Given - branches_data = BranchDataFactory.create_batch(1) - session.set_response({'response': branches_data}) - root_id = uuid.uuid4() - - # When - with pytest.deprecated_call(): - branch = collection.get_by_root_id(branch_root_id=root_id) - - # Then - assert session.calls == [FakeCall( - method='GET', - path=branch_path, - params={'page': 1, 'per_page': 1, 'root': str(root_id), 'version': LATEST_VER} - )] - - -def test_get_by_root_id_not_found_deprecated(session, collection, branch_path): - # Given - session.set_response({'response': []}) - root_id = uuid.uuid4() - - # When - with pytest.deprecated_call(): - with pytest.raises(NotFound) as exc: - collection.get_by_root_id(branch_root_id=root_id) - - # Then - assert str(root_id) in str(exc) - assert "branch root" in str(exc).lower() - assert LATEST_VER in str(exc).lower() - - -def test_branch_data_updates_normal_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - root_branch_id = branch_data["metadata"]["root_id"] - branch_data_get_resp = {"response": [branch_data]} - branch_data_get_params = { - 'page': 1, 'per_page': 1, 'root': str(root_branch_id), 'version': branch_data['metadata']['version'] - } - session.set_response(branch_data) - - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - data_updates = BranchDataUpdateFactory() - v2branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(root_id=root_branch_id)) - session.set_responses(branch_data_get_resp, data_updates, v2branch_data) - with pytest.deprecated_call(): - v2branch = collection.update_data(branch) - - # Then - next_version_call = FakeCall(method='POST', - path=f'{branch_path}/next-version-predictor', - params={'root': str(root_branch_id), - 'retrain_models': False}, - json={ - 'data_updates': [ - { - 'current': data_updates['data_updates'][0]['current'], - 'latest': data_updates['data_updates'][0]['latest'], - 'type': 'DataVersionUpdate' - } - ], - 'use_predictors': [ - { - 'predictor_id': data_updates['predictors'][0]['predictor_id'], - 'predictor_version': data_updates['predictors'][0]['predictor_version'] - } - ] - }, - version='v2') - assert session.calls == [ - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}'), - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}/data-version-updates-predictor'), - next_version_call - ] - assert str(v2branch.root_id) == root_branch_id - - -def test_branch_data_updates_latest_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] - branch_data_get_resp = {"response": [branch_data]} - branch_data_get_params = { - 'page': 1, 'per_page': 1, 'root': str(root_branch_id), 'version': branch_data['metadata']['version'] - } - session.set_response(branch_data) - - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - data_updates = BranchDataUpdateFactory() - v2branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(root_id=root_branch_id)) - session.set_responses(branch_data_get_resp, data_updates, v2branch_data) - with pytest.deprecated_call(): - v2branch = collection.update_data(branch, use_existing=False, retrain_models=True) - - # Then - next_version_call = FakeCall(method='POST', - path=f'{branch_path}/next-version-predictor', - params={'root': str(root_branch_id), - 'retrain_models': True}, - json={ - 'data_updates': [ - { - 'current': data_updates['data_updates'][0]['current'], - 'latest': data_updates['data_updates'][0]['latest'], - 'type': 'DataVersionUpdate' - } - ], - 'use_predictors': [] - }, - version='v2') - assert session.calls == [ - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}'), - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}/data-version-updates-predictor'), - next_version_call - ] - assert str(v2branch.root_id) == root_branch_id - - -def test_branch_data_updates_nochange_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - branch_data_get_resp = {"response": [branch_data]} - session.set_response(branch_data) - - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - data_updates = BranchDataUpdateFactory(data_updates=[], predictors=[]) - session.set_responses(branch_data, branch_data_get_resp, data_updates) - with pytest.deprecated_call(): - v2branch = collection.update_data(branch.uid) - - assert v2branch is None - - -def test_branch_get_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - session.set_response(branch_data) - - # When - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - # Then - assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}') - - -def test_branch_archive_deprecated(session, collection, branch_path): - # Given - branch_id = uuid.uuid4() - session.set_response(BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=True))) - - # When - with pytest.deprecated_call(): - archived_branch = collection.archive(branch_id) - - # Then - assert session.num_calls == 1 - expected_path = f'{branch_path}/{branch_id}/archive' - assert session.last_call == FakeCall(method='PUT', path=expected_path, json={}) - assert archived_branch.archived is True - - -def test_branch_restore_deprecated(session, collection, branch_path): - # Given - branch_id = uuid.uuid4() - session.set_response(BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=False))) - - # When - with pytest.deprecated_call(): - restored_branch = collection.restore(branch_id) - - # Then - assert session.num_calls == 1 - expected_path = f'{branch_path}/{branch_id}/restore' - assert session.last_call == FakeCall(method='PUT', path=expected_path, json={}) - assert restored_branch.archived is False - - -def test_branch_data_updates_deprecated(session, collection, branch_path): - # Given - branch_id = uuid.uuid4() - expected_data_updates = BranchDataUpdateFactory() - session.set_response(expected_data_updates) - - # When - with pytest.deprecated_call(): - actual_data_updates = collection.data_updates(branch_id) - - # Then - assert session.num_calls == 1 - expected_path = f'{branch_path}/{branch_id}/data-version-updates-predictor' - assert session.last_call == FakeCall(method='GET', - path=expected_path, - version='v2') - assert expected_data_updates['data_updates'][0]['current'] == actual_data_updates.data_updates[0].current - assert expected_data_updates['data_updates'][0]['latest'] == actual_data_updates.data_updates[0].latest - assert expected_data_updates['predictors'][0]['predictor_id'] == str(actual_data_updates.predictors[0].uid) - - -def test_branch_next_version_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] - session.set_response(branch_data) - data_updates = [DataVersionUpdate(current="gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::1", - latest="gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::2")] - predictors = [PredictorRef("aa971886-d17c-43b4-b602-5af7b44fcd5a", 2)] - req = NextBranchVersionRequest(data_updates=data_updates, use_predictors=predictors) - - # When - with pytest.deprecated_call(): - branchv2 = collection.next_version(root_branch_id, branch_instructions=req, retrain_models=False) - - # Then - expected_path = f'{branch_path}/next-version-predictor' - expected_call = FakeCall(method='POST', - path=expected_path, - params={'root': str(root_branch_id), - 'retrain_models': False}, - json={ - 'data_updates': [ - { - 'current': 'gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::1', - 'latest': 'gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::2', - 'type': 'DataVersionUpdate' - } - ], - 'use_predictors': [ - { - 'predictor_id': 'aa971886-d17c-43b4-b602-5af7b44fcd5a', - 'predictor_version': 2 - } - ] - }, - version='v2') - assert session.num_calls == 1 - assert session.last_call == expected_call - assert str(branchv2.root_id) == root_branch_id - - -def test_get_both_ids(collection): - with pytest.deprecated_call(): - with pytest.raises(ValueError): - collection.get(uuid.uuid4(), root_id=uuid.uuid4()) - - -def test_get_neither_id(collection): - with pytest.raises(ValueError): - collection.get() - - -def test_archive_neither_id(collection): - with pytest.raises(ValueError): - collection.archive() - - -def test_restore_neither_id(collection): - with pytest.raises(ValueError): - collection.restore() - - -def test_update_data_both_ids(collection): - with pytest.raises(ValueError): - collection.update_data(uuid.uuid4(), root_id=uuid.uuid4()) - - -def test_update_data_neither_id(collection): - with pytest.raises(ValueError): - collection.update_data() - - -def test_data_updates_both_ids(collection): - with pytest.deprecated_call(): - with pytest.raises(ValueError): - collection.data_updates(uuid.uuid4(), root_id=uuid.uuid4()) - - -def test_data_updates_neither_id(collection): - with pytest.raises(ValueError): - collection.data_updates() diff --git a/tests/resources/test_descriptors.py b/tests/resources/test_descriptors.py index 2bdec0d22..b289025d4 100644 --- a/tests/resources/test_descriptors.py +++ b/tests/resources/test_descriptors.py @@ -105,45 +105,3 @@ def test_from_data_source(): assert session.last_call.path == '/projects/{}/material-descriptors/from-data-source'\ .format(descriptors.project_id) assert session.last_call.method == 'POST' - - -def test_deprecated_descriptors_from_data_source(): - session = FakeSession() - col = 'smiles' - response_json = { - 'descriptors': [ # shortened sample response - { - 'type': 'Real', - 'descriptor_key': 'khs.sNH3 KierHallSmarts for {}'.format(col), - 'units': '', - 'lower_bound': 0, - 'upper_bound': 1000000000 - }, - { - 'type': 'Real', - 'descriptor_key': 'khs.dsN KierHallSmarts for {}'.format(col), - 'units': '', - 'lower_bound': 0, - 'upper_bound': 1000000000 - }, - ] - } - session.set_response(response_json) - descriptors = DescriptorMethods(uuid4(), session) - data_source = GemTableDataSource(table_id='43357a66-3644-4959-8115-77b2630aca45', table_version=123) - - with pytest.deprecated_call(): - results = descriptors.descriptors_from_data_source(data_source=data_source) - - assert results == [ - RealDescriptor( - key=r['descriptor_key'], - lower_bound=r['lower_bound'], - upper_bound=r['upper_bound'], - units=r['units'] - ) for r in response_json['descriptors'] - ] - assert session.last_call.path == '/projects/{}/material-descriptors/from-data-source'\ - .format(descriptors.project_id) - assert session.last_call.method == 'POST' - diff --git a/tests/resources/test_design_executions.py b/tests/resources/test_design_executions.py index 60d561c17..87a54a440 100644 --- a/tests/resources/test_design_executions.py +++ b/tests/resources/test_design_executions.py @@ -59,8 +59,6 @@ def test_build_new_execution(collection, design_execution_dict): assert execution._session == collection.session assert execution.in_progress() and not execution.succeeded() and not execution.failed() assert execution.status_detail - with pytest.deprecated_call(): - assert execution.status_info == [detail.msg for detail in execution.status_detail] def test_trigger_workflow_execution(collection: DesignExecutionCollection, design_execution_dict, session): @@ -117,9 +115,3 @@ def test_list(collection: DesignExecutionCollection, session): def test_delete(collection): with pytest.raises(NotImplementedError): collection.delete(uuid.uuid4()) - - -def test_deprecated(): - from citrine.resources.design_execution import DesignExecution - with pytest.deprecated_call(): - DesignExecution() diff --git a/tests/resources/test_design_space.py b/tests/resources/test_design_space.py index e440e1e99..b4d26a5a9 100644 --- a/tests/resources/test_design_space.py +++ b/tests/resources/test_design_space.py @@ -75,15 +75,11 @@ def test_design_space_build_with_status_detail(valid_product_design_space_data): # Then status_detail_tuples = {(detail.level, detail.msg) for detail in design_space.status_detail} assert status_detail_tuples == status_detail_data - with pytest.deprecated_call(): - assert design_space.status_info == [args[1] for args in status_detail_data] def test_formulation_build(valid_formulation_design_space_data): pc = DesignSpaceCollection(uuid.uuid4(), None) design_space = pc.build(valid_formulation_design_space_data) - with pytest.deprecated_call(): - assert design_space.archived assert design_space.name == 'formulation design space' assert design_space.description == 'formulates some things' assert design_space.formulation_descriptor.key == FormulationKey.HIERARCHICAL.value @@ -320,131 +316,6 @@ def test_get_none(): assert "uid=None" in str(excinfo.value) -def test_register_dehydrated_design_spaces_deprecated(valid_product_design_space_data, valid_product_design_space): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - - subspace_id = str(uuid.uuid4()) - - subspace_data = valid_product_design_space_data["data"]["instance"]["subspaces"][0] - ds = DesignSpace.build(deepcopy(valid_product_design_space_data)) - ds.subspaces[0] = subspace_id - - session.set_responses(_ds_dict_to_response(subspace_data), deepcopy(valid_product_design_space_data), valid_product_design_space_data) - - with pytest.deprecated_call(): - retval = dsc.register(ds) - - base_path = f"/projects/{dsc.project_id}/design-spaces" - assert session.calls == [ - FakeCall(method='GET', path=f"{base_path}/{subspace_id}"), - FakeCall(method='POST', path=base_path, json=valid_product_design_space.dump()), - FakeCall(method='PUT', path=f"{base_path}/{retval.uid}/validate", json={}) - ] - assert retval.dump() == valid_product_design_space.dump() - - -def test_update_dehydrated_design_spaces_deprecated(valid_product_design_space_data, valid_product_design_space): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - - subspace_id = str(uuid.uuid4()) - - subspace_data = valid_product_design_space_data["data"]["instance"]["subspaces"][0] - ds = DesignSpace.build(deepcopy(valid_product_design_space_data)) - ds.subspaces[0] = subspace_id - - session.set_responses( - _ds_dict_to_response(subspace_data), - deepcopy(valid_product_design_space_data), - deepcopy(valid_product_design_space_data) - ) - - with pytest.deprecated_call(): - retval = dsc.update(ds) - - base_path = f"/projects/{dsc.project_id}/design-spaces" - assert session.calls == [ - FakeCall(method='GET', path=f"{base_path}/{subspace_id}"), - FakeCall(method='PUT', path=f"{base_path}/{ds.uid}", json=valid_product_design_space.dump()), - FakeCall(method='PUT', path=f"{base_path}/{ds.uid}/validate", json={}) - ] - assert retval.dump() == valid_product_design_space.dump() - - -def test_deprecated_archive_via_update(valid_product_design_space_data): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - archived_data = deepcopy(valid_product_design_space_data) - archived_data["metadata"]["archived"] = archived_data["metadata"]["created"] - validating_data = deepcopy(archived_data) - validating_data["metadata"]["status"]["name"] = "VALIDATING" - session.set_responses( - valid_product_design_space_data, - archived_data, - validating_data - ) - - design_space = dsc.build(deepcopy(valid_product_design_space_data)) - with pytest.deprecated_call(): - design_space.archived = True - - design_space_path = DesignSpaceCollection._path_template.format(project_id=dsc.project_id) - entity_path = f"{design_space_path}/{valid_product_design_space_data['id']}" - expected_calls = [ - FakeCall(method="PUT", path=entity_path, json=design_space.dump()), - FakeCall(method="PUT", path=f"{entity_path}/archive", json={}), - FakeCall(method="PUT", path=f"{entity_path}/validate", json={}), - ] - - archived_design_space = dsc.update(design_space) - - assert session.calls == expected_calls - assert archived_design_space.is_archived is True - assert archived_design_space._archived is None - -def test_deprecated_restore_via_update(valid_product_design_space_data): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - archived_data = deepcopy(valid_product_design_space_data) - archived_data["metadata"]["archived"] = archived_data["metadata"]["created"] - validating_data = deepcopy(valid_product_design_space_data) - validating_data["metadata"]["status"]["name"] = "VALIDATING" - session.set_responses(archived_data, valid_product_design_space_data, validating_data) - - design_space = dsc.build(deepcopy(archived_data)) - with pytest.deprecated_call(): - design_space.archived = False - - design_space_path = DesignSpaceCollection._path_template.format(project_id=dsc.project_id) - entity_path = f"{design_space_path}/{archived_data['id']}" - expected_calls = [ - FakeCall(method="PUT", path=entity_path, json=design_space.dump()), - FakeCall(method="PUT", path=f"{entity_path}/restore", json={}), - FakeCall(method="PUT", path=f"{entity_path}/validate", json={}), - ] - - restored_design_space = dsc.update(design_space) - - assert session.calls == expected_calls - assert restored_design_space.is_archived is False - assert restored_design_space._archived is None - - -def test_deprecated_archived_property(valid_product_design_space_data): - dsc = DesignSpaceCollection(uuid.uuid4(), FakeSession()) - - design_space = dsc.build(valid_product_design_space_data) - - with pytest.deprecated_call(): - assert design_space.archived == design_space.is_archived - - with pytest.deprecated_call(): - design_space.archived = True - - assert design_space._archived is True - - def test_failed_register(valid_product_design_space_data): response_data = deepcopy(valid_product_design_space_data) response_data['metadata']['status']['name'] = 'INVALID' diff --git a/tests/resources/test_design_workflows.py b/tests/resources/test_design_workflows.py index 2163b29bd..b5279d9e0 100644 --- a/tests/resources/test_design_workflows.py +++ b/tests/resources/test_design_workflows.py @@ -41,16 +41,6 @@ def collection(branch_data, collection_without_branch) -> DesignWorkflowCollecti ) -@pytest.fixture -def collection_with_branch_id(branch_data, collection_without_branch) -> DesignWorkflowCollection: - with pytest.deprecated_call(): - return DesignWorkflowCollection( - project_id=collection_without_branch.project_id, - session=collection_without_branch.session, - branch_id=uuid.UUID(branch_data['id']), - ) - - @pytest.fixture def workflow(collection, branch_data, design_workflow_dict) -> DesignWorkflow: design_workflow_dict["branch_id"] = branch_data["id"] @@ -106,20 +96,6 @@ def test_basic_methods(workflow, collection, design_workflow_dict): assert workflow.design_executions.project_id == workflow.project_id -def test_collection_branch_id(session) -> DesignWorkflowCollection: - with pytest.deprecated_call(): - DesignWorkflowCollection(project_id=uuid.uuid4(), session=session, branch_id=uuid.uuid4()) - - -def test_collection_both_ids(session) -> DesignWorkflowCollection: - with pytest.deprecated_call(): - with pytest.raises(ValueError): - DesignWorkflowCollection(project_id=uuid.uuid4(), - session=session, - branch_id=uuid.uuid4(), - branch_root_id=uuid.uuid4()) - - @pytest.mark.parametrize("optional_args", all_combination_lengths(OPTIONAL_ARGS)) def test_register(session, branch_data, workflow_minimal, collection, optional_args): workflow = workflow_minimal @@ -152,37 +128,6 @@ def test_register(session, branch_data, workflow_minimal, collection, optional_a assert_workflow(new_workflow, workflow) -@pytest.mark.parametrize("optional_args", all_combination_lengths(OPTIONAL_ARGS)) -def test_register_with_branch_id_deprecated(session, branch_data, workflow_minimal, collection_with_branch_id, optional_args): - collection = collection_with_branch_id - workflow = workflow_minimal - branch_id = branch_data['id'] - - # Set a random value for all optional args selected for this run. - for name, factory in optional_args: - setattr(workflow, name, factory()) - - # Given - post_dict = {**workflow.dump(), "branch_id": str(branch_id)} - session.set_responses({**post_dict, 'status_description': 'status'}, branch_data) - - # When - with pytest.deprecated_call(): - new_workflow = collection.register(workflow) - - # Then - assert session.calls == [ - FakeCall(method='POST', path=workflow_path(collection), json=post_dict), - FakeCall(method='GET', path=branches_path(collection, branch_id)), - ] - - with pytest.deprecated_call(): - assert str(new_workflow.branch_id) == branch_id - assert str(new_workflow.branch_root_id) == branch_data['metadata']['root_id'] - assert new_workflow.branch_version == branch_data['metadata']['version'] - assert_workflow(new_workflow, workflow) - - def test_register_conflicting_branches(session, branch_data, workflow, collection): # Given old_branch_root_id = uuid.uuid4() @@ -295,39 +240,6 @@ def test_update(session, branch_data, workflow, collection_without_branch): assert_workflow(new_workflow, workflow) -def test_update_branch_id_deprecated(session, branch_data, workflow, collection_with_branch_id): - # Given - collection = collection_with_branch_id - workflow.branch_root_id = None - workflow.branch_version = None - with pytest.deprecated_call(): - workflow.branch_id = branch_data['id'] - - post_dict = workflow.dump() - session.set_responses( - {"per_page": 1, "next": "", "response": []}, - {**post_dict, 'status_description': 'status'}, - branch_data - ) - - # When - with pytest.deprecated_call(): - new_workflow = collection.update(workflow) - - # Then - executions_path = f'/projects/{collection.project_id}/design-workflows/{workflow.uid}/executions' - assert session.calls == [ - FakeCall(method='GET', path=executions_path, params={'page': 1, 'per_page': 100}), - FakeCall(method='PUT', path=workflow_path(collection, workflow), json=post_dict), - FakeCall(method='GET', path=branches_path(collection, branch_data["id"])), - ] - assert_workflow(new_workflow, workflow) - with pytest.deprecated_call(): - assert new_workflow.branch_id == workflow.branch_id - assert new_workflow.branch_root_id == uuid.UUID(branch_data['metadata']['root_id']) - assert new_workflow.branch_version == branch_data['metadata']['version'] - - def test_update_failure_with_existing_execution(session, branch_data, workflow, collection_without_branch, design_execution_dict): branch_data_get_resp = {"response": [branch_data]} workflow.branch_root_id = uuid.uuid4() @@ -350,17 +262,6 @@ def test_update_with_mismatched_branch_root_ids(session, workflow, collection): collection.update(workflow) -def test_update_with_mismatched_branch_ids(session, workflow, collection_with_branch_id): - # Given - with pytest.deprecated_call(): - workflow.branch_id = uuid.uuid4() - - # Then/When - with pytest.deprecated_call(): - with pytest.raises(ValueError): - collection_with_branch_id.update(workflow) - - def test_update_model_missing_branch_root_id(session, workflow, collection_without_branch): # Given workflow.branch_root_id = None diff --git a/tests/resources/test_experiment_datasource.py b/tests/resources/test_experiment_datasource.py index d1432522e..37cdca38f 100644 --- a/tests/resources/test_experiment_datasource.py +++ b/tests/resources/test_experiment_datasource.py @@ -115,31 +115,6 @@ def test_list(session, collection, erds_base_path): ] -def test_list_deprecated(session, collection, erds_base_path): - branch_id = uuid.uuid4() - - session.set_response({"response": []}) - - list(collection.list()) - with pytest.deprecated_call(): - list(collection.list(branch_id=branch_id)) - list(collection.list(version=4)) - list(collection.list(version=LATEST_VER)) - with pytest.deprecated_call(): - list(collection.list(branch_id=branch_id, version=12)) - with pytest.deprecated_call(): - list(collection.list(branch_id=branch_id, version=LATEST_VER)) - - assert session.calls == [ - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "branch": str(branch_id), 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "version": 4, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "version": LATEST_VER, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "branch": str(branch_id), "version": 12, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "branch": str(branch_id), "version": LATEST_VER, 'page': 1}) - ] - - def test_read_and_retrieve(session, collection, erds_dict, erds_base_path): erds_id = uuid.uuid4() erds_path = f"{erds_base_path}/{erds_id}" diff --git a/tests/resources/test_file_link.py b/tests/resources/test_file_link.py index aa6bde7a3..4caf22ba6 100644 --- a/tests/resources/test_file_link.py +++ b/tests/resources/test_file_link.py @@ -9,7 +9,7 @@ from citrine.resources.api_error import ValidationError from citrine.resources.file_link import FileCollection, FileLink, GEMDFileLink, _Uploader, \ - FileProcessingType, _get_ids_from_url + _get_ids_from_url from citrine.resources.ingestion import Ingestion, IngestionCollection from citrine.exceptions import NotFound @@ -520,89 +520,6 @@ def test_external_file_download(collection: FileCollection, session, tmpdir): assert local_path.read_text() == '010111011' -def test_process_file(collection: FileCollection, session): - """Test processing an existing file.""" - - file_id, version_id = str(uuid4()), str(uuid4()) - full_url = collection._get_path(uid=file_id, version=version_id) - file_link = collection.build(FileLinkDataFactory(url=full_url, id=file_id, version=version_id)) - - job_id_resp = { - 'job_id': str(uuid4()) - } - job_execution_resp = { - 'status': 'Success', - 'job_type': 'something', - 'tasks': [] - } - file_processing_result_resp = { - 'results': [ - { - 'processing_type': 'VALIDATE_CSV', - 'data': { - 'columns': [ - { - 'name': 'a', - 'bounds': { - 'type': 'integer_bounds', - 'lower_bound': 0, - 'upper_bound': 10 - }, - 'exact_range_bounds': { - 'type': 'integer_bounds', - 'lower_bound': 0, - 'upper_bound': 10 - } - } - ], - 'record_count': 123 - } - } - ] - } - - # First does a PUT on the /processed endpoint - # then does a GET on the job executions endpoint - # then gets the file processing result - session.set_responses(job_id_resp, job_execution_resp, file_processing_result_resp) - with pytest.warns(DeprecationWarning): - collection.process(file_link=file_link, processing_type=FileProcessingType.VALIDATE_CSV) - - -def test_process_file_no_waiting(collection: FileCollection, session): - """Test processing an existing file without waiting on the result.""" - - file_id, version_id = str(uuid4()), str(uuid4()) - full_url = collection._get_path(uid=file_id, version=version_id) - file_link = collection.build(FileLinkDataFactory(url=full_url, id=file_id, version=version_id)) - - job_id_resp = { - 'job_id': str(uuid4()) - } - - # First does a PUT on the /processed endpoint - # then does a GET on the job executions endpoint - session.set_response(job_id_resp) - with pytest.warns(DeprecationWarning): - resp = collection.process(file_link=file_link, processing_type=FileProcessingType.VALIDATE_CSV, - wait_for_response=False) - assert str(resp.job_id) == job_id_resp['job_id'] - - -def test_process_file_exceptions(collection: FileCollection, session): - """Test processing an existing file without waiting on the result.""" - full_url = f'http://www.files.com/file.path' - file_link = collection.build(FileLinkDataFactory(url=full_url)) - collection._get_path() - # First does a PUT on the /processed endpoint - # then does a GET on the job executions endpoint - with pytest.raises(ValueError, match="on-platform resources"): - with pytest.warns(DeprecationWarning): - collection.process(file_link=file_link, - processing_type=FileProcessingType.VALIDATE_CSV, - wait_for_response=False) - - def test_ingest(collection: FileCollection, session): """Test the on-platform ingest route.""" good_file1 = collection.build({"filename": "good.csv", "id": str(uuid4()), "version": str(uuid4())}) diff --git a/tests/resources/test_generative_design_execution.py b/tests/resources/test_generative_design_execution.py index a956b5fad..0a5327d83 100644 --- a/tests/resources/test_generative_design_execution.py +++ b/tests/resources/test_generative_design_execution.py @@ -44,8 +44,6 @@ def test_build_new_execution(collection, generative_design_execution_dict): assert execution._session == collection.session assert execution.in_progress() and not execution.succeeded() and not execution.failed() assert execution.status_detail - with pytest.deprecated_call(): - assert execution.status_info == [detail.msg for detail in execution.status_detail] def test_trigger_execution(collection: GenerativeDesignExecutionCollection, generative_design_execution_dict, session): diff --git a/tests/resources/test_job_client.py b/tests/resources/test_job_client.py deleted file mode 100644 index a921f5270..000000000 --- a/tests/resources/test_job_client.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from uuid import UUID, uuid4 - -from citrine.jobs.job import TaskNode, JobStatusResponse, JobSubmissionResponse -import citrine.resources.job as oldjobs -from citrine.resources.gemtables import GemTableCollection -from citrine.resources.project import Project -from citrine.resources.table_config import TableConfig -from tests.utils.session import FakeSession - - -def task_node_1() -> dict: - tn1 = {'id': 'dave_id1', 'task_type': 'dave_type', 'status': 'dave_status', - 'dependencies': ['dep1', 'dep2']} - return tn1 - - -def task_node_2() -> dict: - tn2 = {'id': 'dave_id2', 'task_type': 'dave_type', 'status': 'dave_status', 'failure_reason': 'because I failed', - 'dependencies': ['dep3', 'dep4']} - return tn2 - - -def job_status() -> dict: - js = {'job_type': "dave_job_type", 'status': "david_job_status", "tasks": [task_node_1(), task_node_2()]} - return js - - -def job_status_with_output() -> dict: - js = {'job_type': "dave_job_type", - 'status': "david_job_status", - "tasks": [task_node_1(), task_node_2()], - "output": {"key1": "val1", "key2": "val2"} - } - return js - - -@pytest.fixture -def session() -> FakeSession: - return FakeSession() - - -@pytest.fixture -def collection(session) -> GemTableCollection: - return GemTableCollection( - project_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session - ) - - -@pytest.fixture -def table_config() -> TableConfig: - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], columns=[]) - table_config.version_number = 1 - table_config.config_uid = UUID('12345678-1234-1234-1234-123456789bbb') - return table_config - - -@pytest.fixture -def project(session: FakeSession) -> Project: - project = Project( - name="Test GEM Tables project", - session=session - ) - project.uid = UUID('6b608f78-e341-422c-8076-35adc8828545') - return project - - -def test_tn_serde(): - tn = TaskNode.build(task_node_1()) - expected = task_node_1() - expected['failure_reason'] = None - assert tn.dump() == expected - - -def test_js_serde(): - js = JobStatusResponse.build(job_status()) - expected = job_status() - expected['tasks'][0]['failure_reason'] = None - expected['output'] = None - assert js.dump() == expected - - -def test_js_serde_with_output(): - js = JobStatusResponse.build(job_status_with_output()) - expected = job_status_with_output() - expected['tasks'][0]['failure_reason'] = None - assert js.dump() == expected - - -def test_build_job(collection: GemTableCollection, table_config: TableConfig): - collection.session.set_response({"job_id": '12345678-1234-1234-1234-123456789ccc'}) - resp = collection.initiate_build(table_config) - assert isinstance(resp, JobSubmissionResponse) - assert resp.job_id == UUID('12345678-1234-1234-1234-123456789ccc') - - -def test_renamed_classes_are_the_same(): - # Mostly make code coverage happy - assert issubclass(oldjobs.JobSubmissionResponse, JobSubmissionResponse) - with pytest.deprecated_call(): - oldjobs.JobSubmissionResponse.build({"job_id": uuid4()}) diff --git a/tests/resources/test_predictor_evaluation_executions.py b/tests/resources/test_predictor_evaluation_executions.py index 520f052cc..312701ad7 100644 --- a/tests/resources/test_predictor_evaluation_executions.py +++ b/tests/resources/test_predictor_evaluation_executions.py @@ -61,8 +61,6 @@ def test_build_new_execution(collection, predictor_evaluation_execution_dict): assert execution._session == collection.session assert execution.in_progress() and not execution.succeeded() and not execution.failed() assert execution.status_detail - with pytest.deprecated_call(): - assert execution.status_info == [detail.msg for detail in execution.status_detail] def test_workflow_execution_results(workflow_execution: PredictorEvaluationExecution, session, @@ -156,9 +154,3 @@ def test_restore(workflow_execution, collection): def test_delete(collection): with pytest.raises(NotImplementedError): collection.delete(uuid.uuid4()) - - -def test_deprecated(): - from citrine.resources.predictor_evaluation_execution import PredictorEvaluationExecution - with pytest.deprecated_call(): - PredictorEvaluationExecution() diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index 445f8a6d3..5900f7c89 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -183,11 +183,6 @@ def test_design_spaces_get_project_id(project): assert project.uid == project.design_spaces.project_id -def test_modules_get_project_id_deprecated(project): - with pytest.deprecated_call(): - assert project.uid == project.modules.project_id - - def test_descriptors_get_project_id(project): assert project.uid == project.descriptors.project_id diff --git a/tests/resources/test_workflow.py b/tests/resources/test_workflow.py index 84211d10a..2151ac551 100644 --- a/tests/resources/test_workflow.py +++ b/tests/resources/test_workflow.py @@ -78,14 +78,3 @@ def test_list_workflows(session, basic_design_workflow_data): assert 2 == session.num_calls assert len(workflows) == 1 assert isinstance(workflows[0], DesignWorkflow) - - -def test_status_info(session, failed_design_workflow_data): - branch_data = BranchDataFactory() - session.set_response(branch_data) - dwc = DesignWorkflowCollection(project_id=uuid.uuid4(), session=session) - - dw = dwc.build(failed_design_workflow_data) - - with pytest.deprecated_call(): - assert dw.status_info == [status.msg for status in dw.status_detail] diff --git a/tests/utils/fakes/fake_workflow_collection.py b/tests/utils/fakes/fake_workflow_collection.py index e9dad1936..89651dfa1 100644 --- a/tests/utils/fakes/fake_workflow_collection.py +++ b/tests/utils/fakes/fake_workflow_collection.py @@ -24,10 +24,9 @@ def register(self, workflow: WorkflowType) -> WorkflowType: workflow.project_id = self.project_id return workflow - def archive(self, uid: Union[UUID, str] = None, workflow_id: Union[UUID, str] = None): + def archive(self, uid: Union[UUID, str]): # Search for workflow via UID to ensure exists # If found, flip archived=True with no return - uid = migrate_deprecated_argument(uid, "uid", workflow_id, "workflow_id") workflow = self.get(uid) workflow.archived = True self.update(workflow) @@ -48,4 +47,4 @@ def create_default(self, *, predictor_id: UUID) -> PredictorEvaluationWorkflow: pew.project_id = self.project_id pew.uid = uuid4() pew._session = self.session - return pew \ No newline at end of file + return pew From e11ac2239b6b7d8155ea3d3798cf5876c1bf81bf Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Tue, 2 Jan 2024 11:40:44 -0500 Subject: [PATCH 03/22] [PLA-12471] Change ratio constraint basis types. basis_ingredients and basis_labels are treated as lists of names now, so the types should match. Additionally, the basis_ingredient_names and basis_label_names methods should be deprecated. They were only added to allow a smooth transition to 3.x. --- .../ingredient_ratio_constraint.py | 38 ++++++++------- tests/informatics/test_constraints.py | 46 +++++++------------ 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py index d86dcd7f3..0149a0bfd 100644 --- a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py @@ -1,5 +1,5 @@ import warnings -from typing import Set, Mapping, Optional, Tuple, Union +from typing import Set, Optional, Tuple from citrine._serialization import properties from citrine._serialization.serializable import Serializable @@ -64,8 +64,8 @@ def __init__(self, *, max: float, ingredient: Optional[Tuple[str, float]] = None, label: Optional[Tuple[str, float]] = None, - basis_ingredients: Union[Set[str], Mapping[str, float]] = set(), - basis_labels: Union[Set[str], Mapping[str, float]] = set()): + basis_ingredients: Set[str] = set(), + basis_labels: Set[str] = set()): self.formulation_descriptor = formulation_descriptor self.min = min self.max = max @@ -95,50 +95,54 @@ def label(self, value: Optional[Tuple[str, float]]): self._labels = self._numerator_validate(value, "Label") @property - def basis_ingredients(self) -> Mapping[str, float]: + def basis_ingredients(self) -> Set[str]: """Retrieve the ingredients in the denominator of the ratio.""" - warnings.warn("basis_ingredients is deprecated as of 2.13.0 and will change in 3.0. " - "Please use basis_ingredient_names instead.", DeprecationWarning) - return self._basis_ingredients + return set(self._basis_ingredients.keys()) @basis_ingredients.setter def basis_ingredients(self, value: Set[str]): """Set the ingredients in the denominator of the ratio.""" - self.basis_ingredient_names = value + self._basis_ingredients = dict.fromkeys(value, 1) @property def basis_ingredient_names(self) -> Set[str]: """Retrieve the names of all ingredients in the denominator of the ratio.""" - return set(self._basis_ingredients.keys()) + warnings.warn("basis_ingredient_names is deprecated as of 3.0.0 and will be dropped in " + "4.0. Please use basis_ingredients instead.", DeprecationWarning) + return self.basis_ingredients # This is for symmetry; it's not strictly necessary. @basis_ingredient_names.setter def basis_ingredient_names(self, value: Set[str]): """Set the names of all ingredients in the denominator of the ratio.""" - self._basis_ingredients = dict.fromkeys(value, 1) + warnings.warn("basis_ingredient_names is deprecated as of 3.0.0 and will be dropped in " + "4.0. Please use basis_ingredients instead.", DeprecationWarning) + self.basis_ingredients = value @property - def basis_labels(self) -> Mapping[str, float]: + def basis_labels(self) -> Set[str]: """Retrieve the labels in the denominator of the ratio.""" - warnings.warn("basis_labels is deprecated as of 2.13.0 and will change in 3.0. Please use " - "basis_label_names instead.", DeprecationWarning) - return self._basis_labels + return set(self._basis_labels.keys()) @basis_labels.setter def basis_labels(self, value: Set[str]): """Set the labels in the denominator of the ratio.""" - self.basis_label_names = value + self._basis_labels = dict.fromkeys(value, 1) @property def basis_label_names(self) -> Set[str]: """Retrieve the names of all labels in the denominator of the ratio.""" - return set(self._basis_labels.keys()) + warnings.warn("basis_label_names is deprecated as of 3.0.0 and will be dropped in 4.0. " + "Please use basis_labels instead.", DeprecationWarning) + return self.basis_labels # This is for symmetry; it's not strictly necessary. @basis_label_names.setter def basis_label_names(self, value: Set[str]): """Set the names of all labels in the denominator of the ratio.""" - self._basis_labels = dict.fromkeys(value, 1) + warnings.warn("basis_label_names is deprecated as of 3.0.0 and will be dropped in 4.0. " + "Please use basis_labels instead.", DeprecationWarning) + self.basis_labels = value def _numerator_read(self, num_dict): if num_dict: diff --git a/tests/informatics/test_constraints.py b/tests/informatics/test_constraints.py index eab638042..78fb32f69 100644 --- a/tests/informatics/test_constraints.py +++ b/tests/informatics/test_constraints.py @@ -142,8 +142,8 @@ def test_ingredient_ratio_initialization(ingredient_ratio_constraint): assert ingredient_ratio_constraint.max == 1e6 assert ingredient_ratio_constraint.ingredient == ("foo", 1.0) assert ingredient_ratio_constraint.label == ("foolabel", 0.5) - assert ingredient_ratio_constraint.basis_ingredient_names == {"baz", "bat"} - assert ingredient_ratio_constraint.basis_label_names == {"bazlabel", "batlabel"} + assert ingredient_ratio_constraint.basis_ingredients == {"baz", "bat"} + assert ingredient_ratio_constraint.basis_labels == {"bazlabel", "batlabel"} def test_ingredient_ratio_interaction(ingredient_ratio_constraint): @@ -181,43 +181,31 @@ def test_ingredient_ratio_interaction(ingredient_ratio_constraint): ingredient_ratio_constraint.label = [] assert ingredient_ratio_constraint.label is None - newval_dict = {"foobasis": 3} - with pytest.deprecated_call(): - ingredient_ratio_constraint.basis_ingredients = newval_dict - with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_ingredients == dict.fromkeys(newval_dict.keys(), 1) - ingredient_ratio_constraint.basis_ingredient_names = set(newval_dict.keys()) - - newval_set = {"foobasis2"} + newval_set = {"foobasis1"} ingredient_ratio_constraint.basis_ingredients = newval_set + assert ingredient_ratio_constraint.basis_ingredients == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_ingredients == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_ingredient_names = newval_set + assert ingredient_ratio_constraint.basis_ingredient_names == newval_set - newval_set = {"foobasis3"} - ingredient_ratio_constraint.basis_ingredient_names = newval_set - with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_ingredients == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_ingredient_names = newval_set - - newval_dict = {"foolabelbasis": 3} + newval_set = {"foobasis2"} with pytest.deprecated_call(): - ingredient_ratio_constraint.basis_labels = newval_dict + ingredient_ratio_constraint.basis_ingredient_names = newval_set + assert ingredient_ratio_constraint.basis_ingredients == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_labels == dict.fromkeys(newval_dict.keys(), 1) - ingredient_ratio_constraint.basis_label_names = set(newval_dict.keys()) + assert ingredient_ratio_constraint.basis_ingredient_names == newval_set - newval_set = {"foolabelbasis2"} + newval_set = {"foolabelbasis1"} ingredient_ratio_constraint.basis_labels = newval_set + assert ingredient_ratio_constraint.basis_labels == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_labels == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_label_names = newval_set + assert ingredient_ratio_constraint.basis_label_names == newval_set - newval_set = {"foolabelbasis3"} - ingredient_ratio_constraint.basis_label_names = newval_set + newval_set = {"foolabelbasis1"} + with pytest.deprecated_call(): + ingredient_ratio_constraint.basis_label_names = newval_set + assert ingredient_ratio_constraint.basis_labels == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_labels == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_label_names = newval_set + assert ingredient_ratio_constraint.basis_label_names == newval_set def test_range_defaults(): From 7d7039d39c8510229cfb5b2b3177091834f690bf Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Fri, 5 Jan 2024 14:15:41 -0500 Subject: [PATCH 04/22] [PLA-13767] Drop ModuleCollection and Module. Modules no longer exist as a unified concept in the backend, so these objects don't make sense any more. --- src/citrine/informatics/modules.py | 33 ------------------------------ src/citrine/jobs/waiting.py | 13 ++++++------ src/citrine/resources/module.py | 33 ------------------------------ tests/resources/test_module.py | 18 ---------------- 4 files changed, 6 insertions(+), 91 deletions(-) delete mode 100644 src/citrine/informatics/modules.py delete mode 100644 src/citrine/resources/module.py delete mode 100644 tests/resources/test_module.py diff --git a/src/citrine/informatics/modules.py b/src/citrine/informatics/modules.py deleted file mode 100644 index 8b798e17a..000000000 --- a/src/citrine/informatics/modules.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tools for working with module resources.""" -from typing import Type, Optional -from uuid import UUID - -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable -from citrine._session import Session -from citrine._rest.asynchronous_object import AsynchronousObject - -__all__ = ['Module'] - - -class Module(PolymorphicSerializable['Module'], AsynchronousObject): - """A Citrine Module is a reusable computational tool used to construct a workflow. - - Abstract type that returns the proper type given a serialized dict. - - All modules must inherit AIResourceMetadata, and hence have a ``status`` field. - Possible statuses are CREATED, VALIDATING, INVALID, ERROR, and READY. - - """ - - _response_key = None - _project_id: Optional[UUID] = None - _session: Optional[Session] = None - _in_progress_statuses = ["VALIDATING", "CREATED"] - _succeeded_statuses = ["READY"] - _failed_statuses = ["INVALID", "ERROR"] - - @classmethod - def get_type(cls, data) -> Type['Module']: - """Return the subtype.""" - from citrine.informatics.design_spaces import DesignSpace - return DesignSpace diff --git a/src/citrine/jobs/waiting.py b/src/citrine/jobs/waiting.py index e05ed0f32..62207947a 100644 --- a/src/citrine/jobs/waiting.py +++ b/src/citrine/jobs/waiting.py @@ -8,7 +8,6 @@ from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution from citrine.informatics.executions import PredictorEvaluationExecution -from citrine.informatics.modules import Module class ConditionTimeoutError(RuntimeError): @@ -90,20 +89,20 @@ def is_finished(): def wait_while_validating( *, - collection: Collection[Module], - module: Module, + collection: Collection[AsynchronousObject], + module: AsynchronousObject, print_status_info: bool = False, timeout: float = 1800.0, interval: float = 3.0, -) -> Module: +) -> AsynchronousObject: """ Wait until module is validated. Parameters ---------- - collection : Collection[Module] + collection : Collection[AsynchronousObject,] Collection containing module - module : Module + module : AsynchronousObject, Module in Collection print_status_info : bool, optional Whether to print status info, by default False @@ -114,7 +113,7 @@ def wait_while_validating( Returns ------- - Module + AsynchronousObject Module in Collection after validation Raises diff --git a/src/citrine/resources/module.py b/src/citrine/resources/module.py deleted file mode 100644 index 22c47962e..000000000 --- a/src/citrine/resources/module.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Resources that represent collections of modules.""" -from uuid import UUID - -from citrine._rest.collection import Collection -from citrine._session import Session -from citrine.informatics.modules import Module - - -class ModuleCollection(Collection[Module]): - """Represents the collection of all modules as well as the resources belonging to it. - - Parameters - ---------- - project_id: UUID - the UUID of the project - - """ - - _path_template = '/projects/{project_id}/design-spaces' - _api_version = 'v3' - _individual_key = None - _resource = Module - _collection_key = 'response' - - def __init__(self, project_id: UUID, session: Session): - self.project_id = project_id - self.session: Session = session - - def build(self, data: dict) -> Module: - """Build an individual module.""" - module = Module.build(data) - module.session = self.session - return module diff --git a/tests/resources/test_module.py b/tests/resources/test_module.py deleted file mode 100644 index 7c8543252..000000000 --- a/tests/resources/test_module.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Tests predictor collection""" -import mock -import uuid - -from citrine.resources.module import ModuleCollection -from citrine.informatics.design_spaces import ProductDesignSpace - - -def test_build(valid_product_design_space_data): - session = mock.Mock() - session.get_resource.return_value = { - 'id': str(uuid.uuid4()), - 'status': 'VALID', - 'report': {} - } - collection = ModuleCollection(uuid.uuid4(), session) - module = collection.build(valid_product_design_space_data) - assert type(module) == ProductDesignSpace From d1fd44f30ffb717b1d8bec0effbd9ec6146b4898 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 21 Dec 2023 08:00:31 -0500 Subject: [PATCH 05/22] [PLA-13742] Drop python 3.7 support. It hit EOL in mid-2023. --- .travis.yml | 1 - CONTRIBUTING.md | 4 ++-- setup.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56a0ea571..a490b3fb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python dist: bionic python: - - '3.7' - '3.8' - '3.9' - '3.10' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2dcddc547..f3b54ae9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,11 +21,11 @@ Clone from github: git clone git@github.com:CitrineInformatics/citrine-python ``` -Create a virtual environment using Python >= 3.7. +Create a virtual environment using Python >= 3.8. One option is to use conda, but it is not required. ```bash -conda create -n python=3.7 +conda create -n python=3.8 conda activate ``` diff --git a/setup.py b/setup.py index 8e3336c55..f4cdf8aaf 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup(name='citrine', # Update this in src/citrine/__version__.py version=about['__version__'], - python_requires='>=3.7', + python_requires='>=3.8', url='http://github.com/CitrineInformatics/citrine-python', description='Python library for the Citrine Platform', long_description=long_description, @@ -47,7 +47,6 @@ }, classifiers=[ 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', From 1c54ff36284ddf2bd31c0ebdaf88a967559fcab7 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 21 Dec 2023 15:16:37 -0500 Subject: [PATCH 06/22] Bump to 3.0 --- src/citrine/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index 5b733e58e..528787cfc 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "2.42.2" +__version__ = "3.0.0" From ca117ef7063881b179da6d35f20047926c12dfd0 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 21 Dec 2023 15:33:25 -0500 Subject: [PATCH 07/22] [PLA-13767] Remove all deprecated code. Cleans out all deprecated code, be it through the deprecated package, a DeprecationWarning, or any other form. --- docs/source/workflows/predictors.rst | 121 +------ src/citrine/_rest/ai_resource_metadata.py | 10 - src/citrine/_rest/engine_resource.py | 37 +-- src/citrine/_utils/functions.py | 11 - .../ingredient_ratio_constraint.py | 15 +- src/citrine/informatics/data_sources.py | 15 +- src/citrine/informatics/descriptors.py | 11 - .../design_spaces/data_source_design_space.py | 4 +- .../informatics/design_spaces/design_space.py | 14 - .../design_spaces/enumerated_design_space.py | 4 +- .../design_spaces/formulation_design_space.py | 4 +- .../hierarchical_design_space.py | 25 +- .../design_spaces/product_design_space.py | 4 +- .../informatics/executions/execution.py | 11 +- src/citrine/informatics/modules.py | 33 -- .../predictors/auto_ml_predictor.py | 3 - .../informatics/predictors/graph_predictor.py | 19 +- .../ingredients_to_formulation_predictor.py | 14 +- .../predictors/mean_property_predictor.py | 3 - src/citrine/informatics/predictors/node.py | 157 +-------- .../predictors/simple_mixture_predictor.py | 24 +- .../informatics/workflows/design_workflow.py | 20 +- src/citrine/jobs/waiting.py | 13 +- src/citrine/resources/branch.py | 192 +++-------- src/citrine/resources/descriptors.py | 8 - src/citrine/resources/design_execution.py | 15 - src/citrine/resources/design_space.py | 35 +- src/citrine/resources/design_workflow.py | 71 ++--- .../resources/experiment_datasource.py | 11 - src/citrine/resources/file_link.py | 186 +---------- src/citrine/resources/job.py | 45 --- src/citrine/resources/module.py | 33 -- src/citrine/resources/predictor.py | 70 ---- .../predictor_evaluation_execution.py | 14 +- src/citrine/resources/project.py | 9 - .../sample_design_space_execution.py | 3 - tests/_util/test_functions.py | 27 +- tests/informatics/test_data_source.py | 9 - tests/informatics/test_descriptors.py | 6 - tests/informatics/test_design_spaces.py | 19 -- tests/informatics/test_predictors.py | 92 ------ tests/resources/test_branch.py | 301 +----------------- tests/resources/test_descriptors.py | 42 --- tests/resources/test_design_executions.py | 8 - tests/resources/test_design_space.py | 129 -------- tests/resources/test_design_workflows.py | 99 ------ tests/resources/test_experiment_datasource.py | 25 -- tests/resources/test_file_link.py | 85 +---- .../test_generative_design_execution.py | 2 - tests/resources/test_job_client.py | 102 ------ tests/resources/test_module.py | 18 -- tests/resources/test_predictor.py | 125 -------- .../test_predictor_evaluation_executions.py | 8 - tests/resources/test_project.py | 5 - tests/resources/test_workflow.py | 11 - tests/utils/fakes/fake_workflow_collection.py | 5 +- 56 files changed, 107 insertions(+), 2275 deletions(-) delete mode 100644 src/citrine/informatics/modules.py delete mode 100644 src/citrine/resources/job.py delete mode 100644 src/citrine/resources/module.py delete mode 100644 tests/resources/test_job_client.py delete mode 100644 tests/resources/test_module.py diff --git a/docs/source/workflows/predictors.rst b/docs/source/workflows/predictors.rst index 67df62014..28675dcde 100644 --- a/docs/source/workflows/predictors.rst +++ b/docs/source/workflows/predictors.rst @@ -28,7 +28,7 @@ Each AutoMLPredictor is defined by a set of inputs and outputs. Inputs are used as input features to the machine learning model. The outputs are the properties that you would like the model to predict. Currently, only one output per AutoML predictor is supported, and there must be at least one input. -Unlike the `SimpleMLPredictor <#simple-ml-predictor>`__, only one model is trained from inputs to the outputs. +Only one model is trained from inputs to the outputs. AutoMLPredictors support both regression and classification. For each :class:`~citrine.informatics.descriptors.RealDescriptor` output, regression is performed. @@ -688,125 +688,6 @@ This predictor will compute 2 responses, ``solute share in formulation`` and ``s ) -Simple ML predictor -------------------- - -.. Warning:: - Simple ML Predictors are not supported on the Web UI and will be deprecated as of v2.0. - For analogous behavior use the build_simple_ml method to construct a Graph Predictor. - Use the convert_and_update (`source `_). - method to convert existing Simple ML Predictors to Graph Predictors. - - -The :class:`~citrine.informatics.predictors.simple_ml_predictor.SimpleMLPredictor` predicts material properties using a machine-learned model. -Each predictor is defined by a set of inputs, outputs, and latent variables. -Inputs are used as input features to the machine learning model. -Outputs are the properties that you would like the model to predict. -There must be at least one input and one output. -Latent variables are properties that you would like the model to predict and you think could also be useful in predicting the outputs. -If defined, latent variables are used to build hierarchical models. -One model is trained from inputs to latent variables, and another is trained from inputs and latent variables to outputs. -Thus, all inputs and latent variables are used to predict outputs. - -Models are trained using data provided by a :class:`~citrine.informatics.data_sources.DataSource` specified when creating a predictor. - -The following example demonstrates how to use the python SDK to create a :class:`~citrine.informatics.predictors.simple_ml_predictor.SimpleMLPredictor`, register the predictor to a project and wait for validation: - -.. code:: python - - from citrine import Citrine - from citrine.seeding.find_or_create import (find_or_create_project, - create_or_update - ) - from citrine.jobs.waiting import wait_while_validating - from citrine.informatics.predictors import SimpleMLPredictor - from citrine.informatics.data_sources import GemTableDataSource - - # create a session with citrine using your API key - session = Citrine(api_key=API_KEY) - - # find project by name 'Example project' or create it if not found - project = find_or_create_project(project_collection=session.projects, - project_name='Example project' - ) - - # create SimpleMLPredictor (assumes descriptors for - # inputs/outputs/latent variables have already been created) - simple_ml_predictor = SimpleMLPredictor( - name = 'Predictor name', - description = 'Predictor description', - inputs = [input_descriptor_1, input_descriptor_2], - outputs = [output_descriptor_1, output_descriptor_2], - latent_variables = [latent_variable_descriptor_1], - training_data = [GemTableDataSource(table_id=training_data_table_uid, table_version=1)] - ) - - # register predictor or update predictor of same name if it already - # exists in the project. - predictor = create_or_update(collection=project.predictors, - resource=simple_ml_predictor - ) - - # wait while the predictor is validating and print status information - # while waiting. - predictor = wait_while_validating(collection=project.predictors, - module=predictor, - print_status_info=True - ) - -Often, a :class:`~citrine.informatics.predictors.simple_ml_predictor.SimpleMLPredictor` will include outputs from other predictors as inputs to its model. -Instead of entering these manually, outputs from a predictor can be retrieved programmatically using ``outputs = project.descriptors.from_predictor_responses(predictor, inputs)``, where ``outputs`` is the list of descriptors returned by the ``predictor`` given a list of descriptors as ``inputs``. - -The following demonstrates how to create an :class:`~citrine.informatics.predictors.ingredient_fractions_predictor.IngredientFractionsPredictor` and use its outputs as inputs to a :class:`~citrine.informatics.predictors.simple_ml_predictor.SimpleMLPredictor`. - -.. code:: python - - from citrine import Citrine - from citrine.seeding.find_or_create import find_or_create_project - from citrine.informatics.predictors import SimpleMLPredictor - from citrine.informatics.data_sources import GemTableDataSource - from citrine.informatics.predictors import IngredientFractionsPredictor - from citrine.informatics.descriptors import FormulationDescriptor - - # create a session with citrine using your API key - session = Citrine(api_key=API_KEY) - - # find project by name 'Example project' or create it if not found - project = find_or_create_project(project_collection=session.projects, - project_name='Example project' - ) - - # create a descriptor to store formulations - formulation_descriptor = FormulationDescriptor.flat() - - # create a predictor that computes ingredient fractions - ingredient_fractions = IngredientFractionsPredictor( - name = 'Ingredient Fractions Predictor', - description = 'Computes fractions of provided ingredients', - input_descriptor = formulation_descriptor, - ingredients = ['water', 'salt', 'boric acid'] - ) - - # get the descriptors the ingredient fractions predictor returns given the formulation descriptor - ingredient_fraction_descriptors = project.descriptors.from_predictor_responses( - predictor=ingredient_fractions, - inputs=[formulation_descriptor] - ) - # ^^ in this case, ingredient_fraction_descriptors will contain 3 real descriptors: one for each featurized ingredient - - simple_ml_predictor = SimpleMLPredictor( - name = 'Predictor name', - description = 'Predictor description', - inputs = ingredient_fraction_descriptors, - outputs = [output_descriptor], - latent_variables = [], - training_data = GemTableDataSource( - table_id=training_data_table_uid, - table_version=1 - ) - ) - - Predictor reports ----------------- diff --git a/src/citrine/_rest/ai_resource_metadata.py b/src/citrine/_rest/ai_resource_metadata.py index 627a4864b..4b3175ed7 100644 --- a/src/citrine/_rest/ai_resource_metadata.py +++ b/src/citrine/_rest/ai_resource_metadata.py @@ -1,10 +1,6 @@ -from typing import List - from citrine.resources.status_detail import StatusDetail from citrine._serialization import properties -from deprecation import deprecated - class AIResourceMetadata(): """Abstract class for representing common metadata for Resources.""" @@ -34,9 +30,3 @@ class AIResourceMetadata(): status_detail = properties.List(properties.Object(StatusDetail), 'status_detail', default=[], serializable=False) """:List[StatusDetail]: a list of structured status info, containing the message and level""" - - @property - @deprecated(deprecated_in="2.2.0", removed_in="3.0.0", details="Use status_detail instead.") - def status_info(self) -> List[str]: - """:List[str]: human-readable explanations of the status.""" - return [detail.msg for detail in self.status_detail] diff --git a/src/citrine/_rest/engine_resource.py b/src/citrine/_rest/engine_resource.py index 2d5deeec0..09c956ae9 100644 --- a/src/citrine/_rest/engine_resource.py +++ b/src/citrine/_rest/engine_resource.py @@ -1,12 +1,10 @@ -from typing import List, TypeVar +from typing import TypeVar from citrine._rest.resource import Resource, ResourceTypeEnum from citrine._serialization import properties from citrine._serialization.include_parent_properties import IncludeParentProperties from citrine.resources.status_detail import StatusDetail -from deprecation import deprecated - Self = TypeVar('Self', bound='Resource') @@ -75,36 +73,3 @@ class VersionedEngineResource(EngineResource[Self], IncludeParentProperties[Self def build(cls, data: dict): """Build an instance of this object from given data.""" return super().build_with_parent(data, __class__) - - -class ModuleEngineResource(EngineResource[Self], IncludeParentProperties[Self]): - """Base resource for metadata from stand-alone AI Engine modules with deprecated fields.""" - - # Due to the way object construction is done at present, __init__ is not executed on Resource - # objects, so initializing _archived doesn't work. - _archived = properties.Optional(properties.Boolean(), '', default=None, serializable=False, - deserializable=False) - - @property - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", - details="Please use the 'is_archived' property instead.'") - def archived(self): - """[DEPRECATED] whether the design space is archived.""" - return self.is_archived - - @archived.setter - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", - details="Please use archive() and restore() on DesignSpaceCollection instead.") - def archived(self, value): - self._archived = value - - @property - @deprecated(deprecated_in="2.2.0", removed_in="3.0.0", details="Use status_detail instead.") - def status_info(self) -> List[str]: - """:List[str]: human-readable explanations of the status.""" - return [detail.msg for detail in self.status_detail] - - @classmethod - def build(cls, data: dict): - """Build an instance of this object from given data.""" - return super().build_with_parent(data, __class__) diff --git a/src/citrine/_utils/functions.py b/src/citrine/_utils/functions.py index 03af2684c..e22826cf9 100644 --- a/src/citrine/_utils/functions.py +++ b/src/citrine/_utils/functions.py @@ -1,6 +1,4 @@ from abc import ABCMeta -from deprecation import deprecated -import inspect import os from pathlib import Path from typing import Any, Dict, Optional, Sequence, Union @@ -223,15 +221,6 @@ class _CustomMeta(MigratedClassMeta, type(target)): return _CustomMeta -@deprecated(deprecated_in="2.22.1", removed_in="3.0.0", - details="Use MigratedClassMeta to explicitly deprecate migrated classes.") -def shadow_classes_in_module(source_module, target_module): - """Shadow classes from a source to a target module, for backwards compatibility purposes.""" - for c in [cls for _, cls in inspect.getmembers(source_module, inspect.isclass) if - cls.__module__ == source_module.__name__]: - setattr(target_module, c.__qualname__, c) - - def migrate_deprecated_argument( new_arg: Optional[Any], new_arg_name: str, diff --git a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py index 0f7b0163c..d86dcd7f3 100644 --- a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py @@ -102,14 +102,8 @@ def basis_ingredients(self) -> Mapping[str, float]: return self._basis_ingredients @basis_ingredients.setter - def basis_ingredients(self, value: Union[Set[str], Mapping[str, float]]): + def basis_ingredients(self, value: Set[str]): """Set the ingredients in the denominator of the ratio.""" - if isinstance(value, dict): - warnings.warn("As of version 2.13.0, multipliers for all basis ingredients are " - "ignored, so basis_ingredients should be a list of ingredient names.", - DeprecationWarning) - value = set(value.keys()) - self.basis_ingredient_names = value @property @@ -131,13 +125,8 @@ def basis_labels(self) -> Mapping[str, float]: return self._basis_labels @basis_labels.setter - def basis_labels(self, value: Union[Set[str], Mapping[str, float]]): + def basis_labels(self, value: Set[str]): """Set the labels in the denominator of the ratio.""" - if isinstance(value, dict): - warnings.warn("As of version 2.13.0, multipliers for all basis labels are ignored, so " - "basis_labels should be a list of label names.", DeprecationWarning) - value = set(value.keys()) - self.basis_label_names = value @property diff --git a/src/citrine/informatics/data_sources.py b/src/citrine/informatics/data_sources.py index a96282df1..39b5d4ecf 100644 --- a/src/citrine/informatics/data_sources.py +++ b/src/citrine/informatics/data_sources.py @@ -1,5 +1,4 @@ """Tools for working with Descriptors.""" -import warnings from abc import abstractmethod from typing import Type, List, Mapping, Optional, Union from uuid import UUID @@ -7,7 +6,7 @@ from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable -from citrine.informatics.descriptors import Descriptor, FormulationDescriptor +from citrine.informatics.descriptors import Descriptor from citrine.resources.file_link import FileLink __all__ = ['DataSource', @@ -109,20 +108,10 @@ def _attrs(self) -> List[str]: def __init__(self, *, table_id: UUID, - table_version: Union[int, str], - formulation_descriptor: Optional[FormulationDescriptor] = None): + table_version: Union[int, str]): self.table_id: UUID = table_id self.table_version: Union[int, str] = table_version - if formulation_descriptor is not None: - warnings.warn( - "The field `formulation_descriptor` on a GemTableDataSource is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - "FormulationDescriptor with key 'Formulation' for tables containing formulations.", - DeprecationWarning - ) - self.formulation_descriptor = None - class ExperimentDataSourceRef(Serializable['ExperimentDataSourceRef'], DataSource): """A reference to a data source based on an experiment result hosted on the data platform. diff --git a/src/citrine/informatics/descriptors.py b/src/citrine/informatics/descriptors.py index a509a6d69..692dfe91e 100644 --- a/src/citrine/informatics/descriptors.py +++ b/src/citrine/informatics/descriptors.py @@ -1,7 +1,6 @@ """Tools for working with Descriptors.""" from typing import Type, Set, Union -from deprecation import deprecated from gemd.enumeration.base_enumeration import BaseEnumeration from citrine._serialization.serializable import Serializable @@ -154,16 +153,6 @@ def __str__(self): def __repr__(self): return "IntegerDescriptor({}, {}, {})".format(self.key, self.lower_bound, self.upper_bound) - @property - @deprecated( - deprecated_in="2.27.0", - removed_in="3.0.0", - details="Integer descriptors are always dimensionless." - ) - def units(self) -> str: - """Return 'dimensionless' for the units of an integer descriptor.""" - return "dimensionless" - class ChemicalFormulaDescriptor(Serializable['ChemicalFormulaDescriptor'], Descriptor): """Captures domain-specific context about a stoichiometric chemical formula. diff --git a/src/citrine/informatics/design_spaces/data_source_design_space.py b/src/citrine/informatics/design_spaces/data_source_design_space.py index c87d3cb4a..dc8ef4409 100644 --- a/src/citrine/informatics/design_spaces/data_source_design_space.py +++ b/src/citrine/informatics/design_spaces/data_source_design_space.py @@ -1,4 +1,4 @@ -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.data_sources import DataSource from citrine.informatics.design_spaces.design_space import DesignSpace @@ -6,7 +6,7 @@ __all__ = ['DataSourceDesignSpace'] -class DataSourceDesignSpace(ModuleEngineResource['DataSourceDesignSpace'], DesignSpace): +class DataSourceDesignSpace(EngineResource['DataSourceDesignSpace'], DesignSpace): """An enumeration of candidates stored in a data source. Parameters diff --git a/src/citrine/informatics/design_spaces/design_space.py b/src/citrine/informatics/design_spaces/design_space.py index 4b09c387d..77c17dfb5 100644 --- a/src/citrine/informatics/design_spaces/design_space.py +++ b/src/citrine/informatics/design_spaces/design_space.py @@ -10,8 +10,6 @@ from citrine.resources.sample_design_space_execution import \ SampleDesignSpaceExecutionCollection -from deprecation import deprecated - __all__ = ['DesignSpace'] @@ -23,18 +21,6 @@ class DesignSpace(PolymorphicSerializable['DesignSpace'], AsynchronousObject): """ - @property - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", - details="Please use `isinstance` or `issubclass` instead.") - def module_type(self): - """The type of module.""" - return "DESIGN_SPACE" - - @module_type.setter - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0") - def module_type(self, value): - pass - uid = properties.Optional(properties.UUID, 'id', serializable=False) """:Optional[UUID]: Citrine Platform unique identifier""" name = properties.String('data.name') diff --git a/src/citrine/informatics/design_spaces/enumerated_design_space.py b/src/citrine/informatics/design_spaces/enumerated_design_space.py index 891e57a3a..8d1043216 100644 --- a/src/citrine/informatics/design_spaces/enumerated_design_space.py +++ b/src/citrine/informatics/design_spaces/enumerated_design_space.py @@ -1,6 +1,6 @@ from typing import List, Mapping, Any -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.descriptors import Descriptor from citrine.informatics.design_spaces.design_space import DesignSpace @@ -8,7 +8,7 @@ __all__ = ['EnumeratedDesignSpace'] -class EnumeratedDesignSpace(ModuleEngineResource['EnumeratedDesignSpace'], DesignSpace): +class EnumeratedDesignSpace(EngineResource['EnumeratedDesignSpace'], DesignSpace): """An explicit enumeration of candidate materials to score. Enumerated design spaces are intended to capture small spaces with fewer than diff --git a/src/citrine/informatics/design_spaces/formulation_design_space.py b/src/citrine/informatics/design_spaces/formulation_design_space.py index 4e8df7761..a77e65b2d 100644 --- a/src/citrine/informatics/design_spaces/formulation_design_space.py +++ b/src/citrine/informatics/design_spaces/formulation_design_space.py @@ -1,6 +1,6 @@ from typing import Mapping, Optional, Set -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.constraints import Constraint from citrine.informatics.descriptors import FormulationDescriptor @@ -9,7 +9,7 @@ __all__ = ['FormulationDesignSpace'] -class FormulationDesignSpace(ModuleEngineResource['FormulationDesignSpace'], DesignSpace): +class FormulationDesignSpace(EngineResource['FormulationDesignSpace'], DesignSpace): """Design space composed of mixtures of ingredients. Parameters diff --git a/src/citrine/informatics/design_spaces/hierarchical_design_space.py b/src/citrine/informatics/design_spaces/hierarchical_design_space.py index f8518218c..457a35227 100644 --- a/src/citrine/informatics/design_spaces/hierarchical_design_space.py +++ b/src/citrine/informatics/design_spaces/hierarchical_design_space.py @@ -1,10 +1,7 @@ -import warnings from typing import Optional, List from uuid import UUID -from deprecation import deprecated - -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine._serialization.serializable import Serializable from citrine.informatics.data_sources import DataSource @@ -46,29 +43,13 @@ def __init__( material_template: UUID, process_template: UUID, material_template_name: Optional[str] = None, - process_template_name: Optional[str] = None, - template_name: Optional[str] = None + process_template_name: Optional[str] = None ): self.material_template: UUID = material_template self.process_template: UUID = process_template self.material_template_name: Optional[str] = material_template_name self.process_template_name: Optional[str] = process_template_name - if template_name is not None: - warnings.warn( - "The field 'template_name' has been deprecated in v2.36.0 and will be removed " - "in v3.0.0. Please use the field 'material_template_name' instead.", - DeprecationWarning - ) - - @property - @deprecated( - deprecated_in="2.36.0", removed_in="3.0.0", details="Use material_template_name instead." - ) - def template_name(self) -> str: - """Return the name of the material template.""" - return self.material_template_name - class MaterialNodeDefinition(Serializable["MaterialNodeDefinition"]): """A single node in a material history design space. @@ -126,7 +107,7 @@ def __repr__(self): return f"" -class HierarchicalDesignSpace(ModuleEngineResource["HierarchicalDesignSpace"], DesignSpace): +class HierarchicalDesignSpace(EngineResource["HierarchicalDesignSpace"], DesignSpace): """A design space that produces hierarchical candidates representing a material history. A hierarchical design space always contains a root node that defines the diff --git a/src/citrine/informatics/design_spaces/product_design_space.py b/src/citrine/informatics/design_spaces/product_design_space.py index 13e7f60f9..bd7ca704d 100644 --- a/src/citrine/informatics/design_spaces/product_design_space.py +++ b/src/citrine/informatics/design_spaces/product_design_space.py @@ -1,7 +1,7 @@ from typing import List, Union, Optional from uuid import UUID -from citrine._rest.engine_resource import ModuleEngineResource +from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties from citrine.informatics.design_spaces.design_space import DesignSpace from citrine.informatics.dimensions import Dimension @@ -9,7 +9,7 @@ __all__ = ['ProductDesignSpace'] -class ProductDesignSpace(ModuleEngineResource['ProductDesignSpace'], DesignSpace): +class ProductDesignSpace(EngineResource['ProductDesignSpace'], DesignSpace): """A Cartesian product of design spaces. Factors can be other design spaces and/or univariate dimensions. diff --git a/src/citrine/informatics/executions/execution.py b/src/citrine/informatics/executions/execution.py index 079617667..ea873f9eb 100644 --- a/src/citrine/informatics/executions/execution.py +++ b/src/citrine/informatics/executions/execution.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import List, Optional +from typing import Optional from uuid import UUID from citrine._rest.asynchronous_object import AsynchronousObject @@ -10,9 +10,6 @@ from citrine.resources.status_detail import StatusDetail -from deprecation import deprecated - - class Execution(Pageable, AsynchronousObject, ABC): """A base class for execution resources. @@ -49,12 +46,6 @@ class Execution(Pageable, AsynchronousObject, ABC): """:Optional[datetime]: date and time at which the resource was most recently updated, if it has been updated""" - @property - @deprecated(deprecated_in="2.2.0", removed_in="3.0.0", details="Use status_detail instead.") - def status_info(self) -> List[str]: - """:List[str]: human-readable explanations of the status.""" - return [detail.msg for detail in self.status_detail] - def __str__(self): return f'<{self.__class__.__name__} {str(self.uid)!r}>' diff --git a/src/citrine/informatics/modules.py b/src/citrine/informatics/modules.py deleted file mode 100644 index 8b798e17a..000000000 --- a/src/citrine/informatics/modules.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tools for working with module resources.""" -from typing import Type, Optional -from uuid import UUID - -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable -from citrine._session import Session -from citrine._rest.asynchronous_object import AsynchronousObject - -__all__ = ['Module'] - - -class Module(PolymorphicSerializable['Module'], AsynchronousObject): - """A Citrine Module is a reusable computational tool used to construct a workflow. - - Abstract type that returns the proper type given a serialized dict. - - All modules must inherit AIResourceMetadata, and hence have a ``status`` field. - Possible statuses are CREATED, VALIDATING, INVALID, ERROR, and READY. - - """ - - _response_key = None - _project_id: Optional[UUID] = None - _session: Optional[Session] = None - _in_progress_statuses = ["VALIDATING", "CREATED"] - _succeeded_statuses = ["READY"] - _failed_statuses = ["INVALID", "ERROR"] - - @classmethod - def get_type(cls, data) -> Type['Module']: - """Return the subtype.""" - from citrine.informatics.design_spaces import DesignSpace - return DesignSpace diff --git a/src/citrine/informatics/predictors/auto_ml_predictor.py b/src/citrine/informatics/predictors/auto_ml_predictor.py index 81b80ad2f..c636e70e1 100644 --- a/src/citrine/informatics/predictors/auto_ml_predictor.py +++ b/src/citrine/informatics/predictors/auto_ml_predictor.py @@ -7,7 +7,6 @@ from citrine.informatics.data_sources import DataSource from citrine.informatics.descriptors import Descriptor from citrine.informatics.predictors import PredictorNode -from citrine.informatics.predictors.node import _check_deprecated_training_data __all__ = ['AutoMLPredictor', 'AutoMLEstimator'] @@ -91,8 +90,6 @@ def __init__(self, self.inputs: List[Descriptor] = inputs self.estimators: Set[AutoMLEstimator] = estimators or {AutoMLEstimator.RANDOM_FOREST} self.outputs = outputs - - _check_deprecated_training_data(training_data) self.training_data: List[DataSource] = training_data or [] def __str__(self): diff --git a/src/citrine/informatics/predictors/graph_predictor.py b/src/citrine/informatics/predictors/graph_predictor.py index 2577bd53e..2e2cebdca 100644 --- a/src/citrine/informatics/predictors/graph_predictor.py +++ b/src/citrine/informatics/predictors/graph_predictor.py @@ -1,5 +1,4 @@ -import warnings -from typing import List, Optional, Union +from typing import List, Optional from uuid import UUID from citrine._rest.asynchronous_object import AsynchronousObject @@ -72,24 +71,12 @@ def __init__(self, name: str, *, description: str, - predictors: List[Union[UUID, PredictorNode]], + predictors: List[PredictorNode], training_data: Optional[List[DataSource]] = None): self.name: str = name self.description: str = description self.training_data: List[DataSource] = training_data or [] - - uid_predictors = [x for x in predictors if isinstance(x, UUID)] - if len(uid_predictors) > 0: - warnings.warn( - "Referencing predictors by a UUID inside a GraphPredictor is no longer supported " - "on the Citrine Platform. Please remove all references to predictors " - "and add only PredictorNode objects to the `predictors` field.", - DeprecationWarning - ) - - self.predictors: List[PredictorNode] = [ - x for x in predictors if isinstance(x, PredictorNode) - ] + self.predictors: List[PredictorNode] = predictors def __str__(self): return ''.format(self.name) diff --git a/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py b/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py index 5d3d9585e..e068ee3b7 100644 --- a/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py +++ b/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py @@ -1,9 +1,8 @@ -import warnings -from typing import Set, Mapping, Optional +from typing import Set, Mapping from citrine._rest.resource import Resource from citrine._serialization import properties -from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor, FormulationKey +from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor from citrine.informatics.predictors import PredictorNode __all__ = ['IngredientsToFormulationPredictor'] @@ -40,7 +39,6 @@ def __init__(self, name: str, *, description: str, - output: Optional[FormulationDescriptor] = None, id_to_quantity: Mapping[str, RealDescriptor], labels: Mapping[str, Set[str]]): self.name: str = name @@ -48,14 +46,6 @@ def __init__(self, self.id_to_quantity: Mapping[str, RealDescriptor] = id_to_quantity self.labels: Mapping[str, Set[str]] = labels - if output is not None: - warnings.warn( - "The field `output` on an IngredientsToFormulationPredictor is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - f"FormulationDescriptor with key '{FormulationKey.HIERARCHICAL.value}' as output.", - DeprecationWarning - ) - def __str__(self): return ''.format(self.name) diff --git a/src/citrine/informatics/predictors/mean_property_predictor.py b/src/citrine/informatics/predictors/mean_property_predictor.py index f60fe0858..69c568c73 100644 --- a/src/citrine/informatics/predictors/mean_property_predictor.py +++ b/src/citrine/informatics/predictors/mean_property_predictor.py @@ -7,7 +7,6 @@ FormulationDescriptor, RealDescriptor, CategoricalDescriptor ) from citrine.informatics.predictors import PredictorNode -from citrine.informatics.predictors.node import _check_deprecated_training_data __all__ = ['MeanPropertyPredictor'] @@ -105,8 +104,6 @@ def __init__(self, self.impute_properties: bool = impute_properties self.label: Optional[str] = label self.default_properties: Optional[Mapping[str, Union[str, float]]] = default_properties - - _check_deprecated_training_data(training_data) self.training_data: List[DataSource] = training_data or [] def __str__(self): diff --git a/src/citrine/informatics/predictors/node.py b/src/citrine/informatics/predictors/node.py index 2415bbec4..637b43400 100644 --- a/src/citrine/informatics/predictors/node.py +++ b/src/citrine/informatics/predictors/node.py @@ -1,26 +1,8 @@ -import warnings -from datetime import datetime -from typing import Type, Optional, List -from uuid import UUID - -from deprecation import deprecated +from typing import Type from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable -from citrine.informatics.data_sources import DataSource from citrine.informatics.predictors import Predictor -from citrine.resources.status_detail import StatusDetail - - -def _check_deprecated_training_data(training_data: Optional[List[DataSource]]) -> None: - if training_data is not None: - warnings.warn( - f"The field `training_data` on single predictor nodes is deprecated " - "and will be removed in version 3.0.0. Include training data for all " - "sub-predictors on the parent GraphPredictor. Existing training data " - "on this predictor will be moved to the parent GraphPredictor upon registration.", - DeprecationWarning - ) class PredictorNode(PolymorphicSerializable["PredictorNode"], Predictor): @@ -65,140 +47,3 @@ def get_type(cls, data) -> Type['PredictorNode']: '{} is not a valid predictor node type. ' 'Must be in {}.'.format(data['type'], type_dict.keys()) ) - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `uid` on parent GraphPredictor." - ) - def uid(self) -> Optional[UUID]: - """Optional Citrine Platform unique identifier.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `version` on parent GraphPredictor." - ) - def version(self) -> Optional[int]: - """The version number of the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `draft` on parent GraphPredictor." - ) - def draft(self) -> Optional[bool]: - """The draft status of the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `created_by` on parent GraphPredictor." - ) - def created_by(self) -> Optional[UUID]: - """The id of the user who created the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `updated_by` on parent GraphPredictor." - ) - def updated_by(self) -> Optional[UUID]: - """The id of the user who most recently updated the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `archived_by` on parent GraphPredictor." - ) - def archived_by(self) -> Optional[UUID]: - """The id of the user who most recently archived the resource.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `create_time` on parent GraphPredictor." - ) - def create_time(self) -> Optional[datetime]: - """The date and time at which the resource was created.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `update_time` on parent GraphPredictor." - ) - def update_time(self) -> Optional[datetime]: - """The date and time at which the resource was most recently updated.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Use `archive_time` on parent GraphPredictor." - ) - def archive_time(self) -> Optional[datetime]: - """The date and time at which the resource was archived.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `status` on parent GraphPredictor." - ) - def status(self) -> Optional[str]: - """Short description of the resource's status.""" - return None - - @property - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `status_detail` on parent GraphPredictor." - ) - def status_detail(self) -> List[StatusDetail]: - """A list of structured status info, containing the message and level.""" - return [] - - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `in_progress` on parent GraphPredictor." - ) - def in_progress(self) -> bool: - """Whether the backend process is in progress.""" - return False - - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `succeeded` on parent GraphPredictor." - ) - def succeeded(self) -> bool: - """Whether the backend process has completed successfully.""" - return False - - @deprecated( - deprecated_in="2.13.0", - removed_in="3.0.0", - details="Check `failed` on parent GraphPredictor." - ) - def failed(self) -> bool: - """Whether the backend process has completed unsuccessfully.""" - return False diff --git a/src/citrine/informatics/predictors/simple_mixture_predictor.py b/src/citrine/informatics/predictors/simple_mixture_predictor.py index 1f59be38c..5f803abe5 100644 --- a/src/citrine/informatics/predictors/simple_mixture_predictor.py +++ b/src/citrine/informatics/predictors/simple_mixture_predictor.py @@ -1,12 +1,10 @@ -import warnings from typing import List, Optional from citrine._rest.resource import Resource from citrine._serialization import properties from citrine.informatics.data_sources import DataSource -from citrine.informatics.descriptors import FormulationDescriptor, FormulationKey +from citrine.informatics.descriptors import FormulationDescriptor from citrine.informatics.predictors import PredictorNode -from citrine.informatics.predictors.node import _check_deprecated_training_data __all__ = ['SimpleMixturePredictor'] @@ -38,31 +36,11 @@ def __init__(self, name: str, *, description: str, - input_descriptor: Optional[FormulationDescriptor] = None, - output_descriptor: Optional[FormulationDescriptor] = None, training_data: Optional[List[DataSource]] = None): self.name: str = name self.description: str = description - - _check_deprecated_training_data(training_data) self.training_data: List[DataSource] = training_data or [] - if input_descriptor is not None: - warnings.warn( - "The field `input_descriptor` on a SimpleMixturePredictor is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - f"FormulationDescriptor with key '{FormulationKey.HIERARCHICAL.value}' as input.", - DeprecationWarning - ) - - if output_descriptor is not None: - warnings.warn( - "The field `output_descriptor` on a SimpleMixturePredictor is deprecated " - "and will be ignored. The Citrine Platform will automatically generate a " - f"FormulationDescriptor with key '{FormulationKey.FLAT.value}' as output.", - DeprecationWarning - ) - def __str__(self): return ''.format(self.name) diff --git a/src/citrine/informatics/workflows/design_workflow.py b/src/citrine/informatics/workflows/design_workflow.py index 6b81057fa..6b869e2e0 100644 --- a/src/citrine/informatics/workflows/design_workflow.py +++ b/src/citrine/informatics/workflows/design_workflow.py @@ -1,8 +1,6 @@ from typing import Optional, Union from uuid import UUID -from deprecation import deprecated - from citrine._rest.resource import Resource from citrine._serialization import properties from citrine.informatics.workflows.workflow import Workflow @@ -35,7 +33,6 @@ class DesignWorkflow(Resource['DesignWorkflow'], Workflow, AIResourceMetadata): predictor_version = properties.Optional( properties.Union([properties.Integer(), properties.String()]), 'predictor_version') _branch_id: Optional[UUID] = properties.Optional(properties.UUID, 'branch_id') - """:Optional[UUID]: Unique ID of the branch that contains this workflow.""" status_description = properties.String('status_description', serializable=False) """:str: more detailed description of the workflow's status""" @@ -72,21 +69,6 @@ def design_executions(self) -> DesignExecutionCollection: return DesignExecutionCollection( project_id=self.project_id, session=self._session, workflow_id=self.uid) - @property - @deprecated(deprecated_in="2.42.0", removed_in="3.0.0", - details="Please use the branch_root_id and branch_version instead.") - def branch_id(self): - """[deprecated] Retrieve the version ID of the branch this workflow is on.""" - return self._branch_id - - @branch_id.setter - @deprecated(deprecated_in="2.42.0", removed_in="3.0.0", - details="Please set the branch_root_id and branch_version instead.") - def branch_id(self, value): - self._branch_id = value - self._branch_root_id = None - self._branch_version = None - @property def branch_root_id(self): """Retrieve the root ID of the branch this workflow is on.""" @@ -94,6 +76,7 @@ def branch_root_id(self): @branch_root_id.setter def branch_root_id(self, value): + """Set the root ID of the branch this workflow is on.""" self._branch_root_id = value self._branch_id = None @@ -104,5 +87,6 @@ def branch_version(self): @branch_version.setter def branch_version(self, value): + """Set the version of the branch this workflow is on.""" self._branch_version = value self._branch_id = None diff --git a/src/citrine/jobs/waiting.py b/src/citrine/jobs/waiting.py index e05ed0f32..62207947a 100644 --- a/src/citrine/jobs/waiting.py +++ b/src/citrine/jobs/waiting.py @@ -8,7 +8,6 @@ from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution from citrine.informatics.executions import PredictorEvaluationExecution -from citrine.informatics.modules import Module class ConditionTimeoutError(RuntimeError): @@ -90,20 +89,20 @@ def is_finished(): def wait_while_validating( *, - collection: Collection[Module], - module: Module, + collection: Collection[AsynchronousObject], + module: AsynchronousObject, print_status_info: bool = False, timeout: float = 1800.0, interval: float = 3.0, -) -> Module: +) -> AsynchronousObject: """ Wait until module is validated. Parameters ---------- - collection : Collection[Module] + collection : Collection[AsynchronousObject,] Collection containing module - module : Module + module : AsynchronousObject, Module in Collection print_status_info : bool, optional Whether to print status info, by default False @@ -114,7 +113,7 @@ def wait_while_validating( Returns ------- - Module + AsynchronousObject Module in Collection after validation Raises diff --git a/src/citrine/resources/branch.py b/src/citrine/resources/branch.py index 91c2510fe..4d9099304 100644 --- a/src/citrine/resources/branch.py +++ b/src/citrine/resources/branch.py @@ -1,15 +1,11 @@ import functools -import warnings from typing import Iterator, Optional, Union from uuid import UUID -from deprecation import deprecated - from citrine._rest.collection import Collection from citrine._rest.resource import Resource from citrine._serialization import properties from citrine._session import Session -from citrine._utils.functions import migrate_deprecated_argument from citrine.exceptions import NotFound from citrine.resources.data_version_update import BranchDataUpdate, NextBranchVersionRequest from citrine.resources.design_workflow import DesignWorkflowCollection @@ -108,51 +104,18 @@ def build(self, data: dict) -> Branch: branch.project_id = self.project_id return branch - @deprecated(deprecated_in="2.42.0", removed_in="3.0.0", details="Use .get() instead.") - def get_by_root_id(self, - *, - branch_root_id: Union[UUID, str], - version: Optional[Union[int, str]] = LATEST_VER) -> Branch: - """ - Given a branch root ID and (optionally) the version, retrieve the specific branch version. - - Parameters - --------- - branch_root_id: Union[UUID, str] - Unique identifier of the branch root - - version: Union[int, str], optional - The version of the branch to retrieve. If provided, must either be a positive integer, - or "latest". Defaults to "latest". - - Returns - ------- - Branch - The requested version of the branch. - - """ - return self.get(root_id=branch_root_id, version=version) - def get(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER) -> Branch: """ - Retrieve a branch using either the version ID, or the root ID and version number. - - Providing both the version ID and the root ID, or neither, will result in an error. - - Providing the root ID and no version number will retrieve the latest version. + Retrieve a branch by its root ID and, optionally, its version number. - Using the version ID with this method is deprecated in favor of .get_by_version_id(). + Omitting the version number will retrieve the latest version. Parameters --------- - uid: Union[UUID, str], optional - [deprecated] Unqiue ID of the branch version. - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique identifier of the branch root version: Union[int, str], optional @@ -165,30 +128,18 @@ def get(self, The requested version of the branch. """ - if uid: - warnings.warn("Retrieving a branch by its version ID using .get() is deprecated. " - "Please use .get_by_version_id().", - DeprecationWarning) - - if root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - return self.get_by_version_id(version_id=uid) - elif root_id: - version = version or LATEST_VER - params = {"root": str(root_id), "version": version} - branch = next(self._list_with_params(per_page=1, **params), None) - if branch: - return branch - else: - raise NotFound.build( - message=f"Branch root '{root_id}', version {version} not found", - method="GET", - path=self._get_path(), - params=params - ) - + version = version or LATEST_VER + params = {"root": str(root_id), "version": version} + branch = next(self._list_with_params(per_page=1, **params), None) + if branch: + return branch else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + raise NotFound.build( + message=f"Branch root '{root_id}', version {version} not found", + method="GET", + path=self._get_path(), + params=params + ) def get_by_version_id(self, *, version_id: Union[UUID, str]) -> Branch: """ @@ -209,9 +160,9 @@ def get_by_version_id(self, *, version_id: Union[UUID, str]) -> Branch: def list(self, *, per_page: int = 20) -> Iterator[Branch]: """ - List all branches using pagination. + List all active branches using pagination. - Yields all branches, regardless of archive status, paginating over all available pages. + Yields all active branches paginating over all available pages. Parameters --------- @@ -225,10 +176,7 @@ def list(self, *, per_page: int = 20) -> Iterator[Branch]: All branches in this collection. """ - warnings.warn("Beginning in the 3.0 release, this method will only list unarchived " - "branches. To list all branches, use .list_all().", - DeprecationWarning) - return super().list(per_page=per_page) + return self._list_with_params(per_page=per_page, archived=False) def list_archived(self, *, per_page: int = 20) -> Iterator[Branch]: """ @@ -277,19 +225,15 @@ def _list_with_params(self, *, per_page, **kwargs): per_page=per_page) def archive(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER): """ Archive a branch. Parameters ---------- - uid: Union[UUID, str], optional - [deprecated] Unique identifier of the branch - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -297,35 +241,24 @@ def archive(self, Defaults to "latest". """ - if uid: - warnings.warn("Archiving a branch by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - elif root_id: - version = version or LATEST_VER - # The backend API at present expects a branch version ID, so we must look it up. - uid = self.get(root_id=root_id, version=version).uid - else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + version = version or LATEST_VER + # The backend API at present expects a branch version ID, so we must look it up. + uid = self.get(root_id=root_id, version=version).uid url = self._get_path(uid, action="archive") data = self.session.put_resource(url, {}, version=self._api_version) return self.build(data) def restore(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, - version: Optional[int] = None): + root_id: Union[UUID, str], + version: Optional[Union[int, str]] = LATEST_VER): """ Restore an archived branch. Parameters ---------- - uid: Union[UUID, str], optional - [deprecated] Unique identifier of the branch - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -333,25 +266,17 @@ def restore(self, Defaults to "latest". """ - if uid: - warnings.warn("Restoring a branch by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - elif root_id: - version = version or LATEST_VER - # The backend API at present expects a branch version ID, so we must look it up. - uid = self.get(root_id=root_id, version=version).uid - else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + version = version or LATEST_VER + # The backend API at present expects a branch version ID, so we must look it up. + uid = self.get(root_id=root_id, version=version).uid url = self._get_path(uid, action="restore") data = self.session.put_resource(url, {}, version=self._api_version) return self.build(data) def update_data(self, - branch: Optional[Union[UUID, str, Branch]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER, use_existing: bool = True, retrain_models: bool = False) -> Optional[Branch]: @@ -363,10 +288,7 @@ def update_data(self, Parameters ---------- - branch: Union[UUID, str, Branch], optional - [deprecated] Branch Identifier or Branch object - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -387,24 +309,6 @@ def update_data(self, The new branch record after version update or None if no update """ - if branch: - if root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - - if isinstance(branch, Branch): - warnings.warn("Updating a branch's data by its object is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - else: - warnings.warn("Updating a branch's data by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - branch = self.get_by_version_id(version_id=branch) - root_id = branch.root_id - version = branch.version - elif not root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - version = version or LATEST_VER version_updates = self.data_updates(root_id=root_id, version=version) # If no new data sources, then exit, nothing to do @@ -423,19 +327,15 @@ def update_data(self, return branch def data_updates(self, - uid: Optional[Union[UUID, str]] = None, *, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], version: Optional[Union[int, str]] = LATEST_VER) -> BranchDataUpdate: """ Get data updates for a branch. Parameters ---------- - uid: Union[UUID, str], optional - [deprecated] Unqiue ID of the branch version. - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique ID of the branch root version: Union[int, str], optional @@ -448,26 +348,16 @@ def data_updates(self, A list of data updates and compatible predictors """ - if uid: - warnings.warn("Retrieving data updates for a branch by its version ID is deprecated. " - "Please use its root ID and version number.", - DeprecationWarning) - if root_id: - raise ValueError("Please provide exactly one: the version ID or root ID.") - elif root_id: - version = version or LATEST_VER - # The backend API at present expects a branch version ID, so we must look it up. - uid = self.get(root_id=root_id, version=version).uid - else: - raise ValueError("Please provide exactly one: the version ID or root ID.") + version = version or LATEST_VER + # The backend API at present expects a branch version ID, so we must look it up. + uid = self.get(root_id=root_id, version=version).uid path = self._get_path(uid=uid, action="data-version-updates-predictor") data = self.session.get_resource(path, version=self._api_version) return BranchDataUpdate.build(data) def next_version(self, - branch_root_id: Optional[Union[UUID, str]] = None, - root_id: Optional[Union[UUID, str]] = None, + root_id: Union[UUID, str], *, branch_instructions: NextBranchVersionRequest, retrain_models: bool = True): @@ -476,11 +366,7 @@ def next_version(self, Parameters ---------- - branch_root_id: Union[UUID, str], optional - [deprecated] Unique identifier of the branch root to advance to next version. - Deprecated in favor of root_id. - - root_id: Union[UUID, str], optional + root_id: Union[UUID, str] Unique identifier of the branch root to advance to next version branch_instructions: NextBranchVersionRequest @@ -501,8 +387,6 @@ def next_version(self, The new branch record after version update """ - root_id = migrate_deprecated_argument(root_id, "root_id", branch_root_id, "branch_root_id") - path = self._get_path(action="next-version-predictor") data = self.session.post_resource(path, branch_instructions.dump(), version=self._api_version, diff --git a/src/citrine/resources/descriptors.py b/src/citrine/resources/descriptors.py index b0d95ad8e..13c3441e8 100644 --- a/src/citrine/resources/descriptors.py +++ b/src/citrine/resources/descriptors.py @@ -1,8 +1,6 @@ from typing import List, Union from uuid import UUID -from deprecation import deprecated - from citrine._session import Session from citrine._utils.functions import format_escaped_url from citrine.informatics.data_sources import DataSource @@ -76,9 +74,3 @@ def from_data_source(self, *, data_source: DataSource) -> List[Descriptor]: } ) return [Descriptor.build(r) for r in response['descriptors']] - - @deprecated(deprecated_in="2.31.0", removed_in="3.0.0", - details="Use from_data_source instead.") - def descriptors_from_data_source(self, *, data_source: DataSource) -> List[Descriptor]: - """[DEPRECATED] See from_data_source.""" - return self.from_data_source(data_source=data_source) diff --git a/src/citrine/resources/design_execution.py b/src/citrine/resources/design_execution.py index 081325e0f..7bf87fa77 100644 --- a/src/citrine/resources/design_execution.py +++ b/src/citrine/resources/design_execution.py @@ -3,27 +3,12 @@ from uuid import UUID from citrine._rest.collection import Collection -from citrine._utils.functions import MigratedClassMeta from citrine._session import Session from citrine.informatics import executions from citrine.informatics.scores import Score from citrine.resources.response import Response -class DesignExecution(executions.DesignExecution, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """The execution of a DesignWorkflow. - - Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. - Design executions also have a ``status_description`` field with more information. - - DesignExecution should be imported from citrine.informatics.executions. - - """ - - class DesignExecutionCollection(Collection["DesignExecution"]): """A collection of DesignExecutions.""" diff --git a/src/citrine/resources/design_space.py b/src/citrine/resources/design_space.py index 588b6043a..65eeaf5e0 100644 --- a/src/citrine/resources/design_space.py +++ b/src/citrine/resources/design_space.py @@ -1,6 +1,5 @@ """Resources that represent collections of design spaces.""" -import warnings -from typing import List, Optional, TypeVar, Union +from typing import Optional, TypeVar, Union from uuid import UUID from gemd.enumeration.base_enumeration import BaseEnumeration @@ -70,28 +69,11 @@ def _verify_write_request(self, design_space: DesignSpace): "in this EnumeratedDesignSpace" raise ValueError(msg.format(self._enumerated_cell_limit, width * length)) - def _hydrate_design_space(self, design_space: DesignSpace) -> List[DesignSpace]: - if design_space.typ != "ProductDesignSpace": - return design_space - - subspaces = [] - for subspace in design_space.subspaces: - if isinstance(subspace, (str, UUID)): - warnings.warn("Support for UUIDs in subspaces is deprecated as of 2.16.0, and " - "will be dropped in 3.0. Please use DesignSpace objects instead.", - DeprecationWarning) - subspaces.append(self.get(subspace)) - else: - subspaces.append(subspace) - design_space.subspaces = subspaces - return design_space - def register(self, design_space: DesignSpace) -> DesignSpace: """Create a new design space.""" self._verify_write_request(design_space) - hydrated_ds = self._hydrate_design_space(design_space) - registered_ds = super().register(hydrated_ds) + registered_ds = super().register(design_space) # If the initial response is invalid, just return it. # If not, kick off validation, since we never exposed saving a design space without @@ -104,18 +86,7 @@ def register(self, design_space: DesignSpace) -> DesignSpace: def update(self, design_space: DesignSpace) -> DesignSpace: """Update and validate an existing DesignSpace.""" self._verify_write_request(design_space) - hydrated_ds = self._hydrate_design_space(design_space) - updated_ds = super().update(hydrated_ds) - - # The /api/v3/design-spaces endpoint switched archiving from a field on the update payload - # to their own endpoints. To maintain backwards compatibility, all design spaces have an - # _archived field set by the archived property. It will be archived if True, and restored - # if False. It defaults to None, which does nothing. The value is reset afterwards. - if design_space._archived is True: - self.archive(design_space.uid) - elif design_space._archived is False: - self.restore(design_space.uid) - design_space._archived = None + updated_ds = super().update(design_space) # If the initial response is invalid, just return it. # If not, kick off validation, since we never exposed saving a design space without diff --git a/src/citrine/resources/design_workflow.py b/src/citrine/resources/design_workflow.py index da25e4774..48952874e 100644 --- a/src/citrine/resources/design_workflow.py +++ b/src/citrine/resources/design_workflow.py @@ -1,4 +1,3 @@ -import warnings from copy import deepcopy from typing import Callable, Union, Iterable, Optional, Tuple from uuid import UUID @@ -23,21 +22,12 @@ class DesignWorkflowCollection(Collection[DesignWorkflow]): def __init__(self, project_id: UUID, session: Session, - branch_id: Optional[UUID] = None, *, branch_root_id: Optional[UUID] = None, branch_version: Optional[int] = None): self.project_id: UUID = project_id self.session: Session = session - if branch_id: - warnings.warn("Constructing the design workflow interface by its parent branch's " - "version ID is deprecated. Please use its root ID and version number.", - DeprecationWarning) - if branch_root_id: - raise ValueError("Please provide at most one: the version ID or root ID.") - - self.branch_id = branch_id self.branch_root_id = branch_root_id self.branch_version = branch_version @@ -64,9 +54,9 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: """ Upload a new design workflow. - The model's branch ID is ignored in favor of this collection's. If this - collection has a null branch ID, then a branch is first created, then - the design workflow is created on it. + The model's branch root ID and branch version are ignored in favor of this collection's. + If this collection has a null branch root ID or branch version, an exception is raised + directing the caller to use one. Parameters ---------- @@ -80,18 +70,12 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: """ if self.branch_root_id is None or self.branch_version is None: - if self.branch_id is None: - # There are a number of contexts in which hitting design workflow endpoints without - # a branch ID is valid, so only this particular usage is deprecated. - msg = ('A design workflow must be created with a branch. Please use ' - 'branch.design_workflows.register() instead of ' - 'project.design_workflows.register().') - raise RuntimeError(msg) - else: - warnings.warn("Registering a design workflow to a branch by its parent's version " - "ID is deprecated. Please use its root ID and version number.", - DeprecationWarning) - branch_id = self.branch_id + # There are a number of contexts in which hitting design workflow endpoints without + # a branch ID is valid, so only this particular usage is disallowed. + msg = ('A design workflow must be created with a branch. Please use ' + 'branch.design_workflows.register() instead of ' + 'project.design_workflows.register().') + raise RuntimeError(msg) else: # branch_id is in the body of design workflow endpoints, so it must be serialized. # This means the collection branch_id might not match the workflow branch_id. The @@ -99,10 +83,10 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: # represented by this collection. # To avoid modifying the parameter, and to ensure the only change is the branch_id, we # deepcopy, modify, then register it. - branch_id = self._resolve_branch_id(self.branch_root_id, self.branch_version) - model_copy = deepcopy(model) - model_copy._branch_id = branch_id - return super().register(model_copy) + model_copy = deepcopy(model) + model_copy._branch_id = self._resolve_branch_id(self.branch_root_id, + self.branch_version) + return super().register(model_copy) def build(self, data: dict) -> DesignWorkflow: """ @@ -128,9 +112,9 @@ def build(self, data: dict) -> DesignWorkflow: def update(self, model: DesignWorkflow) -> DesignWorkflow: """Update a design workflow. - Identifies the workflow by the model's uid. It must have a branch_id, and if this - collection also has a branch_id, they must match. Prefer updating a workflow through - Project.design_workflows.update. + Identifies the workflow by its uid. It must have a branch_root_id and branch_version, and + if this collection also has a branch_root_id and branch_version, they must match. Prefer + updating a workflow through Project.design_workflows.update. Parameters ---------- @@ -148,25 +132,18 @@ def update(self, model: DesignWorkflow) -> DesignWorkflow: self.branch_version != model.branch_version: raise ValueError('To move a design workflow to another branch, please use ' 'Project.design_workflows.update') - elif self.branch_id is not None: - warnings.warn("Updating a design workflow by its parent's branch version ID is " - "deprecated. Please use its root ID and version number.", - DeprecationWarning) - if self.branch_id != model._branch_id: - raise ValueError('To move a design workflow to another branch, please use ' - 'Project.design_workflows.update') - if model.branch_root_id is not None and model.branch_version is not None: - try: - model._branch_id = self._resolve_branch_id(model.branch_root_id, - model.branch_version) - except NotFound: - raise ValueError('Cannot update a design workflow unless its branch_root_id and ' - 'branch_version exists.') - elif model._branch_id is None: + if model.branch_root_id is None or model.branch_version is None: raise ValueError('Cannot update a design workflow unless its branch_root_id and ' 'branch_version are set.') + try: + model._branch_id = self._resolve_branch_id(model.branch_root_id, + model.branch_version) + except NotFound: + raise ValueError('Cannot update a design workflow unless its branch_root_id and ' + 'branch_version exists.') + # If executions have already been done, warn about future behavior change executions = model.design_executions.list() if next(executions, None) is not None: diff --git a/src/citrine/resources/experiment_datasource.py b/src/citrine/resources/experiment_datasource.py index 39159c312..a0934c034 100644 --- a/src/citrine/resources/experiment_datasource.py +++ b/src/citrine/resources/experiment_datasource.py @@ -9,7 +9,6 @@ from citrine._serialization import properties from citrine._serialization.serializable import Serializable from citrine._session import Session -from citrine._utils.functions import migrate_deprecated_argument from citrine.informatics.experiment_values import ExperimentValue @@ -107,7 +106,6 @@ def build(self, data: dict) -> ExperimentDataSource: def list(self, *, per_page: int = 100, - branch_id: Optional[Union[UUID, str]] = None, branch_version_id: Optional[Union[UUID, str]] = None, version: Optional[Union[int, str]] = None) -> Iterator[ExperimentDataSource]: """Paginate over the experiment data sources. @@ -118,8 +116,6 @@ def list(self, *, Max number of results to return per page. Default is 100. This parameter is used when making requests to the backend service. If the page parameter is specified it limits the maximum number of elements in the response. - branch_id: UUID, optional - [deprecated] Filter the list by the branch version ID. branch_version_id: UUID, optional Filter the list by the branch version ID. version: Union[int, str], optional @@ -131,13 +127,6 @@ def list(self, *, An iterator that can be used to loop over all matching experiment data sources. """ - # migrate_deprecated_argument requires one argument be provided, but this method does not. - if branch_version_id or branch_id: - branch_version_id = migrate_deprecated_argument(branch_version_id, - "branch_version_id", - branch_id, - "branch_id") - params = {} if branch_version_id: params["branch"] = str(branch_version_id) diff --git a/src/citrine/resources/file_link.py b/src/citrine/resources/file_link.py index 9c06cf2fe..7185bd489 100644 --- a/src/citrine/resources/file_link.py +++ b/src/citrine/resources/file_link.py @@ -1,11 +1,10 @@ """A collection of FileLink objects.""" -from deprecation import deprecated import mimetypes import os from pathlib import Path from logging import getLogger from tempfile import TemporaryDirectory -from typing import Optional, Tuple, Union, List, Dict, Iterable, Sequence +from typing import Optional, Tuple, Union, Dict, Iterable, Sequence from urllib.parse import urlparse, unquote_plus from uuid import UUID @@ -21,7 +20,6 @@ from citrine._utils.functions import rewrite_s3_links_locally from citrine._utils.functions import write_file_locally -from citrine.jobs.job import JobSubmissionResponse, _poll_for_job_completion from citrine.resources.response import Response from gemd.entity.dict_serializable import DictSerializableMeta from gemd.entity.bounds.base_bounds import BaseBounds @@ -67,18 +65,6 @@ def __init__(self): self.s3_addressing_style = 'auto' -class FileProcessingType(BaseEnumeration): - """The supported File Processing Types.""" - - VALIDATE_CSV = "VALIDATE_CSV" - - -class FileProcessingData: - """The base class of all File Processing related data implementations.""" - - pass - - class CsvColumnInfo(Serializable): """The info for a CSV Column, contains the name, recommended and exact bounds.""" @@ -96,35 +82,6 @@ def __init__(self, name: str, bounds: BaseBounds, self.exact_range_bounds = exact_range_bounds -class CsvValidationData(FileProcessingData, Serializable): - """The resulting data from the processed CSV file.""" - - columns = properties.Optional(properties.List(properties.Object(CsvColumnInfo)), - 'columns') - """:Optional[List[CsvColumnInfo]]: all of the columns in the CSV""" - record_count = properties.Integer('record_count') - """:int: the number of rows in the CSV""" - - def __init__(self, columns: List[CsvColumnInfo], - record_count: int): # pragma: no cover - self.columns = columns - self.record_count = record_count - - -class FileProcessingResult: - """ - The results of a successful file processing operation. - - The type of the actual data depends on the specific processing type. - """ - - def __init__(self, - processing_type: FileProcessingType, - data: Union[Dict, FileProcessingData]): - self.processing_type = processing_type - self.data = data - - class FileLinkMeta(DictSerializableMeta): """Metaclass for FileLink deserialization.""" @@ -701,147 +658,6 @@ def read(self, *, file_link: Union[str, UUID, FileLink]) -> bytes: download_response = requests.get(final_url) return download_response.content - @deprecated(deprecated_in="2.4.0", - removed_in="3.0.0", - details="The process file protocol is deprecated " - "in favor of ingest()") - def process(self, *, file_link: Union[FileLink, str, UUID], - processing_type: FileProcessingType, - wait_for_response: bool = True, - timeout: float = 2 * 60, - polling_delay: float = 1.0) -> Union[JobSubmissionResponse, - Dict[FileProcessingType, - FileProcessingResult]]: - """ - Start a File Processing async job, returning a pollable job response. - - Parameters - ---------- - file_link: FileLink, str, or UUID - The file to process. - processing_type: FileProcessingType - The type of file processing to invoke. - wait_for_response: bool - Whether to wait for a result, vs. just return a job handle. Default: True - timeout: float - How long to poll for the result before giving up. This is expressed in - (fractional) seconds. Default: 120 seconds. - polling_delay: float - How long to delay between each polling retry attempt. Default: 1 second. - - Returns - ------- - JobSubmissionResponse - A JobSubmissionResponse which can be used to poll for the result. - - """ - if self._is_external_url(file_link.url): - raise ValueError(f"Only on-platform resources can be processed. " - f"Passed URL {file_link.url}.") - file_link = self._resolve_file_link(file_link) - - params = {"processing_type": processing_type.value} - response = self.session.put_resource( - self._get_path_from_file_link(file_link, action="processed"), - json={}, - params=params - ) - job = JobSubmissionResponse.build(response) - logger.info('Build job submitted with job ID {}.'.format(job.job_id)) - - if wait_for_response: - return self.poll_file_processing_job(file_link=file_link, - processing_type=processing_type, - job_id=job.job_id, timeout=timeout, - polling_delay=polling_delay) - else: - return job - - @deprecated(deprecated_in="2.4.0", - removed_in="3.0.0", - details="The process file protocol is deprecated " - "in favor of ingest()") - def poll_file_processing_job(self, *, file_link: FileLink, - processing_type: FileProcessingType, - job_id: UUID, - timeout: float = 2 * 60, - polling_delay: float = 1.0) -> Dict[FileProcessingType, - FileProcessingResult]: - """ - Poll for the result of the file processing task. - - Parameters - ---------- - file_link: FileLink - The file to process. - processing_type: FileProcessingType - The processing algorithm to apply. - job_id: UUID - The background job ID to poll for. - timeout: - How long to poll for the result before giving up. This is expressed in - (fractional) seconds. - polling_delay: - How long to delay between each polling retry attempt. - - Returns - ------- - None - This method will raise an appropriate exception if the job failed, else - it will return None to indicate the job was successful. - - """ - # Poll for job completion - this will raise an error if the job failed - _poll_for_job_completion(self.session, self.project_id, job_id, timeout=timeout, - polling_delay=polling_delay) - - return self.file_processing_result(file_link=file_link, processing_types=[processing_type]) - - @deprecated(deprecated_in="2.4.0", - removed_in="3.0.0", - details="The process file protocol is deprecated " - "in favor of ingest()") - def file_processing_result(self, *, - file_link: FileLink, - processing_types: List[FileProcessingType]) -> \ - Dict[FileProcessingType, FileProcessingResult]: - """ - Return the file processing result for the given file link and processing type. - - Parameters - ---------- - file_link: FileLink - The file to process - processing_types: FileProcessingType - A list of the particular file processing types to retrieve - - Returns - ------- - Map[FileProcessingType, FileProcessingResult] - The file processing results, mapped by processing type. - - """ - processed_results_path = self._get_path_from_file_link(file_link, action="processed") - - params = [] - for proc_type in processing_types: - params.append(('processing_type', proc_type.value)) - - response = self.session.get_resource(processed_results_path, params=params) - results_json = response['results'] - results = {} - for result_json in results_json: - processing_type = FileProcessingType[result_json['processing_type']] - data = result_json['data'] - - if processing_type == FileProcessingType.VALIDATE_CSV: - data = CsvValidationData.build(data) - - result = FileProcessingResult(processing_type, data) - results[processing_type] = result - - return results - def ingest(self, files: Iterable[Union[FileLink, Path, str]], *, diff --git a/src/citrine/resources/job.py b/src/citrine/resources/job.py deleted file mode 100644 index 34d731f6e..000000000 --- a/src/citrine/resources/job.py +++ /dev/null @@ -1,45 +0,0 @@ -import citrine.jobs.job -from citrine.jobs.job import _poll_for_job_completion # noqa -from citrine._utils.functions import MigratedClassMeta - - -class JobSubmissionResponse(citrine.jobs.job.JobSubmissionResponse, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """ - A response to a submit-job request for the job submission framework. - - This is returned as a successful response from the remote service. - - Importing from this package is deprecated; import from citrine.jobs.job instead. - - """ - - -class TaskNode(citrine.jobs.job.TaskNode, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """ - Individual task status. - - The TaskNode describes a component of an overall job. - - Importing from this package is deprecated; import from citrine.jobs.job instead. - - """ - - -class JobStatusResponse(citrine.jobs.job.JobStatusResponse, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """ - A response to a job status check. - - The JobStatusResponse summarizes the status for the entire job. - - Importing from this package is deprecated; import from citrine.jobs.job instead. - - """ diff --git a/src/citrine/resources/module.py b/src/citrine/resources/module.py deleted file mode 100644 index 22c47962e..000000000 --- a/src/citrine/resources/module.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Resources that represent collections of modules.""" -from uuid import UUID - -from citrine._rest.collection import Collection -from citrine._session import Session -from citrine.informatics.modules import Module - - -class ModuleCollection(Collection[Module]): - """Represents the collection of all modules as well as the resources belonging to it. - - Parameters - ---------- - project_id: UUID - the UUID of the project - - """ - - _path_template = '/projects/{project_id}/design-spaces' - _api_version = 'v3' - _individual_key = None - _resource = Module - _collection_key = 'response' - - def __init__(self, project_id: UUID, session: Session): - self.project_id = project_id - self.session: Session = session - - def build(self, data: dict) -> Module: - """Build an individual module.""" - module = Module.build(data) - module.session = self.session - return module diff --git a/src/citrine/resources/predictor.py b/src/citrine/resources/predictor.py index 763fb6077..a6c4f0d0b 100644 --- a/src/citrine/resources/predictor.py +++ b/src/citrine/resources/predictor.py @@ -10,7 +10,6 @@ from citrine._rest.paginator import Paginator from citrine._serialization import properties from citrine._session import Session -from citrine.exceptions import Conflict from citrine.informatics.data_sources import DataSource from citrine.informatics.design_candidate import HierarchicalDesignMaterial from citrine.informatics.predictors import GraphPredictor @@ -107,12 +106,6 @@ def _page_fetcher(self, *, uid: Union[UUID, str], **additional_params): } return partial(self._fetch_page, **fetcher_params) - def _train(self, uid: Union[UUID, str], version: Union[int, str]) -> GraphPredictor: - path = self._construct_path(uid, version, "train") - params = {"create_version": True} - entity = self.session.put_resource(path, {}, params=params, version=self._api_version) - return self.build(entity) - def build(self, data: dict) -> GraphPredictor: """Build an individual Predictor.""" predictor: GraphPredictor = GraphPredictor.build(data) @@ -175,22 +168,6 @@ def restore(self, entity = self.session.put_resource(url, {}, version=self._api_version) return self.build(entity) - def convert_to_graph(self, - uid: Union[UUID, str], - *, - version: Union[int, str] = MOST_RECENT_VER, - retrain_if_needed: bool = False) -> Optional[GraphPredictor]: - path = self._construct_path(uid, version, "convert") - try: - entity = self.session.get_resource(path, version=self._api_version) - except Conflict as exc: - if retrain_if_needed: - self._train(uid, version) - return None - else: - raise exc - return self.build(entity) - def is_stale(self, uid: Union[UUID, str], *, @@ -552,53 +529,6 @@ def get_default_async(self, *, task_id: Union[UUID, str]) -> AsyncDefaultPredict data = self.session.get_resource(path, version=self._api_version) return AsyncDefaultPredictor.build(data) - def convert_to_graph(self, - uid: Union[UUID, str], - retrain_if_needed: bool = False, - *, - version: Union[int, str] = MOST_RECENT_VER) -> GraphPredictor: - """Given a predictor containing SimpleML nodes, get an equivalent Graph predictor. - - Returns a Graph predictor with any SimpleML predictors converted to an equivalent AutoML - predictor. If it's not a SimpleML or Graph predictor, or it's not in the READY state, an - error is raised. SimpleML predictors are deprecated, so this is to aid in your migration. - Note this conversion is not performed in place! That is, the predictor returned is not - persisted on the platform. To persist it, pass the converted predictor to - :py:meth:`citrine.resources.project.PredictorCollection.update`. Or you can do this in - one step with :py:meth:`citrine.resources.project.PredictorCollection.convert_and_update`. - .. code:: python - converted = project.predictors.convert_to_graph(predictor_uid) - project.predictors.update(converted) - # is equivalent to - converted = project.predictors.convert_and_update(predictor_uid) - If a predictor needs to be retrained before conversion, it will raise an HTTP 409 Conflict - error. This may occur when the Citrine Platform has been updated since your predictor was - last trained. If `retrain_if_needed` is `True`, retraining will automatically begin, and - the method will return `None. Once retraining completes, call this method again to get the - converted predictor. For example: - .. code:: python - converted = project.predictors.convert_and_update(pred.uid, retrain_if_needed=True) - if converted is None: - predictor = project.predictors.get(pred.uid) - wait_while_validating(collection=project.predictors, module=predictor) - converted = project.predictors.convert_and_update(pred.uid) - """ - return self._versions_collection.convert_to_graph(uid, - version=version, - retrain_if_needed=retrain_if_needed) - - def convert_and_update(self, - uid: Union[UUID, str], - retrain_if_needed: bool = False, - *, - version: Union[int, str] = MOST_RECENT_VER) -> GraphPredictor: - """Given a SimpleML or Graph predictor, overwrite it with an equivalent Graph predictor. - - See `PredictorCollection.convert_to_graph` for more detail. - """ - new_pred = self.convert_to_graph(uid, version=version, retrain_if_needed=retrain_if_needed) - return self.update(new_pred) if new_pred else None - def is_stale(self, uid: Union[UUID, str], *, version: Union[int, str]) -> bool: """Returns True if a predictor is stale, False otherwise. diff --git a/src/citrine/resources/predictor_evaluation_execution.py b/src/citrine/resources/predictor_evaluation_execution.py index a045853e5..51bc91d5e 100644 --- a/src/citrine/resources/predictor_evaluation_execution.py +++ b/src/citrine/resources/predictor_evaluation_execution.py @@ -6,23 +6,11 @@ from citrine._rest.collection import Collection from citrine._rest.resource import PredictorRef from citrine._session import Session -from citrine._utils.functions import MigratedClassMeta, format_escaped_url +from citrine._utils.functions import format_escaped_url from citrine.informatics.executions import predictor_evaluation_execution from citrine.resources.response import Response -class PredictorEvaluationExecution(predictor_evaluation_execution.PredictorEvaluationExecution, - deprecated_in="2.22.1", - removed_in="3.0.0", - metaclass=MigratedClassMeta): - """The execution of a PredictorEvaluationWorkflow. - - Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. - Predictor evaluation executions also have a ``status_description`` field with more information. - - """ - - class PredictorEvaluationExecutionCollection(Collection["PredictorEvaluationExecution"]): """A collection of PredictorEvaluationExecutions.""" diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index a2b4bbc4e..f08cab49d 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -3,8 +3,6 @@ from typing import Optional, Dict, List, Union, Iterable, Tuple, Iterator from uuid import UUID -from deprecation import deprecated - from gemd.entity.base_entity import BaseEntity from gemd.entity.link_by_uid import LinkByUID @@ -32,7 +30,6 @@ from citrine.resources.measurement_run import MeasurementRunCollection from citrine.resources.measurement_spec import MeasurementSpecCollection from citrine.resources.measurement_template import MeasurementTemplateCollection -from citrine.resources.module import ModuleCollection from citrine.resources.parameter_template import ParameterTemplateCollection from citrine.resources.predictor import PredictorCollection from citrine.resources.predictor_evaluation_execution import \ @@ -107,12 +104,6 @@ def __str__(self): def _path(self): return format_escaped_url('/projects/{project_id}', project_id=self.uid) - @property - @deprecated(deprecated_in="2.26.0", removed_in="3.0.0", details="Use design_spaces instead.") - def modules(self) -> ModuleCollection: - """Return a resource representing all visible design spaces.""" - return ModuleCollection(self.uid, self.session) - @property def branches(self) -> BranchCollection: """Return a resource representing all visible branches.""" diff --git a/src/citrine/resources/sample_design_space_execution.py b/src/citrine/resources/sample_design_space_execution.py index e21db5150..9c5d0f953 100644 --- a/src/citrine/resources/sample_design_space_execution.py +++ b/src/citrine/resources/sample_design_space_execution.py @@ -59,9 +59,6 @@ def list(self, *, Parameters --------- - page: int, optional - The "page" of results to list. Default is to read all pages and yield - all results. This option is deprecated. per_page: int, optional Max number of results to return per page. Default is 100. This parameter is used when making requests to the backend service. If the page parameter diff --git a/tests/_util/test_functions.py b/tests/_util/test_functions.py index 8d899b558..803c99484 100644 --- a/tests/_util/test_functions.py +++ b/tests/_util/test_functions.py @@ -7,8 +7,8 @@ from gemd.entity.link_by_uid import LinkByUID from citrine._utils.functions import get_object_id, validate_type, object_to_link_by_uid, \ - rewrite_s3_links_locally, write_file_locally, shadow_classes_in_module, migrate_deprecated_argument, \ - format_escaped_url, MigratedClassMeta, generate_shared_meta + rewrite_s3_links_locally, write_file_locally, migrate_deprecated_argument, format_escaped_url, \ + MigratedClassMeta, generate_shared_meta from gemd.entity.attribute.property import Property from citrine.resources.condition_template import ConditionTemplate @@ -165,29 +165,6 @@ class MigratedProperty(Simple, assert not issubclass(dict, Simple) -def test_shadow_classes_in_module(): - - # Given - from tests._util import source_mod, target_mod - assert getattr(target_mod, 'ExampleClass', None) == None - - # When - with pytest.deprecated_call(): - shadow_classes_in_module(source_mod, target_mod) - - # Then (ensure the class is copied over) - copied_class = getattr(target_mod, 'ExampleClass', None) # Do this vs a direct ref so IJ doesn't warn us - assert copied_class == source_mod.ExampleClass - - # Python also considers the classes equivalent - assert issubclass(copied_class, source_mod.ExampleClass) - assert issubclass(source_mod.ExampleClass, copied_class) - - # Reset target_mod status - for attr in dir(target_mod): - delattr(target_mod, attr) - - def test_migrate_deprecated_argument(): with pytest.raises(ValueError): # ValueError if neither argument is specified diff --git a/tests/informatics/test_data_source.py b/tests/informatics/test_data_source.py index 46d5f4290..c5819247b 100644 --- a/tests/informatics/test_data_source.py +++ b/tests/informatics/test_data_source.py @@ -39,12 +39,3 @@ def test_invalid_deser(): with pytest.raises(ValueError): DataSource.build({"type": "foo"}) - - -def test_deprecated_formulation_option(): - with pytest.warns(DeprecationWarning): - GemTableDataSource( - table_id=uuid.uuid4(), - table_version=1, - formulation_descriptor=FormulationDescriptor.hierarchical() - ) diff --git a/tests/informatics/test_descriptors.py b/tests/informatics/test_descriptors.py index 4ba6c8795..8c1744756 100644 --- a/tests/informatics/test_descriptors.py +++ b/tests/informatics/test_descriptors.py @@ -67,9 +67,3 @@ def test_to_json(descriptor): def test_formulation_from_string_key(): descriptor = FormulationDescriptor(FormulationKey.HIERARCHICAL.value) assert descriptor.key == FormulationKey.HIERARCHICAL.value - - -def test_integer_units_deprecation(): - descriptor = IntegerDescriptor("integer", lower_bound=5, upper_bound=10) - with pytest.deprecated_call(): - assert descriptor.units == "dimensionless" diff --git a/tests/informatics/test_design_spaces.py b/tests/informatics/test_design_spaces.py index d7b4b8d26..734a59dc2 100644 --- a/tests/informatics/test_design_spaces.py +++ b/tests/informatics/test_design_spaces.py @@ -121,17 +121,6 @@ def test_hierarchical_initialization(hierarchical_design_space): assert "formulation_descriptor" in fds_data -def test_template_link_deprecations(): - with pytest.warns(DeprecationWarning): - link = TemplateLink( - material_template=uuid.uuid4(), - process_template=uuid.uuid4(), - template_name="I am deprecated", - material_template_name="I am not deprecated" - ) - assert link.template_name == link.material_template_name - - def test_data_source_build(valid_data_source_design_space_dict): ds = DesignSpace.build(valid_data_source_design_space_dict) assert ds.name == valid_data_source_design_space_dict["data"]["instance"]["name"] @@ -146,11 +135,3 @@ def test_data_source_create(valid_data_source_design_space_dict): assert ds["data"]["description"] == round_robin.description assert ds["data"]["instance"]["data_source"] == round_robin.data_source.dump() assert "DataSource" in str(ds) - - -def test_deprecated_module_type(product_design_space): - with pytest.deprecated_call(): - product_design_space.module_type = "foo" - - with pytest.deprecated_call(): - assert product_design_space.module_type == "DESIGN_SPACE" diff --git a/tests/informatics/test_predictors.py b/tests/informatics/test_predictors.py index 67b59efc1..b4250ece6 100644 --- a/tests/informatics/test_predictors.py +++ b/tests/informatics/test_predictors.py @@ -420,95 +420,3 @@ def test_single_predict(graph_predictor): prediction_out = graph_predictor.predict(request) assert prediction_out.dump() == prediction_in.dump() assert session.post_resource.call_count == 1 - - -def test_deprecated_training_data(): - with pytest.warns(DeprecationWarning): - AutoMLPredictor( - name="AutoML", - description="", - inputs=[x, y], - outputs=[z], - training_data=[data_source] - ) - - with pytest.warns(DeprecationWarning): - MeanPropertyPredictor( - name="SimpleMixture", - description="", - input_descriptor=flat_formulation, - properties=[x, y, z], - p=1.0, - impute_properties=True, - training_data=[data_source] - ) - - with pytest.warns(DeprecationWarning): - SimpleMixturePredictor( - name="Warning", - description="Description", - training_data=[data_source] - ) - - -def test_formulation_deprecations(): - with pytest.warns(DeprecationWarning): - SimpleMixturePredictor( - name="Warning", - description="Description", - input_descriptor=FormulationDescriptor.hierarchical(), - output_descriptor=FormulationDescriptor.flat() - ) - with pytest.warns(DeprecationWarning): - IngredientsToFormulationPredictor( - name="Warning", - description="Description", - output=FormulationDescriptor.hierarchical(), - id_to_quantity={}, - labels={} - ) - - -def test_deprecated_node_fields(valid_auto_ml_predictor_data): - # Just testing for coverage of methods - aml = AutoMLPredictor.build(valid_auto_ml_predictor_data) - with pytest.deprecated_call(): - assert aml.uid is None - with pytest.deprecated_call(): - assert aml.version is None - with pytest.deprecated_call(): - assert aml.draft is None - with pytest.deprecated_call(): - assert aml.created_by is None - with pytest.deprecated_call(): - assert aml.updated_by is None - with pytest.deprecated_call(): - assert aml.archived_by is None - with pytest.deprecated_call(): - assert aml.create_time is None - with pytest.deprecated_call(): - assert aml.update_time is None - with pytest.deprecated_call(): - assert aml.archive_time is None - with pytest.deprecated_call(): - assert aml.status is None - with pytest.deprecated_call(): - assert len(aml.status_detail) == 0 - with pytest.deprecated_call(): - assert aml.in_progress() is False - with pytest.deprecated_call(): - assert aml.succeeded() is False - with pytest.deprecated_call(): - assert aml.failed() is False - - -def test_unhydrated_graph_deprecation(): - good = SimpleMixturePredictor(name="Warning", description="Description") - bad = uuid.uuid4() - with pytest.warns(DeprecationWarning): - graph = GraphPredictor( - name="Warning", - description="Hydrate me!", - predictors=[good, bad] - ) - assert len(graph.predictors) == 1 diff --git a/tests/resources/test_branch.py b/tests/resources/test_branch.py index 76b7d1b99..c1e8408c4 100644 --- a/tests/resources/test_branch.py +++ b/tests/resources/test_branch.py @@ -117,12 +117,11 @@ def test_branch_list(session, collection, branch_path): session.set_response({'response': branches_data}) # When - with pytest.deprecated_call(): - branches = list(collection.list()) + branches = list(collection.list()) # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=branch_path, params={'page': 1, 'per_page': 20}) + assert session.last_call == FakeCall(method='GET', path=branch_path, params={'archived': False, 'page': 1, 'per_page': 20}) assert len(branches) == branch_count @@ -140,8 +139,6 @@ def test_branch_list_all(session, collection, branch_path): assert session.last_call == FakeCall(method='GET', path=branch_path, params={'per_page': 20, 'page': 1}) - - def test_branch_delete(session, collection, branch_path): # Given branch_id = uuid.uuid4() @@ -546,297 +543,3 @@ def test_experiment_data_source_no_project_id(session): branch.experiment_datasource assert not session.calls - - -def test_get_by_root_id_deprecated(session, collection, branch_path): - # Given - branches_data = BranchDataFactory.create_batch(1) - session.set_response({'response': branches_data}) - root_id = uuid.uuid4() - - # When - with pytest.deprecated_call(): - branch = collection.get_by_root_id(branch_root_id=root_id) - - # Then - assert session.calls == [FakeCall( - method='GET', - path=branch_path, - params={'page': 1, 'per_page': 1, 'root': str(root_id), 'version': LATEST_VER} - )] - - -def test_get_by_root_id_not_found_deprecated(session, collection, branch_path): - # Given - session.set_response({'response': []}) - root_id = uuid.uuid4() - - # When - with pytest.deprecated_call(): - with pytest.raises(NotFound) as exc: - collection.get_by_root_id(branch_root_id=root_id) - - # Then - assert str(root_id) in str(exc) - assert "branch root" in str(exc).lower() - assert LATEST_VER in str(exc).lower() - - -def test_branch_data_updates_normal_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - root_branch_id = branch_data["metadata"]["root_id"] - branch_data_get_resp = {"response": [branch_data]} - branch_data_get_params = { - 'page': 1, 'per_page': 1, 'root': str(root_branch_id), 'version': branch_data['metadata']['version'] - } - session.set_response(branch_data) - - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - data_updates = BranchDataUpdateFactory() - v2branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(root_id=root_branch_id)) - session.set_responses(branch_data_get_resp, data_updates, v2branch_data) - with pytest.deprecated_call(): - v2branch = collection.update_data(branch) - - # Then - next_version_call = FakeCall(method='POST', - path=f'{branch_path}/next-version-predictor', - params={'root': str(root_branch_id), - 'retrain_models': False}, - json={ - 'data_updates': [ - { - 'current': data_updates['data_updates'][0]['current'], - 'latest': data_updates['data_updates'][0]['latest'], - 'type': 'DataVersionUpdate' - } - ], - 'use_predictors': [ - { - 'predictor_id': data_updates['predictors'][0]['predictor_id'], - 'predictor_version': data_updates['predictors'][0]['predictor_version'] - } - ] - }, - version='v2') - assert session.calls == [ - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}'), - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}/data-version-updates-predictor'), - next_version_call - ] - assert str(v2branch.root_id) == root_branch_id - - -def test_branch_data_updates_latest_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] - branch_data_get_resp = {"response": [branch_data]} - branch_data_get_params = { - 'page': 1, 'per_page': 1, 'root': str(root_branch_id), 'version': branch_data['metadata']['version'] - } - session.set_response(branch_data) - - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - data_updates = BranchDataUpdateFactory() - v2branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(root_id=root_branch_id)) - session.set_responses(branch_data_get_resp, data_updates, v2branch_data) - with pytest.deprecated_call(): - v2branch = collection.update_data(branch, use_existing=False, retrain_models=True) - - # Then - next_version_call = FakeCall(method='POST', - path=f'{branch_path}/next-version-predictor', - params={'root': str(root_branch_id), - 'retrain_models': True}, - json={ - 'data_updates': [ - { - 'current': data_updates['data_updates'][0]['current'], - 'latest': data_updates['data_updates'][0]['latest'], - 'type': 'DataVersionUpdate' - } - ], - 'use_predictors': [] - }, - version='v2') - assert session.calls == [ - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}'), - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}/data-version-updates-predictor'), - next_version_call - ] - assert str(v2branch.root_id) == root_branch_id - - -def test_branch_data_updates_nochange_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - branch_data_get_resp = {"response": [branch_data]} - session.set_response(branch_data) - - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - data_updates = BranchDataUpdateFactory(data_updates=[], predictors=[]) - session.set_responses(branch_data, branch_data_get_resp, data_updates) - with pytest.deprecated_call(): - v2branch = collection.update_data(branch.uid) - - assert v2branch is None - - -def test_branch_get_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - session.set_response(branch_data) - - # When - with pytest.deprecated_call(): - branch = collection.get(branch_data['id']) - - # Then - assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=f'{branch_path}/{branch_data["id"]}') - - -def test_branch_archive_deprecated(session, collection, branch_path): - # Given - branch_id = uuid.uuid4() - session.set_response(BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=True))) - - # When - with pytest.deprecated_call(): - archived_branch = collection.archive(branch_id) - - # Then - assert session.num_calls == 1 - expected_path = f'{branch_path}/{branch_id}/archive' - assert session.last_call == FakeCall(method='PUT', path=expected_path, json={}) - assert archived_branch.archived is True - - -def test_branch_restore_deprecated(session, collection, branch_path): - # Given - branch_id = uuid.uuid4() - session.set_response(BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=False))) - - # When - with pytest.deprecated_call(): - restored_branch = collection.restore(branch_id) - - # Then - assert session.num_calls == 1 - expected_path = f'{branch_path}/{branch_id}/restore' - assert session.last_call == FakeCall(method='PUT', path=expected_path, json={}) - assert restored_branch.archived is False - - -def test_branch_data_updates_deprecated(session, collection, branch_path): - # Given - branch_id = uuid.uuid4() - expected_data_updates = BranchDataUpdateFactory() - session.set_response(expected_data_updates) - - # When - with pytest.deprecated_call(): - actual_data_updates = collection.data_updates(branch_id) - - # Then - assert session.num_calls == 1 - expected_path = f'{branch_path}/{branch_id}/data-version-updates-predictor' - assert session.last_call == FakeCall(method='GET', - path=expected_path, - version='v2') - assert expected_data_updates['data_updates'][0]['current'] == actual_data_updates.data_updates[0].current - assert expected_data_updates['data_updates'][0]['latest'] == actual_data_updates.data_updates[0].latest - assert expected_data_updates['predictors'][0]['predictor_id'] == str(actual_data_updates.predictors[0].uid) - - -def test_branch_next_version_deprecated(session, collection, branch_path): - # Given - branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] - session.set_response(branch_data) - data_updates = [DataVersionUpdate(current="gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::1", - latest="gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::2")] - predictors = [PredictorRef("aa971886-d17c-43b4-b602-5af7b44fcd5a", 2)] - req = NextBranchVersionRequest(data_updates=data_updates, use_predictors=predictors) - - # When - with pytest.deprecated_call(): - branchv2 = collection.next_version(root_branch_id, branch_instructions=req, retrain_models=False) - - # Then - expected_path = f'{branch_path}/next-version-predictor' - expected_call = FakeCall(method='POST', - path=expected_path, - params={'root': str(root_branch_id), - 'retrain_models': False}, - json={ - 'data_updates': [ - { - 'current': 'gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::1', - 'latest': 'gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::2', - 'type': 'DataVersionUpdate' - } - ], - 'use_predictors': [ - { - 'predictor_id': 'aa971886-d17c-43b4-b602-5af7b44fcd5a', - 'predictor_version': 2 - } - ] - }, - version='v2') - assert session.num_calls == 1 - assert session.last_call == expected_call - assert str(branchv2.root_id) == root_branch_id - - -def test_get_both_ids(collection): - with pytest.deprecated_call(): - with pytest.raises(ValueError): - collection.get(uuid.uuid4(), root_id=uuid.uuid4()) - - -def test_get_neither_id(collection): - with pytest.raises(ValueError): - collection.get() - - -def test_archive_neither_id(collection): - with pytest.raises(ValueError): - collection.archive() - - -def test_restore_neither_id(collection): - with pytest.raises(ValueError): - collection.restore() - - -def test_update_data_both_ids(collection): - with pytest.raises(ValueError): - collection.update_data(uuid.uuid4(), root_id=uuid.uuid4()) - - -def test_update_data_neither_id(collection): - with pytest.raises(ValueError): - collection.update_data() - - -def test_data_updates_both_ids(collection): - with pytest.deprecated_call(): - with pytest.raises(ValueError): - collection.data_updates(uuid.uuid4(), root_id=uuid.uuid4()) - - -def test_data_updates_neither_id(collection): - with pytest.raises(ValueError): - collection.data_updates() diff --git a/tests/resources/test_descriptors.py b/tests/resources/test_descriptors.py index 2bdec0d22..b289025d4 100644 --- a/tests/resources/test_descriptors.py +++ b/tests/resources/test_descriptors.py @@ -105,45 +105,3 @@ def test_from_data_source(): assert session.last_call.path == '/projects/{}/material-descriptors/from-data-source'\ .format(descriptors.project_id) assert session.last_call.method == 'POST' - - -def test_deprecated_descriptors_from_data_source(): - session = FakeSession() - col = 'smiles' - response_json = { - 'descriptors': [ # shortened sample response - { - 'type': 'Real', - 'descriptor_key': 'khs.sNH3 KierHallSmarts for {}'.format(col), - 'units': '', - 'lower_bound': 0, - 'upper_bound': 1000000000 - }, - { - 'type': 'Real', - 'descriptor_key': 'khs.dsN KierHallSmarts for {}'.format(col), - 'units': '', - 'lower_bound': 0, - 'upper_bound': 1000000000 - }, - ] - } - session.set_response(response_json) - descriptors = DescriptorMethods(uuid4(), session) - data_source = GemTableDataSource(table_id='43357a66-3644-4959-8115-77b2630aca45', table_version=123) - - with pytest.deprecated_call(): - results = descriptors.descriptors_from_data_source(data_source=data_source) - - assert results == [ - RealDescriptor( - key=r['descriptor_key'], - lower_bound=r['lower_bound'], - upper_bound=r['upper_bound'], - units=r['units'] - ) for r in response_json['descriptors'] - ] - assert session.last_call.path == '/projects/{}/material-descriptors/from-data-source'\ - .format(descriptors.project_id) - assert session.last_call.method == 'POST' - diff --git a/tests/resources/test_design_executions.py b/tests/resources/test_design_executions.py index 60d561c17..87a54a440 100644 --- a/tests/resources/test_design_executions.py +++ b/tests/resources/test_design_executions.py @@ -59,8 +59,6 @@ def test_build_new_execution(collection, design_execution_dict): assert execution._session == collection.session assert execution.in_progress() and not execution.succeeded() and not execution.failed() assert execution.status_detail - with pytest.deprecated_call(): - assert execution.status_info == [detail.msg for detail in execution.status_detail] def test_trigger_workflow_execution(collection: DesignExecutionCollection, design_execution_dict, session): @@ -117,9 +115,3 @@ def test_list(collection: DesignExecutionCollection, session): def test_delete(collection): with pytest.raises(NotImplementedError): collection.delete(uuid.uuid4()) - - -def test_deprecated(): - from citrine.resources.design_execution import DesignExecution - with pytest.deprecated_call(): - DesignExecution() diff --git a/tests/resources/test_design_space.py b/tests/resources/test_design_space.py index e440e1e99..b4d26a5a9 100644 --- a/tests/resources/test_design_space.py +++ b/tests/resources/test_design_space.py @@ -75,15 +75,11 @@ def test_design_space_build_with_status_detail(valid_product_design_space_data): # Then status_detail_tuples = {(detail.level, detail.msg) for detail in design_space.status_detail} assert status_detail_tuples == status_detail_data - with pytest.deprecated_call(): - assert design_space.status_info == [args[1] for args in status_detail_data] def test_formulation_build(valid_formulation_design_space_data): pc = DesignSpaceCollection(uuid.uuid4(), None) design_space = pc.build(valid_formulation_design_space_data) - with pytest.deprecated_call(): - assert design_space.archived assert design_space.name == 'formulation design space' assert design_space.description == 'formulates some things' assert design_space.formulation_descriptor.key == FormulationKey.HIERARCHICAL.value @@ -320,131 +316,6 @@ def test_get_none(): assert "uid=None" in str(excinfo.value) -def test_register_dehydrated_design_spaces_deprecated(valid_product_design_space_data, valid_product_design_space): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - - subspace_id = str(uuid.uuid4()) - - subspace_data = valid_product_design_space_data["data"]["instance"]["subspaces"][0] - ds = DesignSpace.build(deepcopy(valid_product_design_space_data)) - ds.subspaces[0] = subspace_id - - session.set_responses(_ds_dict_to_response(subspace_data), deepcopy(valid_product_design_space_data), valid_product_design_space_data) - - with pytest.deprecated_call(): - retval = dsc.register(ds) - - base_path = f"/projects/{dsc.project_id}/design-spaces" - assert session.calls == [ - FakeCall(method='GET', path=f"{base_path}/{subspace_id}"), - FakeCall(method='POST', path=base_path, json=valid_product_design_space.dump()), - FakeCall(method='PUT', path=f"{base_path}/{retval.uid}/validate", json={}) - ] - assert retval.dump() == valid_product_design_space.dump() - - -def test_update_dehydrated_design_spaces_deprecated(valid_product_design_space_data, valid_product_design_space): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - - subspace_id = str(uuid.uuid4()) - - subspace_data = valid_product_design_space_data["data"]["instance"]["subspaces"][0] - ds = DesignSpace.build(deepcopy(valid_product_design_space_data)) - ds.subspaces[0] = subspace_id - - session.set_responses( - _ds_dict_to_response(subspace_data), - deepcopy(valid_product_design_space_data), - deepcopy(valid_product_design_space_data) - ) - - with pytest.deprecated_call(): - retval = dsc.update(ds) - - base_path = f"/projects/{dsc.project_id}/design-spaces" - assert session.calls == [ - FakeCall(method='GET', path=f"{base_path}/{subspace_id}"), - FakeCall(method='PUT', path=f"{base_path}/{ds.uid}", json=valid_product_design_space.dump()), - FakeCall(method='PUT', path=f"{base_path}/{ds.uid}/validate", json={}) - ] - assert retval.dump() == valid_product_design_space.dump() - - -def test_deprecated_archive_via_update(valid_product_design_space_data): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - archived_data = deepcopy(valid_product_design_space_data) - archived_data["metadata"]["archived"] = archived_data["metadata"]["created"] - validating_data = deepcopy(archived_data) - validating_data["metadata"]["status"]["name"] = "VALIDATING" - session.set_responses( - valid_product_design_space_data, - archived_data, - validating_data - ) - - design_space = dsc.build(deepcopy(valid_product_design_space_data)) - with pytest.deprecated_call(): - design_space.archived = True - - design_space_path = DesignSpaceCollection._path_template.format(project_id=dsc.project_id) - entity_path = f"{design_space_path}/{valid_product_design_space_data['id']}" - expected_calls = [ - FakeCall(method="PUT", path=entity_path, json=design_space.dump()), - FakeCall(method="PUT", path=f"{entity_path}/archive", json={}), - FakeCall(method="PUT", path=f"{entity_path}/validate", json={}), - ] - - archived_design_space = dsc.update(design_space) - - assert session.calls == expected_calls - assert archived_design_space.is_archived is True - assert archived_design_space._archived is None - -def test_deprecated_restore_via_update(valid_product_design_space_data): - session = FakeSession() - dsc = DesignSpaceCollection(uuid.uuid4(), session) - archived_data = deepcopy(valid_product_design_space_data) - archived_data["metadata"]["archived"] = archived_data["metadata"]["created"] - validating_data = deepcopy(valid_product_design_space_data) - validating_data["metadata"]["status"]["name"] = "VALIDATING" - session.set_responses(archived_data, valid_product_design_space_data, validating_data) - - design_space = dsc.build(deepcopy(archived_data)) - with pytest.deprecated_call(): - design_space.archived = False - - design_space_path = DesignSpaceCollection._path_template.format(project_id=dsc.project_id) - entity_path = f"{design_space_path}/{archived_data['id']}" - expected_calls = [ - FakeCall(method="PUT", path=entity_path, json=design_space.dump()), - FakeCall(method="PUT", path=f"{entity_path}/restore", json={}), - FakeCall(method="PUT", path=f"{entity_path}/validate", json={}), - ] - - restored_design_space = dsc.update(design_space) - - assert session.calls == expected_calls - assert restored_design_space.is_archived is False - assert restored_design_space._archived is None - - -def test_deprecated_archived_property(valid_product_design_space_data): - dsc = DesignSpaceCollection(uuid.uuid4(), FakeSession()) - - design_space = dsc.build(valid_product_design_space_data) - - with pytest.deprecated_call(): - assert design_space.archived == design_space.is_archived - - with pytest.deprecated_call(): - design_space.archived = True - - assert design_space._archived is True - - def test_failed_register(valid_product_design_space_data): response_data = deepcopy(valid_product_design_space_data) response_data['metadata']['status']['name'] = 'INVALID' diff --git a/tests/resources/test_design_workflows.py b/tests/resources/test_design_workflows.py index 2163b29bd..b5279d9e0 100644 --- a/tests/resources/test_design_workflows.py +++ b/tests/resources/test_design_workflows.py @@ -41,16 +41,6 @@ def collection(branch_data, collection_without_branch) -> DesignWorkflowCollecti ) -@pytest.fixture -def collection_with_branch_id(branch_data, collection_without_branch) -> DesignWorkflowCollection: - with pytest.deprecated_call(): - return DesignWorkflowCollection( - project_id=collection_without_branch.project_id, - session=collection_without_branch.session, - branch_id=uuid.UUID(branch_data['id']), - ) - - @pytest.fixture def workflow(collection, branch_data, design_workflow_dict) -> DesignWorkflow: design_workflow_dict["branch_id"] = branch_data["id"] @@ -106,20 +96,6 @@ def test_basic_methods(workflow, collection, design_workflow_dict): assert workflow.design_executions.project_id == workflow.project_id -def test_collection_branch_id(session) -> DesignWorkflowCollection: - with pytest.deprecated_call(): - DesignWorkflowCollection(project_id=uuid.uuid4(), session=session, branch_id=uuid.uuid4()) - - -def test_collection_both_ids(session) -> DesignWorkflowCollection: - with pytest.deprecated_call(): - with pytest.raises(ValueError): - DesignWorkflowCollection(project_id=uuid.uuid4(), - session=session, - branch_id=uuid.uuid4(), - branch_root_id=uuid.uuid4()) - - @pytest.mark.parametrize("optional_args", all_combination_lengths(OPTIONAL_ARGS)) def test_register(session, branch_data, workflow_minimal, collection, optional_args): workflow = workflow_minimal @@ -152,37 +128,6 @@ def test_register(session, branch_data, workflow_minimal, collection, optional_a assert_workflow(new_workflow, workflow) -@pytest.mark.parametrize("optional_args", all_combination_lengths(OPTIONAL_ARGS)) -def test_register_with_branch_id_deprecated(session, branch_data, workflow_minimal, collection_with_branch_id, optional_args): - collection = collection_with_branch_id - workflow = workflow_minimal - branch_id = branch_data['id'] - - # Set a random value for all optional args selected for this run. - for name, factory in optional_args: - setattr(workflow, name, factory()) - - # Given - post_dict = {**workflow.dump(), "branch_id": str(branch_id)} - session.set_responses({**post_dict, 'status_description': 'status'}, branch_data) - - # When - with pytest.deprecated_call(): - new_workflow = collection.register(workflow) - - # Then - assert session.calls == [ - FakeCall(method='POST', path=workflow_path(collection), json=post_dict), - FakeCall(method='GET', path=branches_path(collection, branch_id)), - ] - - with pytest.deprecated_call(): - assert str(new_workflow.branch_id) == branch_id - assert str(new_workflow.branch_root_id) == branch_data['metadata']['root_id'] - assert new_workflow.branch_version == branch_data['metadata']['version'] - assert_workflow(new_workflow, workflow) - - def test_register_conflicting_branches(session, branch_data, workflow, collection): # Given old_branch_root_id = uuid.uuid4() @@ -295,39 +240,6 @@ def test_update(session, branch_data, workflow, collection_without_branch): assert_workflow(new_workflow, workflow) -def test_update_branch_id_deprecated(session, branch_data, workflow, collection_with_branch_id): - # Given - collection = collection_with_branch_id - workflow.branch_root_id = None - workflow.branch_version = None - with pytest.deprecated_call(): - workflow.branch_id = branch_data['id'] - - post_dict = workflow.dump() - session.set_responses( - {"per_page": 1, "next": "", "response": []}, - {**post_dict, 'status_description': 'status'}, - branch_data - ) - - # When - with pytest.deprecated_call(): - new_workflow = collection.update(workflow) - - # Then - executions_path = f'/projects/{collection.project_id}/design-workflows/{workflow.uid}/executions' - assert session.calls == [ - FakeCall(method='GET', path=executions_path, params={'page': 1, 'per_page': 100}), - FakeCall(method='PUT', path=workflow_path(collection, workflow), json=post_dict), - FakeCall(method='GET', path=branches_path(collection, branch_data["id"])), - ] - assert_workflow(new_workflow, workflow) - with pytest.deprecated_call(): - assert new_workflow.branch_id == workflow.branch_id - assert new_workflow.branch_root_id == uuid.UUID(branch_data['metadata']['root_id']) - assert new_workflow.branch_version == branch_data['metadata']['version'] - - def test_update_failure_with_existing_execution(session, branch_data, workflow, collection_without_branch, design_execution_dict): branch_data_get_resp = {"response": [branch_data]} workflow.branch_root_id = uuid.uuid4() @@ -350,17 +262,6 @@ def test_update_with_mismatched_branch_root_ids(session, workflow, collection): collection.update(workflow) -def test_update_with_mismatched_branch_ids(session, workflow, collection_with_branch_id): - # Given - with pytest.deprecated_call(): - workflow.branch_id = uuid.uuid4() - - # Then/When - with pytest.deprecated_call(): - with pytest.raises(ValueError): - collection_with_branch_id.update(workflow) - - def test_update_model_missing_branch_root_id(session, workflow, collection_without_branch): # Given workflow.branch_root_id = None diff --git a/tests/resources/test_experiment_datasource.py b/tests/resources/test_experiment_datasource.py index d1432522e..37cdca38f 100644 --- a/tests/resources/test_experiment_datasource.py +++ b/tests/resources/test_experiment_datasource.py @@ -115,31 +115,6 @@ def test_list(session, collection, erds_base_path): ] -def test_list_deprecated(session, collection, erds_base_path): - branch_id = uuid.uuid4() - - session.set_response({"response": []}) - - list(collection.list()) - with pytest.deprecated_call(): - list(collection.list(branch_id=branch_id)) - list(collection.list(version=4)) - list(collection.list(version=LATEST_VER)) - with pytest.deprecated_call(): - list(collection.list(branch_id=branch_id, version=12)) - with pytest.deprecated_call(): - list(collection.list(branch_id=branch_id, version=LATEST_VER)) - - assert session.calls == [ - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "branch": str(branch_id), 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "version": 4, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "version": LATEST_VER, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "branch": str(branch_id), "version": 12, 'page': 1}), - FakeCall(method='GET', path=erds_base_path, params={'per_page': 100, "branch": str(branch_id), "version": LATEST_VER, 'page': 1}) - ] - - def test_read_and_retrieve(session, collection, erds_dict, erds_base_path): erds_id = uuid.uuid4() erds_path = f"{erds_base_path}/{erds_id}" diff --git a/tests/resources/test_file_link.py b/tests/resources/test_file_link.py index aa6bde7a3..4caf22ba6 100644 --- a/tests/resources/test_file_link.py +++ b/tests/resources/test_file_link.py @@ -9,7 +9,7 @@ from citrine.resources.api_error import ValidationError from citrine.resources.file_link import FileCollection, FileLink, GEMDFileLink, _Uploader, \ - FileProcessingType, _get_ids_from_url + _get_ids_from_url from citrine.resources.ingestion import Ingestion, IngestionCollection from citrine.exceptions import NotFound @@ -520,89 +520,6 @@ def test_external_file_download(collection: FileCollection, session, tmpdir): assert local_path.read_text() == '010111011' -def test_process_file(collection: FileCollection, session): - """Test processing an existing file.""" - - file_id, version_id = str(uuid4()), str(uuid4()) - full_url = collection._get_path(uid=file_id, version=version_id) - file_link = collection.build(FileLinkDataFactory(url=full_url, id=file_id, version=version_id)) - - job_id_resp = { - 'job_id': str(uuid4()) - } - job_execution_resp = { - 'status': 'Success', - 'job_type': 'something', - 'tasks': [] - } - file_processing_result_resp = { - 'results': [ - { - 'processing_type': 'VALIDATE_CSV', - 'data': { - 'columns': [ - { - 'name': 'a', - 'bounds': { - 'type': 'integer_bounds', - 'lower_bound': 0, - 'upper_bound': 10 - }, - 'exact_range_bounds': { - 'type': 'integer_bounds', - 'lower_bound': 0, - 'upper_bound': 10 - } - } - ], - 'record_count': 123 - } - } - ] - } - - # First does a PUT on the /processed endpoint - # then does a GET on the job executions endpoint - # then gets the file processing result - session.set_responses(job_id_resp, job_execution_resp, file_processing_result_resp) - with pytest.warns(DeprecationWarning): - collection.process(file_link=file_link, processing_type=FileProcessingType.VALIDATE_CSV) - - -def test_process_file_no_waiting(collection: FileCollection, session): - """Test processing an existing file without waiting on the result.""" - - file_id, version_id = str(uuid4()), str(uuid4()) - full_url = collection._get_path(uid=file_id, version=version_id) - file_link = collection.build(FileLinkDataFactory(url=full_url, id=file_id, version=version_id)) - - job_id_resp = { - 'job_id': str(uuid4()) - } - - # First does a PUT on the /processed endpoint - # then does a GET on the job executions endpoint - session.set_response(job_id_resp) - with pytest.warns(DeprecationWarning): - resp = collection.process(file_link=file_link, processing_type=FileProcessingType.VALIDATE_CSV, - wait_for_response=False) - assert str(resp.job_id) == job_id_resp['job_id'] - - -def test_process_file_exceptions(collection: FileCollection, session): - """Test processing an existing file without waiting on the result.""" - full_url = f'http://www.files.com/file.path' - file_link = collection.build(FileLinkDataFactory(url=full_url)) - collection._get_path() - # First does a PUT on the /processed endpoint - # then does a GET on the job executions endpoint - with pytest.raises(ValueError, match="on-platform resources"): - with pytest.warns(DeprecationWarning): - collection.process(file_link=file_link, - processing_type=FileProcessingType.VALIDATE_CSV, - wait_for_response=False) - - def test_ingest(collection: FileCollection, session): """Test the on-platform ingest route.""" good_file1 = collection.build({"filename": "good.csv", "id": str(uuid4()), "version": str(uuid4())}) diff --git a/tests/resources/test_generative_design_execution.py b/tests/resources/test_generative_design_execution.py index a956b5fad..0a5327d83 100644 --- a/tests/resources/test_generative_design_execution.py +++ b/tests/resources/test_generative_design_execution.py @@ -44,8 +44,6 @@ def test_build_new_execution(collection, generative_design_execution_dict): assert execution._session == collection.session assert execution.in_progress() and not execution.succeeded() and not execution.failed() assert execution.status_detail - with pytest.deprecated_call(): - assert execution.status_info == [detail.msg for detail in execution.status_detail] def test_trigger_execution(collection: GenerativeDesignExecutionCollection, generative_design_execution_dict, session): diff --git a/tests/resources/test_job_client.py b/tests/resources/test_job_client.py deleted file mode 100644 index a921f5270..000000000 --- a/tests/resources/test_job_client.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -from uuid import UUID, uuid4 - -from citrine.jobs.job import TaskNode, JobStatusResponse, JobSubmissionResponse -import citrine.resources.job as oldjobs -from citrine.resources.gemtables import GemTableCollection -from citrine.resources.project import Project -from citrine.resources.table_config import TableConfig -from tests.utils.session import FakeSession - - -def task_node_1() -> dict: - tn1 = {'id': 'dave_id1', 'task_type': 'dave_type', 'status': 'dave_status', - 'dependencies': ['dep1', 'dep2']} - return tn1 - - -def task_node_2() -> dict: - tn2 = {'id': 'dave_id2', 'task_type': 'dave_type', 'status': 'dave_status', 'failure_reason': 'because I failed', - 'dependencies': ['dep3', 'dep4']} - return tn2 - - -def job_status() -> dict: - js = {'job_type': "dave_job_type", 'status': "david_job_status", "tasks": [task_node_1(), task_node_2()]} - return js - - -def job_status_with_output() -> dict: - js = {'job_type': "dave_job_type", - 'status': "david_job_status", - "tasks": [task_node_1(), task_node_2()], - "output": {"key1": "val1", "key2": "val2"} - } - return js - - -@pytest.fixture -def session() -> FakeSession: - return FakeSession() - - -@pytest.fixture -def collection(session) -> GemTableCollection: - return GemTableCollection( - project_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session - ) - - -@pytest.fixture -def table_config() -> TableConfig: - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], columns=[]) - table_config.version_number = 1 - table_config.config_uid = UUID('12345678-1234-1234-1234-123456789bbb') - return table_config - - -@pytest.fixture -def project(session: FakeSession) -> Project: - project = Project( - name="Test GEM Tables project", - session=session - ) - project.uid = UUID('6b608f78-e341-422c-8076-35adc8828545') - return project - - -def test_tn_serde(): - tn = TaskNode.build(task_node_1()) - expected = task_node_1() - expected['failure_reason'] = None - assert tn.dump() == expected - - -def test_js_serde(): - js = JobStatusResponse.build(job_status()) - expected = job_status() - expected['tasks'][0]['failure_reason'] = None - expected['output'] = None - assert js.dump() == expected - - -def test_js_serde_with_output(): - js = JobStatusResponse.build(job_status_with_output()) - expected = job_status_with_output() - expected['tasks'][0]['failure_reason'] = None - assert js.dump() == expected - - -def test_build_job(collection: GemTableCollection, table_config: TableConfig): - collection.session.set_response({"job_id": '12345678-1234-1234-1234-123456789ccc'}) - resp = collection.initiate_build(table_config) - assert isinstance(resp, JobSubmissionResponse) - assert resp.job_id == UUID('12345678-1234-1234-1234-123456789ccc') - - -def test_renamed_classes_are_the_same(): - # Mostly make code coverage happy - assert issubclass(oldjobs.JobSubmissionResponse, JobSubmissionResponse) - with pytest.deprecated_call(): - oldjobs.JobSubmissionResponse.build({"job_id": uuid4()}) diff --git a/tests/resources/test_module.py b/tests/resources/test_module.py deleted file mode 100644 index 7c8543252..000000000 --- a/tests/resources/test_module.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Tests predictor collection""" -import mock -import uuid - -from citrine.resources.module import ModuleCollection -from citrine.informatics.design_spaces import ProductDesignSpace - - -def test_build(valid_product_design_space_data): - session = mock.Mock() - session.get_resource.return_value = { - 'id': str(uuid.uuid4()), - 'status': 'VALID', - 'report': {} - } - collection = ModuleCollection(uuid.uuid4(), session) - module = collection.build(valid_product_design_space_data) - assert type(module) == ProductDesignSpace diff --git a/tests/resources/test_predictor.py b/tests/resources/test_predictor.py index 6b8388d19..4610b855d 100644 --- a/tests/resources/test_predictor.py +++ b/tests/resources/test_predictor.py @@ -445,131 +445,6 @@ def test_returned_predictor(valid_graph_predictor_data): assert isinstance(result.predictors[-1], AutoMLPredictor) -@pytest.mark.parametrize("version", (2, "1", "latest", "most_recent", None)) -def test_convert_to_graph(valid_graph_predictor_data, version): - # Given - project_id = uuid.uuid4() - session = FakeSession() - collection = PredictorCollection(project_id, session) - - # Building a predictor may modify the input data object, which interferes with the test - # input later in the test. By making a copy, we don't need to care if the input is mutated. - predictor = collection.build(deepcopy(valid_graph_predictor_data)) - - session.set_response(deepcopy(valid_graph_predictor_data)) - - versions_path = _PredictorVersionCollection._path_template.format(project_id=collection.project_id, uid=predictor.uid) - entity_path = f"{versions_path}/{version or 'most_recent'}" - - # When - kwargs = {"version": version} if version is not None else {} - response = collection.convert_to_graph(predictor.uid, **kwargs) - - # Then - assert session.calls == [FakeCall(method="GET", path=f"{entity_path}/convert")] - assert response.dump() == predictor.dump() - - -@pytest.mark.parametrize("version", (2, "1", "latest", "most_recent", None)) -def test_convert_and_update(valid_graph_predictor_data, version): - # Given - project_id = uuid.uuid4() - predictor_id = valid_graph_predictor_data["id"] - session = FakeSession() - collection = PredictorCollection(project_id, session) - - # Building a graph predictor modifies the input data object, which interferes with the test - # input later in the test. By making a copy, we don't need to care if the input is mutated. - predictor = collection.build(deepcopy(valid_graph_predictor_data)) - - session.set_responses(deepcopy(valid_graph_predictor_data), deepcopy(valid_graph_predictor_data), deepcopy(valid_graph_predictor_data)) - - predictors_path = PredictorCollection._path_template.format(project_id=project_id) - versions_path = _PredictorVersionCollection._path_template.format(project_id=collection.project_id, uid=predictor.uid) - root_path = f"{predictors_path}/{predictor_id}" - entity_path = f"{versions_path}/{version or 'most_recent'}" - expected_calls = [ - FakeCall(method="GET", path=f"{entity_path}/convert"), - FakeCall(method="PUT", path=root_path, json=predictor.dump()), - FakeCall(method="PUT", path=f"{root_path}/train", params={"create_version": True}, json={}), - ] - - # When - kwargs = {"version": version} if version is not None else {} - response = collection.convert_and_update(predictor.uid, **kwargs) - - # Then - assert session.calls == expected_calls - assert response.dump() == predictor.dump() - - -@pytest.mark.parametrize("version", (2, "1", "latest", "most_recent", None)) -@pytest.mark.parametrize("error_args", ((400, BadRequest), (409, Conflict))) -@pytest.mark.parametrize("method_name", ("convert_to_graph", "convert_and_update")) -def test_convert_and_update_errors(version, error_args, method_name): - # Given - project_id = uuid.uuid4() - predictor_id = uuid.uuid4() - session = FakeSession() - collection = PredictorCollection(project_id, session) - convert_path = f"/projects/{project_id}/predictors/{predictor_id}/versions/{version or 'most_recent'}/convert" - - error_code, error_cls = error_args[:] - response = FakeRequestResponse(error_code) - response.request.method = "GET" - session.set_response(error_cls(convert_path, response)) - - # When - kwargs = {"version": version} if version is not None else {} - method = getattr(collection, method_name) - with pytest.raises(error_cls): - method(predictor_id, **kwargs) - - # Then - assert session.num_calls == 1 - expected_call_convert = FakeCall(method="GET", path=convert_path) - assert session.last_call == expected_call_convert - - -@pytest.mark.parametrize("version", (2, "1", "latest", "most_recent", None)) -@pytest.mark.parametrize("method_name", ("convert_to_graph", "convert_and_update")) -def test_convert_auto_retrain(valid_graph_predictor_data, version, method_name): - # Given - project_id = uuid.uuid4() - predictor_id = valid_graph_predictor_data["id"] - session = FakeSession() - collection = PredictorCollection(project_id, session) - predictors_path = collection._path_template.format(project_id=project_id) - version_path = f"{predictors_path}/{predictor_id}/versions/{version or 'most_recent'}" - convert_path = f"{version_path}/convert" - train_path = f"{version_path}/train" - - # Building a graph predictor modifies the input data object, which interferes with the test - # input later in the test. By making a copy, we don't need to care if the input is mutated. - predictor = collection.build(deepcopy(valid_graph_predictor_data)) - - response = FakeRequestResponse(409) - response.request.method = "GET" - - session.set_responses( - Conflict(convert_path, response), - deepcopy(valid_graph_predictor_data) - ) - - # When - method = getattr(collection, method_name) - kwargs = {"version": version} if version is not None else {} - response = method(predictor_id, retrain_if_needed=True, **kwargs) - - # Then - expected_calls = [ - FakeCall(method="GET", path=convert_path), - FakeCall(method="PUT", path=train_path, params={"create_version": True}, json={}), - ] - assert session.calls == expected_calls - assert response is None - - def test_predictor_list_archived(valid_graph_predictor_data): # Given session = FakeSession() diff --git a/tests/resources/test_predictor_evaluation_executions.py b/tests/resources/test_predictor_evaluation_executions.py index 520f052cc..312701ad7 100644 --- a/tests/resources/test_predictor_evaluation_executions.py +++ b/tests/resources/test_predictor_evaluation_executions.py @@ -61,8 +61,6 @@ def test_build_new_execution(collection, predictor_evaluation_execution_dict): assert execution._session == collection.session assert execution.in_progress() and not execution.succeeded() and not execution.failed() assert execution.status_detail - with pytest.deprecated_call(): - assert execution.status_info == [detail.msg for detail in execution.status_detail] def test_workflow_execution_results(workflow_execution: PredictorEvaluationExecution, session, @@ -156,9 +154,3 @@ def test_restore(workflow_execution, collection): def test_delete(collection): with pytest.raises(NotImplementedError): collection.delete(uuid.uuid4()) - - -def test_deprecated(): - from citrine.resources.predictor_evaluation_execution import PredictorEvaluationExecution - with pytest.deprecated_call(): - PredictorEvaluationExecution() diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index 445f8a6d3..5900f7c89 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -183,11 +183,6 @@ def test_design_spaces_get_project_id(project): assert project.uid == project.design_spaces.project_id -def test_modules_get_project_id_deprecated(project): - with pytest.deprecated_call(): - assert project.uid == project.modules.project_id - - def test_descriptors_get_project_id(project): assert project.uid == project.descriptors.project_id diff --git a/tests/resources/test_workflow.py b/tests/resources/test_workflow.py index 84211d10a..2151ac551 100644 --- a/tests/resources/test_workflow.py +++ b/tests/resources/test_workflow.py @@ -78,14 +78,3 @@ def test_list_workflows(session, basic_design_workflow_data): assert 2 == session.num_calls assert len(workflows) == 1 assert isinstance(workflows[0], DesignWorkflow) - - -def test_status_info(session, failed_design_workflow_data): - branch_data = BranchDataFactory() - session.set_response(branch_data) - dwc = DesignWorkflowCollection(project_id=uuid.uuid4(), session=session) - - dw = dwc.build(failed_design_workflow_data) - - with pytest.deprecated_call(): - assert dw.status_info == [status.msg for status in dw.status_detail] diff --git a/tests/utils/fakes/fake_workflow_collection.py b/tests/utils/fakes/fake_workflow_collection.py index e9dad1936..89651dfa1 100644 --- a/tests/utils/fakes/fake_workflow_collection.py +++ b/tests/utils/fakes/fake_workflow_collection.py @@ -24,10 +24,9 @@ def register(self, workflow: WorkflowType) -> WorkflowType: workflow.project_id = self.project_id return workflow - def archive(self, uid: Union[UUID, str] = None, workflow_id: Union[UUID, str] = None): + def archive(self, uid: Union[UUID, str]): # Search for workflow via UID to ensure exists # If found, flip archived=True with no return - uid = migrate_deprecated_argument(uid, "uid", workflow_id, "workflow_id") workflow = self.get(uid) workflow.archived = True self.update(workflow) @@ -48,4 +47,4 @@ def create_default(self, *, predictor_id: UUID) -> PredictorEvaluationWorkflow: pew.project_id = self.project_id pew.uid = uuid4() pew._session = self.session - return pew \ No newline at end of file + return pew From 0339e4f29da2fc91ba70df24f81a1b9091cc3bff Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Tue, 2 Jan 2024 11:40:44 -0500 Subject: [PATCH 08/22] [PLA-12471] Change ratio constraint basis types. basis_ingredients and basis_labels are treated as lists of names now, so the types should match. Additionally, the basis_ingredient_names and basis_label_names methods should be deprecated. They were only added to allow a smooth transition to 3.x. --- .../ingredient_ratio_constraint.py | 38 ++++++++------- tests/informatics/test_constraints.py | 46 +++++++------------ 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py index d86dcd7f3..0149a0bfd 100644 --- a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py @@ -1,5 +1,5 @@ import warnings -from typing import Set, Mapping, Optional, Tuple, Union +from typing import Set, Optional, Tuple from citrine._serialization import properties from citrine._serialization.serializable import Serializable @@ -64,8 +64,8 @@ def __init__(self, *, max: float, ingredient: Optional[Tuple[str, float]] = None, label: Optional[Tuple[str, float]] = None, - basis_ingredients: Union[Set[str], Mapping[str, float]] = set(), - basis_labels: Union[Set[str], Mapping[str, float]] = set()): + basis_ingredients: Set[str] = set(), + basis_labels: Set[str] = set()): self.formulation_descriptor = formulation_descriptor self.min = min self.max = max @@ -95,50 +95,54 @@ def label(self, value: Optional[Tuple[str, float]]): self._labels = self._numerator_validate(value, "Label") @property - def basis_ingredients(self) -> Mapping[str, float]: + def basis_ingredients(self) -> Set[str]: """Retrieve the ingredients in the denominator of the ratio.""" - warnings.warn("basis_ingredients is deprecated as of 2.13.0 and will change in 3.0. " - "Please use basis_ingredient_names instead.", DeprecationWarning) - return self._basis_ingredients + return set(self._basis_ingredients.keys()) @basis_ingredients.setter def basis_ingredients(self, value: Set[str]): """Set the ingredients in the denominator of the ratio.""" - self.basis_ingredient_names = value + self._basis_ingredients = dict.fromkeys(value, 1) @property def basis_ingredient_names(self) -> Set[str]: """Retrieve the names of all ingredients in the denominator of the ratio.""" - return set(self._basis_ingredients.keys()) + warnings.warn("basis_ingredient_names is deprecated as of 3.0.0 and will be dropped in " + "4.0. Please use basis_ingredients instead.", DeprecationWarning) + return self.basis_ingredients # This is for symmetry; it's not strictly necessary. @basis_ingredient_names.setter def basis_ingredient_names(self, value: Set[str]): """Set the names of all ingredients in the denominator of the ratio.""" - self._basis_ingredients = dict.fromkeys(value, 1) + warnings.warn("basis_ingredient_names is deprecated as of 3.0.0 and will be dropped in " + "4.0. Please use basis_ingredients instead.", DeprecationWarning) + self.basis_ingredients = value @property - def basis_labels(self) -> Mapping[str, float]: + def basis_labels(self) -> Set[str]: """Retrieve the labels in the denominator of the ratio.""" - warnings.warn("basis_labels is deprecated as of 2.13.0 and will change in 3.0. Please use " - "basis_label_names instead.", DeprecationWarning) - return self._basis_labels + return set(self._basis_labels.keys()) @basis_labels.setter def basis_labels(self, value: Set[str]): """Set the labels in the denominator of the ratio.""" - self.basis_label_names = value + self._basis_labels = dict.fromkeys(value, 1) @property def basis_label_names(self) -> Set[str]: """Retrieve the names of all labels in the denominator of the ratio.""" - return set(self._basis_labels.keys()) + warnings.warn("basis_label_names is deprecated as of 3.0.0 and will be dropped in 4.0. " + "Please use basis_labels instead.", DeprecationWarning) + return self.basis_labels # This is for symmetry; it's not strictly necessary. @basis_label_names.setter def basis_label_names(self, value: Set[str]): """Set the names of all labels in the denominator of the ratio.""" - self._basis_labels = dict.fromkeys(value, 1) + warnings.warn("basis_label_names is deprecated as of 3.0.0 and will be dropped in 4.0. " + "Please use basis_labels instead.", DeprecationWarning) + self.basis_labels = value def _numerator_read(self, num_dict): if num_dict: diff --git a/tests/informatics/test_constraints.py b/tests/informatics/test_constraints.py index eab638042..78fb32f69 100644 --- a/tests/informatics/test_constraints.py +++ b/tests/informatics/test_constraints.py @@ -142,8 +142,8 @@ def test_ingredient_ratio_initialization(ingredient_ratio_constraint): assert ingredient_ratio_constraint.max == 1e6 assert ingredient_ratio_constraint.ingredient == ("foo", 1.0) assert ingredient_ratio_constraint.label == ("foolabel", 0.5) - assert ingredient_ratio_constraint.basis_ingredient_names == {"baz", "bat"} - assert ingredient_ratio_constraint.basis_label_names == {"bazlabel", "batlabel"} + assert ingredient_ratio_constraint.basis_ingredients == {"baz", "bat"} + assert ingredient_ratio_constraint.basis_labels == {"bazlabel", "batlabel"} def test_ingredient_ratio_interaction(ingredient_ratio_constraint): @@ -181,43 +181,31 @@ def test_ingredient_ratio_interaction(ingredient_ratio_constraint): ingredient_ratio_constraint.label = [] assert ingredient_ratio_constraint.label is None - newval_dict = {"foobasis": 3} - with pytest.deprecated_call(): - ingredient_ratio_constraint.basis_ingredients = newval_dict - with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_ingredients == dict.fromkeys(newval_dict.keys(), 1) - ingredient_ratio_constraint.basis_ingredient_names = set(newval_dict.keys()) - - newval_set = {"foobasis2"} + newval_set = {"foobasis1"} ingredient_ratio_constraint.basis_ingredients = newval_set + assert ingredient_ratio_constraint.basis_ingredients == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_ingredients == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_ingredient_names = newval_set + assert ingredient_ratio_constraint.basis_ingredient_names == newval_set - newval_set = {"foobasis3"} - ingredient_ratio_constraint.basis_ingredient_names = newval_set - with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_ingredients == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_ingredient_names = newval_set - - newval_dict = {"foolabelbasis": 3} + newval_set = {"foobasis2"} with pytest.deprecated_call(): - ingredient_ratio_constraint.basis_labels = newval_dict + ingredient_ratio_constraint.basis_ingredient_names = newval_set + assert ingredient_ratio_constraint.basis_ingredients == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_labels == dict.fromkeys(newval_dict.keys(), 1) - ingredient_ratio_constraint.basis_label_names = set(newval_dict.keys()) + assert ingredient_ratio_constraint.basis_ingredient_names == newval_set - newval_set = {"foolabelbasis2"} + newval_set = {"foolabelbasis1"} ingredient_ratio_constraint.basis_labels = newval_set + assert ingredient_ratio_constraint.basis_labels == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_labels == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_label_names = newval_set + assert ingredient_ratio_constraint.basis_label_names == newval_set - newval_set = {"foolabelbasis3"} - ingredient_ratio_constraint.basis_label_names = newval_set + newval_set = {"foolabelbasis1"} + with pytest.deprecated_call(): + ingredient_ratio_constraint.basis_label_names = newval_set + assert ingredient_ratio_constraint.basis_labels == newval_set with pytest.deprecated_call(): - assert ingredient_ratio_constraint.basis_labels == dict.fromkeys(newval_set, 1) - ingredient_ratio_constraint.basis_label_names = newval_set + assert ingredient_ratio_constraint.basis_label_names == newval_set def test_range_defaults(): From 1847dc7efc79d5b0e0758749afb8627c59f11636 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 4 Jan 2024 13:21:30 -0500 Subject: [PATCH 09/22] [PLA-13068] Drop citrine.builders. It is a little used package, largely unmaintained, and one not many people know exists. Anything useful in it almost certainly belongs in citrinade instead. --- setup.py | 4 - src/citrine/builders/__init__.py | 1 - src/citrine/builders/auto_configure.py | 742 ------------------------ src/citrine/builders/descriptors.py | 237 -------- src/citrine/builders/design_spaces.py | 340 ----------- src/citrine/builders/predictors.py | 260 --------- src/citrine/builders/scores.py | 82 --- test_requirements.txt | 1 - tests/builders/test_auto_configure.py | 471 --------------- tests/builders/test_descriptors.py | 252 -------- tests/builders/test_design_spaces.py | 394 ------------- tests/builders/test_predictors.py | 124 ---- tests/builders/test_scores.py | 78 --- tests/informatics/test_design_spaces.py | 2 +- 14 files changed, 1 insertion(+), 2987 deletions(-) delete mode 100644 src/citrine/builders/__init__.py delete mode 100644 src/citrine/builders/auto_configure.py delete mode 100644 src/citrine/builders/descriptors.py delete mode 100644 src/citrine/builders/design_spaces.py delete mode 100644 src/citrine/builders/predictors.py delete mode 100644 src/citrine/builders/scores.py delete mode 100644 tests/builders/test_auto_configure.py delete mode 100644 tests/builders/test_descriptors.py delete mode 100644 tests/builders/test_design_spaces.py delete mode 100644 tests/builders/test_predictors.py delete mode 100644 tests/builders/test_scores.py diff --git a/setup.py b/setup.py index 8e3336c55..8641ed398 100644 --- a/setup.py +++ b/setup.py @@ -33,13 +33,9 @@ "tqdm>=4.62.3,<5" ], extras_require={ - "builders": [ - "pandas>=1.3.5,<3" - ], "../tests": [ "factory-boy>=2.12.0,<4", "mock>=3.0.5,<5", - "pandas>=1.3.5,<3", "pytest>=6.2.5,<8", "pytz>=2020.1", "requests-mock>=1.7.0,<2", diff --git a/src/citrine/builders/__init__.py b/src/citrine/builders/__init__.py deleted file mode 100644 index 5bd9c51bf..000000000 --- a/src/citrine/builders/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tools for building custom SL assets client-side.""" diff --git a/src/citrine/builders/auto_configure.py b/src/citrine/builders/auto_configure.py deleted file mode 100644 index c52b21eb5..000000000 --- a/src/citrine/builders/auto_configure.py +++ /dev/null @@ -1,742 +0,0 @@ -from logging import getLogger -from uuid import UUID -from typing import Union, Optional, List - -from gemd.entity.link_by_uid import LinkByUID -from gemd.enumeration.base_enumeration import BaseEnumeration - -from citrine.seeding.find_or_create import find_collection, create_or_update -from citrine.jobs.waiting import wait_while_validating -from citrine.informatics.data_sources import GemTableDataSource -from citrine.informatics.executions import DesignExecution -from citrine.informatics.design_candidate import DesignCandidate -from citrine.informatics.design_spaces import DesignSpace -from citrine.informatics.predictor_evaluator import PredictorEvaluator -from citrine.informatics.objectives import Objective -from citrine.informatics.scores import Score -from citrine.informatics.workflows import DesignWorkflow, PredictorEvaluationWorkflow - -from citrine.resources.gemtables import GemTable -from citrine.resources.material_run import MaterialRun -from citrine.resources.predictor import GraphPredictor, AutoConfigureMode -from citrine.resources.project import Project -from citrine.resources.table_config import TableConfig, TableBuildAlgorithm - -from citrine.builders.scores import create_default_score - -logger = getLogger(__name__) - - -class AutoConfigureStatus(BaseEnumeration): - """[ALPHA] The current status of the AutoConfigureWorkflow. - - * START is the initial status before auto-configuration - * TABLE_BUILD is the status after creating a default GEM table - * PREDICTOR_CREATED is the status after creating a default predictor - * PREDICTOR_INVALID is the status if predictor validation fails - * PEW_CREATED is the status after creating a predictor evaluation workflow - * PEW_FAILED is the status if predictor evaluation workflow validation fails - * DESIGN_SPACE_CREATED is the status after design space creation/registration - * DESIGN_SPACE_INVALID is the status if design space validation fails - * DESIGN_WORKFLOW_CREATED is the status after design workflow creation - * DESIGN_WORKFlOW_FAILED is the status if design workflow validation fails - """ - - START = 'START' - TABLE_CREATED = 'TABLE CREATED' - PREDICTOR_CREATED = 'PREDICTOR CREATED' - PREDICTOR_INVALID = 'PREDICTOR INVALID' - PEW_CREATED = 'PREDICTOR EVALUATION WORKFLOW CREATED' - PEW_FAILED = 'PREDICTOR EVALUATION WORKFLOW FAILED' - DESIGN_SPACE_CREATED = 'DESIGN SPACE CREATED' - DESIGN_SPACE_INVALID = 'DESIGN SPACE INVALID' - DESIGN_WORKFLOW_CREATED = 'DESIGN WORKFLOW CREATED' - DESIGN_WORKFLOW_FAILED = 'DESIGN WORKFLOW FAILED' - - -class AutoConfigureWorkflow(): - """[ALPHA] Helper class for configuring and storing default assets on the Citrine Platform. - - Defines methods for creating a default GEM table, predictor, predictor evaluations, - design space, and design workflow in a linear fashion, - starting from a provided material/table/predictor. - - All assets that are registered to the Citrine Platform - during the auto configuration steps are stored as members of this class. - In case this method fails during asset validation, - the previously configured items are accessible - locally and on the Citrine Platform. - - Initializing an AutoConfigureWorkflow will search for assets - on the Citrine Platform based on the specified name, - and re-use/update these items during subsequent runs. - Currently, assets are searched for by name and type in the project, - and duplicately named assets will result in an error being thrown. - - Example usage: - ``` - # Initialize score and workflow - score = LIScore(objectives=[ScalarMaxObjective(descriptor_key='Desc 1'), baselines=[1.0]) - auto_config = AutoConfigureWorkflow(project=project, name='My Project Name') - - # GEM table -> Predictor -> PEW/PEE -> Design Space -> Design Workflow -> Design Execution - auto_config.from_material( - material=sample_material, - score=score, - mode=AutoConfigureMode.PLAIN, - print_status_info=True - ) - - # Check the most recent status - print(f"Workflow ended with status: {auto_config.status}") - - # Retrieve some of the results - table = auto_config.table - predictor = auto_config.predictor - design_space = auto_config.design_space - workflow = auto_config.design_workflow - ``` - - Parameters - ---------- - project: Project - Project to use when accessing the Citrine Platform. - name: str - Name to affix to auto-configured assets on the Citrine Platform. - This name is used as an identifier for finding and re-using - assets related to a given workflow. - - """ - - def __init__(self, *, project: Project, name: str): - self._project = project - self._name = name - self._status = AutoConfigureStatus.START - self._status_info = [] - - # Blank initialize assets - self._table_config = None - self._table = None - self._predictor = None - self._predictor_evaluation_workflow = None - self._design_space = None - self._design_workflow = None - self._design_execution = None - - separator = ": " if self.name else "" - self._default_asset_names = { - "TABLE_CONFIG": f"{self.name}{separator}Auto Configure GEM Table", - "PREDICTOR": f"{self.name}{separator}Auto Configure Predictor", - "PEW": f"{self.name}{separator}Auto Configure PEW", - "DESIGN_SPACE": f"{self.name}{separator}Auto Configure Design Space", - "DESIGN_WORKFLOW": f"{self.name}{separator}Auto Configure Design Workflow" - } - - # Search project for current status - self.update() - - @staticmethod - def _print_status(msg: str): - logger.info(f"AutoConfigureWorkflow: {msg}") - - @property - def project(self) -> Project: - """Get the project used when automatically configuring assets.""" - return self._project - - @property - def name(self) -> str: - """Get the naming label for assets configured by this object.""" - return self._name - - @property - def status(self) -> str: - """Get the most recent status of the auto-configure workflow.""" - return self._status.value - - @property - def status_info(self) -> List[str]: - """Get the most recent status info from asset configuration/validation.""" - return self._status_info - - @property - def table_config(self) -> Optional[TableConfig]: - """Get the table config associated with this workflow.""" - return self._table_config - - @property - def table(self) -> Optional[GemTable]: - """Get the GEM table associated with this workflow.""" - return self._table - - @property - def predictor(self) -> Optional[GraphPredictor]: - """Get the predictor associated with this workflow.""" - return self._predictor - - @property - def predictor_evaluation_workflow(self) -> Optional[PredictorEvaluationWorkflow]: - """Get the predictor evaluation workflow associated with this workflow.""" - return self._predictor_evaluation_workflow - - @property - def design_space(self) -> Optional[DesignSpace]: - """Get the design space associated with this workflow.""" - return self._design_space - - @property - def design_workflow(self) -> Optional[DesignWorkflow]: - """Get the design workflow associated with this workflow.""" - return self._design_workflow - - @property - def design_execution(self) -> Optional[DesignExecution]: - """Get the most recent design execution from this workflow.""" - return self._design_execution - - @property - def score(self) -> Optional[Score]: - """Get the most recent score executed by this workflow.""" - de = self.design_execution - return de.score if de is not None else None - - @property - def candidates(self) -> List[DesignCandidate]: - """Get the candidate list from the most recent design execution.""" - de = self.design_execution - return list(de.candidates()) if de is not None else [] - - @property - def assets(self): - """Get all assets configured by this object.""" - initial_assets = [ - self.table_config, self.table, self.predictor, - self.predictor_evaluation_workflow, - self.design_space, self.design_workflow, - ] - return [asset for asset in initial_assets if asset is not None] - - def update(self): - """Search for existing assets matching the workflow name and update its status.""" - self._update_assets() - self._update_status() - - def _update_assets(self): - """Find and store all assets on the platform matching the workflow name.""" - # Table config - if self.table_config is None: - self._table_config = find_collection( - collection=self.project.table_configs, - name=self._default_asset_names["TABLE_CONFIG"] - ) - else: - self._table_config = self.project.table_configs.get(self.table_config.uid) - logger.info("Found existing: {}".format(self.table_config)) - - # Table - if self.table is None: - if self.table_config is not None: - self._table = next(self.project.tables.list_by_config(self.table_config.uid), None) - if self.table is not None: - logger.info("Found existing: {}".format(self.table)) - else: - self._table = self.project.tables.get(self.table.uid) - logger.info("Found existing: {}".format(self.table)) - - # Predictor - if self.predictor is None: - self._predictor = find_collection( - collection=self.project.predictors, - name=self._default_asset_names["PREDICTOR"] - ) - else: - self._predictor = self.project.predictors.get(self.predictor.uid) - logger.info("Found existing: {}".format(self.predictor)) - - # PEW - if self.predictor_evaluation_workflow is None: - self._predictor_evaluation_workflow = find_collection( - collection=self.project.predictor_evaluation_workflows, - name=self._default_asset_names["PEW"] - ) - else: - self._predictor_evaluation_workflow = self.project.predictor_evaluation_workflows.get( - self.predictor_evaluation_workflow.uid - ) - logger.info("Found existing: {}".format(self.predictor_evaluation_workflow)) - - # Design space - if self.design_space is None: - self._design_space = find_collection( - collection=self.project.design_spaces, - name=self._default_asset_names["DESIGN_SPACE"] - ) - else: - self._design_space = self.project.design_spaces.get(self.design_space.uid) - logger.info("Found existing: {}".format(self.design_space)) - - # Design workflow - if self.design_workflow is None: - self._design_workflow = find_collection( - collection=self.project.design_workflows, - name=self._default_asset_names["DESIGN_WORKFLOW"] - ) - else: - self._design_workflow = self.project.design_workflows.get(self.design_workflow.uid) - logger.info("Found existing: {}".format(self.design_workflow)) - - def _update_status(self): - """Update status info based on currently stored assets.""" - # Work backwards from end of workflow, return early when possible - if self.design_workflow is not None: - self._status_info = [detail.msg for detail in self.design_workflow.status_detail] - if self.design_workflow.failed(): - self._status = AutoConfigureStatus.DESIGN_WORKFLOW_FAILED - else: - self._status = AutoConfigureStatus.DESIGN_WORKFLOW_CREATED - return - - if self.design_space is not None: - self._status_info = [detail.msg for detail in self.predictor.status_detail] - if self.design_space.failed(): - self._status = AutoConfigureStatus.DESIGN_SPACE_INVALID - else: - self._status = AutoConfigureStatus.DESIGN_SPACE_CREATED - return - - if self.predictor_evaluation_workflow is not None: - pew_status_detail = self.predictor_evaluation_workflow.status_detail - self._status_info = [detail.msg for detail in pew_status_detail] - if self.predictor_evaluation_workflow.failed(): - self._status = AutoConfigureStatus.PEW_FAILED - else: - self._status = AutoConfigureStatus.PEW_CREATED - return - - if self.predictor is not None: - self._status_info = [detail.msg for detail in self.predictor.status_detail] - if self.predictor.failed(): - self._status = AutoConfigureStatus.PREDICTOR_INVALID - else: - self._status = AutoConfigureStatus.PREDICTOR_CREATED - return - - if self.table is not None: - self._status = AutoConfigureStatus.TABLE_CREATED - return - - self._status = AutoConfigureStatus.START - - def execute(self, *, score: Union[Score, Objective]) -> DesignExecution: - """[ALPHA] Execute a design execution using the provided score/objective. - - A design workflow must be present after auto-configuration to execute the score. - This method can be called after modifying auto-configured assets - to create a new, custom design execution. - - Parameters - ---------- - score: Union[Score, Objective] - A score or objective used to rank candidates during design execution. - If an objective is passed, a default score is constructed - by reading the GEM table associated with this workflow - to extract baseline information. - - Returns - ---------- - DesignExecution - The design execution triggered with the provided score/objective. - - """ - if self.design_workflow is None: - raise ValueError("Design workflow is missing, cannot execute score.") - - if isinstance(score, Objective): - score = create_default_score(objectives=score, project=self.project, table=self.table) - - self._design_execution = self.design_workflow.design_executions.trigger(score) - return self.design_execution - - def from_material( - self, - *, - material: Union[str, UUID, LinkByUID, MaterialRun], - evaluator: Optional[PredictorEvaluator] = None, - design_space: Optional[DesignSpace] = None, - score: Optional[Union[Score, Objective]] = None, - mode: AutoConfigureMode = AutoConfigureMode.PLAIN, - print_status_info: bool = False, - prefer_valid: bool = True, - ): - """[ALPHA] Auto configure platform assets from material history to design workflow. - - Given a material on the Citrine Platform, - configures a default GEM table, predictor, design space, and design workflow. - - An optional evaluator, design_space, and score can be provided: - evaluator -> A PredictorEvaluator to be used in place of the default - design_space -> A DesignSpace to be used in place of the default - score -> Triggers a design execution from the auto-configured design workflow - - Parameters - ---------- - material: Union[str, UUID, LinkByUID, MaterialRun] - A representation of the material to configure a - default table, predictor, and design space from. - evaluator: Optional[PredictorEvaluator] - A PredictorEvaluator to use in place of the default. - Must contain responses matching outputs of the default predictor. - Default: None - design_space: Optional[DesignSpace] - A DesignSpace object to use in place of the default. - If not registered already, will be registered during configuration. - Default: None - score: Optional[Union[Score, Objective]] - A score or objective used to rank candidates during design execution. - If only an objective is passed, a default `LIScore` is constructed - by reading the GEM table associated with this workflow - to extract baseline scoring information. - Default: None - mode: AutoConfigureMode - The method to be used in the automatic table and predictor configuration. - Default: AutoConfigureMode.PLAIN - print_status_info: bool - Whether to print the status info during validation of assets. - Default: False - prefer_valid: Boolean - If True, enables filtering of sparse descriptors and trimming of - excess graph components in attempt to return a default configuration - that will pass validation. - Default: True. - - """ - if not isinstance(mode, AutoConfigureMode): - raise TypeError('mode must be an option from AutoConfigureMode') - - # Run table build stage - self._table_build_stage(material=material, mode=mode) - - # Finish workflow from table stage - self.from_table( - table=self.table, score=score, evaluator=evaluator, - design_space=design_space, mode=mode, print_status_info=print_status_info, - prefer_valid=prefer_valid - ) - - def from_table( - self, - *, - table: GemTable, - evaluator: Optional[PredictorEvaluator] = None, - design_space: Optional[DesignSpace] = None, - score: Optional[Union[Score, Objective]] = None, - mode: AutoConfigureMode = AutoConfigureMode.PLAIN, - print_status_info: bool = False, - prefer_valid: bool = True, - ): - """[ALPHA] Auto configure platform assets from GEM table to design workflow. - - Given a GEM table on the Citrine Platform, - creates a default predictor, design space, and design workflow. - - An optional evaluator, design_space, and score can be provided: - evaluator -> A PredictorEvaluator to be used in place of the default - design_space -> A DesignSpace to be used in place of the default - score -> Triggers a design execution from the auto-configured design workflow - - Parameters - ---------- - table: Table - A GEM table to configure a default predictor, - design space, and design workflow from. - evaluator: Optional[PredictorEvaluator] - A PredictorEvaluator to use in place of the default. - Must contain responses matching outputs of the default predictor. - Default: None - design_space: Optional[DesignSpace] - A DesignSpace object to use in place of the default. - If not registered already, will be registered during configuration. - Default: None - score: Optional[Union[Score, Objective]] - A score or objective used to rank candidates during design execution. - If only an objective is passed, a default `LIScore` is constructed - by reading the GEM table associated with this workflow - to extract baseline scoring information. - Default: None - mode: AutoConfigureMode - The method to be used in the automatic predictor configuration. - Default: AutoConfigureMode.PLAIN - print_status_info: bool - Whether to print the status info during validation of assets. - Default: False - prefer_valid: Boolean - If True, enables filtering of sparse descriptors and trimming of - excess graph components in attempt to return a default configuration - that will pass validation. - Default: True. - - """ - if not isinstance(mode, AutoConfigureMode): - raise TypeError('mode must be an option from AutoConfigureMode') - - self._table = table # Save here, but cannot force rename w/o config - - # Get default predictor, pass to next stage for registration - data_source = GemTableDataSource(table_id=table.uid, table_version=table.version) - predictor = self.project.predictors.create_default( - training_data=data_source, pattern=mode.value, # Uses same string pattern - prefer_valid=prefer_valid - ) - - # Finish workflow from predictor stage - self.from_predictor( - predictor=predictor, score=score, evaluator=evaluator, - design_space=design_space, print_status_info=print_status_info - ) - - def from_predictor( - self, - *, - predictor: GraphPredictor, - evaluator: Optional[PredictorEvaluator] = None, - design_space: Optional[DesignSpace] = None, - score: Optional[Union[Score, Objective]] = None, - print_status_info: bool = False - ): - """[ALPHA] Auto configure platform assets from predictor to design workflow. - - Given a predictor on the Citrine Platform, - creates a default design space and design workflow. - - An optional evaluator, design_space, and score can be provided: - evaluator -> A PredictorEvaluator to be used in place of the default - design_space -> A DesignSpace to be used in place of the default - score -> Triggers a design execution from the auto-configured design workflow - - Parameters - ---------- - predictor: Predictor - A registered predictor to configure a default design space and design workflow from. - evaluator: Optional[PredictorEvaluator] - A PredictorEvaluator to use in place of the default. - Must contain responses matching outputs of the default predictor. - Default: None - design_space: Optional[DesignSpace] - A DesignSpace object to use in place of the default. - If not registered already, will be registered during configuration. - Default: None - score: Optional[Union[Score, Objective]] - A score or objective used to rank candidates during design execution. - If only an objective is passed, a default `LIScore` is constructed - by reading the GEM table associated with this workflow - to extract baseline scoring information. - Default: None - print_status_info: bool - Whether to print the status info during validation of assets. - Default: False - - """ - # Run the predictor registration stage - self._predictor_registration_stage( - predictor=predictor, - print_status_info=print_status_info - ) - - # Run the predictor evaluation stage - self._predictor_evaluation_stage( - predictor=self.predictor, - evaluator=evaluator, - print_status_info=print_status_info - ) - - # Run the design space stage - self._design_space_build_stage( - predictor=self.predictor, - design_space=design_space, - print_status_info=print_status_info - ) - - # Run the design workflow build stage, trigger score if provided - self._design_workflow_build_stage( - predictor=self.predictor, - design_space=self.design_space, # Use the object from previous stage - score=score, - print_status_info=print_status_info - ) - - def _table_build_stage( - self, - *, - material: Union[str, UUID, LinkByUID, MaterialRun], - mode: AutoConfigureMode - ): - """Create a default GEM table from a material.""" - self._print_status("Configuring GEM table...") - - # TODO: package-wide formatting enum for method - table_algorithm_map = { - 'PLAIN': TableBuildAlgorithm.SINGLE_ROW, - 'FORMULATION': TableBuildAlgorithm.FORMULATIONS - } - table_algorithm = table_algorithm_map[mode.value] - - table_config, _ = self.project.table_configs.default_for_material( - material=material, - name=self._default_asset_names["TABLE_CONFIG"], - algorithm=table_algorithm - ) - table_config = create_or_update( - collection=self.project.table_configs, - resource=table_config - ) - - self._table_config = table_config - self._table = self.project.tables.build_from_config(self.table_config) - self._status = AutoConfigureStatus.TABLE_CREATED - - def _predictor_registration_stage( - self, - *, - predictor: GraphPredictor, - print_status_info: bool - ): - """Register and validate a provided predictor.""" - self._print_status("Configuring predictor...") - - predictor.name = self._default_asset_names["PREDICTOR"] - predictor = create_or_update( - collection=self.project.predictors, - resource=predictor - ) - predictor = wait_while_validating( - collection=self.project.predictors, - module=predictor, - print_status_info=print_status_info - ) - - self._predictor = predictor - self._status = AutoConfigureStatus.PREDICTOR_CREATED - self._status_info = [detail.msg for detail in predictor.status_detail] - - if predictor.failed(): - self._status = AutoConfigureStatus.PREDICTOR_INVALID - raise RuntimeError("Predictor is invalid," - " cannot proceed to design space configuration.") - - def _predictor_evaluation_stage( - self, - *, - predictor: GraphPredictor, - evaluator: Optional[PredictorEvaluator], - print_status_info: bool - ): - """Evaluate the predictor with the specified evaluator, or create a default workflow.""" - self._print_status("Configuring predictor evaluation...") - - if evaluator is None: - # No evaluator, so create a default PEW for the predictor - pew = self.project.predictor_evaluation_workflows.create_default( - predictor_id=predictor.uid - ) - else: - # We got an evaluator, so make a new PEW and register it manually - pew = PredictorEvaluationWorkflow( - name=self._default_asset_names["PEW"], - evaluators=[evaluator] - ) - pew = create_or_update( - collection=self.project.predictor_evaluation_workflows, - resource=pew - ) - - pew = wait_while_validating( - collection=self.project.predictor_evaluation_workflows, - module=pew, - print_status_info=print_status_info - ) - - self._predictor_evaluation_workflow = pew - self._status = AutoConfigureStatus.PEW_CREATED - self._status_info = [detail.msg for detail in pew.status_detail] - - if pew.failed(): - # Can proceed without raising error, but can't get PEE - self._status = AutoConfigureStatus.PEW_FAILED - logger.warning( - "Predictor evaluation workflow failed -- unable to configure execution." - ) - elif evaluator is not None: - # Manually trigger execution - pew.executions.trigger(predictor.uid) - - def _design_space_build_stage( - self, - *, - predictor: GraphPredictor, - design_space: Optional[DesignSpace], - print_status_info: bool - ): - """Create a design space from a specified predictor, or use the provided one.""" - self._print_status("Configuring design space...") - - if design_space is None: - design_space = self.project.design_spaces.create_default(predictor_id=predictor.uid) - - design_space.name = self._default_asset_names["DESIGN_SPACE"] - design_space = create_or_update( - collection=self.project.design_spaces, - resource=design_space - ) - design_space = wait_while_validating( - collection=self.project.design_spaces, module=design_space, - print_status_info=print_status_info - ) - - self._design_space = design_space - self._status = AutoConfigureStatus.DESIGN_SPACE_CREATED - self._status_info = [detail.msg for detail in design_space.status_detail] - - if design_space.failed(): - self._status = AutoConfigureStatus.DESIGN_SPACE_INVALID - raise RuntimeError("Design space is invalid," - " cannot proceed to design workflow configuration.") - - def _design_workflow_build_stage( - self, - *, - predictor: GraphPredictor, - design_space: DesignSpace, - score: Optional[Union[Score, Objective]], - print_status_info: bool - ): - """Create a design workflow, and optionally trigger a design execution.""" - self._print_status("Configuring design workflow...") - - workflow = DesignWorkflow( - name=self._default_asset_names["DESIGN_WORKFLOW"], - predictor_id=predictor.uid, - design_space_id=design_space.uid - ) - workflow = create_or_update( - collection=self.project.design_workflows, - resource=workflow - ) - workflow = wait_while_validating( - collection=self.project.design_workflows, module=workflow, - print_status_info=print_status_info - ) - - self._design_workflow = workflow - self._status = AutoConfigureStatus.DESIGN_WORKFLOW_CREATED - self._status_info = [detail.msg for detail in workflow.status_detail] - - if workflow.failed(): - self._status = AutoConfigureStatus.DESIGN_WORKFLOW_FAILED - if score is None: - self._print_status("No score provided to trigger design execution -- finished.") - else: - raise RuntimeError("Design workflow validation failed," - " cannot trigger design execution.") - elif score is not None: - self._print_status("Triggering design execution...") - return self.execute(score=score) diff --git a/src/citrine/builders/descriptors.py b/src/citrine/builders/descriptors.py deleted file mode 100644 index f43aa5b7b..000000000 --- a/src/citrine/builders/descriptors.py +++ /dev/null @@ -1,237 +0,0 @@ -from itertools import chain -from typing import Iterator, Mapping, Union, List -from uuid import UUID - -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.bounds import RealBounds, CategoricalBounds, MolecularStructureBounds, \ - IntegerBounds, CompositionBounds -from gemd.entity.template.attribute_template import AttributeTemplate -from gemd.entity.template.has_property_templates import HasPropertyTemplates -from gemd.entity.template.has_condition_templates import HasConditionTemplates -from gemd.entity.template.has_parameter_templates import HasParameterTemplates -from gemd.entity.value import EmpiricalFormula -from gemd.util import recursive_flatmap, set_uuids - -from citrine.builders.auto_configure import AutoConfigureMode -from citrine.informatics.descriptors import RealDescriptor, CategoricalDescriptor, \ - MolecularStructureDescriptor, Descriptor, ChemicalFormulaDescriptor, IntegerDescriptor -from citrine.resources.data_concepts import DataConceptsCollection -from citrine.resources.material_run import MaterialRun -from citrine.resources.project import Project - - -class NoEquivalentDescriptorError(ValueError): - """Error that is raised when the bounds in a template have no equivalent descriptor.""" - - pass - - -def template_to_descriptor(template: AttributeTemplate, *, - headers: List[str] = []) -> Descriptor: - """ - Convert a GEMD attribute template into an AI Engine Descriptor. - - IntBounds cannot be converted because they have no matching descriptor type. - CompositionBounds can only be converted when every component is an element, in which case - they are converted to ChemicalFormulaDescriptors. - - Parameters - ---------- - template: AttributeTemplate - Template to convert into a descriptor - headers: List[str] - Names of parent relationships to includes as prefixes - to the template name in the descriptor key - Default: [] - - Returns - ------- - Descriptor - Descriptor with a key matching the template name and type corresponding to the bounds - - """ - headers = headers + [template.name] - descriptor_key = '~'.join(headers) - bounds = template.bounds - if isinstance(bounds, RealBounds): - return RealDescriptor( - key=descriptor_key, - lower_bound=bounds.lower_bound, - upper_bound=bounds.upper_bound, - units=bounds.default_units - ) - if isinstance(bounds, CategoricalBounds): - return CategoricalDescriptor( - key=descriptor_key, - categories=bounds.categories - ) - if isinstance(bounds, MolecularStructureBounds): - return MolecularStructureDescriptor( - key=descriptor_key - ) - if isinstance(bounds, CompositionBounds): - if set(bounds.components).issubset(EmpiricalFormula.all_elements()): - return ChemicalFormulaDescriptor( - key=descriptor_key - ) - else: - msg = "Cannot create descriptor for CompositionBounds with non-atomic components" - raise NoEquivalentDescriptorError(msg) - if isinstance(bounds, IntegerBounds): - return IntegerDescriptor( - key=descriptor_key, - lower_bound=bounds.lower_bound, - upper_bound=bounds.upper_bound - ) - raise ValueError("Template has unrecognized bounds: {}".format(type(bounds))) - - -class PlatformVocabulary(Mapping[str, Descriptor]): - """ - Dictionary of descriptors that define a controlled vocabulary for the AI Engine. - - Parameters - ---------- - entries: Mapping[str, Descriptor] - Entries in the dictionary, indexed by a convenient name. - To build from templates, use PlatformVocabulary.from_templates - To build from a material, use PlatformVocabulary.from_material - - """ - - def __init__(self, *, entries: Mapping[str, Descriptor]): - self._entries = entries - - def __getitem__(self, k: str) -> Descriptor: - return self._entries[k] - - def __len__(self): - return len(self._entries) - - def __iter__(self) -> Iterator[str]: - return iter(self._entries) - - @staticmethod - def from_templates(*, project: Project, scope: str): - """ - Build a PlatformVocabulary from the templates visible to a project. - - All of the templates with the given scope are downloaded and converted into descriptors. - The uid values associated with that scope are used as the index into the dictionary. - For example, using scope "my_templates" with a template with - uids={"my_templates": "density"} would be indexed into the dictionary as "density". - - Parameters - ---------- - project: Project - Project on the Citrine Platform to read templates from - scope: str - Unique ID scope from which to pull the template names - - Returns - ------- - PlatformVocabulary - - """ - def _from_collection(collection: DataConceptsCollection): - return {x.uids[scope]: x for x in collection.list() if scope in x.uids} - - properties = _from_collection(project.property_templates) - parameters = _from_collection(project.parameter_templates) - conditions = _from_collection(project.condition_templates) - - res = {} - for k, v in chain(properties.items(), parameters.items(), conditions.items()): - try: - desc = template_to_descriptor(v) - res[k] = desc - except NoEquivalentDescriptorError: - continue - - return PlatformVocabulary(entries=res) - - @staticmethod - def from_material( - *, - project: Project, - material: Union[str, UUID, LinkByUID, MaterialRun], - mode: AutoConfigureMode = AutoConfigureMode.PLAIN, - full_history: bool = True - ): - """[ALPHA] Build a PlatformVocabulary from templates appearing in a material history. - - All of the attribute templates that appear throughout the material's history - are extracted and converted into descriptors. - - Descriptor keys are formatted according to the option set by mode. - For example, if a condition template with name 'Condition 1' - appears in a parent process with name 'Parent', - the mode option produces the following descriptor key: - - mode = AutoConfigMode.PLAIN --> 'Parent~Condition 1' - mode = AutoConfigMode.FORMULATION --> 'Condition 1' - - Parameters - ---------- - project: Project - Project to use when accessing the Citrine Platform. - material: Union[str, UUID, LinkByUID, MaterialRun] - A representation of the material to extract descriptors from. - mode: AutoConfigureMode - Formatting option for descriptor keys in the platform vocabulary. - Option AutoConfigMode.PLAIN includes headers from the parent object, - whereas option AutoConfigMode.FORMULATION does not. - Default: AutoConfigureMode.PLAIN - full_history: bool - Whether to extract descriptors from the full material history, - or only the provided (terminal) material. - Default: True - - Returns - ------- - PlatformVocabulary - - """ - if not isinstance(mode, AutoConfigureMode): - raise TypeError('mode must be an option from AutoConfigureMode') - - # Full history not needed when full_history = False - # But is convenient to populate templates for terminal material - history = project.material_runs.get_history(id=material) - if full_history: - search_history = recursive_flatmap(history, lambda x: [x], unidirectional=False) - set_uuids(search_history, 'id') - else: - # Limit the search to contain the terminal material/process/measurements - search_history = [history.spec.template, history.process.template] - search_history.extend([msr.template for msr in history.measurements]) - search_history = [x for x in search_history if x is not None] # Edge case safety - - # Extract templates and formatted keys - res = {} - for obj in search_history: - # Extract all templates - templates = [] - if isinstance(obj, HasPropertyTemplates): - for property in obj.properties: - templates.append(property[0]) - if isinstance(obj, HasConditionTemplates): - for condition in obj.conditions: - templates.append(condition[0]) - if isinstance(obj, HasParameterTemplates): - for parameter in obj.parameters: - templates.append(parameter[0]) - - # Assemble to descriptors - headers = [] - if mode == AutoConfigureMode.PLAIN: - headers.append(obj.name) - - for tmpl in templates: - try: - desc = template_to_descriptor(tmpl, headers=headers) - res[desc.key] = desc - except NoEquivalentDescriptorError: - continue - - return PlatformVocabulary(entries=res) diff --git a/src/citrine/builders/design_spaces.py b/src/citrine/builders/design_spaces.py deleted file mode 100644 index 9aef0173d..000000000 --- a/src/citrine/builders/design_spaces.py +++ /dev/null @@ -1,340 +0,0 @@ -import csv -from logging import getLogger -from uuid import UUID - -from citrine.exceptions import BadRequest -from citrine.informatics.data_sources import CSVDataSource -from citrine.resources.dataset import Dataset -from citrine.resources.project import Project - -try: - import pandas as pd -except ImportError: # pragma: no cover - raise ImportError('pandas>=1.1.5 is a requirement for the builders module') -try: - import numpy as np -except ImportError: # pragma: no cover - raise ImportError('numpy is a requirement for the builders module') -from itertools import product -from typing import Mapping, Sequence, List, Optional -from citrine.informatics.design_spaces import EnumeratedDesignSpace, DataSourceDesignSpace -from citrine.informatics.descriptors import Descriptor, RealDescriptor - -logger = getLogger(__name__) - - -def enumerate_cartesian_product( - *, - design_grid: Mapping[str, Sequence], - descriptors: List[Descriptor], - name: str, - description: str = '' -) -> EnumeratedDesignSpace: - """[ALPHA] Enumerate a Cartesian product from 1-D grids. - - Given a dict of lists or tuples of values for each design descriptor, - create the list of candidates representing the Cartesian product of all - values for each descriptor - - Parameters - ---------- - design_grid: Mapping[str, Sequence] - {'descriptor key': [sequence, of, values]} for each descriptor - descriptors: List[Descriptor] - design descriptors representing the degrees of freedom of the design - space. descriptors' keys should match the keys of design_grid. If - none are passed, they will be auto-generated based on the keys - of design_grid. - name: str - name for the EnumeratedDesignSpace - description: str - description for the EnumeratedDesignSpace - - """ - # Check that the grid size is small enough to not cause memory issues - grid_size = np.prod( - [len(grid_points) for grid_points in design_grid.values()] - ) * len(design_grid) - if grid_size > 2E8: - logger.warning( - "Product design grid contains {n} grid points. This may cause memory issues " - "downstream.".format(n=grid_size) - ) - - design_space_tuples = list(product(*design_grid.values())) - design_space_cols = list(design_grid.keys()) - df_ds = pd.DataFrame(data=design_space_tuples, columns=design_space_cols) - data = df_ds.to_dict('records') - design_space = EnumeratedDesignSpace(name=name, - description=description, - descriptors=descriptors, - data=data) - return design_space - - -def enumerate_formulation_grid( - *, - formulation_grid: Mapping[str, Sequence[float]], - balance_ingredient: str, - descriptors: List[Descriptor] = None, - name: str, - description: str = '' -) -> EnumeratedDesignSpace: - """[ALPHA] Enumerate a Cartesian product following formulation constraints. - - Given a dict of grids (lists) of ingredient amounts (fractions, between - 0-1), create candidates that are the Cartesian product of all possible - formulations with combinations of the specified ingredient amounts. - The balance_ingredient will make up the balance of the formulation. In - other words, this function first takes the Cartesian product of all - ingredient amounts *except* the balance_ingredient, then fills in the - amount of balance_ingredient as 1-(other ingredients). Results for which - the balance ingredient falls outside of the min and max values in its list - will be filtered out. Because of this, not all values in the balance - ingredient's list will necessarily be present in the final candidates. - - For example: - - {'balance ingredient':[0.8, 0.85, 0.9], - 'other ingredient':[0.1, 0.2, 0.3, 0.4, 0.5]} - - would yield: - - [{'balance ingredient': 0.8, 'other ingredient': 0.2}, - {'balance ingredient': 0.9, 'other ingredient': 0.1}] - - Parameters - ---------- - formulation_grid: Mapping[str, Sequence] - {'ingredient name':[list, of, values]} for each ingredient - balance_ingredient: str - name of the ingredient that should make up the balance of the - mixture (1-others). Must be a key in formulation_grid. - descriptors: List[Descriptor] - design descriptors representing the degrees of freedom of the design - space. descriptors' keys should match the keys of formulation_grid. - If none are passed, they will be auto-generated based on the keys - of formulation_grid. - name: str - name for the EnumeratedDesignSpace - description: str - description for the EnumeratedDesignSpace - - """ - # Generate descriptors if none passed - if descriptors is None: - descriptors = [RealDescriptor(kk, lower_bound=0, upper_bound=1, units="") - for kk in formulation_grid.keys()] - - # Check that the passed balance ingredient is in the grid keys - if balance_ingredient not in list(formulation_grid.keys()): - raise ValueError("balance_ingredient must be in formulation_grid' keys") - - # Check that all grid values are between 0 and 1 - for kk, val_list in formulation_grid.items(): - if not (all([x >= 0 for x in val_list]) and all([x <= 1 for x in val_list])): - raise ValueError('values in {} are not between 0 and 1'.format(kk)) - - non_balance_ingrs = [ingr for ingr in list(formulation_grid.keys()) - if ingr != balance_ingredient] - non_balance_descriptors = [dd for dd in descriptors - if dd.key != balance_ingredient] - non_balance_grids = { - k: v for k, v in formulation_grid.items() - if k in non_balance_ingrs - } - - # Start by making a naive product design space of non-balance ingredients - form_ds = pd.DataFrame( - enumerate_cartesian_product( - design_grid=non_balance_grids, - descriptors=non_balance_descriptors, - name='', - description='' - ).data - ) - - # Re-calculate the balance ingredient column as 1-all others - form_ds[balance_ingredient] = form_ds.apply( - lambda r: 1 - sum([r[col] for col in non_balance_ingrs]), - axis=1 - ) - - # Order columns so balance ingredient is first - form_ds = form_ds[[balance_ingredient] + non_balance_ingrs] - - # Eliminate out-of-range rows - balance_range = ( - min(formulation_grid[balance_ingredient]), - max(formulation_grid[balance_ingredient]) - ) - form_ds = form_ds[form_ds[balance_ingredient] >= balance_range[0]] - form_ds = form_ds[form_ds[balance_ingredient] <= balance_range[1]] - data = form_ds.to_dict('records') - design_space = EnumeratedDesignSpace(name=name, - description=description, - descriptors=descriptors, - data=data) - return design_space - - -def cartesian_join_design_spaces( - *, - subspaces: List[EnumeratedDesignSpace], - name: str, - description: str = '' -) -> EnumeratedDesignSpace: - """[ALPHA] Cartesian join of multiple enumerated design spaces. - - Given a list of multiple input EnumeratedDesignSpaces, create a new - EnumeratedDesignSpace that is the Cartesian product of candidates from each - input design space. - - Ultimate length will be the product of the number of candidates in each - individual design space - - Parameters - ---------- - subspaces: List[EnumeratedDesignSpace] - A list of EnumeratedDesignSpaces - name: str - name for the output EnumeratedDesignSpace - description: str - description for the output EnumeratedDesignSpace - - """ - # Test for duplicate or invalid descriptor keys - all_keys = [] - for ds in subspaces: - if 'join_key' in ds.data[0].keys(): - raise ValueError('"join_key" cannot be a descriptor key when using this function') - all_keys.extend(list(ds.data[0].keys())) - if len(all_keys) != len(set(all_keys)): - raise ValueError('Duplicate keys are not allowed across design spaces') - - # Check that the grid size is small enough to not cause memory issues - grid_size = np.prod([len(ds.data) for ds in subspaces]) \ - * np.sum([len(ds.data[0]) for ds in subspaces]) - if grid_size > 2E8: - logger.warning( - "Product design grid contains {n} grid points. This may cause memory issues " - "downstream.".format(n=grid_size) - ) - - # Convert data fields of EDS into DataFrames to prep for join - ds_list = [pd.DataFrame(ds.data) for ds in subspaces] - - # Set a dummy column to do the join - for df in ds_list: - df['join_key'] = 0 - - # Perform the join - df_join = ds_list[0] - for df in ds_list[1:]: - df_join = pd.merge(df_join, df, on='join_key') - - # Drop the dummy column and convert back to List[dict] - df_join = df_join.drop(columns='join_key') - data = df_join.to_dict('records') - - # Build the final DesignSpace - descriptors = [] - for ds in subspaces: - descriptors.extend(ds.descriptors) - design_space = EnumeratedDesignSpace(name=name, - description=description, - descriptors=descriptors, - data=data) - return design_space - - -def enumerated_to_data_source(*, - enumerated_ds: EnumeratedDesignSpace, - dataset: Dataset, - filename: Optional[str] = None - ) -> DataSourceDesignSpace: - """[ALPHA] Convert an EnumeratedDesignSpace into a DataSourceDesignSpace. - - Converts an EnumeratedDesignSpace into a DataSourceDesignSpace by writing - the data to a csv file, uploading it to the given dataset, and wrapping it - in a CSVDataSource. - - Parameters - ---------- - enumerated_ds: EnumeratedDesignSpace - data source to convert - dataset: Dataset - dataset into which to upload the data file - filename: Optional[str] - filename to use for the data file (default: design space name + "source data") - - """ - descriptors = {d.key: d for d in enumerated_ds.descriptors} - headers = [d.key for d in enumerated_ds.descriptors] - - csv_filename = filename or "{} source data.csv".format(enumerated_ds.name).replace(" ", "_") - with open(csv_filename, "w", newline="") as csvfile: - writer = csv.writer(csvfile) - writer.writerow(headers) - for datum in enumerated_ds.data: - writer.writerow([datum.get(k, '') for k in headers]) - - file_link = dataset.files.upload(csv_filename) - data_source = CSVDataSource(file_link=file_link, column_definitions=descriptors) - data_source_ds = DataSourceDesignSpace( - name=enumerated_ds.name, description=enumerated_ds.description, data_source=data_source) - return data_source_ds - - -def migrate_enumerated_design_space(*, - project: Project, - uid: UUID, - dataset: Dataset, - filename: Optional[str], - cleanup: bool = True - ) -> DataSourceDesignSpace: - """ - [ALPHA] Migrate an EnumeratedDesignSpace on the Citrine Platform to a DataSourceDesignSpace. - - Parameters - ---------- - project: Project - Project to use when accessing the Citrine Platform - uid: UUID - Unique identifier of the EnumeratedDesignSpace to migrate - dataset: Dataset - Dataset into which to write the data as a CSV. - filename: Optional[str] - Optional string name to specify for the data CSV file. - Defaults to the design space name + "source data" - cleanup: bool - Whether or not to try and archive the migrated design space if the migration is successful - Default: true - - Returns - ------- - DataSourceDesignSpace - The resulting design space, which should have functional parity with - the original design space. - - """ - enumerated_ds = project.design_spaces.get(uid) - if not isinstance(enumerated_ds, EnumeratedDesignSpace): - msg = "Trying to migrate an enumerated design space but this is a {}".format( - type(enumerated_ds)) - raise ValueError(msg) - - # create the new data source design space - data_source_ds = enumerated_to_data_source( - enumerated_ds=enumerated_ds, dataset=dataset, filename=filename) - data_source_ds = project.design_spaces.register(data_source_ds) - - if cleanup: - # archive the old enumerated design space - try: - project.design_spaces.archive(enumerated_ds.uid) - except BadRequest as err: - logger.warning(f"Unable to archive design space with uid {uid}, " - f"received the following response: {err.response_text}") - - return data_source_ds diff --git a/src/citrine/builders/predictors.py b/src/citrine/builders/predictors.py deleted file mode 100644 index 12715f47f..000000000 --- a/src/citrine/builders/predictors.py +++ /dev/null @@ -1,260 +0,0 @@ -from typing import Tuple, List, Optional, Union - -from citrine.resources.project import Project -from citrine.informatics.predictors import ( - MolecularStructureFeaturizer, - ChemicalFormulaFeaturizer, - MeanPropertyPredictor, - AutoMLPredictor, - GraphPredictor -) -from citrine.informatics.descriptors import ( - Descriptor, - FormulationDescriptor, - RealDescriptor, - CategoricalDescriptor, - ChemicalFormulaDescriptor, - MolecularStructureDescriptor, -) -from citrine.informatics.data_sources import DataSource - - -def build_mean_feature_property_predictors( - *, - project: Project, - featurizer: Union[MolecularStructureFeaturizer, ChemicalFormulaFeaturizer], - formulation_descriptor: FormulationDescriptor, - p: int, - impute_properties: bool = True, - make_all_ingredients_model: bool = True, - labels: Optional[List[str]] = None -) -> Tuple[List[MeanPropertyPredictor], List[Descriptor]]: - """[ALPHA] Combine a featurizer model with mean property models. - - Given a featurizer, produce "mean property" models that calculate the mean of the - properties calculated by the featurizer. This is useful if you do not directly know - the numerical properties of ingredients in a formulation, but instead know, for example, - the molecular structure. This builder method determines the real-valued properties that the - featurizer calculates and builds mean property models that use them as input properties. - - Parameters - ---------- - project: Project - Project that contains the predictor - featurizer: Union[MolecularStructureFeaturizer, ChemicalFormulaFeaturizer] - A model that is being used to featurize formulation ingredients. Currently only - accepts a molecular structure featurizer or a chemical formula featurizer. - formulation_descriptor: FormulationDescriptor - Descriptor that represents the formulation being featurized. - p: int - Power of the generalized mean. Only integer powers are supported. - impute_properties: bool - Whether to impute missing ingredient properties by averaging over the entire dataset. - If ``False`` all ingredients must define values for all featurized properties. - Otherwise, the row will not be featurized. - make_all_ingredients_model: bool - Whether to create a mean property predictor that calculates the mean over all ingredients. - If False, models are only constructed for specified labels. Must be True if no labels - are specified. - labels: Optional[List[str]] - List of labels for which a mean property predictor should be created. - - Returns - ------- - Tuple[List[MeanPropertyPredictor], List[Descriptor]] - List of mean property predictors that should be incorporated into the graph predictor, - and a list of all the output descriptors produced by these models. There will be one - model for each label specified, and one model for all ingredients if ``all_ingredients`` - is set to ``True``. In the common case, the output descriptors will all be used - as inputs to one or more ML models. - - """ - if isinstance(featurizer, (MolecularStructureFeaturizer, ChemicalFormulaFeaturizer)): - input_descriptor = featurizer.input_descriptor - else: - raise TypeError(f"Featurizer of type {type(featurizer)} is not supported.") - - if labels is None: - labels = [] - if not isinstance(labels, (list, set)): - raise TypeError("labels must be specified as a list or set of strings.") - if make_all_ingredients_model: - labels = [None] + labels - if len(labels) == 0: - msg = "No mean property predictors requested. " \ - "Set make_all_ingredients_model to True and/or specify labels." - raise ValueError(msg) - - properties = project.descriptors.from_predictor_responses( - predictor=featurizer, inputs=[input_descriptor] - ) - real_properties = [desc for desc in properties if isinstance(desc, RealDescriptor)] - if len(real_properties) == 0: - msg = "Featurizer did not return any real properties to calculate the means of." - raise RuntimeError(msg) - - predictors = [ - _build_mean_property_predictor( - ingredient_descriptor=input_descriptor, - formulation_descriptor=formulation_descriptor, - properties=real_properties, - p=p, - impute_properties=impute_properties, - label=label - ) - for label in labels - ] - - all_outputs = [ - output - for single_model_outputs in [ - project.descriptors.from_predictor_responses( - predictor=predictor, inputs=[formulation_descriptor] - ) - for predictor in predictors - ] - for output in single_model_outputs - ] - - return predictors, all_outputs - - -def _build_mean_property_predictor( - ingredient_descriptor: Descriptor, - formulation_descriptor: FormulationDescriptor, - properties: List[RealDescriptor], - p: int, - impute_properties: bool, - label: Optional[str] -) -> MeanPropertyPredictor: - """Build a MeanPropertyPredictor for given specifications.""" - name = f"mean of {ingredient_descriptor.key} features" - if p != 1: - name = f"{p}-{name}" - if label is not None: - name += f" for label {label}" - name += f" in {formulation_descriptor.key}" - - return MeanPropertyPredictor( - name=name, - description="", - input_descriptor=formulation_descriptor, - properties=properties, - p=p, - impute_properties=impute_properties, - label=label - ) - - -# Add simple ml builder -def build_simple_ml( - *, - project: Project, - name: str, - description: str, - inputs: List[Descriptor], - outputs: List[Descriptor], - latent_variables: List[Descriptor], - training_data: Optional[List[DataSource]] = None -) -> GraphPredictor: - """ - Build a graph predictor that connects all inputs to all latent variables and outputs. - - The structure of the returned, unregistered graph predictor will be analogous to the - structure returned by SimpleMLPredictor. - - Parameters - ---------- - project: Project - Project that contains the predictor to be built - name: str - name of the returned predictor - description: str - the description of the predictor - inputs: list[Descriptor] - Descriptors that represent inputs to relations - outputs: list[Descriptor] - Descriptors that represent outputs of relations - latent_variables: list[Descriptor] - Descriptors that are predicted from inputs and used when predicting the outputs - training_data: Optional[List[DataSource]] - Sources of training data. Each can be either a CSV or an GEM Table. Candidates from - multiple data sources will be combined into a flattened list and de-duplicated by uid and - identifiers. de-duplication is performed if a uid or identifier is shared between two or - more rows. The content of a de-duplicated row will contain the union of data across all - rows that share the same uid or at least 1 identifier. Training data is unnecessary if the - predictor is part of a graph that includes all training data required by this predictor. - - Returns - ------- - GraphPredictor - GraphPredictor connecting all inputs to all latent variables and all inputs and latent - variables to all outputs. - - """ - ml_model_feature_descriptors = [d for d in inputs - if isinstance(d, (RealDescriptor, CategoricalDescriptor))] - chemical_formula_descriptors = [d for d in inputs - if isinstance(d, ChemicalFormulaDescriptor)] - molecular_structure_descriptors = [d for d in inputs - if isinstance(d, MolecularStructureDescriptor)] - - chemical_formula_featurizers = [ - ChemicalFormulaFeaturizer( - name=f'{chemical_formula_descriptor.key} featurizer', - description='', - input_descriptor=chemical_formula_descriptor, - features=['standard'], - ) for chemical_formula_descriptor in chemical_formula_descriptors - ] - - molecular_structure_featurizers = [ - MolecularStructureFeaturizer( - name=f'{molecular_structure_descriptor.key} featurizer', - description='', - input_descriptor=molecular_structure_descriptor, - features=['standard'], - ) for molecular_structure_descriptor in molecular_structure_descriptors - ] - - # TODO: Fix needing to pass the project (if possible) - featurizer_responses = [] - for featurizer in chemical_formula_featurizers + molecular_structure_featurizers: - featurizer_responses.extend( - project.descriptors.from_predictor_responses( - predictor=featurizer, - inputs=[featurizer.input_descriptor], - ) - ) - - automl_lv_predictors = [ - AutoMLPredictor( - name=f'{latent_variable_descriptor.key} Predictor', - description='', - inputs=ml_model_feature_descriptors + featurizer_responses, - outputs=[latent_variable_descriptor], - ) for latent_variable_descriptor in latent_variables - ] - - automl_output_predictors = [ - AutoMLPredictor( - name=f'{output_descriptor.key} Predictor', - description='', - inputs=ml_model_feature_descriptors + latent_variables + featurizer_responses, - outputs=[output_descriptor], - ) for output_descriptor in outputs - ] - - predictor = GraphPredictor( - name=name, - description=description, - predictors=[ - * chemical_formula_featurizers, - * molecular_structure_featurizers, - * automl_lv_predictors, - * automl_output_predictors - ], - training_data=training_data - ) - - return predictor diff --git a/src/citrine/builders/scores.py b/src/citrine/builders/scores.py deleted file mode 100644 index 5a6e1a627..000000000 --- a/src/citrine/builders/scores.py +++ /dev/null @@ -1,82 +0,0 @@ -import csv -from io import StringIO -from typing import List, Iterable - -from citrine.resources.project import Project -from citrine.resources.gemtables import GemTable - -from citrine.informatics.objectives import Objective, ScalarMinObjective -from citrine.informatics.scores import Score, LIScore - - -def create_default_score( - *, objectives: List[Objective], project: Project, table: GemTable -) -> Score: - """[ALPHA] Create a default score from the provided objectives. - - Reads the provided table from S3 storage, - and parses the columns corresponding to each objective's descriptor key - to extract relevant baseline values. - - The baselines are determined as the min/max of the column data, - depending on whether the objective is a ScalarMinObjective or ScalarMaxObjective. - - Parameters - ---------- - objectives: List[Objective] - List of objectives to include in a score. - project: Project - Project to use when accessing the Citrine Platform. - table: GemTable - Table to read baseline data from. - Must contain column definitions matching the descriptor keys - found in each objective. - - Returns - ---------- - Score - The default constructed score. - Currently, this is a LIScore containing containing all provided objectives. - - """ - table_data = project.tables.read_to_memory(table) - data_io = StringIO(table_data) - reader = csv.DictReader(data_io, delimiter=",") - - objectives = objectives if isinstance(objectives, Iterable) else [objectives] - - header_map = {} - objective_values = {} - for objective in objectives: - key = objective.descriptor_key - objective_values[key] = [] - - # Descriptor key has to be mapped to fieldnames of table data - # Search for mean column, avoid matching on std columns - full_key = next(filter(lambda col: f"{key}~Mean" in col, reader.fieldnames), None) - if full_key is None: - raise ValueError(f"Key '{key}' not found in GEM table headers.") - header_map[key] = full_key - - # Iterate table data to extract baseline info - for row in reader: - for objective in objectives: - key = objective.descriptor_key - full_key = header_map[key] - try: - objective_values[key].append(float(row[full_key])) - except ValueError: - continue - - # Determine max/min of extracted values for baseline - baselines = [] - for objective in objectives: - key = objective.descriptor_key - if len(objective_values[key]) == 0: - raise ValueError(f"Unable to parse values for '{key}' as numeric.") - - comparator = min if isinstance(objective, ScalarMinObjective) else max - baselines.append(comparator(objective_values[key])) - - score = LIScore(objectives=objectives, baselines=baselines) - return score diff --git a/test_requirements.txt b/test_requirements.txt index 44956c90e..3b1f1093f 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -8,5 +8,4 @@ pytest-cov==3.0.0 pytest-flake8==1.0.4 factory-boy==2.12.0 requests-mock==1.7.0 -pandas>=1.3.5,<=1.5.0 derp==0.1.1 diff --git a/tests/builders/test_auto_configure.py b/tests/builders/test_auto_configure.py deleted file mode 100644 index 05e01b6aa..000000000 --- a/tests/builders/test_auto_configure.py +++ /dev/null @@ -1,471 +0,0 @@ -import logging -from uuid import uuid4 - -import pytest -import mock - -from citrine.builders.auto_configure import AutoConfigureWorkflow - -from citrine.informatics.design_spaces import EnumeratedDesignSpace -from citrine.informatics.predictors import GraphPredictor -from citrine.informatics.predictor_evaluator import CrossValidationEvaluator -from citrine.informatics.objectives import ScalarMaxObjective -from citrine.informatics.scores import LIScore - -from citrine._session import Session -from citrine.resources.gemtables import GemTable -from citrine.resources.material_run import MaterialRun -from citrine.resources.predictor import AutoConfigureMode -from citrine.resources.project import Project -from citrine.resources.table_config import TableConfig - -from tests.utils.session import FakeSession -from tests.utils.fakes import FakeDesignWorkflow, FakePredictorEvaluationWorkflow -from tests.utils.fakes import FakeProject -from tests.utils.wait import generate_fake_wait_while - -# Return functions that mock wait_while_validating with specified status -fake_wait_while_ready = generate_fake_wait_while(status="READY") -fake_wait_while_succeeded = generate_fake_wait_while(status="SUCCEEDED") -fake_wait_while_invalid = generate_fake_wait_while(status="INVALID") -fake_wait_while_failed = generate_fake_wait_while(status="FAILED") - - -@pytest.fixture -def session() -> Session: - return FakeSession() - - -@pytest.fixture -def project(session) -> Project: - return FakeProject(name="Test Project", description="", session=session) - - -def default_resources(name): - table_config = TableConfig( - name=f"{name}: Auto Configure GEM Table", - description="", rows=[], variables=[], columns=[], datasets=[] - ) - predictor = GraphPredictor( - name=f"{name}: Auto Configure Predictor", description="", predictors=[] - ) - pew = FakePredictorEvaluationWorkflow( - name=f"{name}: Auto Configure PEW", description="", evaluators=[] - ) - design_space = EnumeratedDesignSpace( - name=f"{name}: Auto Configure Design Space", description="", descriptors=[], data=[] - ) - # Workflow must be fake to get appropriate execution collection - workflow = FakeDesignWorkflow( - name=f"{name}: Auto Configure Design Workflow", - predictor_id=uuid4(), - design_space_id=uuid4() - ) - return { - "table_config": table_config, - "predictor": predictor, - "pew": pew, - "design_space": design_space, - "design_workflow": workflow - } - - -def test_auto_config_mode_raises(project): - """Test that we raise errors on bad mode choices.""" - config_name = "Test" - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - - with pytest.raises(TypeError): - auto_config.from_material(material=MaterialRun(name="Fake"), mode="BAD CHOICE") - - with pytest.raises(TypeError): - auto_config.from_table(table=GemTable(), mode="BAD CHOICE") - - -def test_auto_configure_properties(project): - """Test the property access on the auto config workflow.""" - config_name = "Test" - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - - assert auto_config.design_execution is None - assert auto_config.score is None - assert len(auto_config.candidates) == 0 - - -def test_auto_config_init(project): - """Test that the update calls during initialization find appropriate assets.""" - config_name = "Test" - resources = default_resources(config_name) - - # No assets added to project yet, nothing to find - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 0 - assert auto_config.status == "START" - assert auto_config.status_info == [] - - # Add a table/table_config - project.table_configs.register(resources["table_config"]) - project.tables.build_from_config(resources["table_config"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 2 - assert auto_config.status == "TABLE CREATED" - - # Add a predictor - project.predictors.register(resources["predictor"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 3 - assert auto_config.status == "PREDICTOR CREATED" - - # Add a PEW - project.predictor_evaluation_workflows.register(resources["pew"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW CREATED" - - # Add a design space - project.design_spaces.register(resources["design_space"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 5 - assert auto_config.status == "DESIGN SPACE CREATED" - - # Add a design workflow - project.design_workflows.register(resources["design_workflow"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 6 - assert auto_config.status == "DESIGN WORKFLOW CREATED" - - -def test_auto_config_update_status(project): - """Test that update finds the appropriate status.""" - config_name = "Test" - resources = default_resources(config_name) - table_config = resources["table_config"] - predictor = resources["predictor"] - pew = resources["pew"] - design_space = resources["design_space"] - design_workflow = resources["design_workflow"] - - # Create an auto config, blank status - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 0 - assert auto_config.status == "START" - - # Give it a table, check status - project.table_configs.register(table_config) - project.tables.build_from_config(table_config) - auto_config.update() - assert len(auto_config.assets) == 2 - assert auto_config.status == "TABLE CREATED" - - # Give it predictor, check status - project.predictors.register(predictor) - auto_config.update() - assert len(auto_config.assets) == 3 - assert auto_config.status == "PREDICTOR CREATED" - - predictor.status = "INVALID" - project.predictors.update(predictor) - auto_config.update() - assert len(auto_config.assets) == 3 - assert auto_config.status == "PREDICTOR INVALID" - - # Give it PEWS, check status - project.predictor_evaluation_workflows.register(pew) - auto_config.update() - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW CREATED" - - pew.status = "FAILED" - project.predictor_evaluation_workflows.update(pew) - auto_config.update() - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW FAILED" - - # Give it design spaces, check status - project.design_spaces.register(design_space) - auto_config.update() - assert len(auto_config.assets) == 5 - assert auto_config.status == "DESIGN SPACE CREATED" - - design_space.status = "INVALID" - project.design_spaces.update(design_space) - auto_config.update() - assert len(auto_config.assets) == 5 - assert auto_config.status == "DESIGN SPACE INVALID" - - # Give it design workflows, check status - project.design_workflows.register(design_workflow) - auto_config.update() - assert len(auto_config.assets) == 6 - assert auto_config.status == "DESIGN WORKFLOW CREATED" - - design_workflow.status = "FAILED" - project.design_workflows.update(design_workflow) - auto_config.update() - assert len(auto_config.assets) == 6 - assert auto_config.status == "DESIGN WORKFLOW FAILED" - - -def test_auto_config_execute(project): - """Test the score execution on auto config workflow.""" - config_name = "Test" - resources = default_resources(config_name) - project.table_configs.register(resources["table_config"]) - project.predictors.register(resources["predictor"]) - project.predictor_evaluation_workflows.register(resources["pew"]) - project.design_spaces.register(resources["design_space"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert auto_config.design_workflow is None - - # Inputs for execute - objective = ScalarMaxObjective(descriptor_key="Fake Target") - - with pytest.raises(ValueError): - auto_config.execute(score=objective) - - # Now create a config with a working design workflow - project.design_workflows.register(resources["design_workflow"]) - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert auto_config.status == "DESIGN WORKFLOW CREATED" - - # Mock function to bypass create_default_score call internally - def _default_score(*args, **kwargs): - return LIScore(objectives=[], baselines=[]) - - with mock.patch("citrine.builders.auto_configure.create_default_score", _default_score): - auto_config.execute(score=objective) - assert auto_config.design_execution is not None - - -def test_auto_config_table_build(project): - """Test the table build stage of auto configure.""" - config_name = "Test" - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 0 - - auto_config._table_build_stage( - material="Fake Material", - mode=AutoConfigureMode.PLAIN - ) - assert len(auto_config.assets) == 2 - - -def test_auto_configure_predictor_registration(project): - """Test the predictor registration stage of auto configure.""" - # Start from having a table config and table - config_name = "Test" - resources = default_resources(config_name) - project.table_configs.register(resources["table_config"]) - project.tables.build_from_config(resources["table_config"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 2 - assert auto_config.status == "TABLE CREATED" - - # Inputs to pass to method - predictor = resources["predictor"] - - # Mock a valid predictor response - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_ready): - auto_config._predictor_registration_stage( - predictor=predictor, - print_status_info=False - ) - assert len(auto_config.assets) == 3 - assert auto_config.status == "PREDICTOR CREATED" - - # Mock an invalid predictor response - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_invalid): - with pytest.raises(RuntimeError): - auto_config._predictor_registration_stage( - predictor=predictor, - print_status_info=False - ) - assert len(auto_config.assets) == 3 - assert auto_config.status == "PREDICTOR INVALID" - - -@mock.patch("citrine.builders.auto_configure.PredictorEvaluationWorkflow", FakePredictorEvaluationWorkflow) -def test_auto_configure_predictor_evaluation(project, caplog): - """Test the predictor evaluation stage of auto configure.""" - config_name = "Test" - resources = default_resources(config_name) - project.table_configs.register(resources["table_config"]) - project.tables.build_from_config(resources["table_config"]) - project.predictors.register(resources["predictor"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 3 - assert auto_config.status == "PREDICTOR CREATED" - - # Inputs to pass to method - predictor = resources["predictor"] - evaluator = CrossValidationEvaluator(name="Eval", description="", responses=set()) - - # Create default w/ a valid response - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_succeeded): - auto_config._predictor_evaluation_stage( - predictor=predictor, - evaluator=None, - print_status_info=False - ) - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW CREATED" - - # Create default w/ an invalid response - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_failed): - caplog.clear() - with caplog.at_level(logging.WARNING): - auto_config._predictor_evaluation_stage( - predictor=predictor, - evaluator=None, - print_status_info=False - ) - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW FAILED" - assert any(r.levelno == logging.WARNING for r in caplog.records) - - # Create manual w/ a valid response - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_succeeded): - auto_config._predictor_evaluation_stage( - predictor=predictor, - evaluator=evaluator, - print_status_info=False - ) - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW CREATED" - - # Create manual w/ a failed response - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_failed): - caplog.clear() - with caplog.at_level(logging.WARNING): - auto_config._predictor_evaluation_stage( - predictor=predictor, - evaluator=evaluator, - print_status_info=False - ) - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW FAILED" - assert any(r.levelno == logging.WARNING for r in caplog.records) - - -def test_auto_configure_design_space_build(project): - """Test the design space build stage of auto configure.""" - config_name = "Test" - resources = default_resources(config_name) - project.table_configs.register(resources["table_config"]) - project.tables.build_from_config(resources["table_config"]) - project.predictors.register(resources["predictor"]) - project.predictor_evaluation_workflows.register(resources["pew"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 4 - assert auto_config.status == "PREDICTOR EVALUATION WORKFLOW CREATED" - - # Inputs to pass to method - predictor = resources["predictor"] - design_space = resources["design_space"] - - # When validation succeeds - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_ready): - auto_config._design_space_build_stage( - predictor=predictor, - design_space=design_space, - print_status_info=False - ) - assert len(auto_config.assets) == 5 - assert auto_config.status == "DESIGN SPACE CREATED" - - # When validation fails - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_invalid): - with pytest.raises(RuntimeError): - auto_config._design_space_build_stage( - predictor=predictor, - design_space=design_space, - print_status_info=False - ) - assert auto_config.status == "DESIGN SPACE INVALID" - - -@mock.patch.object(AutoConfigureWorkflow, "execute", lambda *args, **kwargs: None) -def test_auto_configure_design_workflow_build(project): - """Test the design workflow build stage of auto configure.""" - config_name = "Test" - resources = default_resources(config_name) - project.table_configs.register(resources["table_config"]) - project.tables.build_from_config(resources["table_config"]) - project.predictors.register(resources["predictor"]) - project.predictor_evaluation_workflows.register(resources["pew"]) - project.design_spaces.register(resources["design_space"]) - - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 5 - assert auto_config.status == "DESIGN SPACE CREATED" - - # Inputs to pass to method - predictor = resources["predictor"] - design_space = resources["design_space"] - score = LIScore(objectives=[], baselines=[]) - - # When validation succeeds - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_succeeded): - auto_config._design_workflow_build_stage( - predictor=predictor, - design_space=design_space, - score=score, - print_status_info=False - ) - assert len(auto_config.assets) == 6 - assert auto_config.status == "DESIGN WORKFLOW CREATED" - - # When validation fails - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_failed): - with pytest.raises(RuntimeError): - auto_config._design_workflow_build_stage( - predictor=predictor, - design_space=design_space, - score=score, - print_status_info=False - ) - assert len(auto_config.assets) == 6 - assert auto_config.status == "DESIGN WORKFLOW FAILED" - - # With no score passed, should not raise error - auto_config._design_workflow_build_stage( - predictor=predictor, - design_space=design_space, - score=None, - print_status_info=False - ) - assert auto_config.status == "DESIGN WORKFLOW FAILED" - - -def test_auto_configure_full_run(project): - """Test the full chain of public methods, from_material -> from_table -> from_predictor.""" - config_name = "Test" - auto_config = AutoConfigureWorkflow(project=project, name=config_name) - assert len(auto_config.assets) == 0 - assert auto_config.status == "START" - - # Create input material - material = MaterialRun(name="I am fake.") - - with mock.patch("citrine.builders.auto_configure.wait_while_validating", fake_wait_while_ready), \ - mock.patch.object(auto_config, 'from_table', wraps=auto_config.from_table) as from_table: - print_status_info = False - auto_config.from_material( - material=material, - mode=AutoConfigureMode.PLAIN, - print_status_info=print_status_info - ) - assert len(auto_config.assets) == 6 - assert auto_config.status == "DESIGN WORKFLOW CREATED" - - assert from_table.call_args.kwargs["print_status_info"] == print_status_info - assert from_table.call_args.kwargs["prefer_valid"] == True diff --git a/tests/builders/test_descriptors.py b/tests/builders/test_descriptors.py deleted file mode 100644 index b8e03d2df..000000000 --- a/tests/builders/test_descriptors.py +++ /dev/null @@ -1,252 +0,0 @@ -from uuid import uuid4, UUID -from typing import Iterator, Union -import pytest -import mock - -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.bounds.base_bounds import BaseBounds -from gemd.entity.value import EmpiricalFormula -from gemd.entity.attribute import Condition, Property, Parameter -from gemd.entity.value import NominalReal, NominalInteger, NominalCategorical -from gemd.entity.template import ( - ConditionTemplate, PropertyTemplate, ParameterTemplate, - MaterialTemplate, MeasurementTemplate, ProcessTemplate -) -from gemd.entity.object import ( - MaterialSpec, MaterialRun, ProcessSpec, ProcessRun, - MeasurementSpec, MeasurementRun -) -from gemd.entity.bounds import ( - RealBounds, CategoricalBounds, MolecularStructureBounds, - CompositionBounds, IntegerBounds -) - -from citrine.builders.descriptors import PlatformVocabulary, template_to_descriptor, \ - NoEquivalentDescriptorError -from citrine.informatics.descriptors import RealDescriptor, IntegerDescriptor, \ - CategoricalDescriptor, MolecularStructureDescriptor, ChemicalFormulaDescriptor -from citrine.resources.condition_template import ConditionTemplateCollection -from citrine.resources.material_run import MaterialRunCollection -from citrine.resources.parameter_template import ParameterTemplateCollection -from citrine.resources.predictor import AutoConfigureMode -from citrine.resources.project import Project -from citrine.resources.property_template import PropertyTemplateCollection - - -density_desc = RealDescriptor("density", lower_bound=0, upper_bound=100, units="gram / centimeter ** 3") -count_desc = IntegerDescriptor("count", lower_bound=0, upper_bound=100) -pressure_desc = RealDescriptor("pressure", lower_bound=0, upper_bound=10000, units="GPa") - - -@pytest.fixture() -def fake_project(): - """Fake project that serves templates from template collection's list method.""" - attributes = [ - ConditionTemplate("volume", bounds=IntegerBounds(lower_bound=0, upper_bound=11), uids={"my_scope": "volume"}), - ParameterTemplate("speed", bounds=CategoricalBounds(categories=["slow", "fast"]), uids={}), - PropertyTemplate("density", bounds=RealBounds(lower_bound=0, upper_bound=100, default_units="g / cm^3"), uids={"my_scope": "density"}) - ] - - values = [ - Condition('volume', template=attributes[0], value=NominalInteger(nominal=4)), - Parameter('speed', template=attributes[1], value=NominalCategorical(category='slow')), - Property('density', template=attributes[2], value=NominalReal(nominal=50.0, units=attributes[2].bounds.default_units)) - ] - - # Object templates - mt = MaterialTemplate('Material') - pt = ProcessTemplate('Processing', conditions=[attributes[0]], parameters=[attributes[1]]) - mst = MeasurementTemplate('Measurement', properties=[attributes[2]]) - - # Specs - ps = ProcessSpec(name=pt.name, template=pt) - mss = MeasurementSpec(name=mst.name, template=mst) - ms = MaterialSpec(name=mt.name, process=ps, template=mt) - - # Runs - pr = ProcessRun(name=ps.name, spec=ps, - conditions=[values[0]], parameters=[values[1]]) - - history = MaterialRun(name=ms.name, process=pr, spec=ms) - msr = MeasurementRun(name=mss.name, spec=mss, material=history, - properties=[values[2]]) - history.measurements.append(msr) - - class FakePropertyTemplateCollection(PropertyTemplateCollection): - def __init__(self): - pass - - def list(self, forward: bool = True, per_page: int = 100) -> Iterator[PropertyTemplate]: - return iter([x for x in attributes if isinstance(x, PropertyTemplate)]) - - class FakeConditionTemplateCollection(ConditionTemplateCollection): - def __init__(self): - pass - - def list(self, forward: bool = True, per_page: int = 100) -> Iterator[ConditionTemplate]: - return iter([x for x in attributes if isinstance(x, ConditionTemplate)]) - - class FakeParameterTemplateCollection(ParameterTemplateCollection): - def __init__(self): - pass - - def list(self, forward: bool = True, per_page: int = 100) -> Iterator[ParameterTemplate]: - return iter([x for x in attributes if isinstance(x, ParameterTemplate)]) - - class FakeMaterialRunCollection(MaterialRunCollection): - def __init__(self): - pass - - def get_history(self, *, id: Union[str, UUID, LinkByUID, MaterialRun]) -> MaterialRun: - return history - - class FakeProject(Project): - def __init__(self): - pass - - @property - def property_templates(self) -> PropertyTemplateCollection: - return FakePropertyTemplateCollection() - - @property - def condition_templates(self) -> ConditionTemplateCollection: - return FakeConditionTemplateCollection() - - @property - def parameter_templates(self) -> ParameterTemplateCollection: - return FakeParameterTemplateCollection() - - @property - def material_runs(self) -> MaterialRunCollection: - return FakeMaterialRunCollection() - - return FakeProject() - - -def test_valid_template_conversions(): - expected = [ - ( - PropertyTemplate(name="density", bounds=RealBounds( - lower_bound=0, upper_bound=100, default_units="g/cm^3")), - density_desc - ), - ( - PropertyTemplate(name="count", bounds=IntegerBounds( - lower_bound=0, upper_bound=100)), count_desc - ), - ( - ConditionTemplate(name="speed", bounds=CategoricalBounds(categories=["low", "high"])), - CategoricalDescriptor(key="speed", categories=["low", "high"]) - ), - ( - ParameterTemplate(name="solvent", bounds=MolecularStructureBounds()), - MolecularStructureDescriptor(key="solvent") - ), - ( - PropertyTemplate(name="formula", bounds=CompositionBounds( - components=EmpiricalFormula.all_elements())), - ChemicalFormulaDescriptor(key="formula") - ) - ] - - for tmpl, desc in expected: - assert template_to_descriptor(tmpl) == desc - - -def test_invalid_template_conversions(): - with pytest.raises(NoEquivalentDescriptorError): - template_to_descriptor( - PropertyTemplate("mixture", bounds=CompositionBounds(components=["sugar", "spice"])) - ) - - class DummyBounds(BaseBounds, typ="dummy_bounds"): - """Fake bounds to test unrecognized bounds.""" - - def contains(self, bounds): - return False - - def union(self, *others): - return self - - def update(self): - pass - - with pytest.raises(ValueError): - template_to_descriptor( - ParameterTemplate("dummy", bounds=DummyBounds()) - ) - - -def test_dict_behavior(): - entries = { - "density": RealDescriptor("density", lower_bound=0, upper_bound=100, units="g/cm^3"), - "pressure": RealDescriptor("pressure", lower_bound=0, upper_bound=10000, units="GPa") - } - - v = PlatformVocabulary(entries=entries) - - assert len(v) == 2 - assert set(v) == {"density", "pressure"} - assert v["density"] == entries["density"] - assert v["pressure"] == entries["pressure"] - - -def test_from_template(fake_project: Project): - """Test that only correct scopes and bounds are loaded from templates.""" - v = PlatformVocabulary.from_templates(project=fake_project, scope="my_scope") - - # no speed since it doesn't have the right scope - assert len(v) == 2 - assert list(v) == ["density", "volume"] - assert v["density"] == density_desc - - # templates that raise NoEquivalentDescriptorError are skipped, mainly for test coverage - with mock.patch("citrine.builders.descriptors.template_to_descriptor") as mock_template_to_descriptor: - mock_template_to_descriptor.side_effect = NoEquivalentDescriptorError - PlatformVocabulary.from_templates(project=fake_project, scope="my_scope") - mock_template_to_descriptor.assert_called() - - -def test_from_material(fake_project: Project): - """Test that correct descriptors and keys are extracted from history.""" - sample_mr = uuid4() # Just a random UUID - v1 = PlatformVocabulary.from_material( - project=fake_project, - material=sample_mr, - mode=AutoConfigureMode.PLAIN, - full_history=True - ) - - density_desc_plain = RealDescriptor( - "Measurement~density", lower_bound=0, upper_bound=100, units="gram / centimeter ** 3") - assert len(v1) == 3 - assert list(v1) == ['Processing~volume', 'Processing~speed', 'Measurement~density'] - assert v1['Measurement~density'] == density_desc_plain - - # Same length for sample history - v2 = PlatformVocabulary.from_material( - project=fake_project, - material=sample_mr, - mode=AutoConfigureMode.PLAIN, - full_history=False - ) - assert len(v1) == len(v2) - - # Raise on not passing enum option - with pytest.raises(TypeError): - PlatformVocabulary.from_material( - project=fake_project, - material=sample_mr, - mode='BAD MODE CHOICE' - ) - - # templates that raise NoEquivalentDescriptorError are skipped, mainly for test coverage - with mock.patch("citrine.builders.descriptors.template_to_descriptor") as mock_template_to_descriptor: - mock_template_to_descriptor.side_effect = NoEquivalentDescriptorError - PlatformVocabulary.from_material( - project=fake_project, - material=sample_mr, - mode=AutoConfigureMode.PLAIN, - full_history=True - ) - mock_template_to_descriptor.assert_called() diff --git a/tests/builders/test_design_spaces.py b/tests/builders/test_design_spaces.py deleted file mode 100644 index 491374c4a..000000000 --- a/tests/builders/test_design_spaces.py +++ /dev/null @@ -1,394 +0,0 @@ -"""Tests for citrine.builders.design_spaces.""" -import logging -import uuid - -import pytest -import numpy as np - -from citrine.informatics.descriptors import RealDescriptor, CategoricalDescriptor -from citrine.informatics.design_spaces import EnumeratedDesignSpace -from citrine.builders import design_spaces -from citrine.builders.design_spaces import enumerate_cartesian_product, \ - enumerate_formulation_grid, cartesian_join_design_spaces, enumerated_to_data_source, migrate_enumerated_design_space -from tests.utils.fakes.fake_dataset_collection import FakeDataset -from tests.utils.fakes.fake_project_collection import FakeProject - - -@pytest.fixture(scope="module") -def to_clean(): - """Clean up files, even if a test fails""" - import os - files_to_clean = [] - yield files_to_clean - for f in files_to_clean: - try: - os.remove(f) - except FileNotFoundError: - pass - - -@pytest.fixture -def basic_cartesian_space() -> EnumeratedDesignSpace: - """Build basic cartesian space for testing.""" - alpha = RealDescriptor('alpha', lower_bound=0, upper_bound=100, units="") - beta = RealDescriptor('beta', lower_bound=0, upper_bound=100, units="") - gamma = CategoricalDescriptor('gamma', categories=['a', 'b', 'c']) - design_grid = { - 'alpha': [0, 50, 100], - 'beta': [0, 25, 50, 75, 100], - 'gamma': ['a', 'b', 'c'] - } - basic_space = enumerate_cartesian_product( - design_grid=design_grid, - descriptors=[alpha, beta, gamma], - name='basic space', - description='' - ) - return basic_space - -@pytest.fixture -def basic_cartesian_space_entity(basic_cartesian_space) -> EnumeratedDesignSpace: - return { - "id": str(uuid.uuid4()), - "data": { - "name": basic_cartesian_space.name, - "description": basic_cartesian_space.description, - "instance": basic_cartesian_space.dump() - }, - "metadata": { - "created": { - "user": str(uuid.uuid4()), - "time": "2020-04-23T15:46:26Z" - }, - "updated": { - "user": str(uuid.uuid4()), - "time": "2020-04-23T15:46:26Z" - }, - "status": { - "name": "CREATED", - "detail": [] - } - } - } - - -@pytest.fixture -def simple_mixture_space() -> EnumeratedDesignSpace: - """Build simple mixture space for testing.""" - formulation_grid = { - 'ing_A': [0.6, 0.7, 0.8, 0.9, 1.0], - 'ing_B': [0, 0.1, 0.2, 0.3, 0.4], - 'ing_C': [0.0, 0.01, 0.02, 0.03, 0.04, 0.05] - } - simple_mixture_space = enumerate_formulation_grid( - formulation_grid=formulation_grid, - balance_ingredient='ing_A', - name='basic simple mixture space', - description='' - ) - return simple_mixture_space - - -@pytest.fixture -def overspec_mixture_space() -> EnumeratedDesignSpace: - """Build overspeced mixture space for testing.""" - formulation_grid = { - 'ing_D': [0.6, 0.7], - 'ing_E': [0, 0.1, 0.2, 0.3, 0.4, 0.5], - 'ing_F': [0.0, 0.2, 0.3, 0.4, 0.5, 0.6] - } - overspec_mixture_space = enumerate_formulation_grid( - formulation_grid=formulation_grid, - balance_ingredient='ing_D', - name='overspeced simple mixture space', - description='' - ) - return overspec_mixture_space - - -@pytest.fixture -def joint_design_space(basic_cartesian_space, simple_mixture_space) -> EnumeratedDesignSpace: - """Build joint design space from above two examples""" - ds_list = [basic_cartesian_space, simple_mixture_space] - joint_space = cartesian_join_design_spaces( - subspaces=ds_list, - name='Joined enumerated design space', - description='', - ) - return joint_space - - -@pytest.fixture -def large_joint_design_space( - basic_cartesian_space, - simple_mixture_space, - overspec_mixture_space -) -> EnumeratedDesignSpace: - """Build joint design space from above two examples""" - ds_list = [basic_cartesian_space, simple_mixture_space, overspec_mixture_space] - joint_space = cartesian_join_design_spaces( - subspaces=ds_list, - name='Joined enumerated design space', - description='', - ) - return joint_space - - -class ExceptionRaiser(logging.Filter): - """Filter that raises an exception on the first warning.""" - def filter(self, record): - if record.levelno == logging.WARNING: - raise Exception(record.msg) - return True - - -@pytest.fixture -def warnings_as_exceptions(): - except_filter = ExceptionRaiser() - lib_logger = logging.getLogger(design_spaces.__name__) - lib_logger.addFilter(except_filter) - yield - lib_logger.removeFilter(except_filter) - - -def test_cartesian(basic_cartesian_space): - """Check data length, uniqueness, completeness""" - assert len(basic_cartesian_space.data) == 45 - assert len(basic_cartesian_space.data) == len( - set([tuple(cc.values()) for cc in basic_cartesian_space.data])) - assert len(set([cc['alpha'] for cc in basic_cartesian_space.data])) == 3 - assert len(set([cc['beta'] for cc in basic_cartesian_space.data])) == 5 - assert len(set([cc['gamma'] for cc in basic_cartesian_space.data])) == 3 - - -def test_formulation(simple_mixture_space, overspec_mixture_space): - """Check data length, uniqueness, completeness for both cases above""" - assert len(simple_mixture_space.data) == 25 - assert len(overspec_mixture_space.data) == 7 - assert len(simple_mixture_space.data) == len( - set([tuple(cc.values()) for cc in simple_mixture_space.data])) - assert len(overspec_mixture_space.data) == len( - set([tuple(cc.values()) for cc in overspec_mixture_space.data])) - # Check that all members of ing_B and ing_C values made it into candidates - assert len(set([cc['ing_B'] for cc in simple_mixture_space.data])) == 5 - assert len(set([cc['ing_C'] for cc in simple_mixture_space.data])) == 6 - # Check that correct number of members for ing_E and ing_F were excluded - assert len(set([cc['ing_E'] for cc in overspec_mixture_space.data])) == 5 - assert len(set([cc['ing_F'] for cc in overspec_mixture_space.data])) == 4 - - -def test_joined(joint_design_space, large_joint_design_space): - """Check data length, number of descriptors for 2 and more spaces""" - assert len(joint_design_space.data) == 1125 - assert len(large_joint_design_space.data) == 7875 - assert len(joint_design_space.descriptors) == 6 - assert len(large_joint_design_space.descriptors) == 9 - assert len(joint_design_space.data) == len( - set([tuple(cc.values()) for cc in joint_design_space.data])) - assert len(large_joint_design_space.data) == len( - set([tuple(cc.values()) for cc in large_joint_design_space.data])) - - -def test_exceptions(basic_cartesian_space, simple_mixture_space): - """Test that exceptions are raised""" - form_grids_1 = { - 'ing_D': [0.6, 1.1], - 'ing_E': [0, 0.1, 0.2, 0.3, 0.4, 0.5], - 'ing_F': [0.0, 0.2, 0.3, 0.4, 0.5, 0.6] - } - # Test the 'incorrect balance ingredient' error - with pytest.raises(ValueError): - enumerate_formulation_grid( - formulation_grid=form_grids_1, - balance_ingredient='wrong', - name='invalid formulation space 1', - description='' - ) - # Test ingredient outside of [0,1] - with pytest.raises(ValueError): - enumerate_formulation_grid( - formulation_grid=form_grids_1, - balance_ingredient='ing_D', - name='invalid formulation space 2', - description='' - ) - # Test the 'join_key' error - form_grids_2 = { - 'ing_D': [0.6, 1.0], - 'ing_E': [0, 0.1, 0.2, 0.3, 0.4, 0.5], - 'join_key': [0.0, 0.2, 0.3, 0.4, 0.5, 0.6] - } - form_ds_2 = enumerate_formulation_grid( - formulation_grid=form_grids_2, - balance_ingredient='ing_D', - name='dummy formulation space 2', - description='' - ) - with pytest.raises(ValueError): - cartesian_join_design_spaces( - subspaces=[ - basic_cartesian_space, - form_ds_2 - ], - name='invalid join space 1', - description='' - ) - # Test the duplicate keys error - form_grids_3 = { - 'ing_C': [0.8, 1.0], - 'ing_D': [0, 0.1, 0.15, 0.2] - } - form_ds_3 = enumerate_formulation_grid( - formulation_grid=form_grids_3, - balance_ingredient='ing_C', - name='dummy formulation space 3', - description='' - ) - with pytest.raises(ValueError): - cartesian_join_design_spaces( - subspaces=[ - simple_mixture_space, - form_ds_3 - ], - name='invalid join space 2', - description='' - ) - - -def test_enumerated_oversize_warnings(caplog, warnings_as_exceptions): - """Test that oversized enumerated space warnings are raised""" - with pytest.raises(Exception, match="648000000"): - # Fail on warning (so code stops running) - caplog.clear() - with caplog.at_level(logging.WARNING): - delta = RealDescriptor('delta', lower_bound=0, upper_bound=100, units="") - epsilon = RealDescriptor('epsilon', lower_bound=0, upper_bound=100, units="") - zeta = RealDescriptor('zeta', lower_bound=0, upper_bound=100, units="") - too_big_enumerated_grid = { - 'delta': np.linspace(0, 100, 600), - 'epsilon': np.linspace(0, 100, 600), - 'zeta': np.linspace(0, 100, 600), - } - enumerate_cartesian_product( - design_grid=too_big_enumerated_grid, - descriptors=[delta, epsilon, zeta], - name='too big space', - description='' - ) - - -def test_formulation_oversize_warnings(caplog, warnings_as_exceptions): - """Test that oversized formulation grid warnings are raised""" - # We need to raise an exception to make this test run in reasonable time - with pytest.raises(Exception, match="1562500000"): - caplog.clear() - with caplog.at_level(logging.WARNING): - too_big_formulation_grid = { - 'ing_F': np.linspace(0, 1, 50), - 'ing_G': np.linspace(0, 1, 50), - 'ing_H': np.linspace(0, 1, 50), - 'ing_I': np.linspace(0, 1, 50), - 'ing_J': np.linspace(0, 1, 50), - 'ing_K': np.linspace(0, 1, 50) - } - enumerate_formulation_grid( - formulation_grid=too_big_formulation_grid, - balance_ingredient='ing_K', - name='too big mixture space', - description='' - ) - - -def test_joined_oversize_warnings(caplog, warnings_as_exceptions, large_joint_design_space): - """Test that oversized joined space warnings are raised""" - with pytest.raises(Exception, match="239203125"): - caplog.clear() - with caplog.at_level(logging.WARNING): - - delta = RealDescriptor('delta', lower_bound=0, upper_bound=100, units="") - epsilon = RealDescriptor('epsilon', lower_bound=0, upper_bound=100, units="") - zeta = CategoricalDescriptor('zeta', categories=['a', 'b', 'c']) - design_grid = { - 'delta': [0, 50, 100], - 'epsilon': [0, 25, 50, 75, 100], - 'zeta': ['a', 'b', 'c'] - } - basic_space_2 = enumerate_cartesian_product( - design_grid=design_grid, - descriptors=[delta, epsilon, zeta], - name='basic space 2', - description='' - ) - - eta = RealDescriptor('eta', lower_bound=0, upper_bound=100, units="") - theta = RealDescriptor('theta', lower_bound=0, upper_bound=100, units="") - iota = CategoricalDescriptor('iota', categories=['a', 'b', 'c']) - design_grid = { - 'eta': [0, 50, 100], - 'theta': [0, 25, 50, 75, 100], - 'iota': ['a', 'b', 'c'] - } - basic_space_3 = enumerate_cartesian_product( - design_grid=design_grid, - descriptors=[eta, theta, iota], - name='basic space 3', - description='' - ) - - cartesian_join_design_spaces( - subspaces=[ - basic_space_2, - basic_space_3, - large_joint_design_space - ], - name='too big join space', - description='' - ) - - -def test_enumerated_to_data_source(basic_cartesian_space, to_clean): - """Test enumerated_to_data_source conversion""" - expected_fname = basic_cartesian_space.name.replace(" ", "_") + "_source_data.csv" - to_clean.append(expected_fname) - - dataset = FakeDataset() - result = enumerated_to_data_source( - enumerated_ds=basic_cartesian_space, dataset=dataset) - - assert result.name == basic_cartesian_space.name - assert result.description == basic_cartesian_space.description - assert result.data_source.file_link.url == expected_fname - expected_keys = {x.key for x in basic_cartesian_space.descriptors} - assert {x for x in result.data_source.column_definitions.keys()} == expected_keys - - -def test_migrate_enumerated(caplog, basic_cartesian_space, to_clean): - """Test migrate_enumerated_design_space with fakes.""" - fname = "foo.csv" # not to conflict with the above test - to_clean.append(fname) - - project = FakeProject(name="foo", description="bar") - dataset = FakeDataset() - old = project.design_spaces.register(basic_cartesian_space) - - # first test that it works when it should - new = migrate_enumerated_design_space( - project=project, uid=old.uid, dataset=dataset, filename=fname) - assert new.name == old.name - # the other equality logic is tested in test_enumerated_to_data_source - assert project.design_spaces.get(old.uid).is_archived - - # test that it doesn't work when it shouldn't - with pytest.raises(ValueError): - migrate_enumerated_design_space( - project=project, uid=new.uid, dataset=dataset, filename=fname) - - # it failed, so it shouldn't have archived the old one - assert not project.design_spaces.get(new.uid).is_archived - - # test that it works for a design space that cannot be archived because it is in use - old_in_use = project.design_spaces.register(basic_cartesian_space) - project.design_spaces.in_use[old_in_use.uid] = True - with caplog.at_level(logging.WARNING): - migrate_enumerated_design_space( - project=project, uid=old_in_use.uid, dataset=dataset, filename=fname) diff --git a/tests/builders/test_predictors.py b/tests/builders/test_predictors.py deleted file mode 100644 index 4e72a48b0..000000000 --- a/tests/builders/test_predictors.py +++ /dev/null @@ -1,124 +0,0 @@ -import pytest - -from citrine.informatics.descriptors import ( - MolecularStructureDescriptor, - ChemicalFormulaDescriptor, - FormulationDescriptor, - CategoricalDescriptor, - RealDescriptor -) -from citrine.informatics.predictors import ( - MolecularStructureFeaturizer, - ChemicalFormulaFeaturizer, - LabelFractionsPredictor, -) -from citrine.builders.predictors import build_mean_feature_property_predictors, build_simple_ml -from tests.utils.fakes import FakeProject - - -def test_mean_feature_properties(): - num_properties = 3 - project = FakeProject(num_properties=num_properties) - smiles = MolecularStructureDescriptor("smiles") - chem = ChemicalFormulaDescriptor("formula") - formulation = FormulationDescriptor.hierarchical() - mol_featurizer = MolecularStructureFeaturizer(name="", description="", input_descriptor=smiles) - chem_featurizer = ChemicalFormulaFeaturizer(name="", description="", input_descriptor=chem) - - for featurizer in [mol_featurizer, chem_featurizer]: - # A standard case. Here we request one model for all ingredients and one for a label. - models, outputs = build_mean_feature_property_predictors( - project=project, - featurizer=featurizer, - formulation_descriptor=formulation, - p=7, - impute_properties=False, - make_all_ingredients_model=True, - labels=["some label"] - ) - - assert len(outputs) == num_properties * 2 - assert len(models) == 2 - for model in models: - assert model.p == 7 - assert model.impute_properties == False - assert model.input_descriptor == formulation - assert len(model.properties) == num_properties - - # It's not necessary for the models to be returned in this order, - # but this is how the logic is currently set up. - assert models[0].label is None - assert models[1].label == "some label" - - - # expect an error if the featurizer model is not of allowed type - not_featurizer = LabelFractionsPredictor(name="", description="", input_descriptor=formulation, labels={"label"}) - with pytest.raises(TypeError): - build_mean_feature_property_predictors( - project=project, - featurizer=not_featurizer, - formulation_descriptor=formulation, - p=1 - ) - - # expect an error if there are no mean property models requested - with pytest.raises(ValueError): - build_mean_feature_property_predictors( - project=project, - featurizer=mol_featurizer, - formulation_descriptor=formulation, - p=1, - make_all_ingredients_model=False, - labels=None - ) - - # expect an error if the featurizer model returns no real properties - no_props_project = FakeProject(num_properties = 0) - with pytest.raises(RuntimeError): - build_mean_feature_property_predictors( - project=no_props_project, - featurizer=mol_featurizer, - formulation_descriptor=formulation, - p=1 - ) - - # expect an error if labels is not specified as a list - with pytest.raises(TypeError): - build_mean_feature_property_predictors( - project=no_props_project, - featurizer=mol_featurizer, - formulation_descriptor=formulation, - p=1, - labels="not inside a list!" - ) - - -def test_simple_ml(): - project = FakeProject() - smiles = MolecularStructureDescriptor("smiles") - chem = ChemicalFormulaDescriptor("formula") - w = CategoricalDescriptor(key="w",categories=["A","B","C"]) - x = RealDescriptor(key="x", lower_bound=0, upper_bound=100, units="") - y = RealDescriptor(key="y", lower_bound=0, upper_bound=100, units="") - z = RealDescriptor(key="z", lower_bound=0, upper_bound=100, units="") - inputs=[w, x, smiles, chem] - latent_variables=[y] - outputs=[z] - - simple_ml = build_simple_ml( - project = project, - name = 'test', - description = '', - inputs = inputs, - outputs = outputs, - latent_variables = latent_variables, - ) - assert len(simple_ml.predictors) == 4 - for predictor in simple_ml.predictors: - if predictor.typ == 'AutoML': - if predictor.outputs == [z]: - assert len(predictor.inputs) == 11 - else: - assert len(predictor.inputs) == 10 - else: - assert predictor.features == ['standard'] diff --git a/tests/builders/test_scores.py b/tests/builders/test_scores.py deleted file mode 100644 index d20031dca..000000000 --- a/tests/builders/test_scores.py +++ /dev/null @@ -1,78 +0,0 @@ -import pytest - -from tests.utils.factories import GemTableDataFactory - -from citrine.resources.gemtables import GemTableCollection, GemTable -from citrine.resources.project import Project -from citrine.informatics.objectives import ScalarMaxObjective, ScalarMinObjective - -from citrine.builders.scores import create_default_score - - -@pytest.fixture -def table_data() -> str: - """Fake table data with mean/std columns, units, and non-numerical data types.""" - header = "x~Mean (lbs),y~Mean (lbs),z~Mean (lbs),z~Std (lbs), pets~Mean\n" - row1 = "1.0,0.0,10.0,1.0,dog\n" - row2 = "0.0,1.0,5.0,0.5,cat" - return header + row1 + row2 - - -@pytest.fixture -def table(): - url = "http://otherhost:4572/anywhere" - return GemTable.build( - GemTableDataFactory(signed_download_url=url, version=2) - ) - - -@pytest.fixture -def project(table_data): - """Fake project for table collection""" - class FakeGemTableCollection(GemTableCollection): - def __init__(self): - pass - - def read_to_memory(self, table: GemTable) -> str: - return table_data - - class FakeProject(Project): - def __init__(self): - pass - - @property - def tables(self) -> FakeGemTableCollection: - return FakeGemTableCollection() - - return FakeProject() - - -def test_create_default_score(project, table): - """Test reading a table to create a default score for some objectives.""" - o1 = ScalarMinObjective(descriptor_key="x") # Valid numerical with mean - o2 = ScalarMaxObjective(descriptor_key="y") # Valid numerical with mean - o3 = ScalarMaxObjective(descriptor_key="z") # Has both a Mean and Std column - o4 = ScalarMaxObjective(descriptor_key="pets") # Non-numeric - o5 = ScalarMaxObjective(descriptor_key="bad") # Not found in the table - - s1 = create_default_score(objectives=o1, project=project, table=table) - assert s1.baselines[0] == 0.0 - - s2 = create_default_score(objectives=o2, project=project, table=table) - assert s2.baselines[0] == 1.0 - - s3 = create_default_score(objectives=[o1, o2], project=project, table=table) - assert s3.baselines[0] == 0.0 - assert s3.baselines[1] == 1.0 - - s4 = create_default_score(objectives=[o3], project=project, table=table) - assert s4.baselines[0] == 10.0 # Make sure we get the mean column, not std column - - # Check errors for poor descriptor choices - with pytest.raises(ValueError): - # Cannot convert data to numeric - create_default_score(objectives=o4, project=project, table=table) - - with pytest.raises(ValueError): - # Not found in header - create_default_score(objectives=o5, project=project, table=table) diff --git a/tests/informatics/test_design_spaces.py b/tests/informatics/test_design_spaces.py index 734a59dc2..73634db4a 100644 --- a/tests/informatics/test_design_spaces.py +++ b/tests/informatics/test_design_spaces.py @@ -125,7 +125,7 @@ def test_data_source_build(valid_data_source_design_space_dict): ds = DesignSpace.build(valid_data_source_design_space_dict) assert ds.name == valid_data_source_design_space_dict["data"]["instance"]["name"] assert ds.data_source == DataSource.build(valid_data_source_design_space_dict["data"]["instance"]["data_source"]) - assert str(ds) == f'' + assert str(ds) == f"" def test_data_source_create(valid_data_source_design_space_dict): From 1d58ee7261a3cb748f5bb69475cb636abe92b401 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 4 Jan 2024 15:27:27 -0500 Subject: [PATCH 10/22] Add tests for 100% coverage. --- tests/informatics/test_design_spaces.py | 20 ++++++----- tests/seeding/test_find_or_create.py | 44 +++++++++++++++++++++++++ tests/utils/factories.py | 1 + 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/tests/informatics/test_design_spaces.py b/tests/informatics/test_design_spaces.py index 73634db4a..d74c3ca78 100644 --- a/tests/informatics/test_design_spaces.py +++ b/tests/informatics/test_design_spaces.py @@ -6,8 +6,7 @@ from citrine.informatics.constraints import IngredientCountConstraint from citrine.informatics.data_sources import DataSource, GemTableDataSource from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor, \ - CategoricalDescriptor, \ - IntegerDescriptor + CategoricalDescriptor, IntegerDescriptor from citrine.informatics.design_spaces import * from citrine.informatics.dimensions import ContinuousDimension, EnumeratedDimension, \ IntegerDimension @@ -124,14 +123,17 @@ def test_hierarchical_initialization(hierarchical_design_space): def test_data_source_build(valid_data_source_design_space_dict): ds = DesignSpace.build(valid_data_source_design_space_dict) assert ds.name == valid_data_source_design_space_dict["data"]["instance"]["name"] + assert ds.description == valid_data_source_design_space_dict["data"]["description"] assert ds.data_source == DataSource.build(valid_data_source_design_space_dict["data"]["instance"]["data_source"]) assert str(ds) == f"" -def test_data_source_create(valid_data_source_design_space_dict): - ds = valid_data_source_design_space_dict - round_robin = DesignSpace.build(ds) - assert ds["data"]["name"] == round_robin.name - assert ds["data"]["description"] == round_robin.description - assert ds["data"]["instance"]["data_source"] == round_robin.data_source.dump() - assert "DataSource" in str(ds) +def test_data_source_initialization(valid_data_source_design_space_dict): + data = valid_data_source_design_space_dict["data"] + data_source = DataSource.build(data["instance"]["data_source"]) + ds = DataSourceDesignSpace(name=data["instance"]["name"], + description=data["description"], + data_source=data_source) + assert ds.name == data["instance"]["name"] + assert ds.description == data["description"] + assert ds.data_source.dump() == data["instance"]["data_source"] diff --git a/tests/seeding/test_find_or_create.py b/tests/seeding/test_find_or_create.py index 8b705615e..fcb0dc533 100644 --- a/tests/seeding/test_find_or_create.py +++ b/tests/seeding/test_find_or_create.py @@ -1,9 +1,11 @@ +import uuid from typing import Optional, Callable from uuid import UUID import pytest from citrine._rest.collection import Collection from citrine.resources.dataset import Dataset, DatasetCollection +from citrine.resources.design_workflow import DesignWorkflowCollection from citrine.resources.process_spec import ProcessSpecCollection, ProcessSpec from citrine.resources.predictor import PredictorCollection from citrine.resources.project import ProjectCollection @@ -13,6 +15,7 @@ get_by_name_or_raise_error, find_or_create_project, find_or_create_dataset, create_or_update, find_or_create_team) +from tests.utils.factories import BranchDataFactory, DesignWorkflowDataFactory from tests.utils.fakes.fake_dataset_collection import FakeDatasetCollection from tests.utils.fakes import FakePredictorCollection from tests.utils.fakes.fake_project_collection import FakeProjectCollection @@ -26,6 +29,15 @@ absent_name = "absent" +# With our testing, it's important that the same session instance is carried throughout. When we +# have code which does a deep copy of a module (such as create_or_update), the session from that +# module will be a different object, which can cause issues. +class LocalDesignWorkflowCollection(DesignWorkflowCollection): + def update(self, model): + model._session = self.session + return super().update(model) + + @pytest.fixture def fake_collection() -> Collection: class FakeCollection(ProcessSpecCollection): @@ -333,6 +345,38 @@ def test_create_or_update_unique_found(predictor_collection): assert updated_pred.description == "I am updated!" +def test_create_or_update_unique_found_design_workflow(session): + # test when there is a single unique resource that exists with the listed name and update + branch_data = BranchDataFactory() + root_id = UUID(branch_data["metadata"]["root_id"]) + version = branch_data["metadata"]["version"] + dw1_dict = DesignWorkflowDataFactory() + dw2_dict = DesignWorkflowDataFactory(branch_root_id=root_id, branch_version=version) + dw3_dict = DesignWorkflowDataFactory() + session.set_responses( + # Build (setup) + branch_data, # Find the model's branch root ID and version + # List + {"response": [branch_data]}, # Find the collection's branch version ID + {"response": [dw1_dict, dw2_dict, dw3_dict]}, # Return the design workflows + branch_data, branch_data, branch_data, # Lookup the branch root ID and version of each design workflow. + # Update + {"response": [branch_data]}, # Lookup the module's branch version ID + {"response": []}, # Check if there are any executions + dw2_dict, # Return the updated design workflow + branch_data # Lookup the updated design workflow branch root ID and version + ) + + collection = LocalDesignWorkflowCollection(project_id=uuid.uuid4(), session=session, branch_root_id=root_id, branch_version=version) + dw2 = collection.build(dw2_dict) + + #verify that the returned object is updated + returned_dw = create_or_update(collection=collection, resource=dw2) + + assert returned_dw.name == dw2.name + assert returned_dw.branch_root_id == collection.branch_root_id == UUID(branch_data["metadata"]["root_id"]) + assert returned_dw.branch_version == collection.branch_version == branch_data["metadata"]["version"] + def test_create_or_update_raise_error_multiple_found(predictor_collection): # test when there are multiple resources that exists with the same listed name and raise error # resource 1 is not a unique name diff --git a/tests/utils/factories.py b/tests/utils/factories.py index 7a6a7cec9..487da4276 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -252,6 +252,7 @@ class DesignWorkflowDataFactory(factory.DictFactory): archived = False design_space_id = factory.Faker("uuid4") predictor_id = factory.Faker("uuid4") + branch_id = factory.Faker("uuid4") status = "SUCCEEDED" status_description = "READY" status_detail = [] From 2348df97f3d6d486c17d97d312bf1ad7cecfc223 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Tue, 9 Jan 2024 13:58:53 -0500 Subject: [PATCH 11/22] Drop all pandas usage. An oversight caused us to miss dropping pandas from a single test. Since that's the only place we use it, we rewrite the test without a DataFrame, allowing us to drop the dependency. --- .travis.yml | 2 +- tests/_util/test_template_util.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 415142a10..a27c76134 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - '3.10' - '3.11' env: - - UPGRADES="-U gemd pandas arrow urllib3 requests pytest boto3" + - UPGRADES="-U gemd arrow urllib3 requests pytest boto3" - UPGRADES="" install: - pip install --only-binary ':all:' -r requirements.txt diff --git a/tests/_util/test_template_util.py b/tests/_util/test_template_util.py index 3dbd4dbe6..c0d869d78 100644 --- a/tests/_util/test_template_util.py +++ b/tests/_util/test_template_util.py @@ -1,10 +1,8 @@ -from numpy.lib.arraysetops import isin from citrine._utils.template_util import make_attribute_table from gemd.entity.object import * from gemd.entity.attribute import * from gemd.entity.value import * from gemd.entity.link_by_uid import LinkByUID -import pandas as pd def _make_list_of_gems(): faux_gems = [ @@ -135,12 +133,10 @@ def test_attribute_alignment(): info_dict = make_attribute_table(_make_list_of_gems()) assert(isinstance(info_dict, list)) assert(isinstance(info_dict[0], dict)) - df = pd.DataFrame(info_dict) - assert isinstance(df.iloc[0,3], NominalReal) - assert isinstance(df.iloc[1,3], NormalReal) - assert isinstance(df.iloc[4,3], NominalReal) - assert isinstance(df.iloc[0,5], InChI) - assert pd.isnull(df.iloc[1,5]) - # Shape asserts that nestled objects are being flattened and - # "like" attributes are being put into the same column - assert df.shape == (6,12) + assert isinstance(info_dict[0]["PARAMETER: param 1"], NominalReal) + assert isinstance(info_dict[1]["PARAMETER: param 1"], NormalReal) + assert isinstance(info_dict[4]["PARAMETER: param 1"], NominalReal) + assert isinstance(info_dict[0]["PARAMETER: attr 1"], InChI) + assert info_dict[1].get("PARAMETER: attr 1") is None + assert len(info_dict) == 6 + assert len({key for adict in info_dict for key in adict}) == 12 From 4298fee2aad5c22e38fe402b97c385995003f002 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Wed, 20 Dec 2023 16:17:15 -0500 Subject: [PATCH 12/22] [PLA-13149] Bump dependency versions. Bumped all dependencies to their latest version, with a few exceptions. pytest-flake8 and pydocstyle don't appear to be in use, so they were dropped instead. botocore only needs to be in our requirements file if we need to pin it separately from boto3, which we currently do not. boto3 imposes some restrictions on urllib3. If Python is < 3.10, urllib3 must be pre-2.0. Since 1.31.62, if Python is more recent, it can support up to (but not including) urllib3 2.1. We've set the pins accordingly. The latest version of Sphinx available for Python 3.8 is 7.1.2, so we pin it to that version across the board. --- requirements.txt | 23 +++++++++++------------ setup.py | 20 ++++++++++---------- test_requirements.txt | 21 +++++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/requirements.txt b/requirements.txt index fdd3d1eb9..4940a3b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ -gemd==1.16.0 -requests==2.31.0 -pyjwt==2.4.0 -arrow==0.15.4 -sphinx==4.3.0 -sphinxcontrib-apidoc==0.3.0 -sphinx-rtd-theme==1.0.0 -boto3==1.17.93 -botocore==1.20.95 +arrow==1.3.0 +boto3==1.34.13 deprecation==2.1.0 -urllib3==1.26.18 -tqdm==4.62.3 -pint==0.18 +gemd==1.16.7 +pyjwt==2.8.0 +requests==2.31.0 +tqdm==4.66.1 + +# boto3 (through botocore) depends on urllib3. Version 1.34.12 requires +# urllib3 < 1.27 when using Python 3.9 or less, and urllib3 < 2.1 otherwise. +urllib3==1.26.18; python_version < "3.10" +urllib3==2.0.7; python_version >= "3.10" diff --git a/setup.py b/setup.py index e18745c0e..e955891e0 100644 --- a/setup.py +++ b/setup.py @@ -24,21 +24,21 @@ install_requires=[ "requests>=2.31.0,<3", "pyjwt>=2,<3", - "arrow>=0.15.4,<2", - "gemd>=1.15.0,<2", - "boto3>=1.17.93,<2", - "botocore>=1.20.95,<2", + "arrow>=1.0.0,<2", + "gemd>=1.16.7,<2", + "boto3>=1.34.12,<2", "deprecation>=2.1.0,<3", "urllib3>=1.26.18,<3", - "tqdm>=4.62.3,<5" + "tqdm>=4.27.0,<5" ], extras_require={ "../tests": [ - "factory-boy>=2.12.0,<4", - "mock>=3.0.5,<5", - "pytest>=6.2.5,<8", - "pytz>=2020.1", - "requests-mock>=1.7.0,<2", + "factory-boy>=3.3.0,<4", + "mock>=5.1.0,<6", + "pandas>=2.0.3,<3", + "pytest>=7.4.4,<8", + "pytz>=2023.3.post1", + "requests-mock>=1.11.0,<2", ] }, classifiers=[ diff --git a/test_requirements.txt b/test_requirements.txt index 3b1f1093f..270278029 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,11 +1,12 @@ -flake8==3.7.8 -flake8-docstrings==1.3.1 -mock==3.0.5 -pytest==6.2.5 -pytz==2020.1 -pydocstyle==3.0.0 -pytest-cov==3.0.0 -pytest-flake8==1.0.4 -factory-boy==2.12.0 -requests-mock==1.7.0 derp==0.1.1 +factory-boy==3.3.0 +flake8==7.0.0 +flake8-docstrings==1.7.0 +mock==5.1.0 +pytest==7.4.4 +pytest-cov==4.1.0 +pytz==2023.3.post1 +requests-mock==1.11.0 +sphinx==7.1.2 +sphinx-rtd-theme==2.0.0 +sphinxcontrib-apidoc==0.4.0 From 6b9415691d6745ebabb2292c08a373d58ba49efe Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 4 Jan 2024 12:47:09 -0500 Subject: [PATCH 13/22] [PLA-13149] Address flake8 changes. --- src/citrine/_rest/collection.py | 4 ++-- src/citrine/_serialization/polymorphic_serializable.py | 5 +++-- src/citrine/_serialization/properties.py | 4 ++-- src/citrine/gemtables/variables.py | 2 +- src/citrine/informatics/predictor_evaluation_result.py | 2 +- src/citrine/resources/ingestion.py | 10 ++++++---- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/citrine/_rest/collection.py b/src/citrine/_rest/collection.py index c879ad68e..22c453180 100644 --- a/src/citrine/_rest/collection.py +++ b/src/citrine/_rest/collection.py @@ -5,14 +5,14 @@ from citrine._rest.pageable import Pageable from citrine._rest.paginator import Paginator -from citrine._rest.resource import ResourceRef +from citrine._rest.resource import Resource, ResourceRef from citrine._utils.functions import resource_path from citrine.exceptions import ModuleRegistrationFailedException, NonRetryableException from citrine.resources.response import Response logger = getLogger(__name__) -ResourceType = TypeVar('ResourceType', bound='Resource') +ResourceType = TypeVar('ResourceType', bound=Resource) # Python does not support a TypeVar being used as a bound for another TypeVar. # Thus, this will never be particularly type safe on its own. The solution is to diff --git a/src/citrine/_serialization/polymorphic_serializable.py b/src/citrine/_serialization/polymorphic_serializable.py index 209b7c38b..bd7028266 100644 --- a/src/citrine/_serialization/polymorphic_serializable.py +++ b/src/citrine/_serialization/polymorphic_serializable.py @@ -1,9 +1,10 @@ -from typing import Generic, TypeVar, Type from abc import abstractmethod +from typing import Generic, TypeVar, Type + from citrine._serialization.serializable import Serializable -SelfType = TypeVar('SelfType', bound='Resource') +SelfType = TypeVar('SelfType', bound='PolymorphicSerializable') class PolymorphicSerializable(Generic[SelfType]): diff --git a/src/citrine/_serialization/properties.py b/src/citrine/_serialization/properties.py index 6e4e7161d..bc0a4c7c5 100644 --- a/src/citrine/_serialization/properties.py +++ b/src/citrine/_serialization/properties.py @@ -783,7 +783,7 @@ def _deserialize(self, data: dict) -> typing.Any: def _serialize(self, obj: typing.Any) -> dict: serialized = {} - if type(obj) != self.klass and isinstance(obj, Serializable): + if type(obj) is not self.klass and isinstance(obj, Serializable): # If the object class doesn't match this one, then it is a subclass # that may have more fields, so defer to them by calling the dump method # it must have as a Serializable @@ -994,7 +994,7 @@ def serialized_types(self): def _deserialize(self, value: typing.Union[dict, list]) -> dict: deserialized = dict() - if type(value) == list: + if type(value) is list: for pair in value: deserialized_key = self.keys_type.deserialize(pair[0]) deserialized_value = self.values_type.deserialize(pair[1]) diff --git a/src/citrine/gemtables/variables.py b/src/citrine/gemtables/variables.py index f78085f60..ce15397f3 100644 --- a/src/citrine/gemtables/variables.py +++ b/src/citrine/gemtables/variables.py @@ -65,7 +65,7 @@ def _attrs(self) -> List[str]: pass # pragma: no cover def __eq__(self, other): - return type(self) == type(other) and all([ + return type(self) is type(other) and all([ getattr(self, key) == getattr(other, key) for key in self._attrs() ]) diff --git a/src/citrine/informatics/predictor_evaluation_result.py b/src/citrine/informatics/predictor_evaluation_result.py index d31557512..b71176a1f 100644 --- a/src/citrine/informatics/predictor_evaluation_result.py +++ b/src/citrine/informatics/predictor_evaluation_result.py @@ -4,7 +4,7 @@ from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable from citrine.informatics.predictor_evaluation_metrics import PredictorEvaluationMetric -from citrine.informatics.predictor_evaluator import PredictorEvaluator, HoldoutSetEvaluator,\ +from citrine.informatics.predictor_evaluator import PredictorEvaluator, HoldoutSetEvaluator, \ CrossValidationEvaluator __all__ = ['MetricValue', diff --git a/src/citrine/resources/ingestion.py b/src/citrine/resources/ingestion.py index f7415c3d8..af8693e9e 100644 --- a/src/citrine/resources/ingestion.py +++ b/src/citrine/resources/ingestion.py @@ -1,6 +1,8 @@ from typing import Optional, Iterator, Iterable from uuid import UUID +from gemd.enumeration.base_enumeration import BaseEnumeration + from citrine._rest.collection import Collection from citrine._rest.resource import Resource from citrine._serialization import properties @@ -8,7 +10,7 @@ from citrine.exceptions import CitrineException, BadRequest from citrine.jobs.job import JobSubmissionResponse, JobFailureError, _poll_for_job_completion from citrine.resources.api_error import ApiError, ValidationError -from gemd.enumeration.base_enumeration import BaseEnumeration +from citrine.resources.file_link import FileLink class IngestionStatusType(BaseEnumeration): @@ -408,7 +410,7 @@ def status(self) -> IngestionStatus: """ if self.raise_errors: raise JobFailureError( - message=f"Ingestion creation failed: self.errors", + message=f"Ingestion creation failed: {self.errors}", job_id=None, failure_reasons=self.errors ) @@ -443,7 +445,7 @@ def __init__(self, project_id: UUID, dataset_id: UUID, session: Session): self.session = session def build_from_file_links(self, - file_links: Iterable["FileLink"], + file_links: Iterable[FileLink], *, raise_errors: bool = True) -> Ingestion: """ @@ -459,7 +461,7 @@ def build_from_file_links(self, """ if len(file_links) == 0: - raise ValueError(f"No files passed.") + raise ValueError("No files passed.") invalid_links = [f for f in file_links if f.uid is None] if len(invalid_links) != 0: raise ValueError(f"{len(invalid_links)} File Links have no on-platform UID.") From a461bf187f03b2d6ec310fa5229093801ba0d8ab Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Fri, 5 Jan 2024 13:07:13 -0500 Subject: [PATCH 14/22] [PLA-13752] Address doc generation warnings. --- docs/source/data_extraction.rst | 4 +- docs/source/getting_started/datasets.rst | 2 +- docs/source/index.rst | 2 +- docs/source/modules.rst | 4 - docs/source/molecular_generation.rst | 74 +++++++++++++++++++ docs/source/setup.rst | 7 -- docs/source/workflows/getting_started.rst | 2 +- .../predictor_evaluation_workflows.rst | 1 + docs/source/workflows/predictors.rst | 3 +- .../ingredient_ratio_constraint.py | 4 +- .../hierarchical_design_space.py | 4 +- src/citrine/resources/dataset.py | 30 +++----- src/citrine/resources/file_link.py | 29 +++----- src/citrine/resources/ingestion.py | 32 ++------ src/citrine/resources/material_run.py | 6 -- src/citrine/resources/process_run.py | 10 --- src/citrine/resources/process_spec.py | 10 --- src/citrine/resources/project.py | 12 +-- src/citrine/resources/team.py | 15 +--- 19 files changed, 120 insertions(+), 131 deletions(-) delete mode 100644 docs/source/modules.rst create mode 100644 docs/source/molecular_generation.rst delete mode 100644 docs/source/setup.rst diff --git a/docs/source/data_extraction.rst b/docs/source/data_extraction.rst index 00b49f4b5..0fc53e6f1 100644 --- a/docs/source/data_extraction.rst +++ b/docs/source/data_extraction.rst @@ -8,7 +8,7 @@ A GEM Table is defined on a set of material histories, and the rows in the resul Columns correspond to data about the material histories, such as the temperature measured in a kiln used at a specific manufacturing step. Defining rows and columns ------------------------- +------------------------- A Row object describes a mapping from a list of Datasets to rows of a table by selecting a set of Material Histories. Each Material History corresponds to exactly one row, though the Material Histories may overlap such that the same objects contribute data to multiple rows. @@ -327,4 +327,4 @@ are compatible with each type of descriptor: - :class:`~citrine.informatics.descriptors.ChemicalFormulaDescriptor`: values of type :class:`~gemd.entity.EmpiricalFormula`, or values of type :class:`~gemd.entity.NominalComposition` when **all** quantity keys are valid atomic symbols - :class:`~citrine.informatics.descriptors.FormulationDescriptor`: all values extracted by ingredient quantity, identifier, and label variables - are used to represent the formulation \ No newline at end of file + are used to represent the formulation diff --git a/docs/source/getting_started/datasets.rst b/docs/source/getting_started/datasets.rst index 9898a752c..bd840d3b0 100644 --- a/docs/source/getting_started/datasets.rst +++ b/docs/source/getting_started/datasets.rst @@ -74,7 +74,7 @@ Assume you have a "band gaps project" with known id, ``band_gaps_project_id``, a Dataset Access, Sharing, and Transfer ------------------------------------- +------------------------------------- When a Dataset is created on the Citrine Platform, only members of the project in which it was created can see it and interact with it. If a Dataset is made public, it (and its entire contents) can be retrieved by any user using any project. diff --git a/docs/source/index.rst b/docs/source/index.rst index dc802834f..491ebd19c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,7 +8,7 @@ Welcome to the Citrine Python client documentation! This site documents the Python SDK for the Citrine Platform. It provides utilities to upload and manage data and design materials using Sequential Learning. -See the :ref:`getting started ` guide for a high-level introduction. +See the :ref:`getting started ` guide for a high-level introduction. The :ref:`workflows ` section documents how to configure and run artificial intelligence (AI) workflows for materials research and development. Installation diff --git a/docs/source/modules.rst b/docs/source/modules.rst deleted file mode 100644 index f026acd70..000000000 --- a/docs/source/modules.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. toctree:: - :maxdepth: 4 - - setup \ No newline at end of file diff --git a/docs/source/molecular_generation.rst b/docs/source/molecular_generation.rst new file mode 100644 index 000000000..04fd10156 --- /dev/null +++ b/docs/source/molecular_generation.rst @@ -0,0 +1,74 @@ +.. generative_design_execution: + +[ALPHA] Generative Design Execution +=================================== +The Citrine Platform offers a Generative Design Execution tool that allows the creation of new molecules by applying mutations to a set of given seed molecules. +To use this feature, you need to provide a set of starting molecules and filtering parameters using the :class:`~citrine.informatics.generative_design.GenerativeDesignInput` class. + +The class requires you to define the seed molecules for generating mutations, the fingerprint type used to calculate the `fingerprint similarity `_, the minimum fingerprint similarity between the seed and mutated molecule, the number of initial mutations attempted per seed, and the minimum substructure counts for each mutated molecule. + +Various fingerprint types are available on the Citrine Platform, including Atom Pairs (AP), Path-Length Connectivity (PHCO), Binary Path (BPF), Paths of Atoms of Heteroatoms (PATH), Extended Connectivity Fingerprint with radius 4 (ECFP4) and radius 6 (ECFP6), and Focused Connectivity Fingerprint with radius 4 (FCFP4) and radius 6 (FCFP6). +Each fingerprint type captures different aspects of molecular structure and influences the generated mutations. +You can access these fingerprint types through the :class:`~citrine.informatics.generative_design.FingerprintType` enum, like `FingerprintType.ECFP4`. + +The `structure_exclusions` parameter allows you to control the structural features of mutated molecules. +It is a sequence of exclusion types corresponding to the types of structural features or elements to exclude from the list of possible mutation steps during the generative design process. +If a type is present in the sequence, the mutation steps generated by the process will avoid using that feature or element. +The available structure exclusion options can be found in the :class:`~citrine.informatics.generative_design.StructureExclusion` class. + +The `min_substructure_counts` parameter is a dictionary for constraining which substructures (represented by SMILES strings) must appear in each mutated molecule, along with integer values representing the minimum number of times each substructure must appear in a molecule to be considered a valid mutation. + +After the generative design process is complete, the mutations are filtered based on their similarity to the starting seed molecules. +Mutations that do not meet the similarity threshold or are duplicates will be discarded. The remaining mutations are returned as a subset of the original mutations in the form of a list of :class:`~citrine.informatics.generative_design.GenerativeDesignResult` objects. +These results contain information about the seed molecule, the mutation, the similarity score, and the fingerprint type used during execution. + +After triggering the execution and waiting for completion, the user can retrieve the results and utilize them in their work.' +The following example demonstrates how to run a generative design execution on the Citrine Platform using the Citrine Python client. + +.. code-block:: python + + import os + from citrine import Citrine + from citrine.jobs.waiting import wait_while_executing + from citrine.informatics.generative_design import GenerativeDesignInput, FingerprintType, StructureExclusion + + session = Citrine( + api_key=os.environ.get("API_KEY"), + scheme="https", + host=os.environ.get("CITRINE_HOST"), + port="443", + ) + + team_uid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + project_uid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + team = session.teams.get(team_uid) + project = team.projects.get(project_uid) + + # Trigger a new generative design execution + generative_design_input = GenerativeDesignInput( + seeds=["CC(O)=O", "CCCCCCCCCCCC"], + fingerprint_type=FingerprintType.ECFP4, + min_fingerprint_similarity=0.1, + mutation_per_seed=1000, + structure_exclusions=[ + StructureExclusion.BROMINE, + StructureExclusion.CHLORINE, + ], + min_substructure_counts={"c1ccccc1": 1} + ) + generative_design_execution = project.generative_design_executions.trigger( + generative_design_input + ) + execution = wait_while_executing( + collection=project.generative_design_executions, execution=generative_design_execution + ) + generated = execution.results() + mutations = [(gen.seed, gen.mutated) for gen in generated] + + # Or get a completed execution by ID + execution_uid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + execution = project.generative_design_executions.get(execution_uid) + generated = execution.results() + mutations = [(gen.seed, gen.mutated) for gen in generated] + +To execute the code, replace the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` placeholders with valid UIDs from your Citrine environment. Ensure that the API key, scheme, host, and port are correctly specified in the `Citrine` initialization. diff --git a/docs/source/setup.rst b/docs/source/setup.rst deleted file mode 100644 index 552eb49d6..000000000 --- a/docs/source/setup.rst +++ /dev/null @@ -1,7 +0,0 @@ -setup module -============ - -.. automodule:: setup - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/workflows/getting_started.rst b/docs/source/workflows/getting_started.rst index 0cf4dcba7..3294151e8 100644 --- a/docs/source/workflows/getting_started.rst +++ b/docs/source/workflows/getting_started.rst @@ -1,4 +1,4 @@ -.. _getting-started: +.. _ai-engine-getting-started: Getting Started =============== diff --git a/docs/source/workflows/predictor_evaluation_workflows.rst b/docs/source/workflows/predictor_evaluation_workflows.rst index 145a8744d..bf88a3859 100644 --- a/docs/source/workflows/predictor_evaluation_workflows.rst +++ b/docs/source/workflows/predictor_evaluation_workflows.rst @@ -15,6 +15,7 @@ Metrics are specified as a set of :class:`PredictorEvaluationMetrics `. .. _Expression Predictor: + Expression predictor -------------------- diff --git a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py index 0149a0bfd..f85364ce1 100644 --- a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py @@ -13,8 +13,10 @@ class IngredientRatioConstraint(Serializable['IngredientRatioConstraint'], Const """A formulation constraint operating on the ratio of quantities of ingredients and a basis. Example: "6 to 7 parts ingredient A per 100 parts ingredient B" becomes + .. code:: python - IngredientRatioConstraint(min=6, max=7, ingredient=("A", 100), basis_ingredients=["B"]) + + IngredientRatioConstraint(min=6, max=7, ingredient=("A", 100), basis_ingredients=["B"]) Parameters ---------- diff --git a/src/citrine/informatics/design_spaces/hierarchical_design_space.py b/src/citrine/informatics/design_spaces/hierarchical_design_space.py index 457a35227..3a7d7b6e5 100644 --- a/src/citrine/informatics/design_spaces/hierarchical_design_space.py +++ b/src/citrine/informatics/design_spaces/hierarchical_design_space.py @@ -124,11 +124,11 @@ class HierarchicalDesignSpace(EngineResource["HierarchicalDesignSpace"], DesignS referencing other sub-nodes, allowing for the linkage of complex material history shapes in the resulting candidates. - Every node also contains a set of :class:`~citrine.informatics.dimensions.Dimension`s + Every node also contains a set of :class:`~citrine.informatics.dimensions.Dimension`\\s used to define any attributes (i.e., properties, processing parameters) that will appear on the materials produced by that node. - :class:`~citrine.informatics.data_sources.DataSource`s can be included on the configuration + :class:`~citrine.informatics.data_sources.DataSource`\\s can be included on the configuration to allow for design over "known" materials. The Citrine Platform will look up the ingredient names from formulation subspaces on the design space nodes in order to inject their composition/properties into the material history of the candidates. diff --git a/src/citrine/resources/dataset.py b/src/citrine/resources/dataset.py index 1583192db..b89a65ea6 100644 --- a/src/citrine/resources/dataset.py +++ b/src/citrine/resources/dataset.py @@ -53,45 +53,33 @@ class Dataset(Resource['Dataset']): unique_name: Optional[str] An optional, globally unique name that can be used to retrieve the dataset. - Attributes - ---------- - uid: UUID - Unique uuid4 identifier of this dataset. - deleted: bool - Flag indicating whether or not this dataset has been deleted. - created_by: UUID - ID of the user who created the dataset. - updated_by: UUID - ID of the user who last updated the dataset. - deleted_by: UUID - ID of the user who deleted the dataset, if it is deleted. - create_time: int - Time the dataset was created, in seconds since epoch. - update_time: int - Time the dataset was most recently updated, in seconds since epoch. - delete_time: int - Time the dataset was deleted, in seconds since epoch, if it is deleted. - public: bool - Flag indicating whether the dataset is publicly readable. - """ _response_key = 'dataset' _resource_type = ResourceTypeEnum.DATASET uid = properties.Optional(properties.UUID(), 'id') + """UUID: Unique uuid4 identifier of this dataset.""" name = properties.String('name') unique_name = properties.Optional(properties.String(), 'unique_name') summary = properties.String('summary') description = properties.String('description') deleted = properties.Optional(properties.Boolean(), 'deleted') + """bool: Flag indicating whether or not this dataset has been deleted.""" created_by = properties.Optional(properties.UUID(), 'created_by') + """UUID: ID of the user who created the dataset.""" updated_by = properties.Optional(properties.UUID(), 'updated_by') + """UUID: ID of the user who last updated the dataset.""" deleted_by = properties.Optional(properties.UUID(), 'deleted_by') + """UUID: ID of the user who deleted the dataset, if it is deleted.""" create_time = properties.Optional(properties.Datetime(), 'create_time') + """int: Time the dataset was created, in seconds since epoch.""" update_time = properties.Optional(properties.Datetime(), 'update_time') + """int: Time the dataset was most recently updated, in seconds since epoch.""" delete_time = properties.Optional(properties.Datetime(), 'delete_time') + """int: Time the dataset was deleted, in seconds since epoch, if it is deleted.""" public = properties.Optional(properties.Boolean(), 'public') + """bool: Flag indicating whether the dataset is publicly readable.""" project_id = properties.Optional(properties.UUID(), 'project_id', serializable=False, deserializable=False) session = properties.Optional(properties.Object(Session), 'session', diff --git a/src/citrine/resources/file_link.py b/src/citrine/resources/file_link.py index 7185bd489..2ea840660 100644 --- a/src/citrine/resources/file_link.py +++ b/src/citrine/resources/file_link.py @@ -132,26 +132,6 @@ class FileLink( url: str URL that can be used to access the file. - Attributes - ---------- - uid: UUID - Unique uuid4 identifier of this file; consistent across versions. - version: UUID - Unique uuid4 identifier of this version of this file - version_number: Integer - How many times this file has been uploaded; - files are the "same" if the share a filename and dataset - created_time: Datetime - Time the file was created on platform. - created_by: UUID - Unique uuid4 identifier of this User who loaded this file - mime_type: String - Encoded string representing the type of the file (IETF RFC 2045) - size: Integer - Size in bytes of the file - description: String - A human-readable description of the file - """ # NOTE: skipping the "metadata" field since it appears to be unused @@ -160,13 +140,22 @@ class FileLink( filename = properties.String('filename') url = properties.String('url') uid = properties.Optional(properties.UUID, 'id', serializable=False) + """UUID: Unique uuid4 identifier of this file; consistent across versions.""" version = properties.Optional(properties.UUID, 'version', serializable=False) + """UUID: Unique uuid4 identifier of this version of this file.""" created_time = properties.Optional(properties.Datetime, 'created_time', serializable=False) + """datetime: Time the file was created on platform.""" created_by = properties.Optional(properties.UUID, 'created_by', serializable=False) + """UUID: Unique uuid4 identifier of this User who loaded this file.""" mime_type = properties.Optional(properties.String, 'mime_type', serializable=False) + """str: Encoded string representing the type of the file (IETF RFC 2045).""" size = properties.Optional(properties.Integer, 'size', serializable=False) + """int: Size in bytes of the file.""" description = properties.Optional(properties.String, 'description', serializable=False) + """str: A human-readable description of the file.""" version_number = properties.Optional(properties.Integer, 'version_number', serializable=False) + """int: How many times this file has been uploaded; files are the "same" if they share a + filename and dataset.""" def __init__(self, filename: str, url: str): GEMDFileLink.__init__(self, filename, url) diff --git a/src/citrine/resources/ingestion.py b/src/citrine/resources/ingestion.py index af8693e9e..a0b3db5fd 100644 --- a/src/citrine/resources/ingestion.py +++ b/src/citrine/resources/ingestion.py @@ -107,19 +107,13 @@ def __repr__(self): class IngestionException(CitrineException): - """ - [ALPHA] An exception that contains details of a failed ingestion. - - Attributes - ---------- - uid: Optional[UUID] - errors: List[IngestionErrorTrace] - - """ + """[ALPHA] An exception that contains details of a failed ingestion.""" uid = properties.Optional(properties.UUID(), 'ingestion_id', default=None) + """Optional[UUID]""" status = properties.Enumeration(IngestionStatusType, "status") errors = properties.List(properties.Object(IngestionErrorTrace), "errors") + """List[IngestionErrorTrace]""" def __init__(self, *, @@ -147,20 +141,14 @@ def from_api_error(cls, source: ApiError) -> "IngestionException": class IngestionStatus(Resource['IngestionStatus']): - """ - [ALPHA] An object that represents the outcome of an ingestion event. - - Attributes - ---------- - uid: String - status: IngestionStatusType - errors: List[IngestionErrorTrace] - - """ + """[ALPHA] An object that represents the outcome of an ingestion event.""" uid = properties.Optional(properties.UUID(), 'ingestion_id', default=None) + """UUID""" status = properties.Enumeration(IngestionStatusType, "status") + """IngestionStatusType""" errors = properties.List(properties.Object(IngestionErrorTrace), "errors") + """List[IngestionErrorTrace]""" def __init__(self, *, @@ -190,14 +178,10 @@ class Ingestion(Resource['Ingestion']): every object in that dataset. A user with write access to a dataset can create, update, and delete objects in the dataset. - Attributes - ---------- - uid: UUID - Unique uuid4 identifier of this ingestion. - """ uid = properties.UUID('ingestion_id') + """UUID: Unique uuid4 identifier of this ingestion.""" project_id = properties.UUID('project_id') dataset_id = properties.UUID('dataset_id') session = properties.Object(Session, 'session', serializable=False) diff --git a/src/citrine/resources/material_run.py b/src/citrine/resources/material_run.py index 0c1157f8c..6420541b8 100644 --- a/src/citrine/resources/material_run.py +++ b/src/citrine/resources/material_run.py @@ -50,12 +50,6 @@ class MaterialRun( file_links: List[FileLink], optional Links to associated files, with resource paths into the files API. - Attributes - ---------- - measurements: List[MeasurementRun], optional - Measurements performed on this material. The link is established by creating the - measurement run and settings its `material` field to this material run. - """ _response_key = GEMDMaterialRun.typ # 'material_run' diff --git a/src/citrine/resources/process_run.py b/src/citrine/resources/process_run.py index 8468487e8..0920eb08f 100644 --- a/src/citrine/resources/process_run.py +++ b/src/citrine/resources/process_run.py @@ -47,16 +47,6 @@ class ProcessRun(GEMDResource['ProcessRun'], ObjectRun, GEMDProcessRun, typ=GEMD source: PerformedSource, optional Information about the person who performed the run and when. - Attributes - ---------- - output_material: MaterialRun - The material run that this process run produces. The link is established by creating - the material run and settings its `process` field to this process run. - - ingredients: List[IngredientRun] - Ingredient runs that act as inputs to this process run. The link is established by - creating each ingredient run and setting its `process` field to this process run. - """ _response_key = GEMDProcessRun.typ # 'process_run' diff --git a/src/citrine/resources/process_spec.py b/src/citrine/resources/process_spec.py index 54439a41a..55f45c97f 100644 --- a/src/citrine/resources/process_spec.py +++ b/src/citrine/resources/process_spec.py @@ -49,16 +49,6 @@ class ProcessSpec( file_links: List[FileLink], optional Links to associated files, with resource paths into the files API. - Attributes - ---------- - output_material: MaterialSpec - The material spec that this process spec produces. The link is established by creating - the material spec and settings its `process` field to this process spec. - - ingredients: List[IngredientSpec], optional - Ingredient specs that act as inputs to this process spec. The link is established by - creating each ingredient spec and setting its `process` field to this process spec. - """ _response_key = GEMDProcessSpec.typ # 'process_spec' diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index f08cab49d..1fb8f90b2 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -63,15 +63,6 @@ class Project(Resource['Project']): session: Session, optional The Citrine session used to connect to the database. - Attributes - ---------- - uid: UUID - Unique uuid4 identifier of this project. - status: str - Status of the project. - created_at: int - Time the project was created, in seconds since epoch. - """ _response_key = 'project' @@ -80,8 +71,11 @@ class Project(Resource['Project']): name = properties.String('name') description = properties.Optional(properties.String(), 'description') uid = properties.Optional(properties.UUID(), 'id') + """UUID: Unique uuid4 identifier of this project.""" status = properties.Optional(properties.String(), 'status') + """str: Status of the project.""" created_at = properties.Optional(properties.Datetime(), 'created_at') + """int: Time the project was created, in seconds since epoch.""" team_id = properties.Optional(properties.UUID, "team.id", serializable=False) def __init__(self, diff --git a/src/citrine/resources/team.py b/src/citrine/resources/team.py index 71f42ed0a..0731ce112 100644 --- a/src/citrine/resources/team.py +++ b/src/citrine/resources/team.py @@ -121,17 +121,6 @@ class Team(Resource['Team']): session: Session, optional The Citrine session used to connect to the database. - Attributes - ---------- - uid: UUID - Unique uuid4 identifier of this team. - created_at: int - Time the team was created, in seconds since epoch. - name: str - Name of the Team - description: str - Description of the Team - """ _response_key = 'team' @@ -139,9 +128,13 @@ class Team(Resource['Team']): _api_version = "v3" name = properties.String('name') + """str: Name of the Team""" description = properties.Optional(properties.String(), 'description') + """str: Description of the Team""" uid = properties.Optional(properties.UUID(), 'id') + """UUID: Unique uuid4 identifier of this team.""" created_at = properties.Optional(properties.Datetime(), 'created_at') + """int: Time the team was created, in seconds since epoch.""" def __init__(self, name: str, From 7202b4aa0dd9fad0010cf0caa8d7fa6c95679d33 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Fri, 5 Jan 2024 15:57:38 -0500 Subject: [PATCH 15/22] [PLA-13752] Add link for API Reference. Since we're generating our API docs, one of our tables of contents must link to it, or else it will throw a warning. So I've added it to the main page. That main page TOC goes multiple levels deep, so it will show the generated page's name. Since we were starting it at src/, that was its name, which wouldn't make much sense to users. So I tweaked it to start at src/citrine/. --- docs/source/conf.py | 4 ++-- docs/source/index.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1e00c88af..5b9aa88be 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,7 +13,7 @@ import citrine import os import sys -sys.path.insert(0, os.path.abspath('../../src')) +sys.path.insert(0, os.path.abspath('../../src/citrine')) # -- Project information ----------------------------------------------------- @@ -44,7 +44,7 @@ # build. # # See: https://github.com/sphinx-contrib/apidoc -apidoc_module_dir = '../../src' +apidoc_module_dir = '../../src/citrine' apidoc_output_dir = 'reference' apidoc_excluded_paths = ['tests'] apidoc_separate_modules = True diff --git a/docs/source/index.rst b/docs/source/index.rst index 491ebd19c..c57f8ed45 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -41,6 +41,7 @@ Table of Contents formulations_example molecular_generation FAQ/index + API Reference Indices and tables ================== From 41d150432c4cc51676b1a5bcd66fa610d7e9b466 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Wed, 10 Jan 2024 16:37:12 -0500 Subject: [PATCH 16/22] [PLA-13800] Cleanup logging. Remove invocation of basicConfig on the root logger, so as to not set up a logger when the user has asked for none. Drop explicitly setting the "default" warning filter. Also get rid of a bunch of places the logger is imported but not used. --- src/citrine/__init__.py | 5 ----- src/citrine/_rest/collection.py | 3 --- src/citrine/_rest/pageable.py | 4 ---- src/citrine/resources/file_link.py | 3 --- src/citrine/resources/material_spec.py | 3 --- tests/resources/test_branch.py | 3 --- tests/resources/test_project.py | 3 --- tests/resources/test_team.py | 3 --- tests/serialization/test_attribute_template.py | 1 - 9 files changed, 28 deletions(-) diff --git a/src/citrine/__init__.py b/src/citrine/__init__.py index 5948bd118..effc6d496 100644 --- a/src/citrine/__init__.py +++ b/src/citrine/__init__.py @@ -6,9 +6,4 @@ """ from citrine.citrine import Citrine # noqa: F401 -import logging -import warnings from .__version__ import __version__ # noqa: F401 - -logging.basicConfig(level=logging.WARNING) -warnings.simplefilter("default", DeprecationWarning) diff --git a/src/citrine/_rest/collection.py b/src/citrine/_rest/collection.py index 22c453180..6f83805ed 100644 --- a/src/citrine/_rest/collection.py +++ b/src/citrine/_rest/collection.py @@ -1,5 +1,4 @@ from abc import abstractmethod -from logging import getLogger from typing import Optional, Union, Generic, TypeVar, Iterable, Iterator, Sequence, Dict from uuid import UUID @@ -10,8 +9,6 @@ from citrine.exceptions import ModuleRegistrationFailedException, NonRetryableException from citrine.resources.response import Response -logger = getLogger(__name__) - ResourceType = TypeVar('ResourceType', bound=Resource) # Python does not support a TypeVar being used as a bound for another TypeVar. diff --git a/src/citrine/_rest/pageable.py b/src/citrine/_rest/pageable.py index 6ca566a95..df09d602a 100644 --- a/src/citrine/_rest/pageable.py +++ b/src/citrine/_rest/pageable.py @@ -1,11 +1,7 @@ -from logging import getLogger from typing import Optional, Iterable, Dict, Tuple, Callable, Union, Sequence from uuid import UUID -logger = getLogger(__name__) - - class Pageable(): """Class that allows paging.""" diff --git a/src/citrine/resources/file_link.py b/src/citrine/resources/file_link.py index 2ea840660..dc7ec1ec1 100644 --- a/src/citrine/resources/file_link.py +++ b/src/citrine/resources/file_link.py @@ -2,7 +2,6 @@ import mimetypes import os from pathlib import Path -from logging import getLogger from tempfile import TemporaryDirectory from typing import Optional, Tuple, Union, Dict, Iterable, Sequence from urllib.parse import urlparse, unquote_plus @@ -26,8 +25,6 @@ from gemd.entity.file_link import FileLink as GEMDFileLink from gemd.enumeration.base_enumeration import BaseEnumeration -logger = getLogger(__name__) - class SearchFileFilterTypeEnum(BaseEnumeration): """ diff --git a/src/citrine/resources/material_spec.py b/src/citrine/resources/material_spec.py index 8f618c62e..55216182e 100644 --- a/src/citrine/resources/material_spec.py +++ b/src/citrine/resources/material_spec.py @@ -1,5 +1,4 @@ """Resources that represent material spec data objects.""" -from logging import getLogger from typing import List, Dict, Optional, Type, Iterator, Union from uuid import UUID @@ -15,8 +14,6 @@ from gemd.entity.object.process_spec import ProcessSpec as GEMDProcessSpec from gemd.entity.template.material_template import MaterialTemplate as GEMDMaterialTemplate -logger = getLogger(__name__) - class MaterialSpec( GEMDResource['MaterialSpec'], diff --git a/tests/resources/test_branch.py b/tests/resources/test_branch.py index c1e8408c4..8fcb4c53b 100644 --- a/tests/resources/test_branch.py +++ b/tests/resources/test_branch.py @@ -1,6 +1,5 @@ import uuid from datetime import datetime -from logging import getLogger import pytest from dateutil import tz @@ -14,8 +13,6 @@ BranchDataFieldFactory, BranchMetadataFieldFactory, BranchDataUpdateFactory from tests.utils.session import FakeSession, FakeCall, FakePaginatedSession -logger = getLogger(__name__) - LATEST_VER = "latest" diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index 5900f7c89..fad6aba94 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -1,5 +1,4 @@ import uuid -from logging import getLogger, WARNING from unittest import mock import pytest @@ -18,8 +17,6 @@ from tests.utils.session import FakeSession, FakeCall, FakePaginatedSession, FakeRequestResponse from citrine.resources.team import READ, TeamMember -logger = getLogger(__name__) - @pytest.fixture def session() -> FakeSession: diff --git a/tests/resources/test_team.py b/tests/resources/test_team.py index 5b32d1fff..214c5bc3d 100644 --- a/tests/resources/test_team.py +++ b/tests/resources/test_team.py @@ -1,5 +1,4 @@ import uuid -from logging import getLogger import pytest from dateutil.parser import parse @@ -11,8 +10,6 @@ from tests.utils.factories import UserDataFactory, TeamDataFactory from tests.utils.session import FakeSession, FakeCall, FakePaginatedSession -logger = getLogger(__name__) - @pytest.fixture def session() -> FakeSession: diff --git a/tests/serialization/test_attribute_template.py b/tests/serialization/test_attribute_template.py index e0a6bea23..673d650ba 100644 --- a/tests/serialization/test_attribute_template.py +++ b/tests/serialization/test_attribute_template.py @@ -1,6 +1,5 @@ """Tests of the attribute template schema.""" import pytest -import logging from citrine.resources.condition_template import ConditionTemplate from citrine.resources.parameter_template import ParameterTemplate from citrine.resources.property_template import PropertyTemplate From a1412c4b82999a9b5ac259bc53f8aca700cf8d25 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Wed, 10 Jan 2024 16:15:52 -0500 Subject: [PATCH 17/22] find_or_create_project requires team_id. The ProjectCollection supports many operations without a team_id, but not creation. And given we were already warning users about passing such a ProjectCollection to find_or_create_project, we should now throw an exception when they try. --- src/citrine/seeding/find_or_create.py | 7 ++- tests/seeding/test_find_or_create.py | 45 ++++++++++---------- tests/utils/fakes/fake_project_collection.py | 8 ++-- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/citrine/seeding/find_or_create.py b/src/citrine/seeding/find_or_create.py index f5abf9279..7e7cd8045 100644 --- a/src/citrine/seeding/find_or_create.py +++ b/src/citrine/seeding/find_or_create.py @@ -1,4 +1,3 @@ -import warnings from copy import deepcopy from logging import getLogger from typing import TypeVar, Optional, Callable @@ -88,9 +87,9 @@ def find_or_create_project(*, If not found, creates a new project with the given name """ if project_collection.team_id is None: - warnings.warn("This method should be called through a team, which can be retrieved by " - "find_or_create_team.", - DeprecationWarning) + raise NotImplementedError("Collection must have a team ID, such as when retrieved with " + "find_or_create_team.") + if raise_error: project = get_by_name_or_raise_error(collection=project_collection, name=project_name) else: diff --git a/tests/seeding/test_find_or_create.py b/tests/seeding/test_find_or_create.py index fcb0dc533..51b6fea88 100644 --- a/tests/seeding/test_find_or_create.py +++ b/tests/seeding/test_find_or_create.py @@ -1,6 +1,5 @@ -import uuid -from typing import Optional, Callable -from uuid import UUID +from typing import Callable, Optional, Union +from uuid import UUID, uuid4 import pytest from citrine._rest.collection import Collection @@ -71,8 +70,8 @@ def session() -> FakeSession: @pytest.fixture def project_collection() -> Callable[[bool], ProjectCollection]: - def _make_project(search_implemented: bool = True): - projects = FakeProjectCollection(search_implemented) + def _make_project(search_implemented: bool = True, team_id: Optional[Union[UUID, str]] = uuid4()): + projects = FakeProjectCollection(search_implemented, team_id) for i in range(0, 5): projects.register("project " + str(i)) for i in range(0, 2): @@ -212,8 +211,7 @@ def test_find_or_create_project_no_exist(project_collection): # test when project doesn't exist collection = project_collection() old_project_count = len(list(collection.list())) - with pytest.warns(DeprecationWarning): - result = find_or_create_project(project_collection=collection, project_name=absent_name) + result = find_or_create_project(project_collection=collection, project_name=absent_name) new_project_count = len(list(collection.list())) assert result.name == absent_name assert new_project_count == old_project_count + 1 @@ -223,8 +221,7 @@ def test_find_or_create_project_exist(project_collection): # test when project exists collection = project_collection() old_project_count = len(list(collection.list())) - with pytest.warns(DeprecationWarning): - result = find_or_create_project(project_collection=collection, project_name="project 2") + result = find_or_create_project(project_collection=collection, project_name="project 2") new_project_count = len(list(collection.list())) assert result.name == "project 2" assert new_project_count == old_project_count @@ -234,8 +231,7 @@ def test_find_or_create_project_exist_no_search(project_collection): # test when project exists collection = project_collection(False) old_project_count = len(list(collection.list())) - with pytest.warns(DeprecationWarning): - result = find_or_create_project(project_collection=collection, project_name="project 2") + result = find_or_create_project(project_collection=collection, project_name="project 2") new_project_count = len(list(collection.list())) assert result.name == "project 2" assert new_project_count == old_project_count @@ -243,24 +239,21 @@ def test_find_or_create_project_exist_no_search(project_collection): def test_find_or_create_project_exist_multiple(project_collection): # test when project exists multiple times - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - find_or_create_project(project_collection=project_collection(), project_name=duplicate_name) + with pytest.raises(ValueError): + find_or_create_project(project_collection=project_collection(), project_name=duplicate_name) def test_find_or_create_raise_error_project_no_exist(project_collection): # test when project doesn't exist and raise_error flag is on - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - find_or_create_project(project_collection=project_collection(), project_name=absent_name, raise_error=True) + with pytest.raises(ValueError): + find_or_create_project(project_collection=project_collection(), project_name=absent_name, raise_error=True) def test_find_or_create_raise_error_project_exist(project_collection): # test when project exists and raise_error flag is on collection = project_collection() old_project_count = len(list(collection.list())) - with pytest.warns(DeprecationWarning): - result = find_or_create_project(project_collection=collection, project_name="project 3", raise_error=True) + result = find_or_create_project(project_collection=collection, project_name="project 3", raise_error=True) new_project_count = len(list(collection.list())) assert result.name == "project 3" assert new_project_count == old_project_count @@ -268,9 +261,15 @@ def test_find_or_create_raise_error_project_exist(project_collection): def test_find_or_create_raise_error_project_exist_multiple(project_collection): # test when project exists multiple times and raise_error flag is on - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - find_or_create_project(project_collection=project_collection(), project_name=duplicate_name, raise_error=True) + with pytest.raises(ValueError): + find_or_create_project(project_collection=project_collection(), project_name=duplicate_name, raise_error=True) + + +def test_find_or_create_project_no_team(project_collection): + # test when project collection has no team + collection = project_collection(team_id=None) + with pytest.raises(NotImplementedError): + find_or_create_project(project_collection=collection, project_name="project 2") def test_find_or_create_dataset_no_exist(dataset_collection): @@ -367,7 +366,7 @@ def test_create_or_update_unique_found_design_workflow(session): branch_data # Lookup the updated design workflow branch root ID and version ) - collection = LocalDesignWorkflowCollection(project_id=uuid.uuid4(), session=session, branch_root_id=root_id, branch_version=version) + collection = LocalDesignWorkflowCollection(project_id=uuid4(), session=session, branch_root_id=root_id, branch_version=version) dw2 = collection.build(dw2_dict) #verify that the returned object is updated diff --git a/tests/utils/fakes/fake_project_collection.py b/tests/utils/fakes/fake_project_collection.py index ff5d1c6e4..da46e0887 100644 --- a/tests/utils/fakes/fake_project_collection.py +++ b/tests/utils/fakes/fake_project_collection.py @@ -1,5 +1,5 @@ -from typing import Optional -from uuid import uuid4 +from typing import Optional, Union +from uuid import UUID, uuid4 from citrine.exceptions import NotFound from citrine.resources.project import Project, ProjectCollection @@ -14,8 +14,8 @@ class FakeProjectCollection(ProjectCollection): - def __init__(self, search_implemented: bool = True): - ProjectCollection.__init__(self, session=FakeSession) + def __init__(self, search_implemented: bool = True, team_id: Optional[Union[UUID, str]] = None): + ProjectCollection.__init__(self, session=FakeSession, team_id=team_id) self.projects = [] self.search_implemented = search_implemented From 5aba493dca93152ac6ff4961dccbb3df92646b52 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Fri, 19 Jan 2024 10:49:50 -0500 Subject: [PATCH 18/22] [PLA-13833] 3.0 migration guide. --- docs/source/FAQ/v3_migration.rst | 158 +++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/source/FAQ/v3_migration.rst diff --git a/docs/source/FAQ/v3_migration.rst b/docs/source/FAQ/v3_migration.rst new file mode 100644 index 000000000..0095fd01c --- /dev/null +++ b/docs/source/FAQ/v3_migration.rst @@ -0,0 +1,158 @@ +================ +Migrating to 3.0 +================ + +Summary +======= + +The newest major release of citrine-python cleans accumulated deprecations as we evolve the Citrine +platform. The intent is to focus users as we aim to reduce confusion by providing a single way to +accomplish each of your tasks. + +Keep in mind that you can keep using 2.x until you're ready to upgrade. But until you do, you won't +get any new features or bug fixes. + +Goals +----- + +The intent of this guide is not to itemize every piece of code removed. The easiest way to +determine what you'll need to change is to upgrade citrine to the latest 2.x release (2.42.2), run +your scripts, and take note of the deprecation warnings. Whenever possible, those warnings will +direct you on how to modify your code such that it will continue to function as desired upon +upgrading to citrine-python 3.x. + +This guide seeks to give a high-level overview of those changes, naming some of the categories of +elements no longer supported and what action to take, as well as some of the more consequential +individual changes. + +Versions / Runtimes +=================== + +The following library versions and Python versions are no longer supported. + +Versions < 2.26.1 +----------------- + +The citrine-python SDK is a client for the Citrine platform. As such, we will occasionally need to +make upgrades to the platform which will break old versions of the SDK. + +At some point after the release of 3.0, we will be making platform upgrades which will change the +way clients must interact with Search Spaces. So if you're using any version of citrine prior to +2.26.1 (released on June 28, 2023), and you're interacting with Search Spaces (e.g. through +:py:attr:`project.design_spaces `), your code will +break. Please upgrade to the latest 2.x release, or to 3.0, to avoid this issue. If this poses any +problems for you, please notify your customer contact so we can work with you. + +Python 3.7 +---------- + +Official upstream support for Python 3.7 by the Python Software Foundation ended in June 2023. As +such, we no longer support its use, and may begin using language features which are not backwards +compatable with it. Upgrade to at least Python 3.8, keeping in mind +`their migration guide `_. + +Features +======== + +The following features are no longer supported. + +Branch ID +--------- + +Previously, branch references were inconsistent. Some used a unique "branch ID", and others the +"root ID". This was further complicated by the web app only ever showing the "root ID". The reason +has to do with the platform implementation, but resulted in a confusing user experience. + +Beginning in 3.0, that "branch ID" is hidden from users. Instead, the SDK will always present the +branch's root ID and version, akin to +:py:class:`Predictor ` ID and +version. To access branches, you'll just use that root ID, and optionally the version: omitting the +version will grab the most recent version, which will almost always be what you want. + +status_info +----------- + +We have completed moving all our assets which previously used :code:`status_info` (such as but not +limited to :py:class:`~citrine.informatics.predictors.predictor.Predictor` and +:py:class:`~citrine.informatics.workflows.design_workflow.DesignWorkflow`) to use +:code:`status_detail`. These objects contain richer information, such as log level and (optionally) +error codes, along with the message. + +DesignSpace.archive and DesignSpace.restore +------------------------------------------- + +In the past, archiving a Search Space required updating it, which carried the risk of accidental +modification. Since 2.26.1, we've supported a separate +:py:meth:`~citrine.resources.design_space.DesignSpaceCollection.archive` and +:py:meth:`~citrine.resources.design_space.DesignSpaceCollection.restore` call. So we no longer +support archiving via :py:meth:`~citrine.resources.design_space.DesignSpaceCollection.update`. + +citrine.builders +---------------- + +This package contained a handful of utilities which were by all accounts unused, and better suited +to live outside citrine-python, anyways. + +formulation_descriptor parameters +--------------------------------- + +In many cases, the Citrine platform will generate an appropriate formulation descriptor on your +behalf. For example, when creating a +:py:class:`~citrine.informatics.predictors.simple_mixture_predictor.SimpleMixturePredictor` or +:py:class:`~citrine.informatics.data_sources.GemTableDataSource`. In such cases, you can no longer +specify a formulation descriptor. + +:py:attr:`project.modules ` +-------------------------------------------------------------------------------- + +This was the remnant of the old Citrine platform, before we began to specialize our assets. For +over a year, it has only returned Search Spaces, for which you should be using +:py:meth:`project.design_spaces `. As +such, both it and :code:`citrine.resources.modules` were dropped. + +Dehydrated Search Spaces +------------------------ + +This is a feature from the early days of the platform. It hasn't been supported for quite some time +due to lack of use and complexity of support. But mechanisms for it were still present in +citrine-python, allowing users to specify subspaces by ID. Fully dropping support completes its +removal. + +Process File Protocol +--------------------- + +This refers to the old method of getting data on the platform using :code:`Dataset.files.process`. +It's been supplanted by :py:meth:`~citrine.resources.file_link.FileCollection.ingest`, rendering +:code:`process` and the whole :code:`citrine.resources.job` module irrelevant. + +convert_to_graph and convert_and_update +--------------------------------------- + +:code:`SimpleMLPredictor` was a very old type of AI Model which hasn't been supported by the +platform in a long time. As such, these methods to convert them into the equivalent +:py:class:`~citrine.informatics.predictors.graph_predictor.GraphPredictor` are no longer needed. If +you think you still use a :code:`SimpleMLPredictor`, please reach out to your customer contact so we can +work with you to convert it. + +:py:meth:`~citrine.seeding.find_or_create.find_or_create_project` requires a team ID +------------------------------------------------------------------------------------ + +In mid-2022, the platform introduced teams, which are collections of projects. As such, starting +with 3.0, :py:meth:`~citrine.seeding.find_or_create.find_or_create_project` can only operate on a +:py:class:`~citrine.resources.project.ProjectCollection` which includes a team ID. That is, instead +of passing :py:attr:`citrine.projects `, you most likely want to +pass :py:attr:`team.projects `. + +Ingredient Ratio Constraint bases are now sets +---------------------------------------------- + +They were initially implemented as Python dictionaries to allow for flexibility. But as we evolved +their usage on the platform, we found we only needed the list of ingredients/labels. To allow +migration while preserving the old behavior, we added +:py:meth:`~citrine.informatics.constraints.ingredient_ratio_constraint.IngredientRatioConstraint.basis_ingredient_names` +and +:py:meth:`~citrine.informatics.constraints.ingredient_ratio_constraint.IngredientRatioConstraint.basis_label_names`. +Note that +once you've upgraded to 3.0, you'll be prompted to move back to +:py:meth:`~citrine.informatics.constraints.ingredient_ratio_constraint.IngredientRatioConstraint.basis_ingredients` and +:py:meth:`~citrine.informatics.constraints.ingredient_ratio_constraint.IngredientRatioConstraint.basis_labels`. From 882ed5b540338613bcd7083c594c0d3aed163ea1 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Tue, 30 Jan 2024 16:24:53 -0500 Subject: [PATCH 19/22] Surface docs warnings. In order to keep the docs build messages useful, we should run a check on PR to surface all warnings. With the release of 3.0, they should be completely clean, so a failure of this job will be meaningful. Even such, we'll probably keep it non-required for now so as to not add unecessarily to the development burden. --- .github/workflows/pr-checks.yml | 44 +++++++++++++++++++++++++++++++++ .github/workflows/pr-tests.yml | 29 +--------------------- 2 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/pr-checks.yml diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 000000000..bdb2d58c4 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,44 @@ +name: PR Checks + +on: + pull_request: + branches: + - main + +jobs: + check-version: + name: Check version bumped + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize the environment + uses: ./.github/actions/initialize + - name: Check version + run: python scripts/validate_version_bump.py + linting: + name: Run linting with flake8 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize the environment + uses: ./.github/actions/initialize + - name: Lint the src/ directory + run: flake8 src/ + check-deprecated: + name: Find code marked for removal in this version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize the environment + uses: ./.github/actions/initialize + - name: Deprecated check + run: derp src/ src/citrine/__version__.py + check-docs: + name: Check docs for warnings + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize the environment + uses: ./.github/actions/initialize + - name: Build Docs + run: make -C docs/ html SPHINXOPTS='-W --keep-going' diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 7f3799bd2..5e4c9eb4f 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -2,37 +2,10 @@ name: PR Tests on: pull_request: - branches: + branches: - main jobs: - check-version: - name: Check version bumped - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Initialize the environment - uses: ./.github/actions/initialize - - name: Check version - run: python scripts/validate_version_bump.py - linting: - name: Run linting with flake8 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Initialize the environment - uses: ./.github/actions/initialize - - name: Lint the src/ directory - run: flake8 src/ - check-deprecated: - name: Find code marked for removal in this version - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Initialize the environment - uses: ./.github/actions/initialize - - name: Deprecated check - run: derp src/ src/citrine/__version__.py run-tests: name: Execute unit tests runs-on: ubuntu-latest From cf153186acaa053cbdfd7cca323358e4aa1b2df2 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Wed, 31 Jan 2024 10:51:33 -0500 Subject: [PATCH 20/22] Link the migration guide. The guide for migrating to v3.0 should be available from the FAQ page. --- docs/source/FAQ/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/FAQ/index.rst b/docs/source/FAQ/index.rst index 58398f934..45b4f75ab 100644 --- a/docs/source/FAQ/index.rst +++ b/docs/source/FAQ/index.rst @@ -6,4 +6,5 @@ FAQ :maxdepth: 2 prohibited_data_patterns - team_management_migration \ No newline at end of file + team_management_migration + v3_migration From 4a129cc9d03c11a8379e92f0bb7ab1ff5cdaa9fe Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Tue, 6 Feb 2024 14:42:03 -0500 Subject: [PATCH 21/22] Final dependency bump. Some time has passed since we prepared the 3.0 release and the date of its release. As such, this does one final pass to upgrade everything. Most importantly, it upgrades gemd to the just released 2.0.0 version. --- docs/source/index.rst | 2 +- requirements.txt | 6 +++--- setup.py | 11 +++++------ test_requirements.txt | 8 ++++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index c57f8ed45..bb03c54cc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,7 +24,7 @@ or a specific version can be installed, for example: .. code:: - pip install citrine==1.0.0 + pip install citrine==3.0.0 Table of Contents diff --git a/requirements.txt b/requirements.txt index 4940a3b7f..a494e53fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ arrow==1.3.0 -boto3==1.34.13 +boto3==1.34.35 deprecation==2.1.0 -gemd==1.16.7 +gemd==2.0.0 pyjwt==2.8.0 requests==2.31.0 tqdm==4.66.1 -# boto3 (through botocore) depends on urllib3. Version 1.34.12 requires +# boto3 (through botocore) depends on urllib3. Version 1.34.35 requires # urllib3 < 1.27 when using Python 3.9 or less, and urllib3 < 2.1 otherwise. urllib3==1.26.18; python_version < "3.10" urllib3==2.0.7; python_version >= "3.10" diff --git a/setup.py b/setup.py index 24d87adda..afd592ca0 100644 --- a/setup.py +++ b/setup.py @@ -25,20 +25,19 @@ "requests>=2.31.0,<3", "pyjwt>=2,<3", "arrow>=1.0.0,<2", - "gemd>=1.16.7,<2", - "boto3>=1.34.12,<2", + "gemd>=2.0.0,<3", + "boto3>=1.34.35,<2", "deprecation>=2.1.0,<3", "urllib3>=1.26.18,<3", - "tqdm>=4.27.0,<5", - "pint>=0.18,<=0.20" + "tqdm>=4.27.0,<5" ], extras_require={ "../tests": [ "factory-boy>=3.3.0,<4", "mock>=5.1.0,<6", "pandas>=2.0.3,<3", - "pytest>=7.4.4,<8", - "pytz>=2023.3.post1", + "pytest>=8.0.0,<9", + "pytz>=2024.1", "requests-mock>=1.11.0,<2", ] }, diff --git a/test_requirements.txt b/test_requirements.txt index 270278029..5151ad7fd 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,10 +3,10 @@ factory-boy==3.3.0 flake8==7.0.0 flake8-docstrings==1.7.0 mock==5.1.0 -pytest==7.4.4 +pytest==8.0.0 pytest-cov==4.1.0 -pytz==2023.3.post1 +pytz==2024.1 requests-mock==1.11.0 -sphinx==7.1.2 +sphinx==7.2.6 sphinx-rtd-theme==2.0.0 -sphinxcontrib-apidoc==0.4.0 +sphinxcontrib-apidoc==0.5.0 From 3c953ccb10b906f08d54b765129225eacfab8d7c Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Wed, 7 Feb 2024 09:33:18 -0500 Subject: [PATCH 22/22] Fix Sphinx and pint dependencies. Python 3.8 support requires a bit more tightly bound requirements. Namely, Sphinx can only go up to 7.1.x, and pint must be kept <= 0.20. --- .github/workflows/pr-checks.yml | 1 + .github/workflows/pr-tests.yml | 2 ++ setup.py | 4 +++- test_requirements.txt | 4 ++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index bdb2d58c4..2e0e2e418 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - 'release/**' jobs: check-version: diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 5e4c9eb4f..effb72a15 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -4,11 +4,13 @@ on: pull_request: branches: - main + - 'release/**' jobs: run-tests: name: Execute unit tests runs-on: ubuntu-latest + continue-on-error: true strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] diff --git a/setup.py b/setup.py index afd592ca0..011fee4af 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,9 @@ "boto3>=1.34.35,<2", "deprecation>=2.1.0,<3", "urllib3>=1.26.18,<3", - "tqdm>=4.27.0,<5" + "tqdm>=4.27.0,<5", + "pint<=0.20; python_version < '3.9'", + "pint<0.24; python_version >= '3.9'" ], extras_require={ "../tests": [ diff --git a/test_requirements.txt b/test_requirements.txt index 5151ad7fd..9581b6e0c 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -7,6 +7,6 @@ pytest==8.0.0 pytest-cov==4.1.0 pytz==2024.1 requests-mock==1.11.0 -sphinx==7.2.6 +sphinx==7.1.2 sphinx-rtd-theme==2.0.0 -sphinxcontrib-apidoc==0.5.0 +sphinxcontrib-apidoc==0.4.0