diff --git a/pyproject.toml b/pyproject.toml index 7593dd914..239cb5e7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,18 @@ docs = [ "sphinxcontrib-apidoc==0.6.0", ] +[tool.ruff] +line-length = 99 + +[tool.ruff.format] +# Collapse collections that fit on one line even if they carry a trailing comma, +# instead of exploding them across multiple lines. +skip-magic-trailing-comma = true + +[tool.ruff.lint.isort] +# Required to be false when format.skip-magic-trailing-comma is true. +split-on-trailing-comma = false + [tool.pytest.ini_options] testpaths = [ "tests", diff --git a/src/citrine/__init__.py b/src/citrine/__init__.py index effc6d496..22de0361f 100644 --- a/src/citrine/__init__.py +++ b/src/citrine/__init__.py @@ -5,5 +5,7 @@ https://citrineinformatics.github.io/citrine-python/index.html """ + from citrine.citrine import Citrine # noqa: F401 + from .__version__ import __version__ # noqa: F401 diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index ba7be38e4..2fe5fde13 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "5.0.0" +__version__ = "5.0.1" diff --git a/src/citrine/_rest/admin_collection.py b/src/citrine/_rest/admin_collection.py index 6eeb5585f..70f2e1793 100644 --- a/src/citrine/_rest/admin_collection.py +++ b/src/citrine/_rest/admin_collection.py @@ -7,9 +7,7 @@ class AdminCollection(Collection[ResourceType]): """Abstract class for representing collections of REST resources with as_admin access.""" - def list( - self, *, per_page: int = 100, as_admin: bool = False - ) -> Iterator[ResourceType]: + def list(self, *, per_page: int = 100, as_admin: bool = False) -> Iterator[ResourceType]: """ Paginate over the elements of the collection. diff --git a/src/citrine/_rest/ai_resource_metadata.py b/src/citrine/_rest/ai_resource_metadata.py index 5ff56e6d0..025aa7a86 100644 --- a/src/citrine/_rest/ai_resource_metadata.py +++ b/src/citrine/_rest/ai_resource_metadata.py @@ -1,32 +1,33 @@ -from citrine.resources.status_detail import StatusDetail from citrine._serialization import properties +from citrine.resources.status_detail import StatusDetail -class AIResourceMetadata(): +class AIResourceMetadata: """Abstract class for representing common metadata for Resources.""" - created_by = properties.Optional(properties.UUID, 'created_by', serializable=False) + created_by = properties.Optional(properties.UUID, "created_by", serializable=False) """:UUID | None: id of the user who created the resource""" - create_time = properties.Optional(properties.Datetime, 'create_time', serializable=False) + create_time = properties.Optional(properties.Datetime, "create_time", serializable=False) """:datetime | None: date and time at which the resource was created""" - updated_by = properties.Optional(properties.UUID, 'updated_by', serializable=False) + updated_by = properties.Optional(properties.UUID, "updated_by", serializable=False) """:UUID | None: id of the user who most recently updated the resource, if it has been updated""" - update_time = properties.Optional(properties.Datetime, 'update_time', serializable=False) + update_time = properties.Optional(properties.Datetime, "update_time", serializable=False) """:datetime | None: date and time at which the resource was most recently updated, if it has been updated""" - archived = properties.Boolean('archived', default=False) + archived = properties.Boolean("archived", default=False) """:bool: whether the resource is archived (hidden but not deleted)""" - archived_by = properties.Optional(properties.UUID, 'archived_by', serializable=False) + archived_by = properties.Optional(properties.UUID, "archived_by", serializable=False) """:UUID | None: id of the user who archived the resource, if it has been archived""" - archive_time = properties.Optional(properties.Datetime, 'archive_time', serializable=False) + archive_time = properties.Optional(properties.Datetime, "archive_time", serializable=False) """:datetime | None: date and time at which the resource was archived, if it has been archived""" - status = properties.Optional(properties.String(), 'status', serializable=False) + status = properties.Optional(properties.String(), "status", serializable=False) """:str | None: short description of the resource's status""" - status_detail = properties.List(properties.Object(StatusDetail), 'status_detail', default=[], - serializable=False) + 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""" diff --git a/src/citrine/_rest/collection.py b/src/citrine/_rest/collection.py index 770bfcf60..6d5a010e9 100644 --- a/src/citrine/_rest/collection.py +++ b/src/citrine/_rest/collection.py @@ -10,12 +10,12 @@ from citrine.exceptions import ModuleRegistrationFailedException, NonRetryableException from citrine.resources.response import Response -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 # have subclasses override the create method. -CreationType = TypeVar('CreationType', bound='Resource') +CreationType = TypeVar("CreationType", bound="Resource") class Collection(Generic[ResourceType], Pageable): @@ -25,21 +25,23 @@ class Collection(Generic[ResourceType], Pageable): _dataset_agnostic_path_template: str = NotImplemented _individual_key: str = NotImplemented _resource: ResourceType = NotImplemented - _collection_key: str = 'entries' + _collection_key: str = "entries" _paginator: Paginator = Paginator() _api_version: str = "v1" - def _get_path(self, - uid: UUID | str | None = None, - *, - ignore_dataset: bool = False, - action: str | Sequence[str] = [], - query_terms: dict[str, str] = {}, - ) -> str: + def _get_path( + self, + uid: UUID | str | None = None, + *, + ignore_dataset: bool = False, + action: str | Sequence[str] = [], + query_terms: dict[str, str] = {}, + ) -> str: """Construct a url from __base_path__ and, optionally, id and/or action.""" base = self._dataset_agnostic_path_template if ignore_dataset else self._path_template - return resource_path(path_template=base, uid=uid, action=action, query_terms=query_terms, - **self.__dict__) + return resource_path( + path_template=base, uid=uid, action=action, query_terms=query_terms, **self.__dict__ + ) @abstractmethod def build(self, data: dict): @@ -85,9 +87,11 @@ def list(self, *, per_page: int = 100) -> Iterator[ResourceType]: Use list() to force evaluation of all results into an in-memory list. """ - return self._paginator.paginate(page_fetcher=self._fetch_page, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=self._fetch_page, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def update(self, model: CreationType) -> CreationType: """Update a particular element of the collection.""" @@ -102,8 +106,7 @@ def delete(self, uid: UUID | str) -> Response: data = self.session.delete_resource(url, version=self._api_version) return Response(body=data) - def _build_collection_elements(self, - collection: Iterable[dict]) -> Iterator[ResourceType]: + def _build_collection_elements(self, collection: Iterable[dict]) -> Iterator[ResourceType]: """ For each element in the collection, build the appropriate resource type. diff --git a/src/citrine/_rest/engine_resource.py b/src/citrine/_rest/engine_resource.py index d9058ca53..c316e9907 100644 --- a/src/citrine/_rest/engine_resource.py +++ b/src/citrine/_rest/engine_resource.py @@ -5,32 +5,35 @@ from citrine._serialization.include_parent_properties import IncludeParentProperties from citrine.resources.status_detail import StatusDetail -Self = TypeVar('Self', bound='Resource') +Self = TypeVar("Self", bound="Resource") class EngineResourceWithoutStatus(Resource[Self]): """Base resource for metadata from stand-alone AI Engine modules.""" - created_by = properties.Optional(properties.UUID, 'metadata.created.user', serializable=False) + created_by = properties.Optional(properties.UUID, "metadata.created.user", serializable=False) """:UUID | None: id of the user who created the resource""" - create_time = properties.Optional(properties.Datetime, 'metadata.created.time', - serializable=False) + create_time = properties.Optional( + properties.Datetime, "metadata.created.time", serializable=False + ) """:datetime | None: date and time at which the resource was created""" - updated_by = properties.Optional(properties.UUID, 'metadata.updated.user', - serializable=False) + updated_by = properties.Optional(properties.UUID, "metadata.updated.user", serializable=False) """:UUID | None: id of the user who most recently updated the resource, if it has been updated""" - update_time = properties.Optional(properties.Datetime, 'metadata.updated.time', - serializable=False) + update_time = properties.Optional( + properties.Datetime, "metadata.updated.time", serializable=False + ) """:datetime | None: date and time at which the resource was most recently updated, if it has been updated""" - archived_by = properties.Optional(properties.UUID, 'metadata.archived.user', - serializable=False) + archived_by = properties.Optional( + properties.UUID, "metadata.archived.user", serializable=False + ) """:UUID | None: id of the user who archived the resource, if it has been archived""" - archive_time = properties.Optional(properties.Datetime, 'metadata.archived.time', - serializable=False) + archive_time = properties.Optional( + properties.Datetime, "metadata.archived.time", serializable=False + ) """:datetime | None: date and time at which the resource was archived, if it has been archived""" @@ -59,10 +62,11 @@ def _post_dump(self, data: dict) -> dict: class EngineResource(EngineResourceWithoutStatus[Self], IncludeParentProperties[Self]): """Base resource for metadata from stand-alone AI Engine modules.""" - status = properties.Optional(properties.String(), 'metadata.status.name', serializable=False) + status = properties.Optional(properties.String(), "metadata.status.name", serializable=False) """:str | None: short description of the resource's status""" - status_detail = properties.List(properties.Object(StatusDetail), 'metadata.status.detail', - default=[], serializable=False) + status_detail = properties.List( + properties.Object(StatusDetail), "metadata.status.detail", default=[], serializable=False + ) """:list[StatusDetail]: a list of structured status info, containing the message and level""" @classmethod @@ -75,10 +79,10 @@ class VersionedEngineResource(EngineResource[Self], IncludeParentProperties[Self """Base resource for metadata from stand-alone AI Engine modules which support versioning.""" """:Integer: The version number of the resource.""" - version = properties.Optional(properties.Integer, 'metadata.version', serializable=False) + version = properties.Optional(properties.Integer, "metadata.version", serializable=False) """:Boolean: The draft status of the resource.""" - draft = properties.Optional(properties.Boolean, 'metadata.draft', serializable=False) + draft = properties.Optional(properties.Boolean, "metadata.draft", serializable=False) @classmethod def build(cls, data: dict): diff --git a/src/citrine/_rest/pageable.py b/src/citrine/_rest/pageable.py index 7fbe9ee38..49914c534 100644 --- a/src/citrine/_rest/pageable.py +++ b/src/citrine/_rest/pageable.py @@ -2,32 +2,34 @@ from uuid import UUID -class Pageable(): +class Pageable: """Class that allows paging.""" _collection_key: str = NotImplemented _api_version: str = "v1" - def _get_path(self, - uid: UUID | str | None = None, - *, - ignore_dataset: bool = False, - action: str | Sequence[str] = [], - query_terms: dict[str, str] = {}, - ) -> str: + def _get_path( + self, + uid: UUID | str | None = None, + *, + ignore_dataset: bool = False, + action: str | Sequence[str] = [], + query_terms: dict[str, str] = {}, + ) -> str: """Construct a url from __base_path__ and, optionally, id.""" raise NotImplementedError # pragma: no cover - def _fetch_page(self, - path: str | None = None, - fetch_func: Callable[..., dict] | None = None, - page: int | None = None, - per_page: int | None = None, - json_body: dict | None = None, - additional_params: dict | None = None, - *, - version: str | None = None - ) -> tuple[Iterable[dict], str]: + def _fetch_page( + self, + path: str | None = None, + fetch_func: Callable[..., dict] | None = None, + page: int | None = None, + per_page: int | None = None, + json_body: dict | None = None, + additional_params: dict | None = None, + *, + version: str | None = None, + ) -> tuple[Iterable[dict], str]: """ Fetch visible elements. This does not handle pagination. @@ -85,7 +87,7 @@ def _fetch_page(self, data = fetch_func(path, params=params, version=version, **json_body) try: - next_uri = data.get('next', "") + next_uri = data.get("next", "") except AttributeError: next_uri = "" @@ -99,10 +101,9 @@ def _fetch_page(self, return collection, next_uri - def _page_params(self, - page: int | None, - per_page: int | None, - module_type: str | None = None) -> dict[str, int]: + def _page_params( + self, page: int | None, per_page: int | None, module_type: str | None = None + ) -> dict[str, int]: params = {} if page is not None: params["page"] = page diff --git a/src/citrine/_rest/paginator.py b/src/citrine/_rest/paginator.py index f912e0b61..5955b212d 100644 --- a/src/citrine/_rest/paginator.py +++ b/src/citrine/_rest/paginator.py @@ -2,7 +2,7 @@ from typing import Any, Generic, TypeVar from uuid import uuid4 -ResourceType = TypeVar('ResourceType') +ResourceType = TypeVar("ResourceType") class Paginator(Generic[ResourceType]): @@ -13,12 +13,14 @@ class Paginator(Generic[ResourceType]): that will be extracted for comparison purposes (to avoid looping on the same items). """ - def paginate(self, - page_fetcher: Callable[[int | None, int], tuple[Iterable[dict], str]], - collection_builder: Callable[[Iterable[dict]], Iterable[ResourceType]], - per_page: int = 100, - search_params: dict | None = None, - deduplicate: bool = True) -> Iterator[ResourceType]: + def paginate( + self, + page_fetcher: Callable[[int | None, int], tuple[Iterable[dict], str]], + collection_builder: Callable[[Iterable[dict]], Iterable[ResourceType]], + per_page: int = 100, + search_params: dict | None = None, + deduplicate: bool = True, + ) -> Iterator[ResourceType]: """ A generic support class to paginate requests into an iterable of a built object. @@ -54,26 +56,25 @@ def paginate(self, """ # To avoid setting default to {} -> reduce mutation risk, and to make more extensible. Also # making 'search_params' key of outermost dict for keyword expansion by page_fetcher func - search_params = {} if search_params is None else {'search_params': search_params} + search_params = {} if search_params is None else {"search_params": search_params} first_entity = None page_idx = 1 uids = set() while True: - subset_collection, next_uri = page_fetcher(page=page_idx, per_page=per_page, - **search_params) + subset_collection, next_uri = page_fetcher( + page=page_idx, per_page=per_page, **search_params + ) subset = collection_builder(subset_collection) count = 0 for idx, element in enumerate(subset): - # escaping from infinite loops where page/per_page are not # honored and are returning the same results regardless of page: current_entity = self._comparison_fields(element) - if first_entity is not None and \ - first_entity == current_entity: + if first_entity is not None and first_entity == current_entity: # TODO: raise an exception once the APIs that ignore pagination are fixed break @@ -107,4 +108,4 @@ def _comparison_fields(self, entity: ResourceType) -> Any: If the 'uid' here isn't found, default to comparing the entire entity. """ - return getattr(entity, 'uid', entity) + return getattr(entity, "uid", entity) diff --git a/src/citrine/_rest/resource.py b/src/citrine/_rest/resource.py index e3fc0e882..0846ee972 100644 --- a/src/citrine/_rest/resource.py +++ b/src/citrine/_rest/resource.py @@ -1,12 +1,13 @@ from typing import TypeVar from uuid import UUID -from citrine._serialization.serializable import Serializable -from citrine._serialization import properties from gemd.entity.dict_serializable import DictSerializable from gemd.enumeration.base_enumeration import BaseEnumeration from gemd.util import make_index, substitute_objects +from citrine._serialization import properties +from citrine._serialization.serializable import Serializable + class ResourceTypeEnum(BaseEnumeration): """The type of the resource; used for modifying access controls. @@ -30,7 +31,7 @@ class ResourceTypeEnum(BaseEnumeration): TABLE_DEFINITION = "TABLE_DEFINITION" -Self = TypeVar('Self', bound='Resource') +Self = TypeVar("Self", bound="Resource") class Resource(Serializable[Self]): @@ -42,13 +43,10 @@ class Resource(Serializable[Self]): def access_control_dict(self) -> dict: """Return an access control entity representation of this resource. Internal use only.""" - return { - "type": self._resource_type.value, - "id": str(self.uid) - } + return {"type": self._resource_type.value, "id": str(self.uid)} -GEMDSelf = TypeVar('GEMDSelf', bound='GEMDResource') +GEMDSelf = TypeVar("GEMDSelf", bound="GEMDResource") class GEMDResource(Resource[GEMDSelf]): @@ -58,8 +56,10 @@ class GEMDResource(Resource[GEMDSelf]): def build(cls, data: dict) -> GEMDSelf: """Convert a raw, nested dictionary into Objects.""" if "context" in data and len(data) == 2: + def _inflate(x): return DictSerializable.class_mapping[x["type"]].build(x) + key = next(k for k in data if k != "context") idx = make_index([_inflate(x) for x in data["context"] + [data[key]]]) lst = [idx[k] for k in idx] @@ -91,13 +91,12 @@ def as_dict(self) -> dict: return result -class PredictorRef(Serializable['PredictorRef']): +class PredictorRef(Serializable["PredictorRef"]): """A reference to a resource by UID.""" - uid = properties.UUID('predictor_id') + uid = properties.UUID("predictor_id") version = properties.Optional( - properties.Union([properties.Integer(), properties.String()]), - 'predictor_version' + properties.Union([properties.Integer(), properties.String()]), "predictor_version" ) def __init__(self, uid: UUID | str, version: int | str | None = None): diff --git a/src/citrine/_serialization/include_parent_properties.py b/src/citrine/_serialization/include_parent_properties.py index 1e9dfcd58..0f4a35f3f 100644 --- a/src/citrine/_serialization/include_parent_properties.py +++ b/src/citrine/_serialization/include_parent_properties.py @@ -2,7 +2,7 @@ from citrine._serialization.serializable import Serializable -Self = TypeVar('Self', bound='Serializable') +Self = TypeVar("Self", bound="Serializable") class IncludeParentProperties(Serializable[Self]): @@ -14,6 +14,7 @@ def build_with_parent(cls, data: dict, base_cls) -> Self: resource = super().build(data) from citrine._serialization import properties + metadata_properties = properties.Object(base_cls).deserialize(data) resource.__dict__.update(metadata_properties.__dict__) diff --git a/src/citrine/_serialization/polymorphic_serializable.py b/src/citrine/_serialization/polymorphic_serializable.py index 109efc9e8..8edbd8c2a 100644 --- a/src/citrine/_serialization/polymorphic_serializable.py +++ b/src/citrine/_serialization/polymorphic_serializable.py @@ -3,8 +3,7 @@ from citrine._serialization.serializable import Serializable - -SelfType = TypeVar('SelfType', bound='PolymorphicSerializable') +SelfType = TypeVar("SelfType", bound="PolymorphicSerializable") class PolymorphicSerializable(Generic[SelfType]): diff --git a/src/citrine/_serialization/properties.py b/src/citrine/_serialization/properties.py index a6a6fb631..b74775045 100644 --- a/src/citrine/_serialization/properties.py +++ b/src/citrine/_serialization/properties.py @@ -1,4 +1,5 @@ """Property objects for typed setting and ser/de.""" + import re import uuid from abc import abstractmethod @@ -10,19 +11,18 @@ from typing import Any, Generic, TypeVar import arrow - -from gemd.enumeration.base_enumeration import BaseEnumeration -from gemd.entity.link_by_uid import LinkByUID from gemd.entity.dict_serializable import DictSerializable +from gemd.entity.link_by_uid import LinkByUID +from gemd.enumeration.base_enumeration import BaseEnumeration from gemd.util.impl import cached_isinstance as isinstance -from citrine._serialization.serializable import Serializable from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable -SerializedType = TypeVar('SerializedType') -DeserializedType = TypeVar('DeserializedType') -SerializedInteger = TypeVar('SerializedInteger', int, str) -SerializedFloat = TypeVar('SerializedFloat', float, str) +SerializedType = TypeVar("SerializedType") +DeserializedType = TypeVar("DeserializedType") +SerializedInteger = TypeVar("SerializedInteger", int, str) +SerializedFloat = TypeVar("SerializedFloat", float, str) class Property(Generic[DeserializedType, SerializedType]): @@ -53,20 +53,21 @@ class Property(Generic[DeserializedType, SerializedType]): """ - def __init__(self, - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): + def __init__( + self, + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): self.serialization_path = serialization_path if override: self._key: None = None else: - self._key: str = '__' + str(uuid.uuid4()) # Make this object key human-readable + self._key: str = "__" + str(uuid.uuid4()) # Make this object key human-readable self.serializable: bool = serializable self.deserializable: bool = deserializable self.default: DeserializedType | None = default @@ -87,31 +88,31 @@ def serialized_types(self) -> SerializedType | tuple[SerializedType, ...]: def _error_source(self, base_class: type) -> str: """Construct a string of the base class name and the parameter that failed.""" if base_class is not None: - return ' for {}:{}'.format(base_class.__name__, self.serialization_path) + return f" for {base_class.__name__}:{self.serialization_path}" elif self.serialization_path: - return ' for {}'.format(self.serialization_path) + return f" for {self.serialization_path}" else: - return '' + return "" - def serialize(self, value: DeserializedType, - base_class: type | None = None) -> SerializedType: + def serialize(self, value: DeserializedType, base_class: type | None = None) -> SerializedType: if not isinstance(value, self.underlying_types): base_name = self._error_source(base_class) raise ValueError( - f'{type(value)} {value} is not one of valid types: ' - f'{self.underlying_types}{base_name}' + f"{type(value)} {value} is not one of valid types: " + f"{self.underlying_types}{base_name}" ) return self._serialize(value) - def deserialize(self, value: SerializedType, - base_class: type | None = None) -> DeserializedType: + def deserialize( + self, value: SerializedType, base_class: type | None = None + ) -> DeserializedType: if not isinstance(value, self.serialized_types): if isinstance(value, self.underlying_types): return value # Don't worry if it was already deserialized base_name = self._error_source(base_class) raise ValueError( - f'{type(value)} {value} is not one of valid types: ' - f'{self.serialized_types}{base_name}' + f"{type(value)} {value} is not one of valid types: " + f"{self.serialized_types}{base_name}" ) return self._deserialize(value) @@ -126,13 +127,15 @@ def _deserialize(self, value: SerializedType) -> DeserializedType: def deserialize_from_dict(self, data: dict) -> DeserializedType: value = data # `serialization_path` is expected to be a sequence of nested dictionary keys - fields = self.serialization_path.split('.') + fields = self.serialization_path.split(".") for field in fields: next_value = value.get(field) if next_value is None: if self.default is None and not self.optional: - msg = "Unable to deserialize {} into {}, missing a required field: {}".format( - data, self.underlying_types, field) + msg = ( + f"Unable to deserialize {data} into {self.underlying_types}, " + f"missing a required field: {field}" + ) raise ValueError(msg) # This occurs if a `field` is unexpectedly not present in the data dictionary # or if its value is null. @@ -146,10 +149,10 @@ def deserialize_from_dict(self, data: dict) -> DeserializedType: def serialize_to_dict(self, data: dict, value: DeserializedType) -> dict: if self.serialization_path is None: - raise ValueError('No serialization path set!') + raise ValueError("No serialization path set!") _data = data - fields = self.serialization_path.split('.') + fields = self.serialization_path.split(".") for field in fields[:-1]: _data = _data.setdefault(field, {}) _data[fields[-1]] = self.serialize(value, base_class=None) # Always a dict @@ -194,11 +197,10 @@ def __set__(self, obj, value: SerializedType | DeserializedType): setattr(obj, self._key, value_to_set) def __str__(self): - return ''.format(self.serialization_path) + return f"" class PropertyCollection(Property[DeserializedType, SerializedType]): - def __set__(self, obj, value: SerializedType | DeserializedType): """ Property setter for container property types. @@ -242,8 +244,7 @@ def _set_elements(self, value: SerializedType | DeserializedType): @lru_cache(maxsize=1024) -def _get_key_and_base_class(prop: Property, klass: Any) -> \ - tuple[str | None, str | None]: +def _get_key_and_base_class(prop: Property, klass: Any) -> tuple[str | None, str | None]: """ Return the base class and class attribute name for the object and property. @@ -259,7 +260,6 @@ def _get_key_and_base_class(prop: Property, klass: Any) -> \ class Integer(Property[int, SerializedInteger]): - @property def underlying_types(self): return int @@ -270,22 +270,21 @@ def serialized_types(self): def _deserialize(self, value: SerializedInteger) -> int: if isinstance(value, bool): - raise TypeError('value must be a Number, not a boolean.') + raise TypeError("value must be a Number, not a boolean.") else: return int(value) def _serialize(self, value: int) -> SerializedInteger: if isinstance(value, bool): - raise TypeError('Boolean cannot be serialized to integer.') + raise TypeError("Boolean cannot be serialized to integer.") else: return value def __str__(self): - return ''.format(self.serialization_path) + return f"" class Float(Property[float, SerializedFloat]): - @property def underlying_types(self): return float @@ -297,7 +296,7 @@ def serialized_types(self): @classmethod def _deserialize(cls, value: SerializedFloat) -> float: if isinstance(value, bool): - raise TypeError('value must be a Number, not a boolean.') + raise TypeError("value must be a Number, not a boolean.") else: return float(value) @@ -306,11 +305,10 @@ def _serialize(cls, value: float) -> SerializedFloat: return value def __str__(self): - return ''.format(self.serialization_path) + return f"" class Raw(Property[Any, Any]): - @property def underlying_types(self): return object @@ -328,11 +326,10 @@ def _serialize(cls, value: Any) -> Any: return value def __str__(self): - return ''.format(self.serialization_path) + return f"" class String(Property[str, str]): - @property def underlying_types(self): return str @@ -344,18 +341,17 @@ def serialized_types(self): def _deserialize(self, value: str) -> str: value = self.default if value is None else value if value is None: - raise ValueError('Value must not be none!') + raise ValueError("Value must not be none!") return str(value) def _serialize(self, value: str) -> str: return str(value) def __str__(self): - return ''.format(self.serialization_path) + return f"" class Boolean(Property[bool, bool]): - @property def underlying_types(self): return bool @@ -371,11 +367,10 @@ def _serialize(self, value: str) -> bool: return bool(value) def __str__(self): - return ''.format(self.serialization_path) + return f"" class UUID(Property[uuid.UUID, str]): - @property def underlying_types(self): return uuid.UUID @@ -392,7 +387,6 @@ def _serialize(self, value: uuid.UUID) -> str: class Datetime(Property[datetime, int]): - @property def underlying_types(self): return datetime @@ -407,7 +401,7 @@ def _deserialize(self, value) -> datetime: if isinstance(value, int): # Backend returns time as ms since epoch, but arrow expects seconds since epoch return arrow.get(value / 1000).datetime - raise TypeError("{} must be an int or a string".format(value)) + raise TypeError(f"{value} must be an int or a string") def _serialize(self, value: datetime) -> int: # Add 100 nanoseconds to avoid floating point truncation issues from microseconds @@ -415,24 +409,25 @@ def _serialize(self, value: datetime) -> int: class List(PropertyCollection[list, list]): - - def __init__(self, - element_type: Property | type[Property], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init - ) + def __init__( + self, + element_type: Property | type[Property], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) self.element_type = element_type if isinstance(element_type, Property) else element_type() @property @@ -469,23 +464,25 @@ def _set_elements(self, value): class Set(PropertyCollection[set, Iterable]): - - def __init__(self, - element_type: Property | type[Property], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init) + def __init__( + self, + element_type: Property | type[Property], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) self.element_type = element_type if isinstance(element_type, Property) else element_type() @property @@ -531,38 +528,48 @@ class Union(Property[Any, Any]): Attempted de/serialization is done in the order in which types are provided in the constructor. """ - def __init__(self, - element_types: Sequence[Property | type[Property]], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init) + def __init__( + self, + element_types: Sequence[Property | type[Property]], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) if not isinstance(element_types, Iterable): - raise ValueError("element types must be iterable: {}".format(element_types)) - self.element_types: list[Property] = \ - [el if isinstance(el, Property) else el() for el in element_types] + raise ValueError(f"element types must be iterable: {element_types}") + self.element_types: list[Property] = [ + el if isinstance(el, Property) else el() for el in element_types + ] @property def underlying_types(self): all_underlying_types = [prop.underlying_types for prop in self.element_types] - return tuple(set(chain(*[typ if isinstance(typ, tuple) - else (typ,) for typ in all_underlying_types]))) + return tuple( + set( + chain(*[typ if isinstance(typ, tuple) else (typ,) for typ in all_underlying_types]) + ) + ) @property def serialized_types(self): all_serialized_types = [prop.serialized_types for prop in self.element_types] - return tuple(set(chain(*[typ if isinstance(typ, tuple) - else (typ,) for typ in all_serialized_types]))) + return tuple( + set( + chain(*[typ if isinstance(typ, tuple) else (typ,) for typ in all_serialized_types]) + ) + ) def _serialize(self, value: Any) -> Any: for prop in self.element_types: @@ -570,8 +577,10 @@ def _serialize(self, value: Any) -> Any: return prop.serialize(value) except ValueError: pass - raise ValueError("An unexpected error occurred while trying to serialize {} to one " - "of the following types: {}.".format(value, self.serialized_types)) + raise ValueError( + f"An unexpected error occurred while trying to serialize {value} to one " + f"of the following types: {self.serialized_types}." + ) def _deserialize(self, value: Any) -> Any: for prop in self.element_types: @@ -579,33 +588,39 @@ def _deserialize(self, value: Any) -> Any: return prop.deserialize(value) except ValueError: pass - raise ValueError("An unexpected error occurred while trying to deserialize {} to " - "one of the following types: {}.".format(value, self.underlying_types)) + raise ValueError( + f"An unexpected error occurred while trying to deserialize {value} to " + f"one of the following types: {self.underlying_types}." + ) class SpecifiedMixedList(PropertyCollection[list, list]): """A finite list in which the type of each entry is specified.""" - def __init__(self, - element_types: Sequence[Property | type[Property]], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init) + def __init__( + self, + element_types: Sequence[Property | type[Property]], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) if not isinstance(element_types, list): - raise ValueError("element types must be a list: {}".format(element_types)) - self.element_types: list[Property] = \ - [el if isinstance(el, Property) else el() for el in element_types] + raise ValueError(f"element types must be a list: {element_types}") + self.element_types: list[Property] = [ + el if isinstance(el, Property) else el() for el in element_types + ] @property def underlying_types(self): @@ -617,28 +632,32 @@ def serialized_types(self): def _deserialize(self, value: list) -> tuple: if len(value) > len(self.element_types): - raise ValueError("Cannot deserialize value {}, as it has more elements " - "than expected for list {}".format(value, self.element_types)) + raise ValueError( + f"Cannot deserialize value {value}, as it has more elements " + f"than expected for list {self.element_types}" + ) deserialized = [] for element, element_type in zip(value, self.element_types): deserialized.append(element_type.deserialize(element)) # If there are more element types than elements, append default values - for element_type in self.element_types[len(value):]: + for element_type in self.element_types[len(value) :]: deserialized.append(element_type.default) return deserialized def _serialize(self, value: tuple) -> list: if len(value) > len(self.element_types): - raise ValueError("Cannot serialize value {}, as it has more elements " - "than expected for list {}".format(value, self.element_types)) + raise ValueError( + f"Cannot serialize value {value}, as it has more elements " + f"than expected for list {self.element_types}" + ) serialized = [] for element, element_type in zip(value, self.element_types): serialized.append(element_type.serialize(element)) # If there are more element types than elements, append serialized default values - for element_type in self.element_types[len(value):]: + for element_type in self.element_types[len(value) :]: serialized.append(element_type.serialize(element_type.default)) return serialized @@ -646,8 +665,10 @@ def _serialize(self, value: tuple) -> list: def _set_elements(self, value): elems = [] if len(value) > len(self.element_types): - raise ValueError("Cannot serialize value {}, as it has more elements " - "than expected for list {}".format(value, self.element_types)) + raise ValueError( + f"Cannot serialize value {value}, as it has more elements " + f"than expected for list {self.element_types}" + ) for element, element_type in zip(value, self.element_types): if isinstance(element_type, PropertyCollection): val_to_append = element_type._set_elements(element) @@ -658,30 +679,32 @@ def _set_elements(self, value): elems.append(val_to_append) # If there are more element types than elements, append serialized default values - for element_type in self.element_types[len(value):]: + for element_type in self.element_types[len(value) :]: elems.append(element_type.default) return elems class Enumeration(Property[BaseEnumeration, str]): - - def __init__(self, - klass: type[Any], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init) + def __init__( + self, + klass: type[Any], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) self.klass = klass @property @@ -705,33 +728,37 @@ def _fields_map(klass: type) -> dict[str, Property]: return { k: v for x in reversed(klass.__mro__) # Classes at the front trump - for k, v in x.__dict__.items() if isinstance(v, Property) + for k, v in x.__dict__.items() + if isinstance(v, Property) } class Object(PropertyCollection[Any, dict]): - - def __init__(self, - klass: type[Any], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init) + def __init__( + self, + klass: type[Any], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) self.klass = klass # We need to use __dict__ here because other access methods will invoke __get__ self.fields: dict[str, Property] = _fields_map(self.klass) - self.polymorphic = "get_type" in self.klass.__dict__ and\ - issubclass(self.klass, PolymorphicSerializable) + self.polymorphic = "get_type" in self.klass.__dict__ and issubclass( + self.klass, PolymorphicSerializable + ) @property def underlying_types(self): @@ -748,8 +775,10 @@ def _deserialize(self, data: dict) -> Any: # Maybe there are no fields because we hit a gemd-python class if issubclass(self.klass, DictSerializable): return DictSerializable.build(data) - raise AttributeError("Tried to deserialize to {!r}, which has no fields and is not an" - " explicitly serializable class".format(self.klass)) + raise AttributeError( + f"Tried to deserialize to {self.klass!r}, which has no fields and is not an" + " explicitly serializable class" + ) values = {} init_props = set() @@ -768,14 +797,13 @@ def _deserialize(self, data: dict) -> Any: # Check if it's because the signature was wrong sig = signature(self.klass.__init__) for arg, param in sig.parameters.items(): - if arg not in init_props | {'self'}: + if arg not in init_props | {"self"}: if param.default is param.empty: raise AttributeError( f"{self.klass} has at least 1 property marked as `use_init`, " f"but required arguments weren't: {e}" ) - else: - raise e + raise e else: instance = self.klass.__new__(self.klass) for property_name in values: @@ -800,8 +828,10 @@ def _serialize(self, obj: Any) -> dict: try: return obj.dump() except AttributeError: - raise AttributeError("Tried to serialize object {!r} of type {}, which has " - "neither fields not a dump() method.".format(obj, type(obj))) + raise AttributeError( + f"Tried to serialize object {obj!r} of type {type(obj)}, which has " + "neither fields not a dump() method." + ) for property_name, field in self.fields.items(): if field.serializable: value = getattr(obj, property_name) @@ -809,7 +839,7 @@ def _serialize(self, obj: Any) -> dict: return serialized def __str__(self): - return ''.format(self.klass.__name__, self.serialization_path) + return f"" def _set_elements(self, value): if issubclass(type(value), self.klass): @@ -837,23 +867,25 @@ class LinkOrElse(PropertyCollection[Serializable | LinkByUID, dict]): generic Link object. """ - def __init__(self, - klass: type[Any] = Serializable, - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): + def __init__( + self, + klass: type[Any] = Serializable, + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): super().__init__( serialization_path=serialization_path, serializable=serializable, deserializable=deserializable, default=default, override=override, - use_init=use_init) + use_init=use_init, + ) self.klass = klass @property @@ -871,8 +903,8 @@ def _serialize(self, value: Any) -> dict: return value.dump() def _deserialize(self, value: dict): - if 'type' in value: - target = DictSerializable.class_mapping[value['type']] + if "type" in value: + target = DictSerializable.class_mapping[value["type"]] try: return target.build(value) except TypeError as e: @@ -885,31 +917,35 @@ def _deserialize(self, value: dict): ) else: raise e - raise Exception("Serializable object that is being pointed to must have a self-contained " - "build() method that does not call deserialize().") + raise Exception( + "Serializable object that is being pointed to must have a self-contained " + "build() method that does not call deserialize()." + ) def _set_elements(self, value): return value class Optional(PropertyCollection[Any | None, Any | None]): - - def __init__(self, - prop: Property | type[Property], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: DeserializedType | None = None, - override: bool = False, - use_init: bool = False - ): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init) + def __init__( + self, + prop: Property | type[Property], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: DeserializedType | None = None, + override: bool = False, + use_init: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) self.prop = prop if isinstance(prop, Property) else prop() self.optional = True @@ -936,7 +972,7 @@ def _serialize(self, obj: Any | None) -> Any | None: return self.prop.serialize(obj) if obj is not None else None def __str__(self): - return ''.format(self.prop, self.serialization_path) + return f"" def _set_elements(self, value): elem = None @@ -960,24 +996,27 @@ class Mapping(PropertyCollection[dict, dict]): key value pairs and converts them to a dict. """ - def __init__(self, - keys_type: Property | type[Property], - values_type: Property | type[Property], - serialization_path: str | None = None, - *, - serializable: bool = True, - deserializable: bool = True, - default: dict | None = None, - override: bool = False, - use_init: bool = False, - ser_as_list_of_pairs: bool = False): - super().__init__(serialization_path=serialization_path, - serializable=serializable, - deserializable=deserializable, - default=default, - override=override, - use_init=use_init - ) + def __init__( + self, + keys_type: Property | type[Property], + values_type: Property | type[Property], + serialization_path: str | None = None, + *, + serializable: bool = True, + deserializable: bool = True, + default: dict | None = None, + override: bool = False, + use_init: bool = False, + ser_as_list_of_pairs: bool = False, + ): + super().__init__( + serialization_path=serialization_path, + serializable=serializable, + deserializable=deserializable, + default=default, + override=override, + use_init=use_init, + ) self.keys_type = keys_type if isinstance(keys_type, Property) else keys_type() self.values_type = values_type if isinstance(values_type, Property) else values_type() diff --git a/src/citrine/_serialization/serializable.py b/src/citrine/_serialization/serializable.py index b8cdff16f..4ddddbdbc 100644 --- a/src/citrine/_serialization/serializable.py +++ b/src/citrine/_serialization/serializable.py @@ -1,7 +1,6 @@ from typing import Generic, TypeVar - -Self = TypeVar('Self', bound='Serializable') +Self = TypeVar("Self", bound="Serializable") class Serializable(Generic[Self]): @@ -16,12 +15,14 @@ def _pre_build(cls, data: dict) -> dict: def build(cls, data: dict) -> Self: """Build an instance of this object from given data.""" from citrine._serialization import properties + pre_built = cls._pre_build(data) return properties.Object(cls).deserialize(pre_built) def dump(self) -> dict: """Dump this instance.""" from citrine._serialization import properties + serialized = properties.Object(type(self)).serialize(self) return self._post_dump(serialized) diff --git a/src/citrine/_session.py b/src/citrine/_session.py index 1f91f00bb..401414a1a 100644 --- a/src/citrine/_session.py +++ b/src/citrine/_session.py @@ -21,7 +21,8 @@ NotFound, Unauthorized, UnauthorizedRefreshToken, - WorkflowNotReadyException) + WorkflowNotReadyException, +) # Choose a 5-second buffer so that there's no chance of the access token # expiring during the check for expiration @@ -32,44 +33,45 @@ class Session(requests.Session): """Wrapper around requests.Session that is both refresh-token and schema aware.""" - def __init__(self, - refresh_token: str = None, - *, - scheme: str = None, - host: str = None, - port: str | None = None): + def __init__( + self, + refresh_token: str = None, + *, + scheme: str = None, + host: str = None, + port: str | None = None, + ): super().__init__() if refresh_token is None: - refresh_token = environ.get('CITRINE_API_KEY') + refresh_token = environ.get("CITRINE_API_KEY") if scheme is None: - scheme = 'https' + scheme = "https" if host is None: - host = environ.get('CITRINE_API_HOST') + host = environ.get("CITRINE_API_HOST") if host is None: - raise ValueError("No host passed and environmental " - "variable CITRINE_API_HOST not set.") + raise ValueError( + "No host passed and environmental variable CITRINE_API_HOST not set." + ) self.scheme: str = scheme - self.authority = ':'.join(([host] if host else []) + ([port] if port else [])) + self.authority = ":".join(([host] if host else []) + ([port] if port else [])) self.refresh_token: str = refresh_token self.access_token: str | None = None self.access_token_expiration: datetime = datetime.now(timezone.utc) - agent = "{}/{} python-requests/{} citrine-python/{}".format( - platform.python_implementation(), - platform.python_version(), - requests.__version__, - citrine.__version__) + agent = ( + f"{platform.python_implementation()}/{platform.python_version()} " + f"python-requests/{requests.__version__} " + f"citrine-python/{citrine.__version__}" + ) # Following scheme:[//authority]path[?query][#fragment] (https://en.wikipedia.org/wiki/URL) - self.headers.update({ - "Content-Type": "application/json", - "User-Agent": agent}) + self.headers.update({"Content-Type": "application/json", "User-Agent": agent}) # Default parameters for S3 connectivity. Can be changed by tests. self.s3_endpoint_url = None self.s3_use_ssl = True - self.s3_addressing_style = 'auto' + self.s3_addressing_style = "auto" # Feature flag for enabling the use of Dataset idempotent PUT. Will be removed # in a future release. @@ -78,32 +80,38 @@ def __init__(self, # Custom adapter so we can use custom retry parameters. The default HTTP status # codes for retries are [503, 413, 429]. We're using status_force list to add # additional codes to retry on, focusing on specific CloudFlare 5XX errors. - retries = Retry(total=10, - connect=5, - read=5, - status=5, - backoff_factor=0.25, - status_forcelist=[500, 502, 504, 520, 521, 522, 524, 527]) + retries = Retry( + total=10, + connect=5, + read=5, + status=5, + backoff_factor=0.25, + status_forcelist=[500, 502, 504, 520, 521, 522, 524, 527], + ) adapter = requests.adapters.HTTPAdapter(max_retries=retries) - self.mount('https://', adapter) - self.mount('http://', adapter) + self.mount("https://", adapter) + self.mount("http://", adapter) # Requests has its own set of exceptions that do not inherit from the # built-in exceptions. The built-in ConnectionError handles 4 different # child exceptions: https://docs.python.org/3/library/exceptions.html#ConnectionError - self.retry_errs = (ConnectionError, - requests.exceptions.ConnectionError, - requests.exceptions.ChunkedEncodingError) + self.retry_errs = ( + ConnectionError, + requests.exceptions.ConnectionError, + requests.exceptions.ChunkedEncodingError, + ) self._refresh_access_token() - def _versioned_base_url(self, version: str = 'v1'): - return urlunsplit(( - self.scheme, - self.authority, - format_escaped_url('api/{}/', version), - '', # query string - '' # fragment - )) + def _versioned_base_url(self, version: str = "v1"): + return urlunsplit( + ( + self.scheme, + self.authority, + format_escaped_url("api/{}/", version), + "", # query string + "", # fragment + ) + ) def _is_access_token_expired(self): buffered_expire = self.access_token_expiration - EXPIRATION_BUFFER @@ -111,21 +119,20 @@ def _is_access_token_expired(self): def _refresh_access_token(self) -> None: """Optionally refresh our access token (if the previous one is about to expire).""" - data = {'refresh_token': self.refresh_token} + data = {"refresh_token": self.refresh_token} - response = self._request_with_retry('POST', self._versioned_base_url() + 'tokens/refresh', - json=data) + response = self._request_with_retry( + "POST", self._versioned_base_url() + "tokens/refresh", json=data + ) if response.status_code != 200: raise UnauthorizedRefreshToken() - self.access_token = response.json()['access_token'] + self.access_token = response.json()["access_token"] self.access_token_expiration = datetime.fromtimestamp( jwt.decode( - self.access_token, - options={"verify_signature": False}, - algorithms=["HS256"] - )['exp'], - timezone.utc + self.access_token, options={"verify_signature": False}, algorithms=["HS256"] + )["exp"], + timezone.utc, ) # Explicitly set an updated 'auth', so as to not rely on implicit cookie handling. @@ -139,30 +146,31 @@ def _request_with_retry(self, method, uri, **kwargs): try: response = self.request(method, uri, **kwargs) except self.retry_errs as e: - logger.warning('{} seen, retrying request'.format(repr(e))) + logger.warning(f"{e!r} seen, retrying request") response = self.request(method, uri, **kwargs) return response - def checked_request(self, method: str, path: str, - version: str = 'v1', **kwargs) -> requests.Response: + def checked_request( + self, method: str, path: str, version: str = "v1", **kwargs + ) -> requests.Response: """Check response status code and throw an exception if relevant.""" - logger.debug('BEGIN request details:') - logger.debug('\tmethod: {}'.format(method)) - logger.debug('\tpath: {}'.format(path)) - logger.debug('\tversion: {}'.format(version)) + logger.debug("BEGIN request details:") + logger.debug(f"\tmethod: {method}") + logger.debug(f"\tpath: {path}") + logger.debug(f"\tversion: {version}") for k, v in kwargs.items(): - logger.debug(f'\t{k}: {v}') + logger.debug(f"\t{k}: {v}") if self._is_access_token_expired(): self._refresh_access_token() - uri = self._versioned_base_url(version) + path.lstrip('/') + uri = self._versioned_base_url(version) + path.lstrip("/") - logger.debug('\turi: {}'.format(uri)) + logger.debug(f"\turi: {uri}") for k, v in kwargs.items(): - logger.debug('\t{}: {}'.format(k, v)) - logger.debug('END request details.') + logger.debug(f"\t{k}: {v}") + logger.debug("END request details.") response = self._request_with_retry(method, uri, **kwargs) @@ -173,42 +181,39 @@ def checked_request(self, method: str, path: str, except AttributeError: # Catch AttributeErrors and log response # The 401 status will be handled further down - logger.error("Failed to decode json from response: {}".format(response.text)) + logger.error(f"Failed to decode json from response: {response.text}") except ValueError: # Ignore ValueErrors thrown by attempting to decode json bodies. This # might occur if we get a 401 response without a JSON body pass if 200 <= response.status_code <= 299: - logger.info('%s %s %s', response.status_code, method, path) + logger.info("%s %s %s", response.status_code, method, path) return response else: stacktrace = self._extract_response_stacktrace(response) if stacktrace is not None: - logger.error('Response arrived with stacktrace:') + logger.error("Response arrived with stacktrace:") logger.error(stacktrace) if response.status_code == 400: - logger.error('%s %s %s', response.status_code, method, path) + logger.error("%s %s %s", response.status_code, method, path) logger.error(response.text) raise BadRequest(path, response) - elif response.status_code == 401: - logger.error('%s %s %s', response.status_code, method, path) - raise Unauthorized(path, response) - elif response.status_code == 403: - logger.error('%s %s %s', response.status_code, method, path) + elif response.status_code == 401 or response.status_code == 403: + logger.error("%s %s %s", response.status_code, method, path) raise Unauthorized(path, response) elif response.status_code == 404: - logger.error('%s %s %s', response.status_code, method, path) + logger.error("%s %s %s", response.status_code, method, path) raise NotFound(path, response) elif response.status_code == 409: - logger.debug('%s %s %s', response.status_code, method, path) + logger.debug("%s %s %s", response.status_code, method, path) raise Conflict(path, response) elif response.status_code == 425: - logger.debug('%s %s %s', response.status_code, method, path) - msg = 'Cant execute at this time. Try again later. Error: {}'.format(response.text) + logger.debug("%s %s %s", response.status_code, method, path) + msg = f"Cant execute at this time. Try again later. Error: {response.text}" raise WorkflowNotReadyException(msg) else: - logger.error('%s %s %s', response.status_code, method, path) + logger.error("%s %s %s", response.status_code, method, path) raise CitrineException(response.text) @staticmethod @@ -216,7 +221,7 @@ def _extract_response_stacktrace(response: Response) -> str | None: try: json_value = response.json() if isinstance(json_value, dict): - return json_value.get('debug_stacktrace') + return json_value.get("debug_stacktrace") except ValueError: pass return None @@ -258,56 +263,63 @@ def _extract_response_json(path, response) -> dict: lacked the required 'application/json' Content-Type in the header.""") except JSONDecodeError as err: - logger.info('Response at path %s with status code %s failed json parsing with' - ' exception %s. Returning empty value.', - path, - response.status_code, - err.msg) + logger.info( + "Response at path %s with status code %s failed json parsing with" + " exception %s. Returning empty value.", + path, + response.status_code, + err.msg, + ) return extracted_response @staticmethod - def cursor_paged_resource(base_method: Callable[..., dict], path: str, - forward: bool = True, per_page: int = 100, - version: str = 'v2', **kwargs) -> Iterator[dict]: + def cursor_paged_resource( + base_method: Callable[..., dict], + path: str, + forward: bool = True, + per_page: int = 100, + version: str = "v2", + **kwargs, + ) -> Iterator[dict]: """ Returns a flat generator of results for an API query. Results are fetched in chunks of size `per_page` and loaded lazily. """ - params = kwargs.get('params', {}) - params['forward'] = forward - params['ascending'] = forward - params['per_page'] = per_page - kwargs['params'] = params + params = kwargs.get("params", {}) + params["forward"] = forward + params["ascending"] = forward + params["per_page"] = per_page + kwargs["params"] = params while True: response_json = base_method(path, version=version, **kwargs) - for obj in response_json['contents']: + for obj in response_json["contents"]: yield obj - cursor = response_json.get('next') + cursor = response_json.get("next") if cursor is None: break - params['cursor'] = cursor + params["cursor"] = cursor def checked_post(self, path: str, json: dict, **kwargs) -> Response: """Execute a POST request to a URL and utilize error filtering on the response.""" - return self.checked_request('POST', path, json=json, **kwargs) + return self.checked_request("POST", path, json=json, **kwargs) def checked_put(self, path: str, json: dict, **kwargs) -> Response: """Execute a PUT request to a URL and utilize error filtering on the response.""" - return self.checked_request('PUT', path, json=json, **kwargs) + return self.checked_request("PUT", path, json=json, **kwargs) def checked_patch(self, path: str, json: dict, **kwargs) -> Response: """Execute a PATCH request to a URL and utilize error filtering on the response.""" - return self.checked_request('PATCH', path, json=json, **kwargs) + return self.checked_request("PATCH", path, json=json, **kwargs) def checked_delete(self, path: str, **kwargs) -> Response: """Execute a DELETE request to a URL and utilize error filtering on the response.""" - return self.checked_request('DELETE', path, **kwargs) + return self.checked_request("DELETE", path, **kwargs) def checked_get(self, path: str, **kwargs) -> Response: """Execute a GET request to a URL and utilize error filtering on the response.""" - return self.checked_request('GET', path, **kwargs) + return self.checked_request("GET", path, **kwargs) class BearerAuth(requests.auth.AuthBase): diff --git a/src/citrine/_utils/batcher.py b/src/citrine/_utils/batcher.py index 6bed14d1f..a56a414b5 100644 --- a/src/citrine/_utils/batcher.py +++ b/src/citrine/_utils/batcher.py @@ -2,10 +2,10 @@ from collections import defaultdict from collections.abc import Iterable -from citrine.resources.data_concepts import DataConcepts - from gemd.util import make_index, writable_sort_order +from citrine.resources.data_concepts import DataConcepts + class Batcher(ABC): """Base class for Data Concepts batching routines.""" @@ -15,12 +15,12 @@ def batch(self, objects: Iterable[DataConcepts], batch_size: int) -> list[list[D """Collect a list of DataConcepts into batches according to some batching algorithm.""" @staticmethod - def by_type() -> 'BatchByType': + def by_type() -> "BatchByType": """Return a BatchByType batcher.""" return BatchByType() @staticmethod - def by_dependency() -> 'BatchByDependency': + def by_dependency() -> "BatchByDependency": """Return a BatchByDependency batcher.""" return BatchByDependency() @@ -45,7 +45,7 @@ def batch(self, objects: Iterable[DataConcepts], batch_size: int) -> list[list[D for typ_group in typ_groups: num_batches = len(typ_group) // batch_size for batch_num in range(num_batches + 1): - batch = typ_group[batch_num * batch_size: (batch_num + 1) * batch_size] + batch = typ_group[batch_num * batch_size : (batch_num + 1) * batch_size] batches.append(batch) for i in reversed(range(len(batches) - 1)): if len(batches[i]) + len(batches[i + 1]) <= batch_size: @@ -84,8 +84,7 @@ def batch(self, objects: Iterable[DataConcepts], batch_size: int) -> list[list[D for subobj in local_set: full_set.update(depends[subobj]) - depends[obj] = sorted(list(full_set), - key=lambda x: writable_sort_order(x)) + depends[obj] = sorted(list(full_set), key=lambda x: writable_sort_order(x)) for dependant in reversed(depends[obj]): supported_by[dependant].append(obj) diff --git a/src/citrine/_utils/functions.py b/src/citrine/_utils/functions.py index bcab2f768..a4c5a33b5 100644 --- a/src/citrine/_utils/functions.py +++ b/src/citrine/_utils/functions.py @@ -13,7 +13,8 @@ def get_object_id(object_or_id): """Extract the citrine id from a data concepts object or LinkByUID.""" from gemd.entity.attribute.base_attribute import BaseAttribute - from citrine.resources.data_concepts import DataConcepts, CITRINE_SCOPE + + from citrine.resources.data_concepts import CITRINE_SCOPE, DataConcepts if isinstance(object_or_id, BaseAttribute): raise ValueError("Attributes do not have ids.") @@ -23,20 +24,23 @@ def get_object_id(object_or_id): if isinstance(object_or_id, LinkByUID): if object_or_id.scope == CITRINE_SCOPE: return object_or_id.id - raise ValueError("LinkByUID must be scoped to citrine scope {}, " - "instead is {}".format(CITRINE_SCOPE, object_or_id.scope)) - raise TypeError("{} must be a data concepts object or LinkByUID".format(object_or_id)) + raise ValueError( + f"LinkByUID must be scoped to citrine scope {CITRINE_SCOPE}, " + f"instead is {object_or_id.scope}" + ) + raise TypeError(f"{object_or_id} must be a data concepts object or LinkByUID") def validate_type(data_dict: dict, type_name: str) -> dict: """Ensure that dict has field 'type' with given value.""" data_dict_copy = data_dict.copy() - if 'type' in data_dict_copy: - if data_dict_copy['type'] != type_name: - raise Exception( - "Object type must be {}, but was instead {}.".format(type_name, data_dict['type'])) + if "type" in data_dict_copy: + if data_dict_copy["type"] != type_name: + raise ValueError( + f"Object type must be {type_name}, but was instead {data_dict['type']}." + ) else: - data_dict_copy['type'] = type_name + data_dict_copy["type"] = type_name return data_dict_copy @@ -70,7 +74,7 @@ def replace_objects_with_links(json: dict) -> dict: def object_to_link(obj: Any) -> Any: """See if an object is a dictionary that can be converted into a Link, and if so, convert.""" if isinstance(obj, dict): - if 'type' in obj and 'uids' in obj and obj['type'] != LinkByUID.typ: + if "type" in obj and "uids" in obj and obj["type"] != LinkByUID.typ: return object_to_link_by_uid(obj) else: return replace_objects_with_links(obj) @@ -82,8 +86,9 @@ def object_to_link(obj: Any) -> Any: def object_to_link_by_uid(json: dict) -> dict: """Convert an object dictionary into a LinkByUID dictionary, if possible.""" from citrine.resources.data_concepts import CITRINE_SCOPE - if 'uids' in json: - uids = json['uids'] + + if "uids" in json: + uids = json["uids"] if not isinstance(uids, dict) or not uids: return json if CITRINE_SCOPE in uids: @@ -112,8 +117,9 @@ def rewrite_s3_links_locally(url: str, s3_endpoint_url: str = None) -> str: if s3_endpoint_url is not None: # Given an explicit endpoint to use instead parsed_s3_endpoint = urlparse(s3_endpoint_url) - return parsed_url._replace(scheme=parsed_s3_endpoint.scheme, - netloc=parsed_s3_endpoint.netloc).geturl() + return parsed_url._replace( + scheme=parsed_s3_endpoint.scheme, netloc=parsed_s3_endpoint.netloc + ).geturl() else: # Else return the URL unmodified return url @@ -132,7 +138,7 @@ def write_file_locally(content: bytes, local_path: str | Path): raise ValueError(f"A filename must be provided in the path ({local_path})") local_path.parent.mkdir(parents=True, exist_ok=True) - local_path.open(mode='wb').write(content) + local_path.open(mode="wb").write(content) class MigratedClassMeta(ABCMeta): @@ -176,18 +182,23 @@ def __init__(cls, name, bases, *args, deprecated_in=None, removed_in=None, **kwa if not any(isinstance(b, MigratedClassMeta) for b in bases): # First generation if len(bases) != 1: - raise TypeError(f"Migrated Classes must reference precisely one target. " - f"{bases} found.") + raise TypeError( + f"Migrated Classes must reference precisely one target. {bases} found." + ) if deprecated_in is None or removed_in is None: - raise TypeError("Migrated Classes must include `deprecated_in` " - "and `removed_in` arguments.") + raise TypeError( + "Migrated Classes must include `deprecated_in` and `removed_in` arguments." + ) cls._deprecation_info[cls] = (bases[0], deprecated_in, removed_in) def _new(*args_, **kwargs_): - warn(f"Importing {name} from {cls.__module__} is deprecated as of " - f"{deprecated_in} and will be removed in {removed_in}. " - f"Please import {bases[0].__name__} from {bases[0].__module__} instead.", - DeprecationWarning, stacklevel=2) + warn( + f"Importing {name} from {cls.__module__} is deprecated as of " + f"{deprecated_in} and will be removed in {removed_in}. " + f"Please import {bases[0].__name__} from {bases[0].__module__} instead.", + DeprecationWarning, + stacklevel=2, + ) return bases[0](*args_[1:], **kwargs_) cls.__new__ = _new @@ -196,18 +207,20 @@ def _new(*args_, **kwargs_): if base in cls._deprecation_info: # Second generation alias, this_deprecated_in, this_removed_in = cls._deprecation_info[base] - warn(f"Importing {base.__name__} from {base.__module__} is deprecated as of " - f"{this_deprecated_in} and will be removed in {this_removed_in}. " - f"Please import {alias.__name__} from {alias.__module__} instead.", - DeprecationWarning, stacklevel=2) + warn( + f"Importing {base.__name__} from {base.__module__} is deprecated as of " + f"{this_deprecated_in} and will be removed in {this_removed_in}. " + f"Please import {alias.__name__} from {alias.__module__} instead.", + DeprecationWarning, + stacklevel=2, + ) def __instancecheck__(cls, instance): - return any(cls.__subclasscheck__(c) - for c in {type(instance), instance.__class__}) + return any(cls.__subclasscheck__(c) for c in {type(instance), instance.__class__}) def __subclasscheck__(cls, subclass): try: - return issubclass(subclass, cls._deprecation_info.get(cls, (type(None), ))[0]) + return issubclass(subclass, cls._deprecation_info.get(cls, (type(None),))[0]) except RecursionError: return False @@ -217,16 +230,15 @@ def generate_shared_meta(target: type): if issubclass(MigratedClassMeta, type(target)): return MigratedClassMeta else: + class _CustomMeta(MigratedClassMeta, type(target)): pass + return _CustomMeta def migrate_deprecated_argument( - new_arg: Any | None, - new_arg_name: str, - old_arg: Any | None, - old_arg_name: str + new_arg: Any | None, new_arg_name: str, old_arg: Any | None, old_arg_name: str ) -> Any: """ Facilitates the migration of an argument's name. @@ -254,22 +266,17 @@ def migrate_deprecated_argument( """ if old_arg is not None: - warn(f"\'{old_arg_name}\' is deprecated in favor of \'{new_arg_name}\'", - DeprecationWarning) + warn(f"'{old_arg_name}' is deprecated in favor of '{new_arg_name}'", DeprecationWarning) if new_arg is None: return old_arg else: - raise ValueError(f"Cannot specify both \'{new_arg_name}\' and \'{new_arg_name}\'") + raise ValueError(f"Cannot specify both '{new_arg_name}' and '{new_arg_name}'") elif new_arg is None: - raise ValueError(f"Please specify \'{new_arg_name}\'") + raise ValueError(f"Please specify '{new_arg_name}'") return new_arg -def format_escaped_url( - template: str, - *args, - **kwargs -) -> str: +def format_escaped_url(template: str, *args, **kwargs) -> str: """ Escape arguments with percent encoding and bind them to a template of a URL. @@ -291,21 +298,23 @@ def format_escaped_url( the formatted URL """ - return template.format(*[quote(str(x), safe='') for x in args], - **{k: quote(str(v), safe='') for (k, v) in kwargs.items()} - ) - - -def resource_path(*, - path_template: str, - uid: UUID | str | None = None, - action: str | Sequence[str] = [], - query_terms: dict[str, str] = {}, - **kwargs - ) -> str: + return template.format( + *[quote(str(x), safe="") for x in args], + **{k: quote(str(v), safe="") for (k, v) in kwargs.items()}, + ) + + +def resource_path( + *, + path_template: str, + uid: UUID | str | None = None, + action: str | Sequence[str] = [], + query_terms: dict[str, str] = {}, + **kwargs, +) -> str: """Construct a url from a base path and, optionally, id and/or action.""" base = urlparse(path_template) - path = base.path.split('/') + path = base.path.split("/") if uid is not None: path.append("{uid}") @@ -317,6 +326,6 @@ def resource_path(*, path.extend(["{}"] * len(action)) query = urlencode(query_terms) - new_url = base._replace(path='/'.join(path), query=query).geturl() + new_url = base._replace(path="/".join(path), query=query).geturl() return format_escaped_url(new_url, *action, **kwargs, uid=uid) diff --git a/src/citrine/_utils/template_util.py b/src/citrine/_utils/template_util.py index f3b0abc86..c025a881e 100644 --- a/src/citrine/_utils/template_util.py +++ b/src/citrine/_utils/template_util.py @@ -1,17 +1,18 @@ from collections.abc import Mapping -from citrine.resources.data_concepts import DataConcepts from gemd.entity.attribute import PropertyAndConditions from gemd.entity.object import ( - ProcessSpec, - ProcessRun, MaterialSpec, - MeasurementSpec, MeasurementRun, + MeasurementSpec, + ProcessRun, + ProcessSpec, ) from gemd.entity.value.base_value import BaseValue from gemd.util.impl import recursive_flatmap +from citrine.resources.data_concepts import DataConcepts + def make_attribute_table(gems: list[DataConcepts]) -> list[Mapping[str, BaseValue]]: """[ALPHA] the current status of make_attribute_table. @@ -41,9 +42,7 @@ def make_attribute_table(gems: list[DataConcepts]) -> list[Mapping[str, BaseValu A list of dictionaries where each dictionary represents an object and its attributes. """ - flattened_gems = recursive_flatmap( - obj=gems, func=lambda x: [x], unidirectional=False - ) + flattened_gems = recursive_flatmap(obj=gems, func=lambda x: [x], unidirectional=False) types_with_attributes = ( ProcessSpec, ProcessRun, @@ -52,9 +51,7 @@ def make_attribute_table(gems: list[DataConcepts]) -> list[Mapping[str, BaseValu MeasurementRun, ) all_rows = [] - attributed_gems = [ - x for x in flattened_gems if isinstance(x, types_with_attributes) - ] + attributed_gems = [x for x in flattened_gems if isinstance(x, types_with_attributes)] for gem in attributed_gems: row_dict = {"object": gem, "object_type": type(gem).__name__} if hasattr(gem, "conditions"): diff --git a/src/citrine/citrine.py b/src/citrine/citrine.py index 8bde3f245..2c18b3807 100644 --- a/src/citrine/citrine.py +++ b/src/citrine/citrine.py @@ -25,28 +25,22 @@ class Citrine: """ - def __init__(self, - api_key: str = None, - *, - scheme: str = None, - host: str = None, - port: str | None = None): + def __init__( + self, api_key: str = None, *, scheme: str = None, host: str = None, port: str | None = None + ): if api_key is None: - api_key = environ.get('CITRINE_API_KEY') + api_key = environ.get("CITRINE_API_KEY") if scheme is None: - scheme = 'https' + scheme = "https" if host is None: - host = environ.get('CITRINE_API_HOST') + host = environ.get("CITRINE_API_HOST") if host is None: - raise ValueError("No host passed and environmental " - "variable CITRINE_API_HOST not set.") + raise ValueError( + "No host passed and environmental variable CITRINE_API_HOST not set." + ) - self.session: Session = Session(refresh_token=api_key, - scheme=scheme, - host=host, - port=port - ) + self.session: Session = Session(refresh_token=api_key, scheme=scheme, host=host, port=port) @property def projects(self) -> ProjectCollection: diff --git a/src/citrine/exceptions.py b/src/citrine/exceptions.py index 28d27d382..a8ee8b987 100644 --- a/src/citrine/exceptions.py +++ b/src/citrine/exceptions.py @@ -1,4 +1,5 @@ """Citrine-specific exceptions.""" + from types import SimpleNamespace from urllib.parse import urlencode from uuid import UUID @@ -9,26 +10,18 @@ class CitrineException(Exception): """The base exception class for Citrine-Python exceptions.""" - pass - class NonRetryableException(CitrineException): """Indicates that a non-retryable error occurred.""" - pass - class RetryableException(CitrineException): """Indicates an error occurred but it is retryable.""" - pass - class UnauthorizedRefreshToken(NonRetryableException): """The token used to refresh authentication is invalid.""" - pass - class NonRetryableHttpException(NonRetryableException): """An exception originating from an HTTP error from a Citrine API.""" @@ -45,19 +38,20 @@ def __init__(self, path: str, response: Response | None = None): method = response.request.method self.detailed_error_info.append( - "{} (code: {}) returned from {} request to path: '{}'".format( - response.reason, self.code, method, path - ) + f"{response.reason} (code: {self.code}) returned from {method} " + f"request to path: '{path}'" ) try: resp_json = response.json() if isinstance(resp_json, dict): from citrine.resources.api_error import ApiError + self.api_error = ApiError.build(resp_json) validation_error_msgs = [ - "{} ({})".format(f.failure_message, f.failure_id) - for f in self.api_error.validation_errors] + f"{f.failure_message} ({f.failure_id})" + for f in self.api_error.validation_errors + ] if self.api_error.message is not None: self.detailed_error_info.append(self.api_error.message) @@ -119,28 +113,22 @@ def build(*, message: str, method: str, path: str, params: dict = {}): status_code=404, request=SimpleNamespace(method=method.upper()), reason="Not Found", - json=lambda self: {"code": 404, "message": message, "validation_errors": []} - ) + json=lambda self: {"code": 404, "message": message, "validation_errors": []}, + ), ) class Unauthorized(NonRetryableHttpException): """The user is unauthorized to make this api call. (http status 401).""" - pass - class BadRequest(NonRetryableHttpException): """The user is trying to perform an invalid operation. (http status 400).""" - pass - class WorkflowConflictException(NonRetryableHttpException): """There is a conflict preventing the workflow from being executed. (http status 409).""" - pass - # A 409 is a Conflict, and can be raised anywhere a conflict occurs, not just in a workflow. Conflict = WorkflowConflictException @@ -149,14 +137,10 @@ class WorkflowConflictException(NonRetryableHttpException): class WorkflowNotReadyException(RetryableException): """The workflow is not ready to be executed. I.e., still validating. (http status 425).""" - pass - class PollingTimeoutError(NonRetryableException): """Polling for an asynchronous result has exceeded the timeout.""" - pass - class JobFailureError(NonRetryableException): """The asynchronous job completed with the given failure message.""" @@ -171,6 +155,5 @@ class ModuleRegistrationFailedException(NonRetryableException): """A module failed to register.""" def __init__(self, moduleType: str, exc: Exception): - err = 'The "{0}" failed to register. {1}: {2}'.format( - moduleType, exc.__class__.__name__, str(exc)) + err = f'The "{moduleType}" failed to register. {exc.__class__.__name__}: {exc!s}' super().__init__(err) diff --git a/src/citrine/gemd_queries/criteria.py b/src/citrine/gemd_queries/criteria.py index 695d47007..ceac0f9fd 100644 --- a/src/citrine/gemd_queries/criteria.py +++ b/src/citrine/gemd_queries/criteria.py @@ -2,17 +2,24 @@ from gemd.enumeration.base_enumeration import BaseEnumeration -from citrine._serialization.serializable import Serializable -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties +from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable from citrine.gemd_queries.filter import PropertyFilterType -__all__ = ['MaterialClassification', 'TextSearchType', 'TagFilterType', - 'AndOperator', 'OrOperator', - 'PropertiesCriteria', 'NameCriteria', - 'MaterialRunClassificationCriteria', 'MaterialTemplatesCriteria', - 'TagsCriteria', 'ConnectivityClassCriteria' - ] +__all__ = [ + "AndOperator", + "ConnectivityClassCriteria", + "MaterialClassification", + "MaterialRunClassificationCriteria", + "MaterialTemplatesCriteria", + "NameCriteria", + "OrOperator", + "PropertiesCriteria", + "TagFilterType", + "TagsCriteria", + "TextSearchType", +] class MaterialClassification(BaseEnumeration): @@ -47,14 +54,19 @@ class Criteria(PolymorphicSerializable): def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" classes: list[type[Criteria]] = [ - AndOperator, OrOperator, - PropertiesCriteria, NameCriteria, MaterialRunClassificationCriteria, - MaterialTemplatesCriteria, TagsCriteria, ConnectivityClassCriteria + AndOperator, + OrOperator, + PropertiesCriteria, + NameCriteria, + MaterialRunClassificationCriteria, + MaterialTemplatesCriteria, + TagsCriteria, + ConnectivityClassCriteria, ] - return {klass.typ: klass for klass in classes}[data['type']] + return {klass.typ: klass for klass in classes}[data["type"]] -class AndOperator(Serializable['AndOperator'], Criteria): +class AndOperator(Serializable["AndOperator"], Criteria): """ Combine multiple criteria, requiring EACH to be true for a match. @@ -66,10 +78,10 @@ class AndOperator(Serializable['AndOperator'], Criteria): """ criteria = properties.List(properties.Object(Criteria), "criteria") - typ = properties.String('type', default="and_operator", deserializable=False) + typ = properties.String("type", default="and_operator", deserializable=False) -class OrOperator(Serializable['OrOperator'], Criteria): +class OrOperator(Serializable["OrOperator"], Criteria): """ Combine multiple criteria, requiring ANY to be true for a match. @@ -81,10 +93,10 @@ class OrOperator(Serializable['OrOperator'], Criteria): """ criteria = properties.List(properties.Object(Criteria), "criteria") - typ = properties.String('type', default="or_operator", deserializable=False) + typ = properties.String("type", default="or_operator", deserializable=False) -class PropertiesCriteria(Serializable['PropertiesCriteria'], Criteria): +class PropertiesCriteria(Serializable["PropertiesCriteria"], Criteria): """ Look for materials with a particular Property and optionally Value types & ranges. @@ -101,10 +113,10 @@ class PropertiesCriteria(Serializable['PropertiesCriteria'], Criteria): value_type_filter = properties.Optional( properties.Object(PropertyFilterType), "value_type_filter" ) - typ = properties.String('type', default="properties_criteria", deserializable=False) + typ = properties.String("type", default="properties_criteria", deserializable=False) -class NameCriteria(Serializable['NameCriteria'], Criteria): +class NameCriteria(Serializable["NameCriteria"], Criteria): """ Look for materials with particular names. @@ -117,14 +129,13 @@ class NameCriteria(Serializable['NameCriteria'], Criteria): """ - name = properties.String('name') - search_type = properties.Enumeration(TextSearchType, 'search_type') - typ = properties.String('type', default="name_criteria", deserializable=False) + name = properties.String("name") + search_type = properties.Enumeration(TextSearchType, "search_type") + typ = properties.String("type", default="name_criteria", deserializable=False) class MaterialRunClassificationCriteria( - Serializable['MaterialRunClassificationCriteria'], - Criteria + Serializable["MaterialRunClassificationCriteria"], Criteria ): """ Look for materials with particular classification, defined by MaterialClassification. @@ -137,16 +148,14 @@ class MaterialRunClassificationCriteria( """ classifications = properties.Set( - properties.Enumeration(MaterialClassification), 'classifications' + properties.Enumeration(MaterialClassification), "classifications" ) typ = properties.String( - 'type', - default="material_run_classification_criteria", - deserializable=False + "type", default="material_run_classification_criteria", deserializable=False ) -class MaterialTemplatesCriteria(Serializable['MaterialTemplatesCriteria'], Criteria): +class MaterialTemplatesCriteria(Serializable["MaterialTemplatesCriteria"], Criteria): """ Look for materials with particular Material Templates and tags. @@ -162,14 +171,13 @@ class MaterialTemplatesCriteria(Serializable['MaterialTemplatesCriteria'], Crite """ material_templates_identifiers = properties.Set( - properties.UUID, - "material_templates_identifiers" + properties.UUID, "material_templates_identifiers" ) - tag_filters = properties.Set(properties.String, 'tag_filters') - typ = properties.String('type', default="material_template_criteria", deserializable=False) + tag_filters = properties.Set(properties.String, "tag_filters") + typ = properties.String("type", default="material_template_criteria", deserializable=False) -class TagsCriteria(Serializable['TagsCriteria'], Criteria): +class TagsCriteria(Serializable["TagsCriteria"], Criteria): """ Look for materials with particular tags. @@ -185,12 +193,12 @@ class TagsCriteria(Serializable['TagsCriteria'], Criteria): """ - tags = properties.Set(properties.String, 'tags') - filter_type = properties.Enumeration(TagFilterType, 'filter_type') - typ = properties.String('type', default="tags_criteria", deserializable=False) + tags = properties.Set(properties.String, "tags") + filter_type = properties.Enumeration(TagFilterType, "filter_type") + typ = properties.String("type", default="tags_criteria", deserializable=False) -class ConnectivityClassCriteria(Serializable['ConnectivityClassCriteria'], Criteria): +class ConnectivityClassCriteria(Serializable["ConnectivityClassCriteria"], Criteria): """ Look for materials with particular connectivity classes. @@ -203,6 +211,6 @@ class ConnectivityClassCriteria(Serializable['ConnectivityClassCriteria'], Crite """ - is_consumed = properties.Optional(properties.Boolean, 'is_consumed') - is_produced = properties.Optional(properties.Boolean, 'is_produced') - typ = properties.String('type', default="connectivity_class_criteria", deserializable=False) + is_consumed = properties.Optional(properties.Boolean, "is_consumed") + is_produced = properties.Optional(properties.Boolean, "is_produced") + typ = properties.String("type", default="connectivity_class_criteria", deserializable=False) diff --git a/src/citrine/gemd_queries/filter.py b/src/citrine/gemd_queries/filter.py index b76540e0d..6757bbb94 100644 --- a/src/citrine/gemd_queries/filter.py +++ b/src/citrine/gemd_queries/filter.py @@ -1,10 +1,10 @@ """Definitions for GemdQuery objects, and their sub-objects.""" -from citrine._serialization.serializable import Serializable -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties +from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable -__all__ = ['AllRealFilter', 'AllIntegerFilter', 'NominalCategoricalFilter'] +__all__ = ["AllIntegerFilter", "AllRealFilter", "NominalCategoricalFilter"] class PropertyFilterType(PolymorphicSerializable): @@ -15,12 +15,13 @@ def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" classes: list[type[PropertyFilterType]] = [ NominalCategoricalFilter, - AllRealFilter, AllIntegerFilter + AllRealFilter, + AllIntegerFilter, ] - return {klass.typ: klass for klass in classes}[data['type']] + return {klass.typ: klass for klass in classes}[data["type"]] -class AllRealFilter(Serializable['AllRealFilter'], PropertyFilterType): +class AllRealFilter(Serializable["AllRealFilter"], PropertyFilterType): """ Filter for any real value that fits certain constraints. @@ -35,13 +36,13 @@ class AllRealFilter(Serializable['AllRealFilter'], PropertyFilterType): """ - lower = properties.Float('lower') - upper = properties.Float('upper') - unit = properties.String('unit') - typ = properties.String('type', default="all_real_filter", deserializable=False) + lower = properties.Float("lower") + upper = properties.Float("upper") + unit = properties.String("unit") + typ = properties.String("type", default="all_real_filter", deserializable=False) -class AllIntegerFilter(Serializable['AllIntegerFilter'], PropertyFilterType): +class AllIntegerFilter(Serializable["AllIntegerFilter"], PropertyFilterType): """ Filter for any integer value that fits certain constraints. @@ -56,13 +57,13 @@ class AllIntegerFilter(Serializable['AllIntegerFilter'], PropertyFilterType): """ - lower = properties.Float('lower') - upper = properties.Float('upper') - inclusive = properties.Optional(properties.Boolean, 'inclusive', default=True) - typ = properties.String('type', default="all_integer_filter", deserializable=False) + lower = properties.Float("lower") + upper = properties.Float("upper") + inclusive = properties.Optional(properties.Boolean, "inclusive", default=True) + typ = properties.String("type", default="all_integer_filter", deserializable=False) -class NominalCategoricalFilter(Serializable['NominalCategoricalFilter'], PropertyFilterType): +class NominalCategoricalFilter(Serializable["NominalCategoricalFilter"], PropertyFilterType): """ Filter based upon a fixed list of Categorical Values. @@ -73,5 +74,5 @@ class NominalCategoricalFilter(Serializable['NominalCategoricalFilter'], Propert """ - categories = properties.Set(properties.String, 'categories') - typ = properties.String('type', default="nominal_categorical_filter", deserializable=False) + categories = properties.Set(properties.String, "categories") + typ = properties.String("type", default="nominal_categorical_filter", deserializable=False) diff --git a/src/citrine/gemd_queries/gemd_query.py b/src/citrine/gemd_queries/gemd_query.py index 28b3deaff..a2f4bb3d7 100644 --- a/src/citrine/gemd_queries/gemd_query.py +++ b/src/citrine/gemd_queries/gemd_query.py @@ -1,8 +1,9 @@ """Definitions for GemdQuery objects, and their sub-objects.""" + from gemd.enumeration.base_enumeration import BaseEnumeration -from citrine._serialization.serializable import Serializable from citrine._serialization import properties +from citrine._serialization.serializable import Serializable from citrine.gemd_queries.criteria import Criteria @@ -27,7 +28,7 @@ class GemdObjectType(BaseEnumeration): MEASUREMENT_SPEC_TYPE = "measurement_spec", "MEASUREMENT_SPEC_TYPE" -class GemdQuery(Serializable['GemdQuery']): +class GemdQuery(Serializable["GemdQuery"]): """ This describes what data objects to fetch (or graph of data objects). @@ -47,17 +48,15 @@ class GemdQuery(Serializable['GemdQuery']): criteria = properties.List(properties.Object(Criteria), "criteria", default=[]) datasets = properties.Set(properties.UUID, "datasets", default=set()) object_types = properties.Set( - properties.Enumeration(GemdObjectType), - 'object_types', - default={x for x in GemdObjectType} + properties.Enumeration(GemdObjectType), "object_types", default={x for x in GemdObjectType} ) - schema_version = properties.Integer('schema_version', default=1) + schema_version = properties.Integer("schema_version", default=1) @classmethod def _pre_build(cls, data: dict) -> dict: """Run data modification before building.""" - version = data.get('schema_version') - if data.get('schema_version') != 1: + version = data.get("schema_version") + if data.get("schema_version") != 1: raise ValueError( f"This version of the library only supports schema_version 1, not '{version}'" ) diff --git a/src/citrine/gemtables/columns.py b/src/citrine/gemtables/columns.py index c09a3f3ee..89d55edd4 100644 --- a/src/citrine/gemtables/columns.py +++ b/src/citrine/gemtables/columns.py @@ -2,9 +2,9 @@ from gemd.enumeration.base_enumeration import BaseEnumeration -from citrine._serialization.serializable import Serializable -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties +from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable from citrine.gemtables.variables import Variable @@ -45,11 +45,12 @@ def _make_data_source(variable_rep: str | Variable) -> str: elif isinstance(variable_rep, Variable): return variable_rep.name else: - raise TypeError("Columns can only be linked by str or Variable." - "Instead got {}.".format(variable_rep)) + raise TypeError( + f"Columns can only be linked by str or Variable.Instead got {variable_rep}." + ) -class Column(PolymorphicSerializable['Column']): +class Column(PolymorphicSerializable["Column"]): """A column in the GEM Table, defined as some operation on a variable. Abstract type that returns the proper type given a serialized dict. @@ -68,19 +69,26 @@ def get_type(cls, data) -> type[Serializable]: raise ValueError("Can only get types from dicts with a 'type' key") types: list[type[Serializable]] = [ IdentityColumn, - MeanColumn, StdDevColumn, QuantileColumn, OriginalUnitsColumn, - MostLikelyCategoryColumn, MostLikelyProbabilityColumn, - FlatCompositionColumn, ComponentQuantityColumn, - NthBiggestComponentNameColumn, NthBiggestComponentQuantityColumn, - MolecularStructureColumn, ConcatColumn + MeanColumn, + StdDevColumn, + QuantileColumn, + OriginalUnitsColumn, + MostLikelyCategoryColumn, + MostLikelyProbabilityColumn, + FlatCompositionColumn, + ComponentQuantityColumn, + NthBiggestComponentNameColumn, + NthBiggestComponentQuantityColumn, + MolecularStructureColumn, + ConcatColumn, ] res = next((x for x in types if x.typ == data["type"]), None) if res is None: - raise ValueError("Unrecognized type: {}".format(data["type"])) + raise ValueError(f"Unrecognized type: {data['type']}") return res -class MeanColumn(Serializable['MeanColumn'], Column): +class MeanColumn(Serializable["MeanColumn"], Column): """Column containing the mean of a real-valued variable. Parameters @@ -96,13 +104,11 @@ class MeanColumn(Serializable['MeanColumn'], Column): """ - data_source = properties.String('data_source') + data_source = properties.String("data_source") target_units = properties.Optional(properties.String, "target_units") - typ = properties.String('type', default="mean_column", deserializable=False) + typ = properties.String("type", default="mean_column", deserializable=False) - def __init__(self, *, - data_source: str | Variable, - target_units: str | None = None): + def __init__(self, *, data_source: str | Variable, target_units: str | None = None): self.data_source = _make_data_source(data_source) self.target_units = target_units @@ -123,13 +129,11 @@ class StdDevColumn(Serializable["StdDevColumn"], Column): """ - data_source = properties.String('data_source') + data_source = properties.String("data_source") target_units = properties.Optional(properties.String, "target_units") - typ = properties.String('type', default="std_dev_column", deserializable=False) + typ = properties.String("type", default="std_dev_column", deserializable=False) - def __init__(self, *, - data_source: str | Variable, - target_units: str | None = None): + def __init__(self, *, data_source: str | Variable, target_units: str | None = None): self.data_source = _make_data_source(data_source) self.target_units = target_units @@ -166,15 +170,14 @@ class QuantileColumn(Serializable["QuantileColumn"], Column): """ - data_source = properties.String('data_source') + data_source = properties.String("data_source") quantile = properties.Float("quantile") target_units = properties.Optional(properties.String, "target_units") - typ = properties.String('type', default="quantile_column", deserializable=False) + typ = properties.String("type", default="quantile_column", deserializable=False) - def __init__(self, *, - data_source: str | Variable, - quantile: float, - target_units: str | None = None): + def __init__( + self, *, data_source: str | Variable, quantile: float, target_units: str | None = None + ): self.data_source = _make_data_source(data_source) self.quantile = quantile self.target_units = target_units @@ -190,8 +193,8 @@ class OriginalUnitsColumn(Serializable["OriginalUnitsColumn"], Column): """ - data_source = properties.String('data_source') - typ = properties.String('type', default="original_units_column", deserializable=False) + data_source = properties.String("data_source") + typ = properties.String("type", default="original_units_column", deserializable=False) def __init__(self, *, data_source: str | Variable): self.data_source = _make_data_source(data_source) @@ -207,8 +210,8 @@ class MostLikelyCategoryColumn(Serializable["MostLikelyCategoryColumn"], Column) """ - data_source = properties.String('data_source') - typ = properties.String('type', default="most_likely_category_column", deserializable=False) + data_source = properties.String("data_source") + typ = properties.String("type", default="most_likely_category_column", deserializable=False) def __init__(self, *, data_source: str | Variable): self.data_source = _make_data_source(data_source) @@ -224,8 +227,8 @@ class MostLikelyProbabilityColumn(Serializable["MostLikelyProbabilityColumn"], C """ - data_source = properties.String('data_source') - typ = properties.String('type', default="most_likely_probability_column", deserializable=False) + data_source = properties.String("data_source") + typ = properties.String("type", default="most_likely_probability_column", deserializable=False) def __init__(self, *, data_source: str | Variable): self.data_source = _make_data_source(data_source) @@ -247,13 +250,11 @@ class FlatCompositionColumn(Serializable["FlatCompositionColumn"], Column): """ - data_source = properties.String('data_source') - sort_order = properties.Enumeration(CompositionSortOrder, 'sort_order') - typ = properties.String('type', default="flat_composition_column", deserializable=False) + data_source = properties.String("data_source") + sort_order = properties.Enumeration(CompositionSortOrder, "sort_order") + typ = properties.String("type", default="flat_composition_column", deserializable=False) - def __init__(self, *, - data_source: str | Variable, - sort_order: CompositionSortOrder): + def __init__(self, *, data_source: str | Variable, sort_order: CompositionSortOrder): self.data_source = _make_data_source(data_source) self.sort_order = sort_order @@ -274,15 +275,14 @@ class ComponentQuantityColumn(Serializable["ComponentQuantityColumn"], Column): """ - data_source = properties.String('data_source') + data_source = properties.String("data_source") component_name = properties.String("component_name") normalize = properties.Boolean("normalize") - typ = properties.String('type', default="component_quantity_column", deserializable=False) + typ = properties.String("type", default="component_quantity_column", deserializable=False) - def __init__(self, *, - data_source: str | Variable, - component_name: str, - normalize: bool = False): + def __init__( + self, *, data_source: str | Variable, component_name: str, normalize: bool = False + ): self.data_source = _make_data_source(data_source) self.component_name = component_name self.normalize = normalize @@ -302,13 +302,11 @@ class NthBiggestComponentNameColumn(Serializable["NthBiggestComponentNameColumn" """ - data_source = properties.String('data_source') + data_source = properties.String("data_source") n = properties.Integer("n") - typ = properties.String('type', default="biggest_component_name_column", deserializable=False) + typ = properties.String("type", default="biggest_component_name_column", deserializable=False) - def __init__(self, *, - data_source: str | Variable, - n: int): + def __init__(self, *, data_source: str | Variable, n: int): self.data_source = _make_data_source(data_source) self.n = n @@ -329,22 +327,20 @@ class NthBiggestComponentQuantityColumn(Serializable["NthBiggestComponentQuantit """ - data_source = properties.String('data_source') + data_source = properties.String("data_source") n = properties.Integer("n") normalize = properties.Boolean("normalize") - typ = properties.String('type', - default="biggest_component_quantity_column", deserializable=False) + typ = properties.String( + "type", default="biggest_component_quantity_column", deserializable=False + ) - def __init__(self, *, - data_source: str | Variable, - n: int, - normalize: bool = False): + def __init__(self, *, data_source: str | Variable, n: int, normalize: bool = False): self.data_source = _make_data_source(data_source) self.n = n self.normalize = normalize -class IdentityColumn(Serializable['IdentityColumn'], Column): +class IdentityColumn(Serializable["IdentityColumn"], Column): """Column containing the value of a string-valued variable. Parameters @@ -354,14 +350,14 @@ class IdentityColumn(Serializable['IdentityColumn'], Column): """ - data_source = properties.String('data_source') - typ = properties.String('type', default="identity_column", deserializable=False) + data_source = properties.String("data_source") + typ = properties.String("type", default="identity_column", deserializable=False) def __init__(self, *, data_source: str | Variable): self.data_source = _make_data_source(data_source) -class MolecularStructureColumn(Serializable['MolecularStructureColumn'], Column): +class MolecularStructureColumn(Serializable["MolecularStructureColumn"], Column): """Column containing a representation of a molecular structure. Parameters @@ -373,16 +369,16 @@ class MolecularStructureColumn(Serializable['MolecularStructureColumn'], Column) """ - data_source = properties.String('data_source') - format = properties.Enumeration(ChemicalDisplayFormat, 'format') - typ = properties.String('type', default="molecular_structure_column", deserializable=False) + data_source = properties.String("data_source") + format = properties.Enumeration(ChemicalDisplayFormat, "format") + typ = properties.String("type", default="molecular_structure_column", deserializable=False) def __init__(self, *, data_source: str | Variable, format: ChemicalDisplayFormat): self.data_source = _make_data_source(data_source) self.format = format -class ConcatColumn(Serializable['ConcatColumn'], Column): +class ConcatColumn(Serializable["ConcatColumn"], Column): """Column that concatenates multiple values produced by a list- or set-valued variable. The input subcolumn need not exist elsewhere in the table config, and its parameters have @@ -398,9 +394,9 @@ class ConcatColumn(Serializable['ConcatColumn'], Column): """ - data_source = properties.String('data_source') - subcolumn = properties.Object(Column, 'subcolumn') - typ = properties.String('type', default="concat_column", deserializable=False) + data_source = properties.String("data_source") + subcolumn = properties.Object(Column, "subcolumn") + typ = properties.String("type", default="concat_column", deserializable=False) def __init__(self, *, data_source: str | Variable, subcolumn: Column): self.data_source = _make_data_source(data_source) diff --git a/src/citrine/gemtables/rows.py b/src/citrine/gemtables/rows.py index 8e050a53c..850e69cae 100644 --- a/src/citrine/gemtables/rows.py +++ b/src/citrine/gemtables/rows.py @@ -1,17 +1,17 @@ """Row definitions for GEM Tables.""" + from uuid import UUID from gemd.entity.link_by_uid import LinkByUID from gemd.entity.template import MaterialTemplate -from citrine._serialization.serializable import Serializable -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties - +from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable from citrine.resources.data_concepts import _make_link_by_uid -class Row(PolymorphicSerializable['Row']): +class Row(PolymorphicSerializable["Row"]): """A rule for defining rows in a GEM Table. Abstract type that returns the proper type given a serialized dict. @@ -28,16 +28,14 @@ def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" if "type" not in data: raise ValueError("Can only get types from dicts with a 'type' key") - types: list[type[Serializable]] = [ - MaterialRunByTemplate - ] + types: list[type[Serializable]] = [MaterialRunByTemplate] res = next((x for x in types if x.typ == data["type"]), None) if res is None: - raise ValueError("Unrecognized type: {}".format(data["type"])) + raise ValueError(f"Unrecognized type: {data['type']}") return res -class MaterialRunByTemplate(Serializable['MaterialRunByTemplate'], Row): +class MaterialRunByTemplate(Serializable["MaterialRunByTemplate"], Row): """Rows corresponding to MaterialRuns, marked by their template. Parameters @@ -51,14 +49,12 @@ class MaterialRunByTemplate(Serializable['MaterialRunByTemplate'], Row): """ templates = properties.List(properties.Object(LinkByUID), "templates") - typ = properties.String('type', default="material_run_by_template", deserializable=False) + typ = properties.String("type", default="material_run_by_template", deserializable=False) tags = properties.Optional(properties.Set(properties.String), "tags") template_type = UUID | str | LinkByUID | MaterialTemplate - def __init__(self, *, - templates: list[template_type], - tags: set[str] = None): + def __init__(self, *, templates: list[template_type], tags: set[str] = None): self.templates = [_make_link_by_uid(x) for x in templates] self.tags = tags diff --git a/src/citrine/gemtables/variables.py b/src/citrine/gemtables/variables.py index fdfa65244..49814a90e 100644 --- a/src/citrine/gemtables/variables.py +++ b/src/citrine/gemtables/variables.py @@ -1,4 +1,5 @@ """Variable definitions for GEM Tables.""" + from uuid import UUID from gemd.entity.bounds.base_bounds import BaseBounds @@ -8,11 +9,25 @@ from gemd.entity.template.base_template import BaseTemplate from gemd.enumeration.base_enumeration import BaseEnumeration -from citrine._serialization.serializable import Serializable -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties +from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable from citrine.resources.data_concepts import CITRINE_SCOPE, _make_link_by_uid +_AttributeType = UUID | str | LinkByUID | AttributeTemplate +_ConstraintType = tuple[_AttributeType, BaseBounds] +_ObjectType = UUID | str | LinkByUID | BaseTemplate +_ProcessType = UUID | str | LinkByUID | ProcessTemplate + + +def _build_attribute_constraints( + attribute_constraints: list[_ConstraintType] | None, +) -> list[tuple[LinkByUID, BaseBounds]] | None: + """Resolve attribute-constraint templates to LinkByUID pairs, preserving None.""" + if attribute_constraints is None: + return None + return [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + class IngredientQuantityDimension(BaseEnumeration): """The dimension of an ingredient quantity. @@ -52,7 +67,7 @@ class DataObjectTypeSelector(BaseEnumeration): ANY = "any" -class Variable(PolymorphicSerializable['Variable']): +class Variable(PolymorphicSerializable["Variable"]): """A variable that can be assigned values present in material histories. Abstract type that returns the proper type given a serialized dict. @@ -70,23 +85,34 @@ def get_type(cls, data) -> type[Serializable]: if "type" not in data: raise ValueError("Can only get types from dicts with a 'type' key") types: list[type[Serializable]] = [ - TerminalMaterialInfo, AttributeByTemplate, AttributeByTemplateAfterProcessTemplate, - AttributeByTemplateAndObjectTemplate, LocalAttribute, LocalAttributeAndObject, - IngredientIdentifierByProcessTemplateAndName, IngredientLabelByProcessAndName, - IngredientLabelsSetByProcessAndName, IngredientQuantityByProcessAndName, - TerminalMaterialIdentifier, AttributeInOutput, - IngredientIdentifierInOutput, IngredientLabelsSetInOutput, IngredientQuantityInOutput, - LocalIngredientIdentifier, LocalIngredientLabelsSet, LocalIngredientQuantity, + TerminalMaterialInfo, + AttributeByTemplate, + AttributeByTemplateAfterProcessTemplate, + AttributeByTemplateAndObjectTemplate, + LocalAttribute, + LocalAttributeAndObject, + IngredientIdentifierByProcessTemplateAndName, + IngredientLabelByProcessAndName, + IngredientLabelsSetByProcessAndName, + IngredientQuantityByProcessAndName, + TerminalMaterialIdentifier, + AttributeInOutput, + IngredientIdentifierInOutput, + IngredientLabelsSetInOutput, + IngredientQuantityInOutput, + LocalIngredientIdentifier, + LocalIngredientLabelsSet, + LocalIngredientQuantity, XOR, ] res = next((x for x in types if x.typ == data["type"]), None) if res is None: - raise ValueError("Unrecognized type: {}".format(data["type"])) + raise ValueError(f"Unrecognized type: {data['type']}") return res -class TerminalMaterialInfo(Serializable['TerminalMaterialInfo'], Variable): +class TerminalMaterialInfo(Serializable["TerminalMaterialInfo"], Variable): """Metadata from the terminal material of the material history. Parameters @@ -101,21 +127,18 @@ class TerminalMaterialInfo(Serializable['TerminalMaterialInfo'], Variable): """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - field = properties.String('field') - typ = properties.String('type', default="root_info", deserializable=False) + name = properties.String("name") + headers = properties.List(properties.String, "headers") + field = properties.String("field") + typ = properties.String("type", default="root_info", deserializable=False) - def __init__(self, - name: str, *, - headers: list[str], - field: str): + def __init__(self, name: str, *, headers: list[str], field: str): self.name = name self.headers = headers self.field = field -class AttributeByTemplate(Serializable['AttributeByTemplate'], Variable): +class AttributeByTemplate(Serializable["AttributeByTemplate"], Variable): """Attribute marked by an attribute template. Parameters @@ -136,38 +159,39 @@ class AttributeByTemplate(Serializable['AttributeByTemplate'], Variable): """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - template = properties.Object(LinkByUID, 'template') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + template = properties.Object(LinkByUID, "template") attribute_constraints = properties.Optional( properties.List( properties.SpecifiedMixedList( [properties.Object(LinkByUID), properties.Object(BaseBounds)] ) - ), 'attribute_constraints') + ), + "attribute_constraints", + ) type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="attribute_by_template", deserializable=False) - - attribute_type = UUID | str | LinkByUID | AttributeTemplate - constraint_type = tuple[attribute_type, BaseBounds] - - def __init__(self, - name: str, - *, - headers: list[str], - template: attribute_type, - attribute_constraints: list[constraint_type] | None = None, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="attribute_by_template", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + template: _AttributeType, + attribute_constraints: list[_ConstraintType] | None = None, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.template = _make_link_by_uid(template) - self.attribute_constraints = None if attribute_constraints is None \ - else [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + self.attribute_constraints = _build_attribute_constraints(attribute_constraints) self.type_selector = type_selector class AttributeByTemplateAfterProcessTemplate( - Serializable['AttributeByTemplateAfterProcessTemplate'], Variable): + Serializable["AttributeByTemplateAfterProcessTemplate"], Variable +): """Attribute of an object marked by an attribute template and a parent process template. Parameters @@ -190,42 +214,42 @@ class AttributeByTemplateAfterProcessTemplate( """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - attribute_template = properties.Object(LinkByUID, 'attribute_template') - process_template = properties.Object(LinkByUID, 'process_template') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + attribute_template = properties.Object(LinkByUID, "attribute_template") + process_template = properties.Object(LinkByUID, "process_template") attribute_constraints = properties.Optional( properties.List( properties.SpecifiedMixedList( [properties.Object(LinkByUID), properties.Object(BaseBounds)] ) - ), 'attribute_constraints') + ), + "attribute_constraints", + ) type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="attribute_after_process", deserializable=False) - - attribute_type = UUID | str | LinkByUID | AttributeTemplate - process_type = UUID | str | LinkByUID | ProcessTemplate - constraint_type = tuple[attribute_type, BaseBounds] - - def __init__(self, - name: str, - *, - headers: list[str], - attribute_template: attribute_type, - process_template: process_type, - attribute_constraints: list[constraint_type] | None = None, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="attribute_after_process", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + attribute_template: _AttributeType, + process_template: _ProcessType, + attribute_constraints: list[_ConstraintType] | None = None, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.attribute_template = _make_link_by_uid(attribute_template) self.process_template = _make_link_by_uid(process_template) - self.attribute_constraints = None if attribute_constraints is None \ - else [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + self.attribute_constraints = _build_attribute_constraints(attribute_constraints) self.type_selector = type_selector class AttributeByTemplateAndObjectTemplate( - Serializable['AttributeByTemplateAndObjectTemplate'], Variable): + Serializable["AttributeByTemplateAndObjectTemplate"], Variable +): """Attribute marked by an attribute template and an object template. For example, one property may be measured by two different measurement techniques. In this @@ -253,41 +277,40 @@ class AttributeByTemplateAndObjectTemplate( """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - attribute_template = properties.Object(LinkByUID, 'attribute_template') - object_template = properties.Object(LinkByUID, 'object_template') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + attribute_template = properties.Object(LinkByUID, "attribute_template") + object_template = properties.Object(LinkByUID, "object_template") attribute_constraints = properties.Optional( properties.List( properties.SpecifiedMixedList( [properties.Object(LinkByUID), properties.Object(BaseBounds)] ) - ), 'attribute_constraints') + ), + "attribute_constraints", + ) type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="attribute_by_object", deserializable=False) - - attribute_type = UUID | str | LinkByUID | AttributeTemplate - object_type = UUID | str | LinkByUID | BaseTemplate - constraint_type = tuple[attribute_type, BaseBounds] - - def __init__(self, - name: str, - *, - headers: list[str], - attribute_template: attribute_type, - object_template: object_type, - attribute_constraints: list[constraint_type] | None = None, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="attribute_by_object", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + attribute_template: _AttributeType, + object_template: _ObjectType, + attribute_constraints: list[_ConstraintType] | None = None, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.attribute_template = _make_link_by_uid(attribute_template) self.object_template = _make_link_by_uid(object_template) - self.attribute_constraints = None if attribute_constraints is None \ - else [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + self.attribute_constraints = _build_attribute_constraints(attribute_constraints) self.type_selector = type_selector -class LocalAttribute(Serializable['LocalAttribute'], Variable): +class LocalAttribute(Serializable["LocalAttribute"], Variable): """[ALPHA] Attribute marked by an attribute template for the root of a material history tree. Parameters @@ -308,37 +331,37 @@ class LocalAttribute(Serializable['LocalAttribute'], Variable): """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - template = properties.Object(LinkByUID, 'template') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + template = properties.Object(LinkByUID, "template") attribute_constraints = properties.Optional( properties.List( properties.SpecifiedMixedList( [properties.Object(LinkByUID), properties.Object(BaseBounds)] ) - ), 'attribute_constraints') + ), + "attribute_constraints", + ) type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="local_attribute", deserializable=False) - - attribute_type = UUID | str | LinkByUID | AttributeTemplate - constraint_type = tuple[attribute_type, BaseBounds] - - def __init__(self, - name: str, - *, - headers: list[str], - template: attribute_type, - attribute_constraints: list[constraint_type] | None = None, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="local_attribute", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + template: _AttributeType, + attribute_constraints: list[_ConstraintType] | None = None, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.template = _make_link_by_uid(template) - self.attribute_constraints = None if attribute_constraints is None \ - else [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + self.attribute_constraints = _build_attribute_constraints(attribute_constraints) self.type_selector = type_selector -class LocalAttributeAndObject(Serializable['LocalAttributeAndObject'], Variable): +class LocalAttributeAndObject(Serializable["LocalAttributeAndObject"], Variable): """[ALPHA] Attribute marked by an attribute template for the root of a material history tree. Parameters @@ -361,42 +384,42 @@ class LocalAttributeAndObject(Serializable['LocalAttributeAndObject'], Variable) """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - template = properties.Object(LinkByUID, 'template') - object_template = properties.Object(LinkByUID, 'object_template') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + template = properties.Object(LinkByUID, "template") + object_template = properties.Object(LinkByUID, "object_template") attribute_constraints = properties.Optional( properties.List( properties.SpecifiedMixedList( [properties.Object(LinkByUID), properties.Object(BaseBounds)] ) - ), 'attribute_constraints') + ), + "attribute_constraints", + ) type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="local_attribute_and_object", deserializable=False) - - attribute_type = UUID | str | LinkByUID | AttributeTemplate - object_type = UUID | str | LinkByUID | BaseTemplate - constraint_type = tuple[attribute_type, BaseBounds] - - def __init__(self, - name: str, - *, - headers: list[str], - template: attribute_type, - object_template: object_type, - attribute_constraints: list[constraint_type] | None = None, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="local_attribute_and_object", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + template: _AttributeType, + object_template: _ObjectType, + attribute_constraints: list[_ConstraintType] | None = None, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.template = _make_link_by_uid(template) self.object_template = _make_link_by_uid(object_template) - self.attribute_constraints = None if attribute_constraints is None \ - else [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + self.attribute_constraints = _build_attribute_constraints(attribute_constraints) self.type_selector = type_selector class IngredientIdentifierByProcessTemplateAndName( - Serializable['IngredientIdentifierByProcessAndName'], Variable): + Serializable["IngredientIdentifierByProcessAndName"], Variable +): """Ingredient identifier associated with a process template and a name. Parameters @@ -416,24 +439,24 @@ class IngredientIdentifierByProcessTemplateAndName( """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - process_template = properties.Object(LinkByUID, 'process_template') - ingredient_name = properties.String('ingredient_name') - scope = properties.String('scope') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + process_template = properties.Object(LinkByUID, "process_template") + ingredient_name = properties.String("ingredient_name") + scope = properties.String("scope") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="ing_id_by_process_and_name", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, - *, - headers: list[str], - process_template: process_type, - ingredient_name: str, - scope: str, # Note that the default is set server side - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="ing_id_by_process_and_name", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + process_template: _ProcessType, + ingredient_name: str, + scope: str, # Note that the default is set server side + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.process_template = _make_link_by_uid(process_template) @@ -442,7 +465,7 @@ def __init__(self, self.type_selector = type_selector -class IngredientLabelByProcessAndName(Serializable['IngredientLabelByProcessAndName'], Variable): +class IngredientLabelByProcessAndName(Serializable["IngredientLabelByProcessAndName"], Variable): """A boolean variable indicating whether a given label is applied. Matches by process template, ingredient name, and the label string to check. @@ -468,24 +491,24 @@ class IngredientLabelByProcessAndName(Serializable['IngredientLabelByProcessAndN """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - process_template = properties.Object(LinkByUID, 'process_template') - ingredient_name = properties.String('ingredient_name') - label = properties.String('label') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + process_template = properties.Object(LinkByUID, "process_template") + ingredient_name = properties.String("ingredient_name") + label = properties.String("label") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="ing_label_by_process_and_name", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, - *, - headers: list[str], - process_template: process_type, - ingredient_name: str, - label: str, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="ing_label_by_process_and_name", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + process_template: _ProcessType, + ingredient_name: str, + label: str, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.process_template = _make_link_by_uid(process_template) @@ -495,8 +518,8 @@ def __init__(self, class IngredientLabelsSetByProcessAndName( - Serializable['IngredientLabelsSetByProcessAndName'], - Variable): + Serializable["IngredientLabelsSetByProcessAndName"], Variable +): """The set of labels on an ingredient when used in a process. For example, the ingredient "ethanol" might be labeled "solvent", "alcohol" and "VOC". @@ -515,22 +538,22 @@ class IngredientLabelsSetByProcessAndName( """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - process_template = properties.Object(LinkByUID, 'process_template') - ingredient_name = properties.String('ingredient_name') - typ = properties.String('type', - default="ing_labels_set_by_process_and_name", - deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, - *, - headers: list[str], - process_template: process_type, - ingredient_name: str): + name = properties.String("name") + headers = properties.List(properties.String, "headers") + process_template = properties.Object(LinkByUID, "process_template") + ingredient_name = properties.String("ingredient_name") + typ = properties.String( + "type", default="ing_labels_set_by_process_and_name", deserializable=False + ) + + def __init__( + self, + name: str, + *, + headers: list[str], + process_template: _ProcessType, + ingredient_name: str, + ): self.name = name self.headers = headers self.process_template = _make_link_by_uid(process_template) @@ -538,7 +561,8 @@ def __init__(self, class IngredientQuantityByProcessAndName( - Serializable['IngredientQuantityByProcessAndName'], Variable): + Serializable["IngredientQuantityByProcessAndName"], Variable +): """The quantity of an ingredient associated with a process template and a name. Parameters @@ -564,27 +588,28 @@ class IngredientQuantityByProcessAndName( """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - process_template = properties.Object(LinkByUID, 'process_template') - ingredient_name = properties.String('ingredient_name') - quantity_dimension = properties.Enumeration(IngredientQuantityDimension, 'quantity_dimension') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + process_template = properties.Object(LinkByUID, "process_template") + ingredient_name = properties.String("ingredient_name") + quantity_dimension = properties.Enumeration(IngredientQuantityDimension, "quantity_dimension") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="ing_quantity_by_process_and_name", - deserializable=False) + typ = properties.String( + "type", default="ing_quantity_by_process_and_name", deserializable=False + ) unit = properties.Optional(properties.String, "unit") - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, - *, - headers: list[str], - process_template: process_type, - ingredient_name: str, - quantity_dimension: IngredientQuantityDimension, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, - unit: str | None = None): + def __init__( + self, + name: str, + *, + headers: list[str], + process_template: _ProcessType, + ingredient_name: str, + quantity_dimension: IngredientQuantityDimension, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + unit: str | None = None, + ): self.name = name self.headers = headers self.process_template = _make_link_by_uid(process_template) @@ -592,8 +617,9 @@ def __init__(self, self.type_selector = type_selector # Cast to make sure the string is valid - self.quantity_dimension = IngredientQuantityDimension.from_str(quantity_dimension, - exception=True) + self.quantity_dimension = IngredientQuantityDimension.from_str( + quantity_dimension, exception=True + ) if quantity_dimension == IngredientQuantityDimension.ABSOLUTE: if unit is None: @@ -604,7 +630,7 @@ def __init__(self, self.unit = unit -class TerminalMaterialIdentifier(Serializable['TerminalMaterialIdentifier'], Variable): +class TerminalMaterialIdentifier(Serializable["TerminalMaterialIdentifier"], Variable): """A unique identifier of the terminal material of the material history, by scope. Parameters @@ -618,22 +644,18 @@ class TerminalMaterialIdentifier(Serializable['TerminalMaterialIdentifier'], Var """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - scope = properties.String('scope') - typ = properties.String('type', default="root_id", deserializable=False) + name = properties.String("name") + headers = properties.List(properties.String, "headers") + scope = properties.String("scope") + typ = properties.String("type", default="root_id", deserializable=False) - def __init__(self, - name: str, - *, - headers: list[str], - scope: str = CITRINE_SCOPE): + def __init__(self, name: str, *, headers: list[str], scope: str = CITRINE_SCOPE): self.name = name self.headers = headers self.scope = scope -class AttributeInOutput(Serializable['AttributeInOutput'], Variable): +class AttributeInOutput(Serializable["AttributeInOutput"], Variable): """Attribute marked by an attribute template in the trunk of the history tree. The search for an attribute that marks the given attribute template starts at the terminal @@ -676,41 +698,40 @@ class AttributeInOutput(Serializable['AttributeInOutput'], Variable): """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - attribute_template = properties.Object(LinkByUID, 'attribute_template') - process_templates = properties.List(properties.Object(LinkByUID), 'process_templates') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + attribute_template = properties.Object(LinkByUID, "attribute_template") + process_templates = properties.List(properties.Object(LinkByUID), "process_templates") attribute_constraints = properties.Optional( properties.List( properties.SpecifiedMixedList( [properties.Object(LinkByUID), properties.Object(BaseBounds)] ) - ), 'attribute_constraints') + ), + "attribute_constraints", + ) type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="attribute_in_trunk", deserializable=False) - - attribute_type = UUID | str | LinkByUID | AttributeTemplate - process_type = UUID | str | LinkByUID | ProcessTemplate - constraint_type = tuple[attribute_type, BaseBounds] - - def __init__(self, - name: str, - *, - headers: list[str], - attribute_template: attribute_type, - process_templates: list[process_type], - attribute_constraints: list[constraint_type] | None = None, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="attribute_in_trunk", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + attribute_template: _AttributeType, + process_templates: list[_ProcessType], + attribute_constraints: list[_ConstraintType] | None = None, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.attribute_template = _make_link_by_uid(attribute_template) self.process_templates = [_make_link_by_uid(x) for x in process_templates] - self.attribute_constraints = None if attribute_constraints is None \ - else [(_make_link_by_uid(x[0]), x[1]) for x in attribute_constraints] + self.attribute_constraints = _build_attribute_constraints(attribute_constraints) self.type_selector = type_selector -class IngredientIdentifierInOutput(Serializable['IngredientIdentifierInOutput'], Variable): +class IngredientIdentifierInOutput(Serializable["IngredientIdentifierInOutput"], Variable): """Ingredient identifier in the trunk of a material history tree. The search for an ingredient starts at the terminal of the material history tree and @@ -756,23 +777,24 @@ class IngredientIdentifierInOutput(Serializable['IngredientIdentifierInOutput'], """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - ingredient_name = properties.String('ingredient_name') - process_templates = properties.List(properties.Object(LinkByUID), 'process_templates') - scope = properties.String('scope') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + ingredient_name = properties.String("ingredient_name") + process_templates = properties.List(properties.Object(LinkByUID), "process_templates") + scope = properties.String("scope") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="ing_id_in_output", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, *, - headers: list[str], - ingredient_name: str, - process_templates: list[process_type], - scope: str = CITRINE_SCOPE, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="ing_id_in_output", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + ingredient_name: str, + process_templates: list[_ProcessType], + scope: str = CITRINE_SCOPE, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.ingredient_name = ingredient_name @@ -781,7 +803,7 @@ def __init__(self, self.type_selector = type_selector -class IngredientLabelsSetInOutput(Serializable['IngredientLabelsSetInOutput'], Variable): +class IngredientLabelsSetInOutput(Serializable["IngredientLabelsSetInOutput"], Variable): """The set of labels on an ingredient in the trunk of a material history tree. The search for an ingredient starts at the terminal of the material history tree and proceeds @@ -824,26 +846,27 @@ class IngredientLabelsSetInOutput(Serializable['IngredientLabelsSetInOutput'], V """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - process_templates = properties.List(properties.Object(LinkByUID), 'process_templates') - ingredient_name = properties.String('ingredient_name') - typ = properties.String('type', default="ing_label_set_in_output", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, *, - headers: list[str], - process_templates: list[process_type], - ingredient_name: str): + name = properties.String("name") + headers = properties.List(properties.String, "headers") + process_templates = properties.List(properties.Object(LinkByUID), "process_templates") + ingredient_name = properties.String("ingredient_name") + typ = properties.String("type", default="ing_label_set_in_output", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + process_templates: list[_ProcessType], + ingredient_name: str, + ): self.name = name self.headers = headers self.process_templates = [_make_link_by_uid(x) for x in process_templates] self.ingredient_name = ingredient_name -class IngredientQuantityInOutput(Serializable['IngredientQuantityInOutput'], Variable): +class IngredientQuantityInOutput(Serializable["IngredientQuantityInOutput"], Variable): """Ingredient quantity in the trunk of a material history tree. The search for an ingredient starts at the terminal of the material history tree and proceeds @@ -897,25 +920,26 @@ class IngredientQuantityInOutput(Serializable['IngredientQuantityInOutput'], Var """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - ingredient_name = properties.String('ingredient_name') - quantity_dimension = properties.Enumeration(IngredientQuantityDimension, 'quantity_dimension') - process_templates = properties.List(properties.Object(LinkByUID), 'process_templates') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + ingredient_name = properties.String("ingredient_name") + quantity_dimension = properties.Enumeration(IngredientQuantityDimension, "quantity_dimension") + process_templates = properties.List(properties.Object(LinkByUID), "process_templates") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") unit = properties.Optional(properties.String, "unit") - typ = properties.String('type', default="ing_quantity_in_output", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, *, - headers: list[str], - ingredient_name: str, - quantity_dimension: IngredientQuantityDimension, - process_templates: list[process_type], - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, - unit: str | None = None): + typ = properties.String("type", default="ing_quantity_in_output", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + ingredient_name: str, + quantity_dimension: IngredientQuantityDimension, + process_templates: list[_ProcessType], + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + unit: str | None = None, + ): self.name = name self.headers = headers self.ingredient_name = ingredient_name @@ -923,8 +947,9 @@ def __init__(self, self.type_selector = type_selector # Cast to make sure the string is valid - self.quantity_dimension = IngredientQuantityDimension.from_str(quantity_dimension, - exception=True) + self.quantity_dimension = IngredientQuantityDimension.from_str( + quantity_dimension, exception=True + ) if quantity_dimension == IngredientQuantityDimension.ABSOLUTE: if unit is None: @@ -935,7 +960,7 @@ def __init__(self, self.unit = unit -class LocalIngredientIdentifier(Serializable['LocalIngredientIdentifier'], Variable): +class LocalIngredientIdentifier(Serializable["LocalIngredientIdentifier"], Variable): """Ingredient identifier for the root process of a material history tree. Get ingredient identifier by name. Stop traversal when encountering any ingredient. @@ -960,21 +985,22 @@ class LocalIngredientIdentifier(Serializable['LocalIngredientIdentifier'], Varia """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - ingredient_name = properties.String('ingredient_name') - scope = properties.String('scope') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + ingredient_name = properties.String("ingredient_name") + scope = properties.String("scope") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") - typ = properties.String('type', default="local_ing_id", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, *, - headers: list[str], - ingredient_name: str, - scope: str = CITRINE_SCOPE, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN): + typ = properties.String("type", default="local_ing_id", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + ingredient_name: str, + scope: str = CITRINE_SCOPE, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + ): self.name = name self.headers = headers self.ingredient_name = ingredient_name @@ -982,7 +1008,7 @@ def __init__(self, self.type_selector = type_selector -class LocalIngredientLabelsSet(Serializable['LocalIngredientLabelsSet'], Variable): +class LocalIngredientLabelsSet(Serializable["LocalIngredientLabelsSet"], Variable): """The set of labels on an ingredient for the root process of a material history tree. Define a variable contains the set of labels that is present on the ingredient @@ -1003,23 +1029,18 @@ class LocalIngredientLabelsSet(Serializable['LocalIngredientLabelsSet'], Variabl """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - ingredient_name = properties.String('ingredient_name') - typ = properties.String('type', default="local_ing_label_set", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate + name = properties.String("name") + headers = properties.List(properties.String, "headers") + ingredient_name = properties.String("ingredient_name") + typ = properties.String("type", default="local_ing_label_set", deserializable=False) - def __init__(self, - name: str, *, - headers: list[str], - ingredient_name: str): + def __init__(self, name: str, *, headers: list[str], ingredient_name: str): self.name = name self.headers = headers self.ingredient_name = ingredient_name -class LocalIngredientQuantity(Serializable['LocalIngredientQuantity'], Variable): +class LocalIngredientQuantity(Serializable["LocalIngredientQuantity"], Variable): """The quantity of an ingredient for the root process of a material history tree. Get ingredient quantity by name. Stop traversal when encountering any ingredient. @@ -1049,31 +1070,33 @@ class LocalIngredientQuantity(Serializable['LocalIngredientQuantity'], Variable) """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - ingredient_name = properties.String('ingredient_name') - quantity_dimension = properties.Enumeration(IngredientQuantityDimension, 'quantity_dimension') + name = properties.String("name") + headers = properties.List(properties.String, "headers") + ingredient_name = properties.String("ingredient_name") + quantity_dimension = properties.Enumeration(IngredientQuantityDimension, "quantity_dimension") type_selector = properties.Enumeration(DataObjectTypeSelector, "type_selector") unit = properties.Optional(properties.String, "unit") - typ = properties.String('type', default="local_ing_quantity", deserializable=False) - - process_type = UUID | str | LinkByUID | ProcessTemplate - - def __init__(self, - name: str, *, - headers: list[str], - ingredient_name: str, - quantity_dimension: IngredientQuantityDimension, - type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, - unit: str | None = None): + typ = properties.String("type", default="local_ing_quantity", deserializable=False) + + def __init__( + self, + name: str, + *, + headers: list[str], + ingredient_name: str, + quantity_dimension: IngredientQuantityDimension, + type_selector: DataObjectTypeSelector = DataObjectTypeSelector.PREFER_RUN, + unit: str | None = None, + ): self.name = name self.headers = headers self.ingredient_name = ingredient_name self.type_selector = type_selector # Cast to make sure the string is valid - self.quantity_dimension = IngredientQuantityDimension.from_str(quantity_dimension, - exception=True) + self.quantity_dimension = IngredientQuantityDimension.from_str( + quantity_dimension, exception=True + ) if quantity_dimension == IngredientQuantityDimension.ABSOLUTE: if unit is None: @@ -1084,7 +1107,7 @@ def __init__(self, self.unit = unit -class XOR(Serializable['XOR'], Variable): +class XOR(Serializable["XOR"], Variable): """Logical exclusive OR for GEM table variables. This variable combines the results of 2 or more variables into a single variable according to @@ -1113,10 +1136,10 @@ class XOR(Serializable['XOR'], Variable): """ - name = properties.String('name') - headers = properties.List(properties.String, 'headers') - variables = properties.List(properties.Object(Variable), 'variables') - typ = properties.String('type', default="xor", deserializable=False) + name = properties.String("name") + headers = properties.List(properties.String, "headers") + variables = properties.List(properties.Object(Variable), "variables") + typ = properties.String("type", default="xor", deserializable=False) def __init__(self, name, *, headers, variables): self.name = name diff --git a/src/citrine/informatics/catalyst/assistant.py b/src/citrine/informatics/catalyst/assistant.py index 7cfbfe975..3a89b1beb 100644 --- a/src/citrine/informatics/catalyst/assistant.py +++ b/src/citrine/informatics/catalyst/assistant.py @@ -1,8 +1,8 @@ -from citrine.informatics.predictors import GraphPredictor from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable from citrine.informatics.catalyst.language_model import LanguageModelChoice +from citrine.informatics.predictors import GraphPredictor class AssistantRequest(Serializable["AssistantRequest"]): @@ -11,14 +11,20 @@ class AssistantRequest(Serializable["AssistantRequest"]): question = properties.String("question") predictor = properties.Object(GraphPredictor, "config") temperature = properties.Optional(properties.Float, "temperature", default=0.0) - language_model = properties.Optional(properties.Enumeration(LanguageModelChoice), - "language_model", default=LanguageModelChoice.GPT_4) - - def __init__(self, *, - question: str, - predictor: GraphPredictor, - temperature: float | None = 0.0, - language_model: LanguageModelChoice | None = LanguageModelChoice.GPT_4): + language_model = properties.Optional( + properties.Enumeration(LanguageModelChoice), + "language_model", + default=LanguageModelChoice.GPT_4, + ) + + def __init__( + self, + *, + question: str, + predictor: GraphPredictor, + temperature: float | None = 0.0, + language_model: LanguageModelChoice | None = LanguageModelChoice.GPT_4, + ): self.question = question self.predictor = predictor self.temperature = temperature @@ -34,22 +40,22 @@ class AssistantResponse(PolymorphicSerializable["AssistantResponse"]): """The parent type for all Model Assistant responses.""" @classmethod - def get_type(cls, data) -> type['AssistantResponse']: + def get_type(cls, data) -> type["AssistantResponse"]: """Return the subtype.""" type_dict = { "message": AssistantResponseMessage, "modified-config": AssistantResponseConfig, "unsupported": AssistantResponseUnsupported, "input-error": AssistantResponseInputErrors, - "exec-error": AssistantResponseExecError + "exec-error": AssistantResponseExecError, } - typ = type_dict.get(data['type']) + typ = type_dict.get(data["type"]) if typ is not None: return typ else: raise ValueError( - f'{data["type"]} is not a valid assistant response type. ' - f'Must be in {type_dict.keys()}.' + f"{data['type']} is not a valid assistant response type. " + f"Must be in {type_dict.keys()}." ) @@ -70,8 +76,9 @@ def _pre_build(cls, data): return data -class AssistantResponseUnsupported(Serializable["AssistantResponseUnsupported"], - AssistantResponse): +class AssistantResponseUnsupported( + Serializable["AssistantResponseUnsupported"], AssistantResponse +): """A successful model assistant invocation, but for an unsupported query. This will cover any user query which the model assistant could not map to a functionality it @@ -91,8 +98,9 @@ class AssistantResponseInputError(Serializable["AssistantResponseInputError"], A error = properties.String("error") -class AssistantResponseInputErrors(Serializable["AssistantResponseInputErrors"], - AssistantResponse): +class AssistantResponseInputErrors( + Serializable["AssistantResponseInputErrors"], AssistantResponse +): """A failed model assistant invocation, due to malformed input. This should only happen if there's some field omitted by the client, or one of its values is diff --git a/src/citrine/informatics/constraints/categorical_constraint.py b/src/citrine/informatics/constraints/categorical_constraint.py index 48c8ae864..09f5b7d32 100644 --- a/src/citrine/informatics/constraints/categorical_constraint.py +++ b/src/citrine/informatics/constraints/categorical_constraint.py @@ -2,10 +2,10 @@ from citrine._serialization.serializable import Serializable from citrine.informatics.constraints.constraint import Constraint -__all__ = ['AcceptableCategoriesConstraint'] +__all__ = ["AcceptableCategoriesConstraint"] -class AcceptableCategoriesConstraint(Serializable['AcceptableCategoriesConstraint'], Constraint): +class AcceptableCategoriesConstraint(Serializable["AcceptableCategoriesConstraint"], Constraint): """ A constraint on a categorical material attribute to be in a set of acceptable values. @@ -18,16 +18,13 @@ class AcceptableCategoriesConstraint(Serializable['AcceptableCategoriesConstrain """ - descriptor_key = properties.String('descriptor_key') - acceptable_categories = properties.List(properties.String(), 'acceptable_classes') - typ = properties.String('type', default='AcceptableCategoriesConstraint') + descriptor_key = properties.String("descriptor_key") + acceptable_categories = properties.List(properties.String(), "acceptable_classes") + typ = properties.String("type", default="AcceptableCategoriesConstraint") - def __init__(self, - *, - descriptor_key: str, - acceptable_categories: list[str]): + def __init__(self, *, descriptor_key: str, acceptable_categories: list[str]): self.descriptor_key = descriptor_key self.acceptable_categories = acceptable_categories def __str__(self): - return ''.format(self.descriptor_key) + return f"" diff --git a/src/citrine/informatics/constraints/constraint.py b/src/citrine/informatics/constraints/constraint.py index 057978555..2cd1dc68d 100644 --- a/src/citrine/informatics/constraints/constraint.py +++ b/src/citrine/informatics/constraints/constraint.py @@ -2,10 +2,10 @@ from citrine._serialization.polymorphic_serializable import PolymorphicSerializable -__all__ = ['Constraint'] +__all__ = ["Constraint"] -class Constraint(PolymorphicSerializable['Constraint']): +class Constraint(PolymorphicSerializable["Constraint"]): """A Citrine Constraint places restrictions on a design space. Abstract type that returns the proper type given a serialized dict. @@ -17,20 +17,21 @@ class Constraint(PolymorphicSerializable['Constraint']): @classmethod def get_type(cls, data): """Return the subtype.""" + from .categorical_constraint import AcceptableCategoriesConstraint from .ingredient_count_constraint import IngredientCountConstraint from .ingredient_fraction_constraint import IngredientFractionConstraint + from .ingredient_ratio_constraint import IngredientRatioConstraint + from .integer_range_constraint import IntegerRangeConstraint from .label_fraction_constraint import LabelFractionConstraint from .scalar_range_constraint import ScalarRangeConstraint - from .integer_range_constraint import IntegerRangeConstraint - from .categorical_constraint import AcceptableCategoriesConstraint - from .ingredient_ratio_constraint import IngredientRatioConstraint + return { - 'Categorical': AcceptableCategoriesConstraint, # Kept for backwards compatibility. - 'AcceptableCategoriesConstraint': AcceptableCategoriesConstraint, - 'IngredientCountConstraint': IngredientCountConstraint, - 'IngredientFractionConstraint': IngredientFractionConstraint, - 'LabelFractionConstraint': LabelFractionConstraint, - 'ScalarRange': ScalarRangeConstraint, - 'IntegerRange': IntegerRangeConstraint, - 'IngredientRatio': IngredientRatioConstraint, - }[data['type']] + "Categorical": AcceptableCategoriesConstraint, # Kept for backwards compatibility. + "AcceptableCategoriesConstraint": AcceptableCategoriesConstraint, + "IngredientCountConstraint": IngredientCountConstraint, + "IngredientFractionConstraint": IngredientFractionConstraint, + "LabelFractionConstraint": LabelFractionConstraint, + "ScalarRange": ScalarRangeConstraint, + "IntegerRange": IntegerRangeConstraint, + "IngredientRatio": IngredientRatioConstraint, + }[data["type"]] diff --git a/src/citrine/informatics/constraints/ingredient_count_constraint.py b/src/citrine/informatics/constraints/ingredient_count_constraint.py index ee261cfff..648d47700 100644 --- a/src/citrine/informatics/constraints/ingredient_count_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_count_constraint.py @@ -3,10 +3,10 @@ from citrine.informatics.constraints.constraint import Constraint from citrine.informatics.descriptors import FormulationDescriptor -__all__ = ['IngredientCountConstraint'] +__all__ = ["IngredientCountConstraint"] -class IngredientCountConstraint(Serializable['IngredientCountConstraint'], Constraint): +class IngredientCountConstraint(Serializable["IngredientCountConstraint"], Constraint): """Represents a constraint on the total number of ingredients in a formulation. Parameters @@ -24,17 +24,20 @@ class IngredientCountConstraint(Serializable['IngredientCountConstraint'], Const """ - formulation_descriptor = properties.Object(FormulationDescriptor, 'formulation_descriptor') - min = properties.Integer('min') - max = properties.Integer('max') - label = properties.Optional(properties.String, 'label') - typ = properties.String('type', default='IngredientCountConstraint') - - def __init__(self, *, - formulation_descriptor: FormulationDescriptor, - min: int, - max: int, - label: str | None = None): + formulation_descriptor = properties.Object(FormulationDescriptor, "formulation_descriptor") + min = properties.Integer("min") + max = properties.Integer("max") + label = properties.Optional(properties.String, "label") + typ = properties.String("type", default="IngredientCountConstraint") + + def __init__( + self, + *, + formulation_descriptor: FormulationDescriptor, + min: int, + max: int, + label: str | None = None, + ): self.formulation_descriptor: FormulationDescriptor = formulation_descriptor self.min: int = min self.max: int = max diff --git a/src/citrine/informatics/constraints/ingredient_fraction_constraint.py b/src/citrine/informatics/constraints/ingredient_fraction_constraint.py index d289b36ec..41c1b0dde 100644 --- a/src/citrine/informatics/constraints/ingredient_fraction_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_fraction_constraint.py @@ -3,10 +3,10 @@ from citrine.informatics.constraints.constraint import Constraint from citrine.informatics.descriptors import FormulationDescriptor -__all__ = ['IngredientFractionConstraint'] +__all__ = ["IngredientFractionConstraint"] -class IngredientFractionConstraint(Serializable['IngredientFractionConstraint'], Constraint): +class IngredientFractionConstraint(Serializable["IngredientFractionConstraint"], Constraint): """Represents a constraint on an ingredient fraction in a formulation. Parameters @@ -27,19 +27,22 @@ class IngredientFractionConstraint(Serializable['IngredientFractionConstraint'], """ - formulation_descriptor = properties.Object(FormulationDescriptor, 'formulation_descriptor') - ingredient = properties.String('ingredient') - min = properties.Optional(properties.Float, 'min') - max = properties.Optional(properties.Float, 'max') - is_required = properties.Boolean('is_required') - typ = properties.String('type', default='IngredientFractionConstraint') - - def __init__(self, *, - formulation_descriptor: FormulationDescriptor, - ingredient: str, - min: float, - max: float, - is_required: bool = True): + formulation_descriptor = properties.Object(FormulationDescriptor, "formulation_descriptor") + ingredient = properties.String("ingredient") + min = properties.Optional(properties.Float, "min") + max = properties.Optional(properties.Float, "max") + is_required = properties.Boolean("is_required") + typ = properties.String("type", default="IngredientFractionConstraint") + + def __init__( + self, + *, + formulation_descriptor: FormulationDescriptor, + ingredient: str, + min: float, + max: float, + is_required: bool = True, + ): self.formulation_descriptor: FormulationDescriptor = formulation_descriptor self.ingredient: str = ingredient self.min: float = min diff --git a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py index d0dbaabd0..8a5528c04 100644 --- a/src/citrine/informatics/constraints/ingredient_ratio_constraint.py +++ b/src/citrine/informatics/constraints/ingredient_ratio_constraint.py @@ -3,10 +3,10 @@ from citrine.informatics.constraints.constraint import Constraint from citrine.informatics.descriptors import FormulationDescriptor -__all__ = ['IngredientRatioConstraint'] +__all__ = ["IngredientRatioConstraint"] -class IngredientRatioConstraint(Serializable['IngredientRatioConstraint'], Constraint): +class IngredientRatioConstraint(Serializable["IngredientRatioConstraint"], Constraint): """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 @@ -34,37 +34,43 @@ class IngredientRatioConstraint(Serializable['IngredientRatioConstraint'], Const """ - formulation_descriptor = properties.Object(FormulationDescriptor, 'formulation_descriptor') - min = properties.Float('min') - max = properties.Float('max') + formulation_descriptor = properties.Object(FormulationDescriptor, "formulation_descriptor") + min = properties.Float("min") + max = properties.Float("max") # The backend provides ingredients and labels as dictionaries, but presently only allows one # between them. To clarify customer interaction, we only allow a single one of each to be set. # Since our serde library doesn't allow extracting from a dict with unknown keys, we do it by # hiding the dictionaries and exposing properties. _ingredients = properties.Mapping( - properties.String, properties.Float, 'ingredients', default={}) - _labels = properties.Mapping(properties.String, properties.Float, 'labels', default={}) + properties.String, properties.Float, "ingredients", default={} + ) + _labels = properties.Mapping(properties.String, properties.Float, "labels", default={}) # The backend provides basis ingredients and basis labels as a dictionary from the key to a # multiplier. However, for ingredient ratio constraints, the multiplier in the denominator # should always be one, so we can't allow users to enter it. We need to use properties for this # behavior. _basis_ingredients = properties.Mapping( - properties.String, properties.Float, 'basis_ingredients', default={}) + properties.String, properties.Float, "basis_ingredients", default={} + ) _basis_labels = properties.Mapping( - properties.String, properties.Float, 'basis_labels', default={}) - - typ = properties.String('type', default='IngredientRatio') - - def __init__(self, *, - formulation_descriptor: FormulationDescriptor, - min: float, - max: float, - ingredient: tuple[str, float] | None = None, - label: tuple[str, float] | None = None, - basis_ingredients: set[str] = set(), - basis_labels: set[str] = set()): + properties.String, properties.Float, "basis_labels", default={} + ) + + typ = properties.String("type", default="IngredientRatio") + + def __init__( + self, + *, + formulation_descriptor: FormulationDescriptor, + min: float, + max: float, + ingredient: tuple[str, float] | None = None, + label: tuple[str, float] | None = None, + basis_ingredients: set[str] = set(), + basis_labels: set[str] = set(), + ): self.formulation_descriptor = formulation_descriptor self.min = min self.max = max diff --git a/src/citrine/informatics/constraints/integer_range_constraint.py b/src/citrine/informatics/constraints/integer_range_constraint.py index cde368925..9ec381dbf 100644 --- a/src/citrine/informatics/constraints/integer_range_constraint.py +++ b/src/citrine/informatics/constraints/integer_range_constraint.py @@ -2,10 +2,10 @@ from citrine._serialization.serializable import Serializable from citrine.informatics.constraints.constraint import Constraint -__all__ = ['IntegerRangeConstraint'] +__all__ = ["IntegerRangeConstraint"] -class IntegerRangeConstraint(Serializable['IntegerRangeConstraint'], Constraint): +class IntegerRangeConstraint(Serializable["IntegerRangeConstraint"], Constraint): """[ALPHA] Represents an inequality constraint on an integer-valued material attribute. Warning: IntegerRangeConstraints are not fully supported by the Citrine Platform web interface @@ -26,18 +26,21 @@ class IntegerRangeConstraint(Serializable['IntegerRangeConstraint'], Constraint) """ - descriptor_key = properties.String('descriptor_key') - lower_bound = properties.Optional(properties.Float, 'min') - upper_bound = properties.Optional(properties.Float, 'max') - typ = properties.String('type', default='IntegerRange') - - def __init__(self, *, - descriptor_key: str, - lower_bound: int | None = None, - upper_bound: int | None = None): + descriptor_key = properties.String("descriptor_key") + lower_bound = properties.Optional(properties.Float, "min") + upper_bound = properties.Optional(properties.Float, "max") + typ = properties.String("type", default="IntegerRange") + + def __init__( + self, + *, + descriptor_key: str, + lower_bound: int | None = None, + upper_bound: int | None = None, + ): self.descriptor_key = descriptor_key self.lower_bound = lower_bound self.upper_bound = upper_bound def __str__(self): - return ''.format(self.descriptor_key) + return f"" diff --git a/src/citrine/informatics/constraints/label_fraction_constraint.py b/src/citrine/informatics/constraints/label_fraction_constraint.py index 816d1ffd0..e5cf53376 100644 --- a/src/citrine/informatics/constraints/label_fraction_constraint.py +++ b/src/citrine/informatics/constraints/label_fraction_constraint.py @@ -3,10 +3,10 @@ from citrine.informatics.constraints.constraint import Constraint from citrine.informatics.descriptors import FormulationDescriptor -__all__ = ['LabelFractionConstraint'] +__all__ = ["LabelFractionConstraint"] -class LabelFractionConstraint(Serializable['LabelFractionConstraint'], Constraint): +class LabelFractionConstraint(Serializable["LabelFractionConstraint"], Constraint): """Represents a constraint on the total amount of ingredients with a given label. Parameters @@ -27,19 +27,22 @@ class LabelFractionConstraint(Serializable['LabelFractionConstraint'], Constrain """ - formulation_descriptor = properties.Object(FormulationDescriptor, 'formulation_descriptor') - label = properties.String('label') - min = properties.Optional(properties.Float, 'min') - max = properties.Optional(properties.Float, 'max') - is_required = properties.Boolean('is_required') - typ = properties.String('type', default='LabelFractionConstraint') - - def __init__(self, *, - formulation_descriptor: FormulationDescriptor, - label: str, - min: float, - max: float, - is_required: bool = True): + formulation_descriptor = properties.Object(FormulationDescriptor, "formulation_descriptor") + label = properties.String("label") + min = properties.Optional(properties.Float, "min") + max = properties.Optional(properties.Float, "max") + is_required = properties.Boolean("is_required") + typ = properties.String("type", default="LabelFractionConstraint") + + def __init__( + self, + *, + formulation_descriptor: FormulationDescriptor, + label: str, + min: float, + max: float, + is_required: bool = True, + ): self.formulation_descriptor: FormulationDescriptor = formulation_descriptor self.label: str = label self.min: float = min diff --git a/src/citrine/informatics/constraints/scalar_range_constraint.py b/src/citrine/informatics/constraints/scalar_range_constraint.py index 141c2412e..7af153991 100644 --- a/src/citrine/informatics/constraints/scalar_range_constraint.py +++ b/src/citrine/informatics/constraints/scalar_range_constraint.py @@ -2,10 +2,10 @@ from citrine._serialization.serializable import Serializable from citrine.informatics.constraints.constraint import Constraint -__all__ = ['ScalarRangeConstraint'] +__all__ = ["ScalarRangeConstraint"] -class ScalarRangeConstraint(Serializable['ScalarRangeConstraint'], Constraint): +class ScalarRangeConstraint(Serializable["ScalarRangeConstraint"], Constraint): """Represents an inequality constraint on a real-valued material attribute. Parameters @@ -23,19 +23,22 @@ class ScalarRangeConstraint(Serializable['ScalarRangeConstraint'], Constraint): """ - descriptor_key = properties.String('descriptor_key') - lower_bound = properties.Optional(properties.Float, 'min') - upper_bound = properties.Optional(properties.Float, 'max') - lower_inclusive = properties.Boolean('min_inclusive') - upper_inclusive = properties.Boolean('max_inclusive') - typ = properties.String('type', default='ScalarRange') - - def __init__(self, *, - descriptor_key: str, - lower_bound: float | None = None, - upper_bound: float | None = None, - lower_inclusive: bool | None = None, - upper_inclusive: bool | None = None): + descriptor_key = properties.String("descriptor_key") + lower_bound = properties.Optional(properties.Float, "min") + upper_bound = properties.Optional(properties.Float, "max") + lower_inclusive = properties.Boolean("min_inclusive") + upper_inclusive = properties.Boolean("max_inclusive") + typ = properties.String("type", default="ScalarRange") + + def __init__( + self, + *, + descriptor_key: str, + lower_bound: float | None = None, + upper_bound: float | None = None, + lower_inclusive: bool | None = None, + upper_inclusive: bool | None = None, + ): self.descriptor_key = descriptor_key self.lower_bound = lower_bound @@ -52,4 +55,4 @@ def __init__(self, *, self.upper_inclusive = upper_inclusive def __str__(self): - return ''.format(self.descriptor_key) + return f"" diff --git a/src/citrine/informatics/data_sources.py b/src/citrine/informatics/data_sources.py index f9cf1d927..bfa52b3c5 100644 --- a/src/citrine/informatics/data_sources.py +++ b/src/citrine/informatics/data_sources.py @@ -1,4 +1,5 @@ """Tools for working with Descriptors.""" + from abc import abstractmethod from uuid import UUID @@ -7,14 +8,10 @@ from citrine._serialization.serializable import Serializable from citrine.resources.gemtables import GemTable -__all__ = [ - 'DataSource', - 'GemTableDataSource', - 'SnapshotDataSource', -] +__all__ = ["DataSource", "GemTableDataSource", "SnapshotDataSource"] -class DataSource(PolymorphicSerializable['DataSource']): +class DataSource(PolymorphicSerializable["DataSource"]): """A source of data for the AI engine. Data source provides a polymorphic interface for specifying different kinds of data as the @@ -66,7 +63,7 @@ def to_data_source_id(self) -> str: """Generate the data_source_id for this DataSource.""" -class GemTableDataSource(Serializable['GemTableDataSource'], DataSource): +class GemTableDataSource(Serializable["GemTableDataSource"], DataSource): """A data source based on a GEM Table hosted on the data platform. Parameters @@ -79,16 +76,13 @@ class GemTableDataSource(Serializable['GemTableDataSource'], DataSource): """ - typ = properties.String('type', default='hosted_table_data_source', deserializable=False) + typ = properties.String("type", default="hosted_table_data_source", deserializable=False) table_id = properties.UUID("table_id") table_version = properties.Integer("table_version") _data_source_type = "gemd" - def __init__(self, - *, - table_id: UUID, - table_version: int | str): + def __init__(self, *, table_id: UUID, table_version: int | str): self.table_id: UUID = table_id self.table_version: int | str = table_version @@ -113,7 +107,7 @@ def from_gemtable(cls, table: GemTable) -> "GemTableDataSource": return GemTableDataSource(table_id=table.uid, table_version=table.version) -class SnapshotDataSource(Serializable['SnapshotDataSource'], DataSource): +class SnapshotDataSource(Serializable["SnapshotDataSource"], DataSource): """A reference to a data source based on a Snapshot on the data platform. Parameters @@ -123,7 +117,7 @@ class SnapshotDataSource(Serializable['SnapshotDataSource'], DataSource): """ - typ = properties.String('type', default='snapshot_data_source', deserializable=False) + typ = properties.String("type", default="snapshot_data_source", deserializable=False) snapshot_id = properties.UUID("snapshot_id") _data_source_type = "snapshot" diff --git a/src/citrine/informatics/descriptors.py b/src/citrine/informatics/descriptors.py index fef37ae02..1c303a85a 100644 --- a/src/citrine/informatics/descriptors.py +++ b/src/citrine/informatics/descriptors.py @@ -2,18 +2,20 @@ from gemd.enumeration.base_enumeration import BaseEnumeration -from citrine._serialization.serializable import Serializable -from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties +from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable -__all__ = ['Descriptor', - 'RealDescriptor', - 'IntegerDescriptor', - 'ChemicalFormulaDescriptor', - 'MolecularStructureDescriptor', - 'CategoricalDescriptor', - 'FormulationDescriptor', - 'FormulationKey'] +__all__ = [ + "CategoricalDescriptor", + "ChemicalFormulaDescriptor", + "Descriptor", + "FormulationDescriptor", + "FormulationKey", + "IntegerDescriptor", + "MolecularStructureDescriptor", + "RealDescriptor", +] class FormulationKey(BaseEnumeration): @@ -28,13 +30,13 @@ class FormulationKey(BaseEnumeration): FLAT = "Flat Formulation" -class Descriptor(PolymorphicSerializable['Descriptor']): +class Descriptor(PolymorphicSerializable["Descriptor"]): """A Descriptor describes the range of values that a quantity can take on. Abstract type that returns the proper type given a serialized dict. """ - key = properties.String('descriptor_key') + key = properties.String("descriptor_key") @classmethod def get_type(cls, data) -> type[Serializable]: @@ -67,14 +69,14 @@ def _equals(self, other, attrs): [self.__getattribute__(key) for key in attrs] try: - return all([ - self.__getattribute__(key) == other.__getattribute__(key) for key in attrs - ]) + return all( + [self.__getattribute__(key) == other.__getattribute__(key) for key in attrs] + ) except AttributeError: return False -class RealDescriptor(Serializable['RealDescriptor'], Descriptor): +class RealDescriptor(Serializable["RealDescriptor"], Descriptor): """A descriptor to hold real-valued numbers. Parameters @@ -90,34 +92,28 @@ class RealDescriptor(Serializable['RealDescriptor'], Descriptor): """ - lower_bound = properties.Float('lower_bound') - upper_bound = properties.Float('upper_bound') - units = properties.String('units', default='') - typ = properties.String('type', default='Real', deserializable=False) + lower_bound = properties.Float("lower_bound") + upper_bound = properties.Float("upper_bound") + units = properties.String("units", default="") + typ = properties.String("type", default="Real", deserializable=False) def __eq__(self, other): return self._equals(other, ["key", "lower_bound", "upper_bound", "units", "typ"]) - def __init__(self, - key: str, - *, - lower_bound: float, - upper_bound: float, - units: str): + def __init__(self, key: str, *, lower_bound: float, upper_bound: float, units: str): self.key: str = key self.lower_bound: float = lower_bound self.upper_bound: float = upper_bound self.units = units def __str__(self): - return "".format(self.key) + return f"" def __repr__(self): - return "RealDescriptor({}, {}, {}, {})".format( - self.key, self.lower_bound, self.upper_bound, self.units) + return f"RealDescriptor({self.key}, {self.lower_bound}, {self.upper_bound}, {self.units})" -class IntegerDescriptor(Serializable['IntegerDescriptor'], Descriptor): +class IntegerDescriptor(Serializable["IntegerDescriptor"], Descriptor): """[ALPHA] A descriptor to hold integer-valued numbers. Warning: IntegerDescriptors are not fully supported by the Citrine Platform web interface @@ -134,9 +130,9 @@ class IntegerDescriptor(Serializable['IntegerDescriptor'], Descriptor): """ - lower_bound = properties.Integer('lower_bound') - upper_bound = properties.Integer('upper_bound') - typ = properties.String('type', default='Integer', deserializable=False) + lower_bound = properties.Integer("lower_bound") + upper_bound = properties.Integer("upper_bound") + typ = properties.String("type", default="Integer", deserializable=False) def __eq__(self, other): return self._equals(other, ["key", "lower_bound", "upper_bound", "typ"]) @@ -147,13 +143,13 @@ def __init__(self, key: str, *, lower_bound: int, upper_bound: int): self.upper_bound: int = upper_bound def __str__(self): - return "".format(self.key) + return f"" def __repr__(self): - return "IntegerDescriptor({}, {}, {})".format(self.key, self.lower_bound, self.upper_bound) + return f"IntegerDescriptor({self.key}, {self.lower_bound}, {self.upper_bound})" -class ChemicalFormulaDescriptor(Serializable['ChemicalFormulaDescriptor'], Descriptor): +class ChemicalFormulaDescriptor(Serializable["ChemicalFormulaDescriptor"], Descriptor): """Captures domain-specific context about a stoichiometric chemical formula. Parameters @@ -163,7 +159,7 @@ class ChemicalFormulaDescriptor(Serializable['ChemicalFormulaDescriptor'], Descr """ - typ = properties.String('type', default='Inorganic', deserializable=False) + typ = properties.String("type", default="Inorganic", deserializable=False) def __eq__(self, other): return self._equals(other, ["key", "typ"]) @@ -172,13 +168,13 @@ def __init__(self, key: str): self.key: str = key def __str__(self): - return "".format(self.key) + return f"" def __repr__(self): - return "ChemicalFormulaDescriptor(key={})".format(self.key) + return f"ChemicalFormulaDescriptor(key={self.key})" -class MolecularStructureDescriptor(Serializable['MolecularStructureDescriptor'], Descriptor): +class MolecularStructureDescriptor(Serializable["MolecularStructureDescriptor"], Descriptor): """ Material descriptor for an organic molecule. @@ -191,7 +187,7 @@ class MolecularStructureDescriptor(Serializable['MolecularStructureDescriptor'], """ - typ = properties.String('type', default='Organic', deserializable=False) + typ = properties.String("type", default="Organic", deserializable=False) def __eq__(self, other): return self._equals(other, ["key", "typ"]) @@ -200,13 +196,13 @@ def __init__(self, key: str): self.key: str = key def __str__(self): - return "".format(self.key) + return f"" def __repr__(self): - return "MolecularStructureDescriptor(key={})".format(self.key) + return f"MolecularStructureDescriptor(key={self.key})" -class CategoricalDescriptor(Serializable['CategoricalDescriptor'], Descriptor): +class CategoricalDescriptor(Serializable["CategoricalDescriptor"], Descriptor): """A descriptor to hold categorical variables. An exhaustive list of categorical values may be supplied. @@ -220,8 +216,8 @@ class CategoricalDescriptor(Serializable['CategoricalDescriptor'], Descriptor): """ - typ = properties.String('type', default='Categorical', deserializable=False) - categories = properties.Set(properties.String, 'descriptor_values') + typ = properties.String("type", default="Categorical", deserializable=False) + categories = properties.Set(properties.String, "descriptor_values") def __eq__(self, other): return self._equals(other, ["key", "categories", "typ"]) @@ -234,13 +230,13 @@ def __init__(self, key: str, *, categories: set[str]): self.categories: set[str] = categories def __str__(self): - return "".format(self.key) + return f"" def __repr__(self): - return "CategoricalDescriptor(key={}, categories={})".format(self.key, self.categories) + return f"CategoricalDescriptor(key={self.key}, categories={self.categories})" -class FormulationDescriptor(Serializable['FormulationDescriptor'], Descriptor): +class FormulationDescriptor(Serializable["FormulationDescriptor"], Descriptor): """A descriptor to hold formulations. Parameters @@ -253,7 +249,7 @@ class FormulationDescriptor(Serializable['FormulationDescriptor'], Descriptor): """ typ = properties.String( - 'type', default=FormulationKey.HIERARCHICAL.value, deserializable=False + "type", default=FormulationKey.HIERARCHICAL.value, deserializable=False ) def __init__(self, key: FormulationKey | str): @@ -266,7 +262,7 @@ def __str__(self): return f"" def __repr__(self): - return "FormulationDescriptor(key={})".format(self.key) + return f"FormulationDescriptor(key={self.key})" @classmethod def hierarchical(cls) -> "FormulationDescriptor": diff --git a/src/citrine/informatics/design_candidate.py b/src/citrine/informatics/design_candidate.py index dfef5e102..f747ac68e 100644 --- a/src/citrine/informatics/design_candidate.py +++ b/src/citrine/informatics/design_candidate.py @@ -2,28 +2,27 @@ from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable - __all__ = [ - 'DesignCandidate', - 'HierarchicalDesignCandidate', - 'DesignMaterial', - 'HierarchicalDesignMaterial', - 'SampleSearchSpaceResultCandidate', - 'DesignVariable', - 'MeanAndStd', - 'TopCategories', - 'Mixture', - 'ChemicalFormula', - 'MolecularStructure', + "ChemicalFormula", + "DesignCandidate", + "DesignMaterial", + "DesignVariable", + "HierarchicalDesignCandidate", + "HierarchicalDesignMaterial", + "MeanAndStd", + "Mixture", + "MolecularStructure", + "SampleSearchSpaceResultCandidate", + "TopCategories", ] class DesignCandidateComment(Serializable["DesignCandidateComment"]): - message = properties.String('message') + message = properties.String("message") """:str: the text of the comment""" - created_by = properties.UUID('created.user') + created_by = properties.UUID("created.user") """:UUID: id of the user who created the comment""" - create_time = properties.Datetime('created.time') + create_time = properties.Datetime("created.time") """:datetime: date and time at which the comment was created""" @@ -45,7 +44,7 @@ def get_type(cls, data) -> type[Serializable]: "C": TopCategories, "M": Mixture, "F": ChemicalFormula, - "S": MolecularStructure + "S": MolecularStructure, }[data["type"]] @@ -55,17 +54,16 @@ class MeanAndStd(Serializable["MeanAndStd"], DesignVariable): This does not imply that the distribution is Normal. """ - mean = properties.Float('m') + mean = properties.Float("m") """:float: mean of the continuous distribution""" - std = properties.Float('s') + std = properties.Float("s") """:float: standard deviation of the continuous distribution""" - typ = properties.String('type', default='R', deserializable=False) + typ = properties.String("type", default="R", deserializable=False) """:str: polymorphic type code""" def __init__(self, *, mean: float, std: float): self.mean = mean self.std = std - pass # pragma: no cover class TopCategories(Serializable["CategoriesAndProbabilities"], DesignVariable): @@ -75,14 +73,13 @@ class TopCategories(Serializable["CategoriesAndProbabilities"], DesignVariable): may have non-zero probabilities. """ - probabilities = properties.Mapping(properties.String, properties.Float, 'cp') + probabilities = properties.Mapping(properties.String, properties.Float, "cp") """:dict[str, float]: mapping from category names to their probabilities""" - typ = properties.String('type', default='C', deserializable=False) + typ = properties.String("type", default="C", deserializable=False) """:str: polymorphic type code""" def __init__(self, *, probabilities: dict): self.probabilities = probabilities - pass # pragma: no cover class Mixture(Serializable["Mixture"], DesignVariable): @@ -92,62 +89,58 @@ class Mixture(Serializable["Mixture"], DesignVariable): truncation (but there may be rounding). """ - quantities = properties.Mapping(properties.String, properties.Float, 'q') + quantities = properties.Mapping(properties.String, properties.Float, "q") """:dict[str, float]: mapping from ingredient identifiers to their quantities""" - labels = properties.Mapping(properties.String, properties.Set(properties.String), 'l') + labels = properties.Mapping(properties.String, properties.Set(properties.String), "l") """:dict[str, set[str]]: mapping from label identifiers to their associated ingredients""" - typ = properties.String('type', default='M', deserializable=False) + typ = properties.String("type", default="M", deserializable=False) """:str: polymorphic type code""" def __init__(self, *, quantities: dict, labels: dict | None = None): self.quantities = quantities self.labels = labels or {} - pass # pragma: no cover class ChemicalFormula(Serializable["ChemicalFormula"], DesignVariable): """Chemical formula as a string.""" - formula = properties.String('f') + formula = properties.String("f") """:str: chemical formula""" - typ = properties.String('type', default='F', deserializable=False) + typ = properties.String("type", default="F", deserializable=False) """:str: polymorphic type code""" def __init__(self, *, formula: str): self.formula = formula - pass # pragma: no cover class MolecularStructure(Serializable["MolecularStructure"], DesignVariable): """SMILES string representation of a molecular structure.""" - smiles = properties.String('s') + smiles = properties.String("s") """:str: SMILES string""" - typ = properties.String('type', default='S', deserializable=False) + typ = properties.String("type", default="S", deserializable=False) """:str: polymorphic type code""" def __init__(self, *, smiles: str): self.smiles = smiles - pass # pragma: no cover class DesignMaterial(Serializable["DesignMaterial"]): """Description of the material that was designed, as a set of DesignVariables.""" - material_id = properties.UUID('identifiers.id') + material_id = properties.UUID("identifiers.id") """:UUID: unique internal Citrine id of the material""" - identifiers = properties.List(properties.String, 'identifiers.external', default=[]) + identifiers = properties.List(properties.String, "identifiers.external", default=[]) """:list[str]: globally unique identifiers assigned to the material""" - process_template = properties.Optional(properties.UUID, 'identifiers.process_template') + process_template = properties.Optional(properties.UUID, "identifiers.process_template") """:UUID | None: GEMD process template that describes the process to create this material""" - material_template = properties.Optional(properties.UUID, 'identifiers.material_template') + material_template = properties.Optional(properties.UUID, "identifiers.material_template") """:UUID | None: GEMD material template that describes this material""" - values = properties.Mapping(properties.String, properties.Object(DesignVariable), 'vars') + values = properties.Mapping(properties.String, properties.Object(DesignVariable), "vars") """:dict[str, DesignVariable]: mapping from descriptor keys to the value for this material""" def __init__(self, *, values: dict): self.values = values - pass class HierarchicalDesignMaterial(Serializable["HierarchicalDesignMaterial"]): @@ -159,11 +152,11 @@ class HierarchicalDesignMaterial(Serializable["HierarchicalDesignMaterial"]): that associates each material (by Citrine ID) with the ingredients that comprise it. """ - root = properties.Object(DesignMaterial, 'terminal') + root = properties.Object(DesignMaterial, "terminal") """:DesignMaterial: root material containing features and predicted properties""" - sub_materials = properties.List(properties.Object(DesignMaterial), 'sub_materials') + sub_materials = properties.List(properties.Object(DesignMaterial), "sub_materials") """:list[DesignMaterial]: all other materials appearing in the history of the root""" - mixtures = properties.Mapping(properties.UUID, properties.Object(Mixture), 'mixtures') + mixtures = properties.Mapping(properties.UUID, properties.Object(Mixture), "mixtures") """:dict[UUID, Mixture]: mapping from Citrine ID to components the material is composed of""" @@ -173,26 +166,26 @@ class DesignCandidate(Serializable["DesignCandidate"]): This class represents the candidate computed by a design execution. """ - uid = properties.UUID('id') + uid = properties.UUID("id") """:UUID: unique external Citrine id of the material""" - material_id = properties.UUID('material_id') + material_id = properties.UUID("material_id") """:UUID: unique internal Citrine id of the material""" - identifiers = properties.List(properties.String(), 'identifiers') + identifiers = properties.List(properties.String(), "identifiers") """:list[str]: globally unique identifiers assigned to the material""" - primary_score = properties.Float('primary_score') + primary_score = properties.Float("primary_score") """:float: numerical score describing how well the candidate satisfies the objectives and constraints (higher is better)""" - material = properties.Object(DesignMaterial, 'material') + material = properties.Object(DesignMaterial, "material") """:DesignMaterial: the material returned by the design workflow""" - name = properties.String('name') + name = properties.String("name") """:str: the name of the candidate""" - hidden = properties.Boolean('hidden') + hidden = properties.Boolean("hidden") """:str: whether the candidate is marked hidden""" - pinned_by = properties.Optional(properties.UUID, 'pinned.user') + pinned_by = properties.Optional(properties.UUID, "pinned.user") """:UUID | None: id of the user who pinned the candidate, if it's been pinned""" - pinned_time = properties.Optional(properties.Datetime, 'pinned.time') + pinned_time = properties.Optional(properties.Datetime, "pinned.time") """:datetime | None: date and time at which the candidate was pinned, if it's been pinned""" - comments = properties.List(properties.Object(DesignCandidateComment), 'comments', default=[]) + comments = properties.List(properties.Object(DesignCandidateComment), "comments", default=[]) """:list[DesignCandidateComment]: the list of comments on the candidate, with metadata.""" @@ -202,9 +195,9 @@ class HierarchicalDesignCandidate(Serializable["HierarchicalDesignCandidate"]): This class represents the candidate computed by a design execution. """ - uid = properties.UUID('id') + uid = properties.UUID("id") """:UUID: unique external Citrine ID of the material""" - primary_score = properties.Float('primary_score') + primary_score = properties.Float("primary_score") """:float: numerical score describing how well the candidate satisfies the objectives and constraints (higher is better)""" rank = properties.Integer("rank") @@ -219,9 +212,9 @@ class SampleSearchSpaceResultCandidate(Serializable["SampleSearchSpaceResultCand This class represents the candidate computed by a design execution. """ - uid = properties.UUID('id') + uid = properties.UUID("id") """:UUID: unique external Citrine ID of the material""" - execution_uid = properties.UUID('id') + execution_uid = properties.UUID("id") """:UUID: unique external Citrine ID of the execution""" material = properties.Object(HierarchicalDesignMaterial, "material") """:HierarchicalDesignMaterial: the material returned by the design workflow""" 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 6ed23ef07..efd881dbb 100644 --- a/src/citrine/informatics/design_spaces/data_source_design_space.py +++ b/src/citrine/informatics/design_spaces/data_source_design_space.py @@ -4,10 +4,10 @@ from citrine.informatics.descriptors import Descriptor from citrine.informatics.design_spaces.subspace import DesignSubspace -__all__ = ['DataSourceDesignSpace'] +__all__ = ["DataSourceDesignSpace"] -class DataSourceDesignSpace(Resource['DataSourceDesignSpace'], DesignSubspace): +class DataSourceDesignSpace(Resource["DataSourceDesignSpace"], DesignSubspace): """An enumeration of candidates stored in a data source. Parameters @@ -24,20 +24,17 @@ class DataSourceDesignSpace(Resource['DataSourceDesignSpace'], DesignSubspace): """ - data_source = properties.Object(DataSource, 'data_source') + data_source = properties.Object(DataSource, "data_source") descriptors = properties.List( - properties.Object(Descriptor), 'descriptors', serializable=False, default=[]) + properties.Object(Descriptor), "descriptors", serializable=False, default=[] + ) - typ = properties.String('type', default='DataSourceDesignSpace', deserializable=False) + typ = properties.String("type", default="DataSourceDesignSpace", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - data_source: DataSource): + def __init__(self, name: str, *, description: str, data_source: DataSource): self.name: str = name self.description: str = description self.data_source: DataSource = data_source def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/design_spaces/design_space.py b/src/citrine/informatics/design_spaces/design_space.py index c87ee28ce..c8c36267a 100644 --- a/src/citrine/informatics/design_spaces/design_space.py +++ b/src/citrine/informatics/design_spaces/design_space.py @@ -1,4 +1,4 @@ -__all__ = ['DesignSpace'] +__all__ = ["DesignSpace"] class DesignSpace: diff --git a/src/citrine/informatics/design_spaces/design_space_settings.py b/src/citrine/informatics/design_spaces/design_space_settings.py index 68df574c7..8963aba5e 100644 --- a/src/citrine/informatics/design_spaces/design_space_settings.py +++ b/src/citrine/informatics/design_spaces/design_space_settings.py @@ -5,7 +5,6 @@ from citrine._rest.resource import Resource from citrine._serialization import properties - __all__ = ["DefaultDesignSpaceMode", "DesignSpaceSettings"] @@ -16,8 +15,8 @@ class DefaultDesignSpaceMode(BaseEnumeration): * HIERARCHICAL results in a hierarchical design space resembling the shape of training data """ - ATTRIBUTE = 'ATTRIBUTE' - HIERARCHICAL = 'HIERARCHICAL' + ATTRIBUTE = "ATTRIBUTE" + HIERARCHICAL = "HIERARCHICAL" class DesignSpaceSettings(Resource["DesignSpaceSettings"]): @@ -25,8 +24,7 @@ class DesignSpaceSettings(Resource["DesignSpaceSettings"]): predictor_id = properties.UUID("predictor_id") predictor_version = properties.Optional( - properties.Union([properties.Integer(), properties.String()]), - 'predictor_version' + properties.Union([properties.Integer(), properties.String()]), "predictor_version" ) mode = properties.Optional(properties.Enumeration(DefaultDesignSpaceMode), "mode") exclude_intermediates = properties.Optional(properties.Boolean(), "exclude_intermediates") @@ -43,16 +41,18 @@ class DesignSpaceSettings(Resource["DesignSpaceSettings"]): properties.Boolean(), "include_parameter_constraints" ) - def __init__(self, - *, - predictor_id: UUID | str, - predictor_version: int | str | None = None, - mode: DefaultDesignSpaceMode | None = None, - exclude_intermediates: bool | None = None, - include_ingredient_fraction_constraints: bool | None = None, - include_label_fraction_constraints: bool | None = None, - include_label_count_constraints: bool | None = None, - include_parameter_constraints: bool | None = None): + def __init__( + self, + *, + predictor_id: UUID | str, + predictor_version: int | str | None = None, + mode: DefaultDesignSpaceMode | None = None, + exclude_intermediates: bool | None = None, + include_ingredient_fraction_constraints: bool | None = None, + include_label_fraction_constraints: bool | None = None, + include_label_count_constraints: bool | None = None, + include_parameter_constraints: bool | None = None, + ): self.predictor_id = predictor_id self.predictor_version = predictor_version self.mode = mode diff --git a/src/citrine/informatics/design_spaces/formulation_design_space.py b/src/citrine/informatics/design_spaces/formulation_design_space.py index f53cbeeee..043b73ba6 100644 --- a/src/citrine/informatics/design_spaces/formulation_design_space.py +++ b/src/citrine/informatics/design_spaces/formulation_design_space.py @@ -6,10 +6,10 @@ from citrine.informatics.descriptors import FormulationDescriptor from citrine.informatics.design_spaces.subspace import DesignSubspace -__all__ = ['FormulationDesignSpace'] +__all__ = ["FormulationDesignSpace"] -class FormulationDesignSpace(Resource['FormulationDesignSpace'], DesignSubspace): +class FormulationDesignSpace(Resource["FormulationDesignSpace"], DesignSubspace): """Design space composed of mixtures of ingredients. Parameters @@ -40,29 +40,31 @@ class FormulationDesignSpace(Resource['FormulationDesignSpace'], DesignSubspace) """ - formulation_descriptor = properties.Object(FormulationDescriptor, 'formulation_descriptor') - ingredients = properties.Set(properties.String, 'ingredients') - labels = properties.Optional(properties.Mapping( - properties.String, - properties.Set(properties.String) - ), 'labels') + formulation_descriptor = properties.Object(FormulationDescriptor, "formulation_descriptor") + ingredients = properties.Set(properties.String, "ingredients") + labels = properties.Optional( + properties.Mapping(properties.String, properties.Set(properties.String)), "labels" + ) untested_ingredients = properties.Optional( - properties.Set(properties.String), 'untested_ingredients') - constraints = properties.Set(properties.Object(Constraint), 'constraints') - resolution = properties.Float('resolution') + properties.Set(properties.String), "untested_ingredients" + ) + constraints = properties.Set(properties.Object(Constraint), "constraints") + resolution = properties.Float("resolution") - typ = properties.String('type', default='FormulationDesignSpace', deserializable=False) + typ = properties.String("type", default="FormulationDesignSpace", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - formulation_descriptor: FormulationDescriptor, - ingredients: set[str], - constraints: set[Constraint], - labels: Mapping[str, set[str]] | None = None, - untested_ingredients: set[str] | None = None, - resolution: float = 0.0001): + def __init__( + self, + name: str, + *, + description: str, + formulation_descriptor: FormulationDescriptor, + ingredients: set[str], + constraints: set[Constraint], + labels: Mapping[str, set[str]] | None = None, + untested_ingredients: set[str] | None = None, + resolution: float = 0.0001, + ): self.name: str = name self.description: str = description self.formulation_descriptor: FormulationDescriptor = formulation_descriptor @@ -73,4 +75,4 @@ def __init__(self, self.resolution: float = resolution def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/design_spaces/hierarchical_design_space.py b/src/citrine/informatics/design_spaces/hierarchical_design_space.py index 6f44a9be2..17fb32567 100644 --- a/src/citrine/informatics/design_spaces/hierarchical_design_space.py +++ b/src/citrine/informatics/design_spaces/hierarchical_design_space.py @@ -4,16 +4,12 @@ from citrine._serialization import properties from citrine._serialization.serializable import Serializable from citrine.informatics.data_sources import DataSource -from citrine.informatics.dimensions import Dimension from citrine.informatics.design_spaces import FormulationDesignSpace -from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace from citrine.informatics.design_spaces.design_space_settings import DesignSpaceSettings +from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace +from citrine.informatics.dimensions import Dimension -__all__ = [ - "TemplateLink", - "MaterialNodeDefinition", - "HierarchicalDesignSpace" -] +__all__ = ["HierarchicalDesignSpace", "MaterialNodeDefinition", "TemplateLink"] class TemplateLink(Serializable["TemplateLink"]): @@ -38,12 +34,12 @@ class TemplateLink(Serializable["TemplateLink"]): process_template_name = properties.Optional(properties.String, "process_template_name") def __init__( - self, - *, - material_template: UUID, - process_template: UUID, - material_template_name: str | None = None, - process_template_name: str | None = None + self, + *, + material_template: UUID, + process_template: UUID, + material_template_name: str | None = None, + process_template_name: str | None = None, ): self.material_template: UUID = material_template self.process_template: UUID = process_template @@ -86,14 +82,14 @@ class MaterialNodeDefinition(Serializable["MaterialNodeDefinition"]): display_name = properties.Optional(properties.String, "display_name") def __init__( - self, - *, - name: str, - scope: str | None = None, - attributes: list[Dimension] | None = None, - formulation_subspace: FormulationDesignSpace | None = None, - template_link: TemplateLink | None = None, - display_name: str | None = None + self, + *, + name: str, + scope: str | None = None, + attributes: list[Dimension] | None = None, + formulation_subspace: FormulationDesignSpace | None = None, + template_link: TemplateLink | None = None, + display_name: str | None = None, ): self.name = name self.scope: str | None = scope @@ -156,21 +152,19 @@ class HierarchicalDesignSpace(EngineResource["HierarchicalDesignSpace"], TopLeve subspaces = properties.List( properties.Object(MaterialNodeDefinition), "data.instance.subspaces" ) - data_sources = properties.List( - properties.Object(DataSource), "data.instance.data_sources" - ) + data_sources = properties.List(properties.Object(DataSource), "data.instance.data_sources") typ = properties.String( "data.instance.type", default="HierarchicalDesignSpace", deserializable=False ) def __init__( - self, - name: str, - *, - description: str, - root: MaterialNodeDefinition, - subspaces: list[MaterialNodeDefinition] | None = None, - data_sources: list[DataSource] | None = None + self, + name: str, + *, + description: str, + root: MaterialNodeDefinition, + subspaces: list[MaterialNodeDefinition] | None = None, + data_sources: list[DataSource] | None = None, ): self.name: str = name self.description: str = description @@ -187,4 +181,4 @@ def _post_dump(self, data: dict) -> dict: return data def __repr__(self): - return f'' + return f"" diff --git a/src/citrine/informatics/design_spaces/product_design_space.py b/src/citrine/informatics/design_spaces/product_design_space.py index 3cfb9fb83..75721ce33 100644 --- a/src/citrine/informatics/design_spaces/product_design_space.py +++ b/src/citrine/informatics/design_spaces/product_design_space.py @@ -1,14 +1,14 @@ from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties -from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace from citrine.informatics.design_spaces.design_space_settings import DesignSpaceSettings from citrine.informatics.design_spaces.subspace import DesignSubspace +from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace from citrine.informatics.dimensions import Dimension -__all__ = ['ProductDesignSpace'] +__all__ = ["ProductDesignSpace"] -class ProductDesignSpace(EngineResource['ProductDesignSpace'], TopLevelDesignSpace): +class ProductDesignSpace(EngineResource["ProductDesignSpace"], TopLevelDesignSpace): """A Cartesian product of design spaces. Factors can be other design spaces and/or univariate dimensions. @@ -28,21 +28,25 @@ class ProductDesignSpace(EngineResource['ProductDesignSpace'], TopLevelDesignSpa _settings = properties.Optional(properties.Object(DesignSpaceSettings), "metadata.settings") - subspaces = properties.List(properties.Object(DesignSubspace), 'data.instance.subspaces', - default=[]) + subspaces = properties.List( + properties.Object(DesignSubspace), "data.instance.subspaces", default=[] + ) dimensions = properties.Optional( - properties.List(properties.Object(Dimension)), 'data.instance.dimensions' + properties.List(properties.Object(Dimension)), "data.instance.dimensions" ) - typ = properties.String('data.instance.type', default='ProductDesignSpace', - deserializable=False) + typ = properties.String( + "data.instance.type", default="ProductDesignSpace", deserializable=False + ) - def __init__(self, - name: str, - *, - description: str, - subspaces: list[DesignSubspace] | None = None, - dimensions: list[Dimension] | None = None): + def __init__( + self, + name: str, + *, + description: str, + subspaces: list[DesignSubspace] | None = None, + dimensions: list[Dimension] | None = None, + ): self.name: str = name self.description: str = description self.subspaces: list[DesignSubspace] = subspaces or [] @@ -57,4 +61,4 @@ def _post_dump(self, data: dict) -> dict: return data def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/design_spaces/sample_design_space.py b/src/citrine/informatics/design_spaces/sample_design_space.py index 64c22a9a9..e7832bbac 100644 --- a/src/citrine/informatics/design_spaces/sample_design_space.py +++ b/src/citrine/informatics/design_spaces/sample_design_space.py @@ -2,7 +2,7 @@ from citrine._serialization.serializable import Serializable -class SampleDesignSpaceInput(Serializable['SampleDesignSpaceInput']): +class SampleDesignSpaceInput(Serializable["SampleDesignSpaceInput"]): """A Citrine Sample Design Space Execution Input. Parameters diff --git a/src/citrine/informatics/design_spaces/subspace.py b/src/citrine/informatics/design_spaces/subspace.py index 0287cccf3..c4a684602 100644 --- a/src/citrine/informatics/design_spaces/subspace.py +++ b/src/citrine/informatics/design_spaces/subspace.py @@ -16,21 +16,21 @@ class DesignSubspace(PolymorphicSerializable["DesignSubspace"], DesignSpace): description = properties.Optional(properties.String(), "description") @classmethod - def get_type(cls, data) -> type['DesignSubspace']: + def get_type(cls, data) -> type["DesignSubspace"]: """Return the subtype.""" from .data_source_design_space import DataSourceDesignSpace from .formulation_design_space import FormulationDesignSpace type_dict = { - 'FormulationDesignSpace': FormulationDesignSpace, - 'DataSourceDesignSpace': DataSourceDesignSpace, + "FormulationDesignSpace": FormulationDesignSpace, + "DataSourceDesignSpace": DataSourceDesignSpace, } - typ = type_dict.get(data['type']) + typ = type_dict.get(data["type"]) if typ is not None: return typ else: raise ValueError( - '{} is not a valid design subspace type. ' - 'Must be in {}.'.format(data['type'], type_dict.keys()) + f"{data['type']} is not a valid design subspace type. " + f"Must be in {type_dict.keys()}." ) diff --git a/src/citrine/informatics/design_spaces/top_level_design_space.py b/src/citrine/informatics/design_spaces/top_level_design_space.py index e1f8db6e6..8343554fc 100644 --- a/src/citrine/informatics/design_spaces/top_level_design_space.py +++ b/src/citrine/informatics/design_spaces/top_level_design_space.py @@ -1,4 +1,5 @@ """Tools for working with design spaces.""" + from uuid import UUID from citrine._rest.asynchronous_object import AsynchronousObject @@ -6,31 +7,29 @@ from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable from citrine._session import Session -from citrine.resources.sample_design_space_execution import \ - SampleDesignSpaceExecutionCollection - +from citrine.resources.sample_design_space_execution import SampleDesignSpaceExecutionCollection -__all__ = ['TopLevelDesignSpace'] +__all__ = ["TopLevelDesignSpace"] -class TopLevelDesignSpace(PolymorphicSerializable['TopLevelDesignSpace'], AsynchronousObject): +class TopLevelDesignSpace(PolymorphicSerializable["TopLevelDesignSpace"], AsynchronousObject): """A top-level Citrine Design Space describes the set of materials that can be made. Abstract type that returns the proper type given a serialized dict. """ - uid = properties.Optional(properties.UUID, 'id', serializable=False) + uid = properties.Optional(properties.UUID, "id", serializable=False) """:UUID | None: Citrine Platform unique identifier""" - name = properties.String('data.name') - description = properties.Optional(properties.String(), 'data.description') + name = properties.String("data.name") + description = properties.Optional(properties.String(), "data.description") - locked_by = properties.Optional(properties.UUID, 'metadata.locked.user', - serializable=False) + locked_by = properties.Optional(properties.UUID, "metadata.locked.user", serializable=False) """:UUID | None: id of the user whose action cause the design space to be locked, if it is locked""" - lock_time = properties.Optional(properties.Datetime, 'metadata.locked.time', - serializable=False) + lock_time = properties.Optional( + properties.Datetime, "metadata.locked.time", serializable=False + ) """:datetime | None: date and time at which the resource was locked, if it is locked""" @@ -44,7 +43,7 @@ def wrap_instance(subspace_data: dict) -> dict: "data": { "name": subspace_data.get("name", ""), "description": subspace_data.get("description", ""), - "instance": subspace_data + "instance": subspace_data, } } @@ -58,13 +57,13 @@ def wrap_instance(subspace_data: dict) -> dict: @classmethod def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" - from .product_design_space import ProductDesignSpace from .hierarchical_design_space import HierarchicalDesignSpace + from .product_design_space import ProductDesignSpace return { - 'ProductDesignSpace': ProductDesignSpace, - 'HierarchicalDesignSpace': HierarchicalDesignSpace - }[data['data']['instance']['type']] + "ProductDesignSpace": ProductDesignSpace, + "HierarchicalDesignSpace": HierarchicalDesignSpace, + }[data["data"]["instance"]["type"]] @property def is_locked(self) -> bool: diff --git a/src/citrine/informatics/dimensions.py b/src/citrine/informatics/dimensions.py index b16ef1b92..5a04b3a53 100644 --- a/src/citrine/informatics/dimensions.py +++ b/src/citrine/informatics/dimensions.py @@ -3,12 +3,12 @@ from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable -from citrine.informatics.descriptors import Descriptor, RealDescriptor, IntegerDescriptor +from citrine.informatics.descriptors import Descriptor, IntegerDescriptor, RealDescriptor -__all__ = ['Dimension', 'ContinuousDimension', 'IntegerDimension', 'EnumeratedDimension'] +__all__ = ["ContinuousDimension", "Dimension", "EnumeratedDimension", "IntegerDimension"] -class Dimension(PolymorphicSerializable['Dimension']): +class Dimension(PolymorphicSerializable["Dimension"]): """A Dimension describes the values that a quantity can take in the context of a design space. Abstract type that returns the proper type given a serialized dict. @@ -19,13 +19,13 @@ class Dimension(PolymorphicSerializable['Dimension']): def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" return { - 'ContinuousDimension': ContinuousDimension, - 'IntegerDimension': IntegerDimension, - 'EnumeratedDimension': EnumeratedDimension - }[data['type']] + "ContinuousDimension": ContinuousDimension, + "IntegerDimension": IntegerDimension, + "EnumeratedDimension": EnumeratedDimension, + }[data["type"]] -class ContinuousDimension(Serializable['ContinuousDimension'], Dimension): +class ContinuousDimension(Serializable["ContinuousDimension"], Dimension): """A continuous, real-valued dimension. Parameters @@ -39,21 +39,24 @@ class ContinuousDimension(Serializable['ContinuousDimension'], Dimension): """ - descriptor = properties.Object(RealDescriptor, 'descriptor') - lower_bound = properties.Float('lower_bound') - upper_bound = properties.Float('upper_bound') - typ = properties.String('type', default='ContinuousDimension', deserializable=False) - - def __init__(self, - descriptor: RealDescriptor, *, - lower_bound: float | None = None, - upper_bound: float | None = None): + descriptor = properties.Object(RealDescriptor, "descriptor") + lower_bound = properties.Float("lower_bound") + upper_bound = properties.Float("upper_bound") + typ = properties.String("type", default="ContinuousDimension", deserializable=False) + + def __init__( + self, + descriptor: RealDescriptor, + *, + lower_bound: float | None = None, + upper_bound: float | None = None, + ): self.descriptor: RealDescriptor = descriptor self.lower_bound = lower_bound if lower_bound is not None else descriptor.lower_bound self.upper_bound = upper_bound if upper_bound is not None else descriptor.upper_bound -class IntegerDimension(Serializable['IntegerDimension'], Dimension): +class IntegerDimension(Serializable["IntegerDimension"], Dimension): """An integer-valued dimension with inclusive lower and upper bounds. Parameters @@ -67,21 +70,24 @@ class IntegerDimension(Serializable['IntegerDimension'], Dimension): """ - descriptor = properties.Object(IntegerDescriptor, 'descriptor') - lower_bound = properties.Integer('lower_bound') - upper_bound = properties.Integer('upper_bound') - typ = properties.String('type', default='IntegerDimension', deserializable=False) - - def __init__(self, - descriptor: IntegerDescriptor, *, - lower_bound: int | None = None, - upper_bound: int | None = None): + descriptor = properties.Object(IntegerDescriptor, "descriptor") + lower_bound = properties.Integer("lower_bound") + upper_bound = properties.Integer("upper_bound") + typ = properties.String("type", default="IntegerDimension", deserializable=False) + + def __init__( + self, + descriptor: IntegerDescriptor, + *, + lower_bound: int | None = None, + upper_bound: int | None = None, + ): self.descriptor: IntegerDescriptor = descriptor self.lower_bound = lower_bound if lower_bound is not None else descriptor.lower_bound self.upper_bound = upper_bound if upper_bound is not None else descriptor.upper_bound -class EnumeratedDimension(Serializable['EnumeratedDimension'], Dimension): +class EnumeratedDimension(Serializable["EnumeratedDimension"], Dimension): """A finite, enumerated dimension. Parameters @@ -93,12 +99,10 @@ class EnumeratedDimension(Serializable['EnumeratedDimension'], Dimension): """ - descriptor = properties.Object(Descriptor, 'descriptor') - values = properties.List(properties.String(), 'list') - typ = properties.String('type', default='EnumeratedDimension', deserializable=False) + descriptor = properties.Object(Descriptor, "descriptor") + values = properties.List(properties.String(), "list") + typ = properties.String("type", default="EnumeratedDimension", deserializable=False) - def __init__(self, - descriptor: Descriptor, *, - values: list[str]): + def __init__(self, descriptor: Descriptor, *, values: list[str]): self.descriptor: Descriptor = descriptor self.values: list[str] = values diff --git a/src/citrine/informatics/executions/design_execution.py b/src/citrine/informatics/executions/design_execution.py index 383cecdfa..d0c9e4eaa 100644 --- a/src/citrine/informatics/executions/design_execution.py +++ b/src/citrine/informatics/executions/design_execution.py @@ -7,9 +7,9 @@ from citrine._utils.functions import format_escaped_url from citrine.informatics.descriptors import Descriptor from citrine.informatics.design_candidate import DesignCandidate, HierarchicalDesignCandidate +from citrine.informatics.executions.execution import Execution from citrine.informatics.predict_request import PredictRequest from citrine.informatics.scores import Score -from citrine.informatics.executions.execution import Execution class DesignExecution(Resource["DesignExecution"], Execution): @@ -21,24 +21,24 @@ class DesignExecution(Resource["DesignExecution"], Execution): """ _paginator: Paginator = Paginator() - _collection_key = 'response' - workflow_id = properties.UUID('workflow_id', serializable=False) + _collection_key = "response" + workflow_id = properties.UUID("workflow_id", serializable=False) """:UUID: Unique identifier of the workflow that was executed""" version_number = properties.Integer("version_number", serializable=False) """:int: Integer identifier that increases each time the workflow is executed. The first execution has version_number = 1.""" - score = properties.Object(Score, 'score') + score = properties.Object(Score, "score") """:Score: score by which this execution was evaluated""" - descriptors = properties.List(properties.Object(Descriptor), 'descriptors') + descriptors = properties.List(properties.Object(Descriptor), "descriptors") """:list[Descriptor]: all of the descriptors in the candidates generated by this execution""" def _path(self): return format_escaped_url( - '/projects/{project_id}/design-workflows/{workflow_id}/executions/{execution_id}', + "/projects/{project_id}/design-workflows/{workflow_id}/executions/{execution_id}", project_id=self.project_id, workflow_id=self.workflow_id, - execution_id=self.uid + execution_id=self.uid, ) @classmethod @@ -48,34 +48,36 @@ def _build_candidates(cls, subset_collection: Iterable[dict]) -> Iterable[Design def candidates(self, *, per_page: int = 100) -> Iterable[DesignCandidate]: """Fetch the Design Candidates for the particular execution, paginated.""" - path = self._path() + '/candidates' + path = self._path() + "/candidates" fetcher = partial(self._fetch_page, path=path, fetch_func=self._session.get_resource) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_candidates, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=fetcher, collection_builder=self._build_candidates, per_page=per_page + ) @classmethod def _build_hierarchical_candidates( - cls, subset_collection: Iterable[dict]) -> Iterable[HierarchicalDesignCandidate]: + cls, subset_collection: Iterable[dict] + ) -> Iterable[HierarchicalDesignCandidate]: for candidate in subset_collection: yield HierarchicalDesignCandidate.build(candidate) def hierarchical_candidates(self, *, per_page: int = 100) -> Iterable[DesignCandidate]: """Fetch the Design Candidates for the particular execution, paginated.""" - path = self._path() + '/candidate-histories' + path = self._path() + "/candidate-histories" fetcher = partial(self._fetch_page, path=path, fetch_func=self._session.get_resource) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_hierarchical_candidates, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_hierarchical_candidates, + per_page=per_page, + ) - def predict(self, - predict_request: PredictRequest) -> DesignCandidate: + def predict(self, predict_request: PredictRequest) -> DesignCandidate: """Invoke a prediction on a design candidate.""" - path = self._path() + '/predict' + path = self._path() + "/predict" res = self._session.post_resource(path, predict_request.dump(), version=self._api_version) return DesignCandidate.build(res) diff --git a/src/citrine/informatics/executions/execution.py b/src/citrine/informatics/executions/execution.py index 43e92f701..ffaa6e2a3 100644 --- a/src/citrine/informatics/executions/execution.py +++ b/src/citrine/informatics/executions/execution.py @@ -16,37 +16,38 @@ class Execution(Pageable, AsynchronousObject, ABC): """ _paginator: Paginator = Paginator() - _collection_key = 'response' + _collection_key = "response" _in_progress_statuses = ["INPROGRESS"] _succeeded_statuses = ["SUCCEEDED"] _failed_statuses = ["FAILED"] _session: Session | None = None project_id: UUID | None = None - uid: UUID = properties.UUID('id', serializable=False) + uid: UUID = properties.UUID("id", serializable=False) """:UUID: Unique identifier of the execution""" - status = properties.Optional(properties.String(), 'status', serializable=False) + status = properties.Optional(properties.String(), "status", serializable=False) """:str | None: short description of the execution's status""" status_description = properties.Optional( - properties.String(), 'status_description', serializable=False) + properties.String(), "status_description", serializable=False + ) """:str | None: more detailed description of the execution's status""" status_detail = properties.List( - properties.Object(StatusDetail), 'status_detail', default=[], serializable=False + properties.Object(StatusDetail), "status_detail", default=[], serializable=False ) """:list[StatusDetail]: a list of structured status info, containing the message and level""" - created_by = properties.Optional(properties.UUID, 'created_by', serializable=False) + created_by = properties.Optional(properties.UUID, "created_by", serializable=False) """:UUID | None: id of the user who created the resource""" - updated_by = properties.Optional(properties.UUID, 'updated_by', serializable=False) + updated_by = properties.Optional(properties.UUID, "updated_by", serializable=False) """:UUID | None: id of the user who most recently updated the resource, if it has been updated""" - create_time = properties.Optional(properties.Datetime, 'create_time', serializable=False) + create_time = properties.Optional(properties.Datetime, "create_time", serializable=False) """:datetime | None: date and time at which the resource was created""" - update_time = properties.Optional(properties.Datetime, 'update_time', serializable=False) + update_time = properties.Optional(properties.Datetime, "update_time", serializable=False) """:datetime | None: date and time at which the resource was most recently updated, if it has been updated""" def __str__(self): - return f'<{self.__class__.__name__} {str(self.uid)!r}>' + return f"<{self.__class__.__name__} {str(self.uid)!r}>" def _path(self): raise NotImplementedError("Subclasses must implement the _path method") # pragma: no cover diff --git a/src/citrine/informatics/executions/generative_design_execution.py b/src/citrine/informatics/executions/generative_design_execution.py index 97b4a0e71..086f46769 100644 --- a/src/citrine/informatics/executions/generative_design_execution.py +++ b/src/citrine/informatics/executions/generative_design_execution.py @@ -4,8 +4,8 @@ from citrine._rest.resource import Resource from citrine._utils.functions import format_escaped_url -from citrine.informatics.generative_design import GenerativeDesignResult from citrine.informatics.executions.execution import Execution +from citrine.informatics.generative_design import GenerativeDesignResult class GenerativeDesignExecution(Resource["GenerativeDesignExecution"], Execution): @@ -18,32 +18,25 @@ class GenerativeDesignExecution(Resource["GenerativeDesignExecution"], Execution def _path(self): return format_escaped_url( - '/projects/{project_id}/generative-design/executions/', - project_id=self.project_id, + "/projects/{project_id}/generative-design/executions/", project_id=self.project_id ) @classmethod - def _build_results( - cls, subset_collection: Iterable[dict] - ) -> Iterable[GenerativeDesignResult]: + def _build_results(cls, subset_collection: Iterable[dict]) -> Iterable[GenerativeDesignResult]: for generation_result in subset_collection: yield GenerativeDesignResult.build(generation_result) def results(self, *, per_page: int = 100) -> Iterable[GenerativeDesignResult]: """Fetch the Generative Design Results for the particular execution, paginated.""" - path = self._path() + f'{self.uid}/results' + path = self._path() + f"{self.uid}/results" fetcher = partial(self._fetch_page, path=path, fetch_func=self._session.get_resource) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_results, - per_page=per_page) - - def result( - self, - *, - result_id: UUID, - ) -> GenerativeDesignResult: + return self._paginator.paginate( + page_fetcher=fetcher, collection_builder=self._build_results, per_page=per_page + ) + + def result(self, *, result_id: UUID) -> GenerativeDesignResult: """Fetch a Generative Design Result for the particular UID.""" - path = self._path() + f'{self.uid}/results/{result_id}' + path = self._path() + f"{self.uid}/results/{result_id}" data = self._session.get_resource(path, version=self._api_version) result = GenerativeDesignResult.build(data) return result diff --git a/src/citrine/informatics/executions/predictor_evaluation.py b/src/citrine/informatics/executions/predictor_evaluation.py index fa99516b2..b62017404 100644 --- a/src/citrine/informatics/executions/predictor_evaluation.py +++ b/src/citrine/informatics/executions/predictor_evaluation.py @@ -13,7 +13,7 @@ from citrine.resources.status_detail import StatusDetail -class PredictorEvaluatorsResponse(Serializable['EvaluatorsPayload']): +class PredictorEvaluatorsResponse(Serializable["EvaluatorsPayload"]): """Container object for a default predictor evaluator response.""" evaluators = properties.List(properties.Object(PredictorEvaluator), "evaluators") @@ -22,39 +22,43 @@ def __init__(self, evaluators: list[PredictorEvaluator]): self.evaluators = evaluators -class PredictorEvaluationRequest(Serializable['EvaluatorsPayload']): +class PredictorEvaluationRequest(Serializable["EvaluatorsPayload"]): """Container object for a predictor evaluation request.""" predictor = properties.Object(PredictorRef, "predictor") evaluators = properties.List(properties.Object(PredictorEvaluator), "evaluators") - def __init__(self, - *, - evaluators: list[PredictorEvaluator], - predictor_id: UUID | str, - predictor_version: int | str | None = None): + def __init__( + self, + *, + evaluators: list[PredictorEvaluator], + predictor_id: UUID | str, + predictor_version: int | str | None = None, + ): self.evaluators = evaluators self.predictor = PredictorRef(predictor_id, predictor_version) -class PredictorEvaluation(EngineResourceWithoutStatus['PredictorEvaluation'], AsynchronousObject): +class PredictorEvaluation(EngineResourceWithoutStatus["PredictorEvaluation"], AsynchronousObject): """The evaluation of a predictor's performance.""" - uid: UUID = properties.UUID('id', serializable=False) + uid: UUID = properties.UUID("id", serializable=False) """:UUID: Unique identifier of the evaluation""" - evaluators = properties.List(properties.Object(PredictorEvaluator), "data.evaluators", - serializable=False) + evaluators = properties.List( + properties.Object(PredictorEvaluator), "data.evaluators", serializable=False + ) """:list[PredictorEvaluator]:the predictor evaluators that were executed. These are used when calling the ``results()`` method.""" - predictor_id = properties.UUID('metadata.predictor_id', serializable=False) + predictor_id = properties.UUID("metadata.predictor_id", serializable=False) """:UUID:""" - predictor_version = properties.Integer('metadata.predictor_version', serializable=False) - status = properties.String('metadata.status.major', serializable=False) + predictor_version = properties.Integer("metadata.predictor_version", serializable=False) + status = properties.String("metadata.status.major", serializable=False) """:str: short description of the evaluation's status""" - status_description = properties.String('metadata.status.minor', serializable=False) + status_description = properties.String("metadata.status.minor", serializable=False) """:str: more detailed description of the evaluation's status""" - status_detail = properties.List(properties.Object(StatusDetail), 'metadata.status.detail', - default=[], serializable=False) + status_detail = properties.List( + properties.Object(StatusDetail), "metadata.status.detail", default=[], serializable=False + ) """:list[StatusDetail]: a list of structured status info, containing the message and level""" project_id: UUID | None = None @@ -65,12 +69,12 @@ class PredictorEvaluation(EngineResourceWithoutStatus['PredictorEvaluation'], As def _path(self): return format_escaped_url( - '/projects/{project_id}/predictor-evaluations/{evaluation_id}', + "/projects/{project_id}/predictor-evaluations/{evaluation_id}", project_id=str(self.project_id), - evaluation_id=str(self.uid) + evaluation_id=str(self.uid), ) - @lru_cache() + @lru_cache def results(self, evaluator_name: str) -> PredictorEvaluationResult: """ Get a specific evaluation result by the name of the evaluator that produced it. diff --git a/src/citrine/informatics/executions/sample_design_space_execution.py b/src/citrine/informatics/executions/sample_design_space_execution.py index 63f68bc7e..298050481 100644 --- a/src/citrine/informatics/executions/sample_design_space_execution.py +++ b/src/citrine/informatics/executions/sample_design_space_execution.py @@ -2,13 +2,13 @@ from functools import partial from uuid import UUID -from citrine.informatics.executions.execution import Execution -from citrine.informatics.design_candidate import SampleSearchSpaceResultCandidate from citrine._rest.resource import Resource from citrine._utils.functions import format_escaped_url +from citrine.informatics.design_candidate import SampleSearchSpaceResultCandidate +from citrine.informatics.executions.execution import Execution -class SampleDesignSpaceExecution(Resource['SampleDesignSpaceExecution'], Execution): +class SampleDesignSpaceExecution(Resource["SampleDesignSpaceExecution"], Execution): """The execution of a Sample Design Space task. Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. @@ -16,12 +16,12 @@ class SampleDesignSpaceExecution(Resource['SampleDesignSpaceExecution'], Executi """ - _api_version = 'v3' + _api_version = "v3" design_space_id: UUID | None = None def _path(self): return format_escaped_url( - '/projects/{project_id}/design-spaces/{design_space_id}/sample/', + "/projects/{project_id}/design-spaces/{design_space_id}/sample/", project_id=self.project_id, design_space_id=self.design_space_id, ) @@ -32,9 +32,9 @@ def _pre_build(cls, data: dict) -> dict: # Flatten the status object in order to match other workflow objects. return { **data, - "status_description": data['status']['minor'], - "status_detail": data['status']['detail'], - "status": data['status']["major"] + "status_description": data["status"]["minor"], + "status_detail": data["status"]["detail"], + "status": data["status"]["major"], } @classmethod @@ -45,25 +45,18 @@ def _build_results( yield SampleSearchSpaceResultCandidate.build(sample_result) def results( - self, - *, - page: int | None = None, - per_page: int = 100, + self, *, page: int | None = None, per_page: int = 100 ) -> Iterable[SampleSearchSpaceResultCandidate]: """Fetch the Sample Design Space Results for the particular execution, paginated.""" - path = self._path() + f'{self.uid}/results' + path = self._path() + f"{self.uid}/results" fetcher = partial(self._fetch_page, path=path, fetch_func=self._session.get_resource) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_results, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=fetcher, collection_builder=self._build_results, per_page=per_page + ) - def result( - self, - *, - result_id: UUID, - ) -> SampleSearchSpaceResultCandidate: + def result(self, *, result_id: UUID) -> SampleSearchSpaceResultCandidate: """Fetch a Sample Design Space Result for the particular UID.""" - path = self._path() + f'{self.uid}/results/{result_id}' + path = self._path() + f"{self.uid}/results/{result_id}" data = self._session.get_resource(path, version=self._api_version) result = SampleSearchSpaceResultCandidate.build(data) return result diff --git a/src/citrine/informatics/feature_effects.py b/src/citrine/informatics/feature_effects.py index 50289ac0d..4326724b0 100644 --- a/src/citrine/informatics/feature_effects.py +++ b/src/citrine/informatics/feature_effects.py @@ -7,16 +7,17 @@ class ShapleyMaterial(Resource): """The feature effect of a material.""" - material_id = properties.UUID('material_id', serializable=False) - value = properties.Float('value', serializable=False) + material_id = properties.UUID("material_id", serializable=False) + value = properties.Float("value", serializable=False) class ShapleyFeature(Resource): """All feature effects for this feature by material.""" - feature = properties.String('feature', serializable=False) - materials = properties.List(properties.Object(ShapleyMaterial), 'materials', - serializable=False) + feature = properties.String("feature", serializable=False) + materials = properties.List( + properties.Object(ShapleyMaterial), "materials", serializable=False + ) @property def material_dict(self) -> dict[UUID, float]: @@ -27,8 +28,8 @@ def material_dict(self) -> dict[UUID, float]: class ShapleyOutput(Resource): """All feature effects for this output by feature.""" - output = properties.String('output', serializable=False) - features = properties.List(properties.Object(ShapleyFeature), 'features', serializable=False) + output = properties.String("output", serializable=False) + features = properties.List(properties.Object(ShapleyFeature), "features", serializable=False) @property def feature_dict(self) -> dict[str, dict[UUID, float]]: @@ -39,14 +40,16 @@ def feature_dict(self) -> dict[str, dict[UUID, float]]: class FeatureEffects(Resource): """Captures information about the feature effects associated with a predictor.""" - predictor_id = properties.UUID('metadata.predictor_id', serializable=False) - predictor_version = properties.Integer('metadata.predictor_version', serializable=False) - status = properties.String('metadata.status', serializable=False) - failure_reason = properties.Optional(properties.String(), 'metadata.failure_reason', - serializable=False) + predictor_id = properties.UUID("metadata.predictor_id", serializable=False) + predictor_version = properties.Integer("metadata.predictor_version", serializable=False) + status = properties.String("metadata.status", serializable=False) + failure_reason = properties.Optional( + properties.String(), "metadata.failure_reason", serializable=False + ) - outputs = properties.Optional(properties.List(properties.Object(ShapleyOutput)), 'resultobj', - serializable=False) + outputs = properties.Optional( + properties.List(properties.Object(ShapleyOutput)), "resultobj", serializable=False + ) @classmethod def _pre_build(cls, data: dict) -> dict: @@ -62,10 +65,7 @@ def _pre_build(cls, data: dict) -> dict: for feature, values in feature_dict.items(): items = zip(material_ids, values) materials = [{"material_id": mid, "value": value} for mid, value in items] - features.append({ - "feature": feature, - "materials": materials - }) + features.append({"feature": feature, "materials": materials}) outputs.append({"output": output, "features": features}) diff --git a/src/citrine/informatics/generative_design.py b/src/citrine/informatics/generative_design.py index 67f350a2a..af42ffe5c 100644 --- a/src/citrine/informatics/generative_design.py +++ b/src/citrine/informatics/generative_design.py @@ -1,6 +1,7 @@ +from gemd.enumeration.base_enumeration import BaseEnumeration + from citrine._serialization import properties from citrine._serialization.serializable import Serializable -from gemd.enumeration.base_enumeration import BaseEnumeration class FingerprintType(BaseEnumeration): @@ -74,8 +75,8 @@ def _pre_build(cls, data: dict) -> dict: data.update(result) return data - uid = properties.UUID('id') - execution_id = properties.UUID('execution_id') + uid = properties.UUID("id") + execution_id = properties.UUID("execution_id") seed = properties.String("seed") """The seed used to generate the molecule.""" @@ -90,7 +91,7 @@ def __init__(self): pass # pragma: no cover -class GenerativeDesignInput(Serializable['GenerativeDesignInput']): +class GenerativeDesignInput(Serializable["GenerativeDesignInput"]): """A Citrine Generative Design Execution Input. Parameters @@ -116,20 +117,20 @@ class GenerativeDesignInput(Serializable['GenerativeDesignInput']): """ - seeds = properties.List(properties.String(), 'seeds') + seeds = properties.List(properties.String(), "seeds") fingerprint_type = properties.Enumeration(FingerprintType, "fingerprint_type") min_fingerprint_similarity = properties.Float("min_fingerprint_similarity") mutation_per_seed = properties.Integer("mutation_per_seed") structure_exclusions = properties.List( - properties.Enumeration(StructureExclusion), - "structure_exclusions" + properties.Enumeration(StructureExclusion), "structure_exclusions" ) min_substructure_counts = properties.Mapping( - properties.String(), properties.Integer(), "min_substructure_counts", + properties.String(), properties.Integer(), "min_substructure_counts" ) def __init__( - self, *, + self, + *, seeds: list[str], fingerprint_type: FingerprintType, min_fingerprint_similarity: float, diff --git a/src/citrine/informatics/objectives.py b/src/citrine/informatics/objectives.py index 55231bfe4..285a28b86 100644 --- a/src/citrine/informatics/objectives.py +++ b/src/citrine/informatics/objectives.py @@ -1,13 +1,13 @@ """Tools for working with Objectives.""" + from citrine._serialization import properties -from citrine._serialization.serializable import Serializable from citrine._serialization.polymorphic_serializable import PolymorphicSerializable +from citrine._serialization.serializable import Serializable - -__all__ = ['Objective', 'ScalarMaxObjective', 'ScalarMinObjective'] +__all__ = ["Objective", "ScalarMaxObjective", "ScalarMinObjective"] -class Objective(PolymorphicSerializable['Objective']): +class Objective(PolymorphicSerializable["Objective"]): """ An Objective describes a goal for a score associated with a single descriptor. @@ -20,13 +20,10 @@ class Objective(PolymorphicSerializable['Objective']): @classmethod def get_type(cls, data): """Return the subtype.""" - return { - 'ScalarMax': ScalarMaxObjective, - 'ScalarMin': ScalarMinObjective - }[data['type']] + return {"ScalarMax": ScalarMaxObjective, "ScalarMin": ScalarMinObjective}[data["type"]] -class ScalarMaxObjective(Serializable['ScalarMaxObjective'], Objective): +class ScalarMaxObjective(Serializable["ScalarMaxObjective"], Objective): """ Simple single-response maximization objective with optional bounds. @@ -37,17 +34,17 @@ class ScalarMaxObjective(Serializable['ScalarMaxObjective'], Objective): """ - descriptor_key = properties.String('descriptor_key') - typ = properties.String('type', default='ScalarMax') + descriptor_key = properties.String("descriptor_key") + typ = properties.String("type", default="ScalarMax") def __init__(self, descriptor_key: str): self.descriptor_key = descriptor_key def __str__(self): - return ''.format(self.descriptor_key) + return f"" -class ScalarMinObjective(Serializable['ScalarMinObjective'], Objective): +class ScalarMinObjective(Serializable["ScalarMinObjective"], Objective): """ Simple single-response minimization objective with optional bounds. @@ -58,11 +55,11 @@ class ScalarMinObjective(Serializable['ScalarMinObjective'], Objective): """ - descriptor_key = properties.String('descriptor_key') - typ = properties.String('type', default='ScalarMin') + descriptor_key = properties.String("descriptor_key") + typ = properties.String("type", default="ScalarMin") def __init__(self, descriptor_key: str): self.descriptor_key = descriptor_key def __str__(self): - return ''.format(self.descriptor_key) + return f"" diff --git a/src/citrine/informatics/predict_request.py b/src/citrine/informatics/predict_request.py index 606f8d637..c2cbaa9bf 100644 --- a/src/citrine/informatics/predict_request.py +++ b/src/citrine/informatics/predict_request.py @@ -11,18 +11,21 @@ class PredictRequest(Serializable["PredictRequest"]): This class represents the candidate computed by a design execution. """ - material_id = properties.UUID('material_id') - identifiers = properties.List(properties.String(), 'identifiers') - material = properties.Object(DesignMaterial, 'material') - created_from_id = properties.UUID('created_from_id') - random_seed = properties.Optional(properties.Integer, 'random_seed') + material_id = properties.UUID("material_id") + identifiers = properties.List(properties.String(), "identifiers") + material = properties.Object(DesignMaterial, "material") + created_from_id = properties.UUID("created_from_id") + random_seed = properties.Optional(properties.Integer, "random_seed") - def __init__(self, material_id: UUID, - identifiers: list[str], - material: DesignMaterial, - created_from_id: UUID, - *, - random_seed: int | None = None): + def __init__( + self, + material_id: UUID, + identifiers: list[str], + material: DesignMaterial, + created_from_id: UUID, + *, + random_seed: int | None = None, + ): self.material_id = material_id self.identifiers = identifiers self.material = material diff --git a/src/citrine/informatics/predictor_evaluation_metrics.py b/src/citrine/informatics/predictor_evaluation_metrics.py index fb85e93e6..545e2df40 100644 --- a/src/citrine/informatics/predictor_evaluation_metrics.py +++ b/src/citrine/informatics/predictor_evaluation_metrics.py @@ -5,15 +5,17 @@ from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable -__all__ = ['PredictorEvaluationMetric', - 'RMSE', - 'NDME', - 'RSquared', - 'StandardRMSE', - 'PVA', - 'F1', - 'AreaUnderROC', - 'CoverageProbability'] +__all__ = [ + "F1", + "NDME", + "PVA", + "RMSE", + "AreaUnderROC", + "CoverageProbability", + "PredictorEvaluationMetric", + "RSquared", + "StandardRMSE", +] logger = getLogger(__name__) @@ -170,11 +172,10 @@ def __init__(self, *, coverage_level: str | float = "0.683"): raw_float = float(coverage_level) except ValueError: raise ValueError( - "Invalid coverage level string '{requested_level}'. " + f"Invalid coverage level string '{coverage_level}'. " "Coverage level must represent a floating point number between " - "0 and 1 (non-inclusive).".format( - requested_level=coverage_level - )) + "0 and 1 (non-inclusive)." + ) elif isinstance(coverage_level, float): raw_float = coverage_level else: @@ -186,16 +187,14 @@ def __init__(self, *, coverage_level: str | float = "0.683"): if not isclose(_level_float, raw_float): logger.warning( "Coverage level can only be specified to 3 decimal places." - "Requested level '{requested_level}' will be rounded " - "to {rounded_level}.".format( - requested_level=coverage_level, - rounded_level=_level_float - )) + f"Requested level '{coverage_level}' will be rounded " + f"to {_level_float}." + ) - self._level_str = "{:5.3f}".format(_level_float) + self._level_str = f"{_level_float:5.3f}" def __repr__(self): - return "coverage_probability_{}".format(self._level_str) + return f"coverage_probability_{self._level_str}" def __str__(self): - return "Coverage Probability ({})".format(self._level_str) + return f"Coverage Probability ({self._level_str})" diff --git a/src/citrine/informatics/predictor_evaluation_result.py b/src/citrine/informatics/predictor_evaluation_result.py index 127d4c432..3ce0cf798 100644 --- a/src/citrine/informatics/predictor_evaluation_result.py +++ b/src/citrine/informatics/predictor_evaluation_result.py @@ -2,18 +2,23 @@ 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, \ - CrossValidationEvaluator - -__all__ = ['MetricValue', - 'RealMetricValue', - 'PredictedVsActualRealPoint', - 'PredictedVsActualCategoricalPoint', - 'RealPredictedVsActual', - 'CategoricalPredictedVsActual', - 'ResponseMetrics', - 'PredictorEvaluationResult', - 'CrossValidationResult'] +from citrine.informatics.predictor_evaluator import ( + CrossValidationEvaluator, + HoldoutSetEvaluator, + PredictorEvaluator, +) + +__all__ = [ + "CategoricalPredictedVsActual", + "CrossValidationResult", + "MetricValue", + "PredictedVsActualCategoricalPoint", + "PredictedVsActualRealPoint", + "PredictorEvaluationResult", + "RealMetricValue", + "RealPredictedVsActual", + "ResponseMetrics", +] class MetricValue(PolymorphicSerializable["MetricValue"]): @@ -21,7 +26,6 @@ class MetricValue(PolymorphicSerializable["MetricValue"]): def __init__(self): """These are results, so they should be built rather than constructed.""" - pass # pragma: no cover @classmethod def get_type(cls, data) -> type[Serializable]: @@ -29,7 +33,7 @@ def get_type(cls, data) -> type[Serializable]: return { "RealMetricValue": RealMetricValue, "RealPredictedVsActual": RealPredictedVsActual, - "CategoricalPredictedVsActual": CategoricalPredictedVsActual + "CategoricalPredictedVsActual": CategoricalPredictedVsActual, }[data["type"]] @@ -40,7 +44,7 @@ class RealMetricValue(Serializable["RealMetricValue"], MetricValue): """:float: Mean value""" standard_error = properties.Optional(properties.Float(), "standard_error") """:float | None: Standard error of the mean""" - typ = properties.String('type', default='RealMetricValue', deserializable=False) + typ = properties.String("type", default="RealMetricValue", deserializable=False) def __eq__(self, other): if isinstance(other, RealMetricValue): @@ -98,7 +102,7 @@ class CategoricalPredictedVsActual(Serializable["CategoricalPredictedVsActual"], """:list[PredictedVsActualCategoricalPoint]: List of predicted vs. actual data computed during a predictor evaluation. This is a flattened list that contains data for all trials and folds.""" - typ = properties.String('type', default='CategoricalPredictedVsActual', deserializable=False) + typ = properties.String("type", default="CategoricalPredictedVsActual", deserializable=False) def __iter__(self): return iter(self.value) @@ -114,7 +118,7 @@ class RealPredictedVsActual(Serializable["RealPredictedVsActual"], MetricValue): """:list[PredictedVsActualRealPoint]: List of predicted vs. actual data computed during a predictor evaluation. This is a flattened list that contains data for all trials and folds.""" - typ = properties.String('type', default='RealPredictedVsActual', deserializable=False) + typ = properties.String("type", default="RealPredictedVsActual", deserializable=False) def __iter__(self): return iter(self.value) @@ -147,7 +151,7 @@ def __getitem__(self, item): elif isinstance(item, PredictorEvaluationMetric): return self.metrics[repr(item)] else: - raise TypeError("Cannot index ResponseMetrics with a {}".format(type(item))) + raise TypeError(f"Cannot index ResponseMetrics with a {type(item)}") class PredictorEvaluationResult(PolymorphicSerializable["PredictorEvaluationResult"]): @@ -164,7 +168,7 @@ def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" return { "CrossValidationResult": CrossValidationResult, - "HoldoutSetResult": HoldoutSetResult + "HoldoutSetResult": HoldoutSetResult, }[data["type"]] @property @@ -195,9 +199,10 @@ class CrossValidationResult(Serializable["CrossValidationResult"], PredictorEval """ _evaluator = properties.Object(CrossValidationEvaluator, "evaluator") - _response_results = properties.Mapping(properties.String, properties.Object(ResponseMetrics), - "response_results") - typ = properties.String('type', default='CrossValidationResult', deserializable=False) + _response_results = properties.Mapping( + properties.String, properties.Object(ResponseMetrics), "response_results" + ) + typ = properties.String("type", default="CrossValidationResult", deserializable=False) def __getitem__(self, item): return self._response_results[item] @@ -233,9 +238,10 @@ class HoldoutSetResult(Serializable["HoldoutSetResult"], PredictorEvaluationResu """ _evaluator = properties.Object(HoldoutSetEvaluator, "evaluator") - _response_results = properties.Mapping(properties.String, properties.Object(ResponseMetrics), - "response_results") - typ = properties.String('type', default='HoldoutSetResult', deserializable=False) + _response_results = properties.Mapping( + properties.String, properties.Object(ResponseMetrics), "response_results" + ) + typ = properties.String("type", default="HoldoutSetResult", deserializable=False) def __getitem__(self, item): return self._response_results[item] diff --git a/src/citrine/informatics/predictor_evaluator.py b/src/citrine/informatics/predictor_evaluator.py index 5f4c81b76..94bba76b9 100644 --- a/src/citrine/informatics/predictor_evaluator.py +++ b/src/citrine/informatics/predictor_evaluator.py @@ -1,13 +1,10 @@ from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable -from citrine.informatics.predictor_evaluation_metrics import PredictorEvaluationMetric from citrine.informatics.data_sources import DataSource +from citrine.informatics.predictor_evaluation_metrics import PredictorEvaluationMetric -__all__ = ['PredictorEvaluator', - 'CrossValidationEvaluator', - 'HoldoutSetEvaluator' - ] +__all__ = ["CrossValidationEvaluator", "HoldoutSetEvaluator", "PredictorEvaluator"] class PredictorEvaluator(PolymorphicSerializable["PredictorEvaluator"]): @@ -18,7 +15,7 @@ def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" return { "CrossValidationEvaluator": CrossValidationEvaluator, - "HoldoutSetEvaluator": HoldoutSetEvaluator + "HoldoutSetEvaluator": HoldoutSetEvaluator, }[data["type"]] def __eq__(self, other): @@ -26,13 +23,13 @@ def __eq__(self, other): self_dict = self.dump() other_dict = other.dump() - self_dict['responses'] = set(self_dict.get('responses', [])) - self_dict['metrics'] = frozenset( - frozenset((k, v) for k, v in dct.items()) for dct in self_dict.get('metrics', []) + self_dict["responses"] = set(self_dict.get("responses", [])) + self_dict["metrics"] = frozenset( + frozenset((k, v) for k, v in dct.items()) for dct in self_dict.get("metrics", []) ) - other_dict['responses'] = set(other_dict.get('responses', [])) - other_dict['metrics'] = frozenset( - frozenset((k, v) for k, v in dct.items()) for dct in other_dict.get('metrics', []) + other_dict["responses"] = set(other_dict.get("responses", [])) + other_dict["metrics"] = frozenset( + frozenset((k, v) for k, v in dct.items()) for dct in other_dict.get("metrics", []) ) return self_dict == other_dict @@ -103,21 +100,25 @@ class CrossValidationEvaluator(Serializable["CrossValidationEvaluator"], Predict _responses = properties.Set(properties.String, "responses") n_folds = properties.Integer("n_folds") n_trials = properties.Integer("n_trials") - _metrics = properties.Optional(properties.Set(properties.Object(PredictorEvaluationMetric)), - "metrics") - ignore_when_grouping = properties.Optional(properties.Set(properties.String), - "ignore_when_grouping") + _metrics = properties.Optional( + properties.Set(properties.Object(PredictorEvaluationMetric)), "metrics" + ) + ignore_when_grouping = properties.Optional( + properties.Set(properties.String), "ignore_when_grouping" + ) typ = properties.String("type", default="CrossValidationEvaluator", deserializable=False) - def __init__(self, - name: str, - *, - description: str = "", - responses: set[str], - n_folds: int = 5, - n_trials: int = 3, - metrics: set[PredictorEvaluationMetric] | None = None, - ignore_when_grouping: set[str] | None = None): + def __init__( + self, + name: str, + *, + description: str = "", + responses: set[str], + n_folds: int = 5, + n_trials: int = 3, + metrics: set[PredictorEvaluationMetric] | None = None, + ignore_when_grouping: set[str] | None = None, + ): self.name: str = name self.description: str = description self._responses: set[str] = responses @@ -161,16 +162,20 @@ class HoldoutSetEvaluator(Serializable["HoldoutSetEvaluator"], PredictorEvaluato description = properties.String("description") _responses = properties.Set(properties.String, "responses") data_source = properties.Object(DataSource, "data_source") - _metrics = properties.Optional(properties.Set(properties.Object(PredictorEvaluationMetric)), - "metrics") + _metrics = properties.Optional( + properties.Set(properties.Object(PredictorEvaluationMetric)), "metrics" + ) typ = properties.String("type", default="HoldoutSetEvaluator", deserializable=False) - def __init__(self, - name: str, *, - description: str = "", - responses: set[str], - data_source: DataSource, - metrics: set[PredictorEvaluationMetric] | None = None): + def __init__( + self, + name: str, + *, + description: str = "", + responses: set[str], + data_source: DataSource, + metrics: set[PredictorEvaluationMetric] | None = None, + ): self.name: str = name self.description: str = description self._responses: set[str] = responses diff --git a/src/citrine/informatics/predictors/attribute_accumulation_predictor.py b/src/citrine/informatics/predictors/attribute_accumulation_predictor.py index 696eed6e6..3597fbe95 100644 --- a/src/citrine/informatics/predictors/attribute_accumulation_predictor.py +++ b/src/citrine/informatics/predictors/attribute_accumulation_predictor.py @@ -11,21 +11,18 @@ class AttributeAccumulationPredictor(Resource["AttributeAccumulationPredictor"], create it when necessary. """ - attributes = _properties.List(_properties.Object(Descriptor), 'attributes') - sequential = _properties.Boolean('sequential') + attributes = _properties.List(_properties.Object(Descriptor), "attributes") + sequential = _properties.Boolean("sequential") - typ = _properties.String('type', default='AttributeAccumulation', deserializable=False) + typ = _properties.String("type", default="AttributeAccumulation", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - attributes: list[Descriptor], - sequential: bool): + def __init__( + self, name: str, *, description: str, attributes: list[Descriptor], sequential: bool + ): self.name = name self.description = description self.attributes = attributes self.sequential = sequential def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/auto_ml_predictor.py b/src/citrine/informatics/predictors/auto_ml_predictor.py index 06dd21777..8e781b321 100644 --- a/src/citrine/informatics/predictors/auto_ml_predictor.py +++ b/src/citrine/informatics/predictors/auto_ml_predictor.py @@ -5,7 +5,7 @@ from citrine.informatics.descriptors import Descriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['AutoMLPredictor', 'AutoMLEstimator'] +__all__ = ["AutoMLEstimator", "AutoMLPredictor"] class AutoMLEstimator(BaseEnumeration): @@ -51,23 +51,25 @@ class AutoMLPredictor(Resource["AutoMLPredictor"], PredictorNode): """ - inputs = _properties.List(_properties.Object(Descriptor), 'inputs') - outputs = _properties.List(_properties.Object(Descriptor), 'outputs') + inputs = _properties.List(_properties.Object(Descriptor), "inputs") + outputs = _properties.List(_properties.Object(Descriptor), "outputs") estimators = _properties.Set( _properties.Enumeration(AutoMLEstimator), - 'estimators', - default={AutoMLEstimator.RANDOM_FOREST} + "estimators", + default={AutoMLEstimator.RANDOM_FOREST}, ) - typ = _properties.String('type', default='AutoML', deserializable=False) + typ = _properties.String("type", default="AutoML", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - outputs: list[Descriptor], - inputs: list[Descriptor], - estimators: set[AutoMLEstimator] | None = None): + def __init__( + self, + name: str, + *, + description: str, + outputs: list[Descriptor], + inputs: list[Descriptor], + estimators: set[AutoMLEstimator] | None = None, + ): self.name: str = name self.description: str = description self.inputs: list[Descriptor] = inputs @@ -75,4 +77,4 @@ def __init__(self, self.outputs = outputs def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/chemical_formula_featurizer.py b/src/citrine/informatics/predictors/chemical_formula_featurizer.py index 98f235a37..dc6105be8 100644 --- a/src/citrine/informatics/predictors/chemical_formula_featurizer.py +++ b/src/citrine/informatics/predictors/chemical_formula_featurizer.py @@ -3,7 +3,7 @@ from citrine.informatics.descriptors import ChemicalFormulaDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['ChemicalFormulaFeaturizer'] +__all__ = ["ChemicalFormulaFeaturizer"] class ChemicalFormulaFeaturizer(Resource["ChemicalFormulaFeaturizer"], PredictorNode): @@ -128,21 +128,23 @@ class ChemicalFormulaFeaturizer(Resource["ChemicalFormulaFeaturizer"], Predictor """ - input_descriptor = properties.Object(ChemicalFormulaDescriptor, 'input') - features = properties.List(properties.String, 'features') - excludes = properties.List(properties.String, 'excludes', default=[]) - powers = properties.List(properties.Float, 'powers') - - typ = properties.String('type', default='ChemicalFormulaFeaturizer', deserializable=False) - - def __init__(self, - name: str, - *, - description: str, - input_descriptor: ChemicalFormulaDescriptor, - features: list[str] | None = None, - excludes: list[str] | None = None, - powers: list[float] | None = None): + input_descriptor = properties.Object(ChemicalFormulaDescriptor, "input") + features = properties.List(properties.String, "features") + excludes = properties.List(properties.String, "excludes", default=[]) + powers = properties.List(properties.Float, "powers") + + typ = properties.String("type", default="ChemicalFormulaFeaturizer", deserializable=False) + + def __init__( + self, + name: str, + *, + description: str, + input_descriptor: ChemicalFormulaDescriptor, + features: list[str] | None = None, + excludes: list[str] | None = None, + powers: list[float] | None = None, + ): self.name = name self.description = description self.input_descriptor = input_descriptor @@ -151,4 +153,4 @@ def __init__(self, self.powers = powers if powers is not None else [1.0] def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/expression_predictor.py b/src/citrine/informatics/predictors/expression_predictor.py index 0382e4ed9..187a66004 100644 --- a/src/citrine/informatics/predictors/expression_predictor.py +++ b/src/citrine/informatics/predictors/expression_predictor.py @@ -5,7 +5,7 @@ from citrine.informatics.descriptors import RealDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['ExpressionPredictor'] +__all__ = ["ExpressionPredictor"] class ExpressionPredictor(Resource["ExpressionPredictor"], PredictorNode): @@ -30,21 +30,23 @@ class ExpressionPredictor(Resource["ExpressionPredictor"], PredictorNode): """ - expression = _properties.String('expression') - output = _properties.Object(RealDescriptor, 'output') + expression = _properties.String("expression") + output = _properties.Object(RealDescriptor, "output") aliases = _properties.Mapping( - _properties.String, _properties.Object(RealDescriptor), 'aliases' + _properties.String, _properties.Object(RealDescriptor), "aliases" ) - typ = _properties.String('type', default='AnalyticExpression', deserializable=False) - - def __init__(self, - name: str, - *, - description: str, - expression: str, - output: RealDescriptor, - aliases: Mapping[str, RealDescriptor]): + typ = _properties.String("type", default="AnalyticExpression", deserializable=False) + + def __init__( + self, + name: str, + *, + description: str, + expression: str, + output: RealDescriptor, + aliases: Mapping[str, RealDescriptor], + ): self.name: str = name self.description: str = description self.expression: str = expression @@ -52,4 +54,4 @@ def __init__(self, self.aliases: Mapping[str, RealDescriptor] = aliases def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/graph_predictor.py b/src/citrine/informatics/predictors/graph_predictor.py index 3ebb4c6bf..c8bf7847f 100644 --- a/src/citrine/informatics/predictors/graph_predictor.py +++ b/src/citrine/informatics/predictors/graph_predictor.py @@ -7,16 +7,16 @@ from citrine._utils.functions import format_escaped_url from citrine.informatics.data_sources import DataSource from citrine.informatics.feature_effects import FeatureEffects +from citrine.informatics.predictors import Predictor, PredictorNode from citrine.informatics.predictors.single_predict_request import SinglePredictRequest from citrine.informatics.predictors.single_prediction import SinglePrediction -from citrine.informatics.predictors import PredictorNode, Predictor from citrine.informatics.reports import Report from citrine.resources.report import ReportResource -__all__ = ['GraphPredictor'] +__all__ = ["GraphPredictor"] -class GraphPredictor(VersionedEngineResource['GraphPredictor'], AsynchronousObject, Predictor): +class GraphPredictor(VersionedEngineResource["GraphPredictor"], AsynchronousObject, Predictor): """A predictor interface that stitches individual predictor nodes together. The GraphPredictor is the only predictor that can be registered on the Citrine Platform @@ -41,21 +41,21 @@ class GraphPredictor(VersionedEngineResource['GraphPredictor'], AsynchronousObje """ - uid = properties.Optional(properties.UUID, 'id', serializable=False) + uid = properties.Optional(properties.UUID, "id", serializable=False) """:UUID | None: Citrine Platform unique identifier""" - name = properties.String('data.name') - description = properties.Optional(properties.String(), 'data.description') - predictors = properties.List(properties.Object(PredictorNode), 'data.instance.predictors') + name = properties.String("data.name") + description = properties.Optional(properties.String(), "data.description") + predictors = properties.List(properties.Object(PredictorNode), "data.instance.predictors") training_data = properties.List( - properties.Object(DataSource), 'data.instance.training_data', default=[] + properties.Object(DataSource), "data.instance.training_data", default=[] ) version = properties.Optional( properties.Union([properties.Integer(), properties.String()]), - 'metadata.version', - serializable=False + "metadata.version", + serializable=False, ) _api_version = "v3" @@ -66,26 +66,28 @@ class GraphPredictor(VersionedEngineResource['GraphPredictor'], AsynchronousObje _succeeded_statuses = ["READY"] _failed_statuses = ["INVALID", "ERROR"] - def __init__(self, - name: str, - *, - description: str, - predictors: list[PredictorNode], - training_data: list[DataSource] | None = None): + def __init__( + self, + name: str, + *, + description: str, + predictors: list[PredictorNode], + training_data: list[DataSource] | None = None, + ): self.name: str = name self.description: str = description self.training_data: list[DataSource] = training_data or [] self.predictors: list[PredictorNode] = predictors def __str__(self): - return ''.format(self.name) + return f"" def _path(self): return format_escaped_url( - '/projects/{project_id}/predictors/{predictor_id}/versions/{version}', + "/projects/{project_id}/predictors/{predictor_id}/versions/{version}", project_id=self._project_id, predictor_id=str(self.uid), - version=self.version + version=self.version, ) @staticmethod @@ -98,15 +100,19 @@ def wrap_instance(predictor_data: dict) -> dict: "data": { "name": predictor_data.get("name", ""), "description": predictor_data.get("description", ""), - "instance": predictor_data + "instance": predictor_data, } } @property def report(self) -> Report: """Fetch the predictor report.""" - if self.uid is None or self._session is None or self._project_id is None \ - or getattr(self, "version", None) is None: + if ( + self.uid is None + or self._session is None + or self._project_id is None + or getattr(self, "version", None) is None + ): msg = "Cannot get the report for a predictor that wasn't read from the platform" raise ValueError(msg) report_resource = ReportResource(self._project_id, self._session) @@ -115,12 +121,12 @@ def report(self) -> Report: @property def feature_effects(self) -> FeatureEffects: """Retrieve the feature effects for all outputs in the predictor's training data..""" - path = self._path() + '/shapley/query' + path = self._path() + "/shapley/query" response = self._session.post_resource(path, {}, version=self._api_version) return FeatureEffects.build(response) def predict(self, predict_request: SinglePredictRequest) -> SinglePrediction: """Make a one-off prediction with this predictor.""" - path = self._path() + '/predict' + path = self._path() + "/predict" res = self._session.post_resource(path, predict_request.dump(), version=self._api_version) return SinglePrediction.build(res) diff --git a/src/citrine/informatics/predictors/ingredient_fractions_predictor.py b/src/citrine/informatics/predictors/ingredient_fractions_predictor.py index 4fcc18d8e..0bd7d6069 100644 --- a/src/citrine/informatics/predictors/ingredient_fractions_predictor.py +++ b/src/citrine/informatics/predictors/ingredient_fractions_predictor.py @@ -3,7 +3,7 @@ from citrine.informatics.descriptors import FormulationDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['IngredientFractionsPredictor'] +__all__ = ["IngredientFractionsPredictor"] class IngredientFractionsPredictor(Resource["IngredientFractionsPredictor"], PredictorNode): @@ -23,21 +23,23 @@ class IngredientFractionsPredictor(Resource["IngredientFractionsPredictor"], Pre """ - input_descriptor = _properties.Object(FormulationDescriptor, 'input') - ingredients = _properties.Set(_properties.String, 'ingredients') + input_descriptor = _properties.Object(FormulationDescriptor, "input") + ingredients = _properties.Set(_properties.String, "ingredients") - typ = _properties.String('type', default='IngredientFractions', deserializable=False) + typ = _properties.String("type", default="IngredientFractions", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - input_descriptor: FormulationDescriptor, - ingredients: set[str]): + def __init__( + self, + name: str, + *, + description: str, + input_descriptor: FormulationDescriptor, + ingredients: set[str], + ): self.name: str = name self.description: str = description self.input_descriptor: FormulationDescriptor = input_descriptor self.ingredients: set[str] = ingredients def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py b/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py index 111e57c65..b6b6a03f9 100644 --- a/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py +++ b/src/citrine/informatics/predictors/ingredients_to_formulation_predictor.py @@ -5,7 +5,7 @@ from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['IngredientsToFormulationPredictor'] +__all__ = ["IngredientsToFormulationPredictor"] class IngredientsToFormulationPredictor( @@ -29,25 +29,27 @@ class IngredientsToFormulationPredictor( """ id_to_quantity = properties.Mapping( - properties.String, properties.Object(RealDescriptor), 'id_to_quantity' + properties.String, properties.Object(RealDescriptor), "id_to_quantity" ) - labels = properties.Mapping(properties.String, properties.Set(properties.String), 'labels') - - typ = properties.String('type', default='IngredientsToSimpleMixture', deserializable=False) - - def __init__(self, - name: str, - *, - description: str, - id_to_quantity: Mapping[str, RealDescriptor], - labels: Mapping[str, set[str]]): + labels = properties.Mapping(properties.String, properties.Set(properties.String), "labels") + + typ = properties.String("type", default="IngredientsToSimpleMixture", deserializable=False) + + def __init__( + self, + name: str, + *, + description: str, + id_to_quantity: Mapping[str, RealDescriptor], + labels: Mapping[str, set[str]], + ): self.name: str = name self.description: str = description self.id_to_quantity: Mapping[str, RealDescriptor] = id_to_quantity self.labels: Mapping[str, set[str]] = labels def __str__(self): - return ''.format(self.name) + return f"" @property def output(self) -> FormulationDescriptor: diff --git a/src/citrine/informatics/predictors/label_fractions_predictor.py b/src/citrine/informatics/predictors/label_fractions_predictor.py index 6bf2bbd96..2b81b6d66 100644 --- a/src/citrine/informatics/predictors/label_fractions_predictor.py +++ b/src/citrine/informatics/predictors/label_fractions_predictor.py @@ -3,7 +3,7 @@ from citrine.informatics.descriptors import FormulationDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['LabelFractionsPredictor'] +__all__ = ["LabelFractionsPredictor"] class LabelFractionsPredictor(Resource["LabelFractionsPredictor"], PredictorNode): @@ -22,21 +22,23 @@ class LabelFractionsPredictor(Resource["LabelFractionsPredictor"], PredictorNode """ - input_descriptor = _properties.Object(FormulationDescriptor, 'input') - labels = _properties.Set(_properties.String, 'labels') + input_descriptor = _properties.Object(FormulationDescriptor, "input") + labels = _properties.Set(_properties.String, "labels") - typ = _properties.String('type', default='LabelFractions', deserializable=False) + typ = _properties.String("type", default="LabelFractions", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - input_descriptor: FormulationDescriptor, - labels: set[str]): + def __init__( + self, + name: str, + *, + description: str, + input_descriptor: FormulationDescriptor, + labels: set[str], + ): self.name: str = name self.description: str = description self.input_descriptor: FormulationDescriptor = input_descriptor self.labels: set[str] = labels def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/mean_property_predictor.py b/src/citrine/informatics/predictors/mean_property_predictor.py index 9cd0c494a..45e1aaebf 100644 --- a/src/citrine/informatics/predictors/mean_property_predictor.py +++ b/src/citrine/informatics/predictors/mean_property_predictor.py @@ -3,11 +3,13 @@ from citrine._rest.resource import Resource from citrine._serialization import properties as _properties from citrine.informatics.descriptors import ( - CategoricalDescriptor, FormulationDescriptor, RealDescriptor + CategoricalDescriptor, + FormulationDescriptor, + RealDescriptor, ) from citrine.informatics.predictors import PredictorNode -__all__ = ['MeanPropertyPredictor'] +__all__ = ["MeanPropertyPredictor"] class MeanPropertyPredictor(Resource["MeanPropertyPredictor"], PredictorNode): @@ -54,36 +56,37 @@ class MeanPropertyPredictor(Resource["MeanPropertyPredictor"], PredictorNode): """ - input_descriptor = _properties.Object(FormulationDescriptor, 'input') + input_descriptor = _properties.Object(FormulationDescriptor, "input") properties = _properties.List( _properties.Union( [_properties.Object(RealDescriptor), _properties.Object(CategoricalDescriptor)] ), - 'properties' + "properties", ) - p = _properties.Float('p') - impute_properties = _properties.Boolean('impute_properties') - label = _properties.Optional(_properties.String, 'label') + p = _properties.Float("p") + impute_properties = _properties.Boolean("impute_properties") + label = _properties.Optional(_properties.String, "label") default_properties = _properties.Optional( _properties.Mapping( - _properties.String, - _properties.Union([_properties.String, _properties.Float]) + _properties.String, _properties.Union([_properties.String, _properties.Float]) ), - 'default_properties' + "default_properties", ) - typ = _properties.String('type', default='MeanProperty', deserializable=False) + typ = _properties.String("type", default="MeanProperty", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - input_descriptor: FormulationDescriptor, - properties: list[RealDescriptor | CategoricalDescriptor], - p: float, - impute_properties: bool, - label: str | None = None, - default_properties: Mapping[str, str | float] | None = None): + def __init__( + self, + name: str, + *, + description: str, + input_descriptor: FormulationDescriptor, + properties: list[RealDescriptor | CategoricalDescriptor], + p: float, + impute_properties: bool, + label: str | None = None, + default_properties: Mapping[str, str | float] | None = None, + ): self.name: str = name self.description: str = description self.input_descriptor: FormulationDescriptor = input_descriptor @@ -94,4 +97,4 @@ def __init__(self, self.default_properties: Mapping[str, str | float] | None = default_properties def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/molecular_structure_featurizer.py b/src/citrine/informatics/predictors/molecular_structure_featurizer.py index 0b5fd2af7..f7bcd8cfe 100644 --- a/src/citrine/informatics/predictors/molecular_structure_featurizer.py +++ b/src/citrine/informatics/predictors/molecular_structure_featurizer.py @@ -7,7 +7,7 @@ from citrine.informatics.descriptors import MolecularStructureDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['MolecularStructureFeaturizer'] +__all__ = ["MolecularStructureFeaturizer"] class MolecularStructureFeaturizer(Resource["MolecularStructureFeaturizer"], PredictorNode): @@ -77,19 +77,21 @@ class MolecularStructureFeaturizer(Resource["MolecularStructureFeaturizer"], Pre """ - input_descriptor = _properties.Object(MolecularStructureDescriptor, 'descriptor') - features = _properties.List(_properties.String, 'features') - excludes = _properties.List(_properties.String, 'excludes') + input_descriptor = _properties.Object(MolecularStructureDescriptor, "descriptor") + features = _properties.List(_properties.String, "features") + excludes = _properties.List(_properties.String, "excludes") - typ = _properties.String('type', default='MoleculeFeaturizer', deserializable=False) + typ = _properties.String("type", default="MoleculeFeaturizer", deserializable=False) - def __init__(self, - name: str, - *, - description: str, - input_descriptor: MolecularStructureDescriptor, - features: list[str] | None = None, - excludes: list[str] | None = None): + def __init__( + self, + name: str, + *, + description: str, + input_descriptor: MolecularStructureDescriptor, + features: list[str] | None = None, + excludes: list[str] | None = None, + ): self.name: str = name self.description: str = description self.input_descriptor = input_descriptor @@ -97,4 +99,4 @@ def __init__(self, self.excludes = excludes if excludes is not None else [] def __str__(self): - return ''.format(self.name) + return f"" diff --git a/src/citrine/informatics/predictors/node.py b/src/citrine/informatics/predictors/node.py index dd766ec1d..11590bc9c 100644 --- a/src/citrine/informatics/predictors/node.py +++ b/src/citrine/informatics/predictors/node.py @@ -15,10 +15,10 @@ class PredictorNode(PolymorphicSerializable["PredictorNode"], Predictor): description = properties.Optional(properties.String(), "description") @classmethod - def get_type(cls, data) -> type['PredictorNode']: + def get_type(cls, data) -> type["PredictorNode"]: """Return the subtype.""" - from .auto_ml_predictor import AutoMLPredictor from .attribute_accumulation_predictor import AttributeAccumulationPredictor + from .auto_ml_predictor import AutoMLPredictor from .chemical_formula_featurizer import ChemicalFormulaFeaturizer from .expression_predictor import ExpressionPredictor from .ingredient_fractions_predictor import IngredientFractionsPredictor @@ -27,6 +27,7 @@ def get_type(cls, data) -> type['PredictorNode']: from .mean_property_predictor import MeanPropertyPredictor from .molecular_structure_featurizer import MolecularStructureFeaturizer from .simple_mixture_predictor import SimpleMixturePredictor + type_dict = { "AnalyticExpression": ExpressionPredictor, "AttributeAccumulation": AttributeAccumulationPredictor, @@ -39,11 +40,11 @@ def get_type(cls, data) -> type['PredictorNode']: "MoleculeFeaturizer": MolecularStructureFeaturizer, "SimpleMixture": SimpleMixturePredictor, } - typ = type_dict.get(data['type']) + typ = type_dict.get(data["type"]) if typ is not None: return typ else: raise ValueError( - '{} is not a valid predictor node type. ' - 'Must be in {}.'.format(data['type'], type_dict.keys()) + f"{data['type']} is not a valid predictor node type. " + f"Must be in {type_dict.keys()}." ) diff --git a/src/citrine/informatics/predictors/predictor.py b/src/citrine/informatics/predictors/predictor.py index 24090dcce..31f85f05a 100644 --- a/src/citrine/informatics/predictors/predictor.py +++ b/src/citrine/informatics/predictors/predictor.py @@ -1,4 +1,4 @@ -__all__ = ['Predictor'] +__all__ = ["Predictor"] class Predictor: diff --git a/src/citrine/informatics/predictors/simple_mixture_predictor.py b/src/citrine/informatics/predictors/simple_mixture_predictor.py index 05bb71f51..184fea8e7 100644 --- a/src/citrine/informatics/predictors/simple_mixture_predictor.py +++ b/src/citrine/informatics/predictors/simple_mixture_predictor.py @@ -3,7 +3,7 @@ from citrine.informatics.descriptors import FormulationDescriptor from citrine.informatics.predictors import PredictorNode -__all__ = ['SimpleMixturePredictor'] +__all__ = ["SimpleMixturePredictor"] class SimpleMixturePredictor(Resource["SimpleMixturePredictor"], PredictorNode): @@ -18,14 +18,14 @@ class SimpleMixturePredictor(Resource["SimpleMixturePredictor"], PredictorNode): """ - typ = properties.String('type', default='SimpleMixture', deserializable=False) + typ = properties.String("type", default="SimpleMixture", deserializable=False) def __init__(self, name: str, *, description: str): self.name: str = name self.description: str = description def __str__(self): - return ''.format(self.name) + return f"" @property def input_descriptor(self) -> FormulationDescriptor: diff --git a/src/citrine/informatics/predictors/single_predict_request.py b/src/citrine/informatics/predictors/single_predict_request.py index 7605561d0..cdf01dcc5 100644 --- a/src/citrine/informatics/predictors/single_predict_request.py +++ b/src/citrine/informatics/predictors/single_predict_request.py @@ -4,7 +4,7 @@ from citrine._serialization.serializable import Serializable from citrine.informatics.design_candidate import DesignMaterial -__all__ = ['SinglePredictRequest'] +__all__ = ["SinglePredictRequest"] class SinglePredictRequest(Serializable["SinglePredictRequest"]): @@ -13,16 +13,19 @@ class SinglePredictRequest(Serializable["SinglePredictRequest"]): This class represents a request to make a prediction against a predictor. """ - material_id = properties.UUID('material_id') - identifiers = properties.List(properties.String(), 'identifiers') - material = properties.Object(DesignMaterial, 'material') - random_seed = properties.Optional(properties.Integer, 'random_seed') + material_id = properties.UUID("material_id") + identifiers = properties.List(properties.String(), "identifiers") + material = properties.Object(DesignMaterial, "material") + random_seed = properties.Optional(properties.Integer, "random_seed") - def __init__(self, material_id: UUID, - identifiers: list[str], - material: DesignMaterial, - *, - random_seed: int | None = None): + def __init__( + self, + material_id: UUID, + identifiers: list[str], + material: DesignMaterial, + *, + random_seed: int | None = None, + ): self.material_id = material_id self.identifiers = identifiers self.material = material diff --git a/src/citrine/informatics/predictors/single_prediction.py b/src/citrine/informatics/predictors/single_prediction.py index 3680687d1..723d1d2c8 100644 --- a/src/citrine/informatics/predictors/single_prediction.py +++ b/src/citrine/informatics/predictors/single_prediction.py @@ -4,7 +4,7 @@ from citrine._serialization.serializable import Serializable from citrine.informatics.design_candidate import DesignMaterial -__all__ = ['SinglePrediction'] +__all__ = ["SinglePrediction"] class SinglePrediction(Serializable["SinglePrediction"]): @@ -13,13 +13,11 @@ class SinglePrediction(Serializable["SinglePrediction"]): This class represents the result of a prediction made using a predictor. """ - material_id = properties.UUID('material_id') - identifiers = properties.List(properties.String(), 'identifiers') - material = properties.Object(DesignMaterial, 'material') + material_id = properties.UUID("material_id") + identifiers = properties.List(properties.String(), "identifiers") + material = properties.Object(DesignMaterial, "material") - def __init__(self, material_id: UUID, - identifiers: list[str], - material: DesignMaterial): + def __init__(self, material_id: UUID, identifiers: list[str], material: DesignMaterial): self.material_id = material_id self.identifiers = identifiers self.material = material diff --git a/src/citrine/informatics/reports.py b/src/citrine/informatics/reports.py index 5d3c37508..99cf15582 100644 --- a/src/citrine/informatics/reports.py +++ b/src/citrine/informatics/reports.py @@ -1,23 +1,24 @@ """Tools for working with reports.""" + from abc import abstractmethod from collections.abc import Iterable from itertools import groupby from logging import getLogger from typing import Any, TypeVar +from citrine._rest.asynchronous_object import AsynchronousObject from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable -from citrine._rest.asynchronous_object import AsynchronousObject from citrine.informatics.descriptors import Descriptor from citrine.informatics.predictor_evaluation_result import ResponseMetrics -SelfType = TypeVar('SelfType', bound='Report') +SelfType = TypeVar("SelfType", bound="Report") logger = getLogger(__name__) -class Report(PolymorphicSerializable['Report'], AsynchronousObject): +class Report(PolymorphicSerializable["Report"], AsynchronousObject): """A Citrine Report contains information related to a module. Abstract type that returns the proper type given a serialized dict. @@ -51,17 +52,18 @@ class FeatureImportanceReport(Serializable["FeatureImportanceReport"]): should not be user-instantiated. """ - output_key = properties.String('response_key') + output_key = properties.String("response_key") """:str: output descriptor key for which these feature importances are applicable""" - importances = properties.Mapping(keys_type=properties.String, values_type=properties.Float, - serialization_path='importances') + importances = properties.Mapping( + keys_type=properties.String, values_type=properties.Float, serialization_path="importances" + ) """:dict[str, float]: map from feature name to its importance""" def __init__(self): pass # pragma: no cover def __str__(self): - return "".format(self.output_key) # pragma: no cover + return f"" # pragma: no cover class ModelEvaluationResult(Serializable["ModelEvaluationResult"]): @@ -71,18 +73,16 @@ class ModelEvaluationResult(Serializable["ModelEvaluationResult"]): and should not be user-instantiated. """ - model_settings = properties.Raw('model_settings') + model_settings = properties.Raw("model_settings") _response_results = properties.Mapping( - properties.String, - properties.Object(ResponseMetrics), - "response_results" + properties.String, properties.Object(ResponseMetrics), "response_results" ) def __init__(self): pass # pragma: no cover def __str__(self): - return '' # pragma: no cover + return "" # pragma: no cover def __getitem__(self, item): return self._response_results[item] @@ -103,51 +103,49 @@ class ModelSelectionReport(Serializable["ModelSelectionReport"]): should not be user-instantiated. """ - n_folds = properties.Integer('n_folds') + n_folds = properties.Integer("n_folds") evaluation_results = properties.List( - properties.Object(ModelEvaluationResult), - "evaluation_results" + properties.Object(ModelEvaluationResult), "evaluation_results" ) def __init__(self): pass # pragma: no cover def __str__(self): - return '' # pragma: no cover + return "" # pragma: no cover -class ModelSummary(Serializable['ModelSummary']): +class ModelSummary(Serializable["ModelSummary"]): """Summary of information about a single model in a predictor. ModelSummary objects are constructed from saved models and should not be user-instantiated. """ - name = properties.String('name') + name = properties.String("name") """:str: the name of the model""" - type_ = properties.String('type') + type_ = properties.String("type") """:str: the type of the model (e.g., "ML Model", "Featurizer", etc.)""" inputs = properties.List( - properties.Union([properties.Object(Descriptor), properties.String()]), - 'inputs' + properties.Union([properties.Object(Descriptor), properties.String()]), "inputs" ) """:list[Descriptor]: list of input descriptors""" outputs = properties.List( - properties.Union([properties.Object(Descriptor), properties.String()]), - 'outputs' + properties.Union([properties.Object(Descriptor), properties.String()]), "outputs" ) """:list[Descriptor]: list of output descriptors""" - model_settings = properties.Raw('model_settings') + model_settings = properties.Raw("model_settings") """:dict: model settings, as a dictionary (keys depend on the model type)""" feature_importances = properties.List( - properties.Object(FeatureImportanceReport), 'feature_importances') + properties.Object(FeatureImportanceReport), "feature_importances" + ) """:list[FeatureImportanceReport]: feature importance reports for each output""" selection_summary = properties.Optional( properties.Object(ModelSelectionReport), "selection_summary" ) """:ModelSelectionReport | None: optional results of AutoML model selection""" - predictor_name = properties.String('predictor_configuration_name', default='') + predictor_name = properties.String("predictor_configuration_name", default="") """:str: the name of the predictor that created this model""" - predictor_uid = properties.Optional(properties.UUID(), 'predictor_configuration_uid') + predictor_uid = properties.Optional(properties.UUID(), "predictor_configuration_uid") """:UUID | None: the unique Citrine id of the predictor that created this model""" training_data_count = properties.Optional(properties.Integer, "training_data_count") """:int: Number of rows in the training data for the model, if applicable.""" @@ -156,10 +154,10 @@ def __init__(self): pass # pragma: no cover def __str__(self): - return ''.format(self.name) # pragma: no cover + return f"" # pragma: no cover -class PredictorReport(Serializable['PredictorReport'], Report): +class PredictorReport(Serializable["PredictorReport"], Report): """The performance metrics corresponding to a predictor. PredictorReport objects are constructed from saved models and should not be user-instantiated. @@ -169,13 +167,13 @@ class PredictorReport(Serializable['PredictorReport'], Report): _succeeded_statuses = ["OK"] _failed_statuses = ["ERROR"] - uid = properties.Optional(properties.UUID, 'id', serializable=False) + uid = properties.Optional(properties.UUID, "id", serializable=False) """:UUID: Unique Citrine id of the predictor report""" - status = properties.String('status') + status = properties.String("status") """:str: The status of the report. Possible statuses are PENDING, ERROR, and OK.""" - descriptors = properties.List(properties.Object(Descriptor), 'report.descriptors', default=[]) + descriptors = properties.List(properties.Object(Descriptor), "report.descriptors", default=[]) """:list[Descriptor]: All descriptors that appear in the predictor""" - model_summaries = properties.List(properties.Object(ModelSummary), 'report.models', default=[]) + model_summaries = properties.List(properties.Object(ModelSummary), "report.models", default=[]) """:list[ModelSummary]: Summaries of all models in the predictor""" def __init__(self): @@ -202,14 +200,18 @@ def _fill_out_descriptors(self): try: model.inputs[j] = descriptor_map[input_key] except KeyError: - raise RuntimeError("Model {} contains input \'{}\', but no descriptor found " - "with that key".format(model.name, input_key)) + raise RuntimeError( + f"Model {model.name} contains input '{input_key}', but no descriptor " + "found with that key" + ) for j, output_key in enumerate(model.outputs): try: model.outputs[j] = descriptor_map[output_key] except KeyError: - raise RuntimeError("Model {} contains output \'{}\', but no descriptor found " - "with that key".format(model.name, output_key)) + raise RuntimeError( + f"Model {model.name} contains output '{output_key}', but no descriptor " + "found with that key" + ) @staticmethod def _get_sole_descriptor(it: Iterable): @@ -227,9 +229,11 @@ def _get_sole_descriptor(it: Iterable): as_list = list(it) if len(as_list) > 1: serialized_descriptors = [d.dump() for d in as_list] - logger.warning("Warning: found multiple descriptors with the key \'{}\', arbitrarily " - "selecting the first one. The descriptors are: {}" - .format(as_list[0].key, serialized_descriptors)) + logger.warning( + "Warning: found multiple descriptors with the key " + f"'{as_list[0].key}', arbitrarily selecting the first one. " + f"The descriptors are: {serialized_descriptors}" + ) return as_list[0] @staticmethod @@ -241,14 +245,15 @@ def _collapse_model_settings(raw_settings: dict[str, Any]): top-level dictionary with keys given by "name" and values given by "value." """ + def _recurse_model_settings(settings: dict[str, str], list_or_dict): """Recursively traverse the model settings, adding name-value pairs to dictionary.""" if isinstance(list_or_dict, list): for setting in list_or_dict: _recurse_model_settings(settings, setting) elif isinstance(list_or_dict, dict): - settings[list_or_dict['name']] = list_or_dict['value'] - _recurse_model_settings(settings, list_or_dict['children']) + settings[list_or_dict["name"]] = list_or_dict["value"] + _recurse_model_settings(settings, list_or_dict["children"]) collapsed = dict() _recurse_model_settings(collapsed, raw_settings) diff --git a/src/citrine/informatics/scores.py b/src/citrine/informatics/scores.py index 2caa89b8f..8963788c5 100644 --- a/src/citrine/informatics/scores.py +++ b/src/citrine/informatics/scores.py @@ -6,30 +6,26 @@ from citrine.informatics.constraints import Constraint from citrine.informatics.objectives import Objective -__all__ = ['Score', 'LIScore', 'EIScore', 'EVScore'] +__all__ = ["EIScore", "EVScore", "LIScore", "Score"] -class Score(PolymorphicSerializable['Score']): +class Score(PolymorphicSerializable["Score"]): """A Score is used to rank materials according to objectives and constraints. Abstract type that returns the proper type given a serialized dict. """ - _name = properties.String('name') - _description = properties.String('description') + _name = properties.String("name") + _description = properties.String("description") @classmethod def get_type(cls, data): """Return the subtype.""" - return { - 'MLI': LIScore, - 'MEI': EIScore, - 'MEV': EVScore - }[data['type']] + return {"MLI": LIScore, "MEI": EIScore, "MEV": EVScore}[data["type"]] -class LIScore(Serializable['LIScore'], Score): +class LIScore(Serializable["LIScore"], Score): """Evaluates the likelihood of scoring better than some baselines for given objectives. Parameters @@ -45,15 +41,18 @@ class LIScore(Serializable['LIScore'], Score): """ - baselines = properties.List(properties.Float, 'baselines') - objectives = properties.List(properties.Object(Objective), 'objectives') - constraints = properties.List(properties.Object(Constraint), 'constraints') - typ = properties.String('type', default='MLI') - - def __init__(self, *, - objectives: list[Objective], - baselines: list[float], - constraints: list[Constraint] | None = None): + baselines = properties.List(properties.Float, "baselines") + objectives = properties.List(properties.Object(Objective), "objectives") + constraints = properties.List(properties.Object(Constraint), "constraints") + typ = properties.String("type", default="MLI") + + def __init__( + self, + *, + objectives: list[Objective], + baselines: list[float], + constraints: list[Constraint] | None = None, + ): self.objectives: list[Objective] = objectives self.baselines: list[float] = baselines self.constraints: list[Constraint] = constraints or [] @@ -61,10 +60,10 @@ def __init__(self, *, self._description = "" def __str__(self): - return '' + return "" -class EIScore(Serializable['EIScore'], Score): +class EIScore(Serializable["EIScore"], Score): """ Evaluates the expected magnitude of improvement beyond baselines for a given objective. @@ -80,15 +79,18 @@ class EIScore(Serializable['EIScore'], Score): """ - baselines = properties.List(properties.Float, 'baselines') - objectives = properties.List(properties.Object(Objective), 'objectives') - constraints = properties.List(properties.Object(Constraint), 'constraints') - typ = properties.String('type', default='MEI') - - def __init__(self, *, - objectives: list[Objective], - baselines: list[float], - constraints: list[Constraint] | None = None): + baselines = properties.List(properties.Float, "baselines") + objectives = properties.List(properties.Object(Objective), "objectives") + constraints = properties.List(properties.Object(Constraint), "constraints") + typ = properties.String("type", default="MEI") + + def __init__( + self, + *, + objectives: list[Objective], + baselines: list[float], + constraints: list[Constraint] | None = None, + ): self.objectives: list[Objective] = objectives self.baselines: list[float] = baselines self.constraints: list[Constraint] = constraints or [] @@ -96,10 +98,10 @@ def __init__(self, *, self._description = "" def __str__(self): - return '' + return "" -class EVScore(Serializable['EVScore'], Score): +class EVScore(Serializable["EVScore"], Score): """ Evaluates the expected value for given objectives. @@ -115,17 +117,17 @@ class EVScore(Serializable['EVScore'], Score): """ - objectives = properties.List(properties.Object(Objective), 'objectives') - constraints = properties.List(properties.Object(Constraint), 'constraints') - typ = properties.String('type', default='MEV') + objectives = properties.List(properties.Object(Objective), "objectives") + constraints = properties.List(properties.Object(Constraint), "constraints") + typ = properties.String("type", default="MEV") - def __init__(self, *, - objectives: list[Objective], - constraints: list[Constraint] | None = None): + def __init__( + self, *, objectives: list[Objective], constraints: list[Constraint] | None = None + ): self.objectives: list[Objective] = objectives self.constraints: list[Constraint] = constraints or [] self._name = "Expected Value" self._description = "" def __str__(self): - return '' + return "" diff --git a/src/citrine/informatics/workflows/analysis_workflow.py b/src/citrine/informatics/workflows/analysis_workflow.py index 383cc67d0..e56ab0612 100644 --- a/src/citrine/informatics/workflows/analysis_workflow.py +++ b/src/citrine/informatics/workflows/analysis_workflow.py @@ -3,40 +3,43 @@ from citrine._rest.engine_resource import EngineResourceWithoutStatus from citrine._rest.resource import Resource from citrine._serialization import properties -from citrine.informatics.workflows.workflow import Workflow from citrine.gemd_queries.gemd_query import GemdQuery +from citrine.informatics.workflows.workflow import Workflow -class LatestBuild(Resource['LatestBuild']): +class LatestBuild(Resource["LatestBuild"]): """Info on the latest analysis workflow build.""" - status = properties.Optional(properties.String, 'status', serializable=False) - failures = properties.List(properties.String, 'failure_reason', default=[], serializable=False) - query = properties.Optional(properties.Object(GemdQuery), 'query', serializable=False) + status = properties.Optional(properties.String, "status", serializable=False) + failures = properties.List(properties.String, "failure_reason", default=[], serializable=False) + query = properties.Optional(properties.Object(GemdQuery), "query", serializable=False) -class AnalysisWorkflow(EngineResourceWithoutStatus['AnalysisWorkflow'], Workflow): +class AnalysisWorkflow(EngineResourceWithoutStatus["AnalysisWorkflow"], Workflow): """An analysis workflow stored on the platform. Note that plots are not fully supported. They're captured as raw JSON in order to facilitate cloning an existing workflow, but no facilities are provided to validate them in the client. """ - uid = properties.UUID('id', serializable=False) - name = properties.String('data.name') - description = properties.String('data.description') - snapshot_id = properties.Optional(properties.UUID, 'data.snapshot_id') - _plots = properties.List(properties.Raw, 'data.plots', default=[]) - - latest_build = properties.Optional(properties.Object(LatestBuild), 'metadata.latest_build', - serializable=False) - - def __init__(self, - *, - name: str, - description: str, - snapshot_id: UUID | str | None = None, - plots: list[dict] = []): + uid = properties.UUID("id", serializable=False) + name = properties.String("data.name") + description = properties.String("data.description") + snapshot_id = properties.Optional(properties.UUID, "data.snapshot_id") + _plots = properties.List(properties.Raw, "data.plots", default=[]) + + latest_build = properties.Optional( + properties.Object(LatestBuild), "metadata.latest_build", serializable=False + ) + + def __init__( + self, + *, + name: str, + description: str, + snapshot_id: UUID | str | None = None, + plots: list[dict] = [], + ): self.name = name self.description = description self.snapshot_id = snapshot_id @@ -52,7 +55,7 @@ def _post_dump(self, data: dict) -> dict: return super()._post_dump(data) -class AnalysisWorkflowUpdatePayload(Resource['AnalysisWorkflowUpdatePayload']): +class AnalysisWorkflowUpdatePayload(Resource["AnalysisWorkflowUpdatePayload"]): """An object capturing the analysis workflow upload payload. Making this a separate payload makes it explicit that you can only update name and description. @@ -60,15 +63,13 @@ class AnalysisWorkflowUpdatePayload(Resource['AnalysisWorkflowUpdatePayload']): changing the other. """ - uid = properties.UUID('id', serializable=False) - name = properties.Optional(properties.String, 'name') - description = properties.Optional(properties.String, 'description') + uid = properties.UUID("id", serializable=False) + name = properties.Optional(properties.String, "name") + description = properties.Optional(properties.String, "description") - def __init__(self, - uid: UUID | str, - *, - name: str | None = None, - description: str | None = None): + def __init__( + self, uid: UUID | str, *, name: str | None = None, description: str | None = None + ): self.uid = uid self.name = name self.description = description diff --git a/src/citrine/informatics/workflows/design_workflow.py b/src/citrine/informatics/workflows/design_workflow.py index 651679b8e..b46238223 100644 --- a/src/citrine/informatics/workflows/design_workflow.py +++ b/src/citrine/informatics/workflows/design_workflow.py @@ -1,16 +1,16 @@ from uuid import UUID +from citrine._rest.ai_resource_metadata import AIResourceMetadata from citrine._rest.resource import Resource from citrine._serialization import properties from citrine.informatics.data_sources import DataSource from citrine.informatics.workflows.workflow import Workflow from citrine.resources.design_execution import DesignExecutionCollection -from citrine._rest.ai_resource_metadata import AIResourceMetadata -__all__ = ['DesignWorkflow'] +__all__ = ["DesignWorkflow"] -class DesignWorkflow(Resource['DesignWorkflow'], Workflow, AIResourceMetadata): +class DesignWorkflow(Resource["DesignWorkflow"], Workflow, AIResourceMetadata): """Object that generates scored materials that may approach higher values of the score. Parameters @@ -28,30 +28,33 @@ class DesignWorkflow(Resource['DesignWorkflow'], Workflow, AIResourceMetadata): """ - design_space_id = properties.Optional(properties.UUID, 'design_space_id') - predictor_id = properties.Optional(properties.UUID, 'predictor_id') + design_space_id = properties.Optional(properties.UUID, "design_space_id") + predictor_id = properties.Optional(properties.UUID, "predictor_id") predictor_version = properties.Optional( - properties.Union([properties.Integer, properties.String]), 'predictor_version') - branch_root_id: UUID | None = properties.Optional(properties.UUID, 'branch_root_id') + properties.Union([properties.Integer, properties.String]), "predictor_version" + ) + branch_root_id: UUID | None = properties.Optional(properties.UUID, "branch_root_id") """:UUID | None: Root ID of the branch that contains this workflow.""" - branch_version: int | None = properties.Optional(properties.Integer, 'branch_version') + branch_version: int | None = properties.Optional(properties.Integer, "branch_version") """:int | None: Version number of the branch that contains this workflow.""" data_source = properties.Optional(properties.Object(DataSource), "data_source") - status_description = properties.String('status_description', serializable=False) + status_description = properties.String("status_description", serializable=False) """:str: more detailed description of the workflow's status""" - typ = properties.String('type', default='DesignWorkflow', deserializable=False) - - _branch_id: UUID | None = properties.Optional(properties.UUID, 'branch_id', serializable=False) - - def __init__(self, - name: str, - *, - design_space_id: UUID | None = None, - predictor_id: UUID | None = None, - predictor_version: int | str | None = None, - data_source: DataSource | None = None, - description: str | None = None): + typ = properties.String("type", default="DesignWorkflow", deserializable=False) + + _branch_id: UUID | None = properties.Optional(properties.UUID, "branch_id", serializable=False) + + def __init__( + self, + name: str, + *, + design_space_id: UUID | None = None, + predictor_id: UUID | None = None, + predictor_version: int | str | None = None, + data_source: DataSource | None = None, + description: str | None = None, + ): self.name = name self.design_space_id = design_space_id self.predictor_id = predictor_id @@ -60,7 +63,7 @@ def __init__(self, self.description = description def __str__(self): - return ''.format(self.name) + return f"" @classmethod def _pre_build(cls, data: dict) -> dict: @@ -82,10 +85,11 @@ def _post_dump(self, data: dict) -> dict: @property def design_executions(self) -> DesignExecutionCollection: """Return a resource representing all visible executions of this workflow.""" - if getattr(self, 'project_id', None) is None: - raise AttributeError('Cannot initialize execution without project reference!') + if getattr(self, "project_id", None) is None: + raise AttributeError("Cannot initialize execution without project reference!") return DesignExecutionCollection( - project_id=self.project_id, session=self._session, workflow_id=self.uid) + project_id=self.project_id, session=self._session, workflow_id=self.uid + ) @property def data_source_id(self) -> str | None: diff --git a/src/citrine/informatics/workflows/workflow.py b/src/citrine/informatics/workflows/workflow.py index 101d07b36..19f736866 100644 --- a/src/citrine/informatics/workflows/workflow.py +++ b/src/citrine/informatics/workflows/workflow.py @@ -1,12 +1,12 @@ """Tools for working with workflow resources.""" + from uuid import UUID from citrine._rest.asynchronous_object import AsynchronousObject -from citrine._session import Session from citrine._serialization import properties +from citrine._session import Session - -__all__ = ['Workflow'] +__all__ = ["Workflow"] class Workflow(AsynchronousObject): @@ -26,7 +26,7 @@ class Workflow(AsynchronousObject): project_id: UUID | None = None """:UUID | None: Unique ID of the project that contains this workflow.""" - name = properties.String('name') - description = properties.Optional(properties.String, 'description') - uid = properties.Optional(properties.UUID, 'id', serializable=False) + name = properties.String("name") + description = properties.Optional(properties.String, "description") + uid = properties.Optional(properties.UUID, "id", serializable=False) """:UUID | None: Citrine Platform unique identifier""" diff --git a/src/citrine/jobs/job.py b/src/citrine/jobs/job.py index e6af018fe..a370dc77e 100644 --- a/src/citrine/jobs/job.py +++ b/src/citrine/jobs/job.py @@ -1,19 +1,21 @@ -from gemd.enumeration.base_enumeration import BaseEnumeration from logging import getLogger -from time import time, sleep +from time import sleep, time from uuid import UUID +from gemd.enumeration.base_enumeration import BaseEnumeration + from citrine._rest.resource import Resource -from citrine._serialization.properties import Set as PropertySet, String, Object from citrine._serialization import properties +from citrine._serialization.properties import Object, String +from citrine._serialization.properties import Set as PropertySet from citrine._session import Session from citrine._utils.functions import format_escaped_url -from citrine.exceptions import PollingTimeoutError, JobFailureError +from citrine.exceptions import JobFailureError, PollingTimeoutError logger = getLogger(__name__) -class JobSubmissionResponse(Resource['JobSubmissionResponse']): +class JobSubmissionResponse(Resource["JobSubmissionResponse"]): """A response to a submit-job request for the job submission framework. This is returned as a successful response from the remote service. @@ -33,7 +35,7 @@ class JobStatus(BaseEnumeration): FAILURE = "Failure" -class TaskNode(Resource['TaskNode']): +class TaskNode(Resource["TaskNode"]): """Individual task status. The TaskNode describes a component of an overall job. @@ -60,7 +62,7 @@ def status(self, value: JobStatus | str) -> None: self._status = value -class JobStatusResponse(Resource['JobStatusResponse']): +class JobStatusResponse(Resource["JobStatusResponse"]): """A response to a job status check. The JobStatusResponse summarizes the status for the entire job. @@ -72,7 +74,7 @@ class JobStatusResponse(Resource['JobStatusResponse']): """:str: The status of the job. One of "Running", "Success", or "Failure".""" tasks = properties.List(Object(TaskNode), "tasks") """:list[TaskNode]: all of the constituent task required to complete this job""" - output = properties.Optional(properties.Mapping(String, String), 'output') + output = properties.Optional(properties.Mapping(String, String), "output") """:dict[str, str] | None: job output properties and results""" @property @@ -85,20 +87,23 @@ def status(self, value: JobStatus | str) -> None: if resolved := JobStatus.from_str(value, exception=True): valid = [JobStatus.RUNNING, JobStatus.SUCCESS, JobStatus.FAILURE] if resolved not in valid: - raise ValueError(f"{value} is not a valid JobStatus for a JobStatusResponse; " - f"valid choices are {[x for x in valid]}") + raise ValueError( + f"{value} is not a valid JobStatus for a JobStatusResponse; " + f"valid choices are {[x for x in valid]}" + ) self._status = value -def _poll_for_job_completion(session: Session, - job: JobSubmissionResponse | UUID | str, - *, - team_id: UUID | str, - timeout: float = 2 * 60, - polling_delay: float = 2.0, - raise_errors: bool = True, - ) -> JobStatusResponse: +def _poll_for_job_completion( + session: Session, + job: JobSubmissionResponse | UUID | str, + *, + team_id: UUID | str, + timeout: float = 2 * 60, + polling_delay: float = 2.0, + raise_errors: bool = True, +) -> JobStatusResponse: """ Polls for job completion given a timeout. @@ -129,8 +134,8 @@ def _poll_for_job_completion(session: Session, job_id = job.job_id else: job_id = job # pragma: no cover - path = format_escaped_url('teams/{}/execution/job-status', team_id) - params = {'job_id': job_id} + path = format_escaped_url("teams/{}/execution/job-status", team_id) + params = {"job_id": job_id} start_time = time() while True: response = session.get_resource(path=path, params=params) @@ -139,17 +144,19 @@ def _poll_for_job_completion(session: Session, break elif time() - start_time < timeout: logger.info( - f'Job still in progress, polling status again in {polling_delay:.2f} seconds.' + f"Job still in progress, polling status again in {polling_delay:.2f} seconds." ) sleep(polling_delay) else: - logger.error(f'Job exceeded user timeout of {timeout} seconds. ' - f'Note job on server is unaffected by this timeout.') - logger.debug('Last status: {}'.format(status.dump())) - raise PollingTimeoutError('Job {} timed out.'.format(job_id)) + logger.error( + f"Job exceeded user timeout of {timeout} seconds. " + f"Note job on server is unaffected by this timeout." + ) + logger.debug(f"Last status: {status.dump()}") + raise PollingTimeoutError(f"Job {job_id} timed out.") if status.status == JobStatus.FAILURE: - logger.debug(f'Job terminated with Failure status: {status.dump()}') + logger.debug(f"Job terminated with Failure status: {status.dump()}") if raise_errors: failure_reasons = [] for task in status.tasks: @@ -157,9 +164,10 @@ def _poll_for_job_completion(session: Session, logger.error(f'Task {task.id} failed with reason "{task.failure_reason}"') failure_reasons.append(task.failure_reason) raise JobFailureError( - message=f'Job {job_id} terminated with Failure status. ' - f'Failure reasons: {failure_reasons}', + message=f"Job {job_id} terminated with Failure status. " + f"Failure reasons: {failure_reasons}", job_id=job_id, - failure_reasons=failure_reasons) + failure_reasons=failure_reasons, + ) return status diff --git a/src/citrine/jobs/waiting.py b/src/citrine/jobs/waiting.py index 1c048efc4..efbe60965 100644 --- a/src/citrine/jobs/waiting.py +++ b/src/citrine/jobs/waiting.py @@ -1,32 +1,28 @@ import time from pprint import pprint -from citrine._rest.collection import Collection from citrine._rest.asynchronous_object import AsynchronousObject +from citrine._rest.collection import Collection +from citrine.informatics.executions import PredictorEvaluation from citrine.informatics.executions.design_execution import DesignExecution from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution -from citrine.informatics.executions import PredictorEvaluation - -ExecutionType = PredictorEvaluation \ - | DesignExecution \ - | GenerativeDesignExecution \ - | SampleDesignSpaceExecution +ExecutionType = ( + PredictorEvaluation | DesignExecution | GenerativeDesignExecution | SampleDesignSpaceExecution +) class ConditionTimeoutError(RuntimeError): """Error that is raised when timeout is reached but the checked condition is still False.""" - pass - def _print_string_status( status: str, start_time: float, line_start: str = "", line_end: str = "\r" ): print( - "{}Status = {:<25}Elapsed time".format(line_start, status), - " = {}s".format(str(int(time.time() - start_time)).rjust(3)), + f"{line_start}Status = {status:<25}Elapsed time", + f" = {str(int(time.time() - start_time)).rjust(3)}s", end=line_end, ) @@ -37,7 +33,7 @@ def wait_for_asynchronous_object( collection: Collection[AsynchronousObject], print_status_info: bool = False, timeout: float = 1800.0, - interval: float = 3.0 + interval: float = 3.0, ) -> AsynchronousObject: """ Wait until an asynchronous object has finished. @@ -80,13 +76,11 @@ def is_finished(): time.sleep(interval) if not is_finished(): raise ConditionTimeoutError( - "Timeout of {timeout_length} seconds " - "reached, but task {uid} is still in progress".format( - timeout_length=timeout, uid=resource.uid) + f"Timeout of {timeout} seconds reached, but task {resource.uid} is still in progress" ) current_resource = collection.get(resource.uid) - if print_status_info and hasattr(current_resource, 'status_detail'): + if print_status_info and hasattr(current_resource, "status_detail"): print("\nStatus info:") pprint([detail.msg for detail in current_resource.status_detail]) return current_resource @@ -127,9 +121,13 @@ def wait_while_validating( If fails to validate within timeout """ - return wait_for_asynchronous_object(resource=module, collection=collection, - print_status_info=print_status_info, timeout=timeout, - interval=interval) + return wait_for_asynchronous_object( + resource=module, + collection=collection, + print_status_info=print_status_info, + timeout=timeout, + interval=interval, + ) def wait_while_executing( @@ -138,7 +136,7 @@ def wait_while_executing( execution: ExecutionType, print_status_info: bool = False, timeout: float = 1800.0, - interval: float = 3.0 + interval: float = 3.0, ) -> ExecutionType: """ Wait until execution is finished. @@ -167,6 +165,10 @@ def wait_while_executing( If fails to finish execution within timeout """ - return wait_for_asynchronous_object(resource=execution, collection=collection, - print_status_info=print_status_info, timeout=timeout, - interval=interval) + return wait_for_asynchronous_object( + resource=execution, + collection=collection, + print_status_info=print_status_info, + timeout=timeout, + interval=interval, + ) diff --git a/src/citrine/resources/_default_labels.py b/src/citrine/resources/_default_labels.py index 3169ac0cf..682e949dd 100644 --- a/src/citrine/resources/_default_labels.py +++ b/src/citrine/resources/_default_labels.py @@ -1,6 +1,6 @@ from citrine.resources.data_concepts import CITRINE_TAG_PREFIX -_CITRINE_DEFAULT_LABEL_PREFIX = f'{CITRINE_TAG_PREFIX}::mat_label' +_CITRINE_DEFAULT_LABEL_PREFIX = f"{CITRINE_TAG_PREFIX}::mat_label" def _inject_default_label_tags( @@ -9,9 +9,7 @@ def _inject_default_label_tags( if default_labels is None: all_tags = original_tags else: - labels_as_tags = [ - f"{_CITRINE_DEFAULT_LABEL_PREFIX}::{label}" for label in default_labels - ] + labels_as_tags = [f"{_CITRINE_DEFAULT_LABEL_PREFIX}::{label}" for label in default_labels] if original_tags is None: all_tags = labels_as_tags else: diff --git a/src/citrine/resources/analysis_workflow.py b/src/citrine/resources/analysis_workflow.py index 9571d2007..ab3bf670b 100644 --- a/src/citrine/resources/analysis_workflow.py +++ b/src/citrine/resources/analysis_workflow.py @@ -2,10 +2,12 @@ from collections.abc import Iterator from uuid import UUID -from citrine.informatics.workflows.analysis_workflow import AnalysisWorkflow, \ - AnalysisWorkflowUpdatePayload from citrine._rest.collection import Collection from citrine._session import Session +from citrine.informatics.workflows.analysis_workflow import ( + AnalysisWorkflow, + AnalysisWorkflowUpdatePayload, +) class AnalysisWorkflowCollection(Collection[AnalysisWorkflow]): @@ -18,11 +20,11 @@ class AnalysisWorkflowCollection(Collection[AnalysisWorkflow]): """ - _api_version = 'v1' - _path_template = '/teams/{team_id}/analysis-workflows' + _api_version = "v1" + _path_template = "/teams/{team_id}/analysis-workflows" _individual_key = None _resource = AnalysisWorkflow - _collection_key = 'response' + _collection_key = "response" def __init__(self, session: Session, *, team_id: UUID): self.session = session @@ -61,9 +63,11 @@ def list(self, *, per_page: int = 20) -> Iterator[AnalysisWorkflow]: def _list_with_params(self, *, per_page: int, **kwargs) -> Iterator[AnalysisWorkflow]: page_fetcher = functools.partial(self._fetch_page, additional_params=kwargs) - return self._paginator.paginate(page_fetcher=page_fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=page_fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def archive(self, uid: UUID | str) -> AnalysisWorkflow: """Archive an analysis workflow, hiding it from default listings.""" @@ -77,11 +81,9 @@ def restore(self, uid: UUID | str) -> AnalysisWorkflow: entity = self.session.put_resource(url, {}, version=self._api_version) return self.build(entity) - def update(self, - uid: UUID | str, - *, - name: str | None = None, - description: str | None = None) -> AnalysisWorkflow: + def update( + self, uid: UUID | str, *, name: str | None = None, description: str | None = None + ) -> AnalysisWorkflow: """Update the name and/or description of the analysis workflow.""" aw_update = AnalysisWorkflowUpdatePayload(uid=uid, name=name, description=description) return super().update(aw_update) diff --git a/src/citrine/resources/attribute_templates.py b/src/citrine/resources/attribute_templates.py index 069e41df8..468291434 100644 --- a/src/citrine/resources/attribute_templates.py +++ b/src/citrine/resources/attribute_templates.py @@ -1,10 +1,12 @@ """Top-level class for all attribute template objects and collections thereof.""" + from abc import ABC from typing import TypeVar -from citrine._serialization.properties import Object, Optional, String -from gemd.entity.template.attribute_template import AttributeTemplate as GEMDAttributeTemplate from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.template.attribute_template import AttributeTemplate as GEMDAttributeTemplate + +from citrine._serialization.properties import Object, Optional, String from citrine.resources.templates import Template, TemplateCollection @@ -15,9 +17,9 @@ class AttributeTemplate(Template, GEMDAttributeTemplate, ABC): AttributeTemplate must be extended along with `Resource` """ - name = String('name') - description = Optional(String(), 'description') - bounds = Object(BaseBounds, 'bounds', override=True) + name = String("name") + description = Optional(String(), "description") + bounds = Object(BaseBounds, "bounds", override=True) AttributeTemplateResourceType = TypeVar("AttributeTemplateResourceType", bound="AttributeTemplate") diff --git a/src/citrine/resources/audit_info.py b/src/citrine/resources/audit_info.py index 2ff82a55c..4a6d17444 100644 --- a/src/citrine/resources/audit_info.py +++ b/src/citrine/resources/audit_info.py @@ -1,18 +1,19 @@ -from citrine._serialization.serializable import Serializable -from citrine._serialization import properties from gemd.entity.dict_serializable import DictSerializable +from citrine._serialization import properties +from citrine._serialization.serializable import Serializable + class AuditInfo(Serializable, DictSerializable, typ="audit_info"): """Model that holds audit metadata. AuditInfo objects should not be created by the user.""" - created_by = properties.Optional(properties.UUID, 'created_by') + created_by = properties.Optional(properties.UUID, "created_by") """:UUID | None: ID of the user who created the object""" - created_at = properties.Optional(properties.Datetime, 'created_at') + created_at = properties.Optional(properties.Datetime, "created_at") """:datetime | None: Time, in ms since epoch, at which the object was created""" - updated_by = properties.Optional(properties.UUID, 'updated_by') + updated_by = properties.Optional(properties.UUID, "updated_by") """:UUID | None: ID of the user who most recently updated the object""" - updated_at = properties.Optional(properties.Datetime, 'updated_at') + updated_at = properties.Optional(properties.Datetime, "updated_at") """:datetime | None: Time, in ms since epoch, at which the object was most recently updated""" @@ -20,18 +21,19 @@ def __init__(self): pass # pragma: no cover def __repr__(self): - return 'Created by: {!r}\nCreated at: {!r}\nUpdated by: {!r}\nUpdated at: {!r}'.format( - self.created_by, self.created_at, self.updated_by, self.updated_at + return ( + f"Created by: {self.created_by!r}\n" + f"Created at: {self.created_at!r}\n" + f"Updated by: {self.updated_by!r}\n" + f"Updated at: {self.updated_at!r}" ) def __str__(self): - create_str = 'Created by user {} at time {}'.format( - self.created_by, self.created_at) + create_str = f"Created by user {self.created_by} at time {self.created_at}" if self.updated_by is not None or self.updated_at is not None: - update_str = '\nUpdated by user {} at time {}'.format( - self.updated_by, self.updated_at) + update_str = f"\nUpdated by user {self.updated_by} at time {self.updated_at}" else: - update_str = '' + update_str = "" return create_str + update_str def __eq__(self, other): diff --git a/src/citrine/resources/branch.py b/src/citrine/resources/branch.py index dccd0742e..46e641f6e 100644 --- a/src/citrine/resources/branch.py +++ b/src/citrine/resources/branch.py @@ -10,49 +10,49 @@ from citrine.resources.data_version_update import BranchDataUpdate, NextBranchVersionRequest from citrine.resources.design_workflow import DesignWorkflowCollection - LATEST_VER = "latest" # Refers to the most recently created branch version. -class Branch(Resource['Branch']): +class Branch(Resource["Branch"]): """ A project branch. A branch is a container for design workflows. """ - name = properties.String('data.name') - uid = properties.Optional(properties.UUID(), 'id') - archived = properties.Boolean('metadata.archived', serializable=False) - created_at = properties.Optional(properties.Datetime(), 'metadata.created.time', - serializable=False) - updated_at = properties.Optional(properties.Datetime(), 'metadata.updated.time', - serializable=False) + name = properties.String("data.name") + uid = properties.Optional(properties.UUID(), "id") + archived = properties.Boolean("metadata.archived", serializable=False) + created_at = properties.Optional( + properties.Datetime(), "metadata.created.time", serializable=False + ) + updated_at = properties.Optional( + properties.Datetime(), "metadata.updated.time", serializable=False + ) # added in v2 - root_id = properties.UUID('metadata.root_id', serializable=False) - version = properties.Integer('metadata.version', serializable=False) + root_id = properties.UUID("metadata.root_id", serializable=False) + version = properties.Integer("metadata.version", serializable=False) project_id: UUID | None = None - def __init__(self, - name: str, - *, - session: Session | None = None): + def __init__(self, name: str, *, session: Session | None = None): self.name: str = name self.session: Session = session def __str__(self): - return f'' + return f"" @property def design_workflows(self) -> DesignWorkflowCollection: """Return a resource representing all workflows contained within this branch.""" - if getattr(self, 'project_id', None) is None: - raise AttributeError('Cannot initialize workflow without project reference!') - return DesignWorkflowCollection(project_id=self.project_id, - session=self.session, - branch_root_id=self.root_id, - branch_version=self.version) + if getattr(self, "project_id", None) is None: + raise AttributeError("Cannot initialize workflow without project reference!") + return DesignWorkflowCollection( + project_id=self.project_id, + session=self.session, + branch_root_id=self.root_id, + branch_version=self.version, + ) def _post_dump(self, data: dict) -> dict: # Only the data portion of an entity is sent to the server. @@ -63,11 +63,11 @@ def _post_dump(self, data: dict) -> dict: class BranchCollection(Collection[Branch]): """A collection of Branches.""" - _path_template = '/projects/{project_id}/branches' + _path_template = "/projects/{project_id}/branches" _individual_key = None - _collection_key = 'response' + _collection_key = "response" _resource = Branch - _api_version = 'v2' + _api_version = "v2" def __init__(self, project_id: UUID, session: Session): self.project_id: UUID = project_id @@ -93,10 +93,7 @@ def build(self, data: dict) -> Branch: branch.project_id = self.project_id return branch - def get(self, - *, - root_id: UUID | str, - version: int | str | None = LATEST_VER) -> Branch: + def get(self, *, root_id: UUID | str, version: int | str | None = LATEST_VER) -> Branch: """ Retrieve a branch by its root ID and, optionally, its version number. @@ -127,7 +124,7 @@ def get(self, message=f"Branch root '{root_id}', version {version} not found", method="GET", path=self._get_path(), - params=params + params=params, ) def get_by_version_id(self, *, version_id: UUID | str) -> Branch: @@ -209,14 +206,13 @@ def list_all(self, *, per_page: int = 20) -> Iterator[Branch]: def _list_with_params(self, *, per_page, **kwargs): fetcher = functools.partial(self._fetch_page, additional_params=kwargs) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) - - def archive(self, - *, - root_id: UUID | str, - version: int | str | None = LATEST_VER): + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) + + def archive(self, *, root_id: UUID | str, version: int | str | None = LATEST_VER): """ Archive a branch. @@ -238,10 +234,7 @@ def archive(self, data = self.session.put_resource(url, {}, version=self._api_version) return self.build(data) - def restore(self, - *, - root_id: UUID | str, - version: int | str | None = LATEST_VER): + def restore(self, *, root_id: UUID | str, version: int | str | None = LATEST_VER): """ Restore an archived branch. @@ -263,12 +256,14 @@ def restore(self, data = self.session.put_resource(url, {}, version=self._api_version) return self.build(data) - def update_data(self, - *, - root_id: UUID | str, - version: int | str | None = LATEST_VER, - use_existing: bool = True, - retrain_models: bool = False) -> Branch | None: + def update_data( + self, + *, + root_id: UUID | str, + version: int | str | None = LATEST_VER, + use_existing: bool = True, + retrain_models: bool = False, + ) -> Branch | None: """ Automatically advance the branch to the next version. @@ -308,17 +303,17 @@ def update_data(self, if use_existing: use_predictors = version_updates.predictors - branch_instructions = NextBranchVersionRequest(data_updates=version_updates.data_updates, - use_predictors=use_predictors) - branch = self.next_version(root_id=root_id, - branch_instructions=branch_instructions, - retrain_models=retrain_models) + branch_instructions = NextBranchVersionRequest( + data_updates=version_updates.data_updates, use_predictors=use_predictors + ) + branch = self.next_version( + root_id=root_id, branch_instructions=branch_instructions, retrain_models=retrain_models + ) return branch - def data_updates(self, - *, - root_id: UUID | str, - version: int | str | None = LATEST_VER) -> BranchDataUpdate: + def data_updates( + self, *, root_id: UUID | str, version: int | str | None = LATEST_VER + ) -> BranchDataUpdate: """ Get data updates for a branch. @@ -345,11 +340,13 @@ def data_updates(self, data = self.session.get_resource(path, version=self._api_version) return BranchDataUpdate.build(data) - def next_version(self, - root_id: UUID | str, - *, - branch_instructions: NextBranchVersionRequest, - retrain_models: bool = True): + def next_version( + self, + root_id: UUID | str, + *, + branch_instructions: NextBranchVersionRequest, + retrain_models: bool = True, + ): """ Move a branch to the next version. @@ -377,9 +374,10 @@ def next_version(self, """ path = self._get_path(action="next-version-predictor") - data = self.session.post_resource(path, branch_instructions.dump(), - version=self._api_version, - params={ - 'root': str(root_id), - 'retrain_models': retrain_models}) + data = self.session.post_resource( + path, + branch_instructions.dump(), + version=self._api_version, + params={"root": str(root_id), "retrain_models": retrain_models}, + ) return self.build(data) diff --git a/src/citrine/resources/catalyst.py b/src/citrine/resources/catalyst.py index 9307e75f5..080b5da7a 100644 --- a/src/citrine/resources/catalyst.py +++ b/src/citrine/resources/catalyst.py @@ -1,16 +1,16 @@ -from citrine.informatics.catalyst.insights import InsightsResponse, InsightsRequest -from citrine.informatics.catalyst.assistant import AssistantResponse, AssistantRequest -from citrine.informatics.predictors.graph_predictor import GraphPredictor -from citrine.resources.user import UserCollection from citrine._session import Session from citrine._utils.functions import resource_path +from citrine.informatics.catalyst.assistant import AssistantRequest, AssistantResponse +from citrine.informatics.catalyst.insights import InsightsRequest, InsightsResponse +from citrine.informatics.predictors.graph_predictor import GraphPredictor +from citrine.resources.user import UserCollection class CatalystResource: """Encapsulates th ability to invoke Catalyst.""" - _path_template: str = '/catalyst' - _api_version = 'v1' + _path_template: str = "/catalyst" + _api_version = "v1" def __init__(self, session: Session): self.session: Session = session diff --git a/src/citrine/resources/condition_template.py b/src/citrine/resources/condition_template.py index 77d1b9bb8..cc6696c8d 100644 --- a/src/citrine/resources/condition_template.py +++ b/src/citrine/resources/condition_template.py @@ -1,16 +1,17 @@ """Resources that represent condition templates.""" -from citrine._rest.resource import GEMDResource -from citrine.resources.attribute_templates import AttributeTemplate, AttributeTemplateCollection from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.template.condition_template import ConditionTemplate as GEMDConditionTemplate +from citrine._rest.resource import GEMDResource +from citrine.resources.attribute_templates import AttributeTemplate, AttributeTemplateCollection + class ConditionTemplate( - GEMDResource['ConditionTemplate'], + GEMDResource["ConditionTemplate"], AttributeTemplate, GEMDConditionTemplate, - typ=GEMDConditionTemplate.typ + typ=GEMDConditionTemplate.typ, ): """ A condition template. @@ -36,29 +37,31 @@ class ConditionTemplate( _response_key = GEMDConditionTemplate.typ # 'condition_template' - def __init__(self, - name: str, - *, - bounds: BaseBounds, - uids: dict[str, str] | None = None, - description: str | None = None, - tags: list[str] | None = None - ): + def __init__( + self, + name: str, + *, + bounds: BaseBounds, + uids: dict[str, str] | None = None, + description: str | None = None, + tags: list[str] | None = None, + ): if uids is None: uids = dict() super(AttributeTemplate, self).__init__() - GEMDConditionTemplate.__init__(self, name=name, bounds=bounds, tags=tags, - uids=uids, description=description) + GEMDConditionTemplate.__init__( + self, name=name, bounds=bounds, tags=tags, uids=uids, description=description + ) def __str__(self): - return ''.format(self.name) + return f"" class ConditionTemplateCollection(AttributeTemplateCollection[ConditionTemplate]): """A collection of condition templates.""" - _individual_key = 'condition_template' - _collection_key = 'condition_templates' + _individual_key = "condition_template" + _collection_key = "condition_templates" _resource = ConditionTemplate @classmethod diff --git a/src/citrine/resources/data_concepts.py b/src/citrine/resources/data_concepts.py index d0cdbdad5..775f28278 100644 --- a/src/citrine/resources/data_concepts.py +++ b/src/citrine/resources/data_concepts.py @@ -1,19 +1,22 @@ """Top-level class for all data concepts objects and collections thereof.""" + +import builtins import re -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from collections.abc import Iterable, Iterator -from typing import List, TypeVar +from typing import TypeVar from uuid import UUID, uuid4 -from gemd.entity.dict_serializable import DictSerializable, DictSerializableMeta from gemd.entity.base_entity import BaseEntity +from gemd.entity.dict_serializable import DictSerializable, DictSerializableMeta from gemd.entity.link_by_uid import LinkByUID from gemd.json import GEMDJson from gemd.util import recursive_foreach, set_uuids from citrine._rest.collection import Collection from citrine._serialization.polymorphic_serializable import PolymorphicSerializable -from citrine._serialization.properties import List as PropertyList, UUID as PropertyUUID +from citrine._serialization.properties import UUID as PropertyUUID +from citrine._serialization.properties import List as PropertyList from citrine._serialization.properties import Mapping, Object, Optional, String from citrine._serialization.serializable import Serializable from citrine._session import Session @@ -23,8 +26,8 @@ from citrine.resources.audit_info import AuditInfo from citrine.resources.response import Response -CITRINE_SCOPE = 'id' -CITRINE_TAG_PREFIX = 'citr_auto' +CITRINE_SCOPE = "id" +CITRINE_TAG_PREFIX = "citr_auto" class DataConceptsMeta(DictSerializableMeta): @@ -32,17 +35,16 @@ class DataConceptsMeta(DictSerializableMeta): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) - resolved = next((b.typ for b in cls.__bases__ if getattr(b, "typ", None) is not None), - None) + resolved = next( + (b.typ for b in cls.__bases__ if getattr(b, "typ", None) is not None), None + ) if resolved is not None: cls._typ_stash = resolved cls.typ = String("type") class DataConcepts( - PolymorphicSerializable['DataConcepts'], - BaseEntity, - metaclass=DataConceptsMeta + PolymorphicSerializable["DataConcepts"], BaseEntity, metaclass=DataConceptsMeta ): """ An abstract data concepts object. @@ -57,8 +59,8 @@ class DataConcepts( """ """Properties inherited from GEMD Base Entitiy.""" - uids = Optional(Mapping(String('scope'), String('id')), 'uids', override=True) - tags = Optional(PropertyList(String()), 'tags', override=True) + uids = Optional(Mapping(String("scope"), String("id")), "uids", override=True) + tags = Optional(PropertyList(String()), "tags", override=True) _type_key = "type" """str: key used to determine type of serialized object.""" @@ -120,7 +122,7 @@ def get_type(cls, data) -> type[Serializable]: """ if isinstance(data, DictSerializable): data = data.as_dict() - return DictSerializable.class_mapping[data['type']] + return DictSerializable.class_mapping[data["type"]] @classmethod def get_collection_type(cls, data) -> "type[DataConceptsCollection]": @@ -147,31 +149,41 @@ def get_collection_type(cls, data) -> "type[DataConceptsCollection]": DataConcepts._make_collection_dict() if isinstance(data, DictSerializable): data = data.as_dict() - return DataConcepts.collection_dict[data['type']] + return DataConcepts.collection_dict[data["type"]] @staticmethod def _make_collection_dict(): """Construct a dictionary from each type key to the associated collection.""" from citrine.resources.condition_template import ConditionTemplateCollection - from citrine.resources.parameter_template import ParameterTemplateCollection - from citrine.resources.property_template import PropertyTemplateCollection - from citrine.resources.material_template import MaterialTemplateCollection - from citrine.resources.measurement_template import MeasurementTemplateCollection - from citrine.resources.process_template import ProcessTemplateCollection - from citrine.resources.ingredient_spec import IngredientSpecCollection - from citrine.resources.material_spec import MaterialSpecCollection - from citrine.resources.measurement_spec import MeasurementSpecCollection - from citrine.resources.process_spec import ProcessSpecCollection from citrine.resources.ingredient_run import IngredientRunCollection + from citrine.resources.ingredient_spec import IngredientSpecCollection from citrine.resources.material_run import MaterialRunCollection + from citrine.resources.material_spec import MaterialSpecCollection + from citrine.resources.material_template import MaterialTemplateCollection from citrine.resources.measurement_run import MeasurementRunCollection + from citrine.resources.measurement_spec import MeasurementSpecCollection + from citrine.resources.measurement_template import MeasurementTemplateCollection + from citrine.resources.parameter_template import ParameterTemplateCollection from citrine.resources.process_run import ProcessRunCollection + from citrine.resources.process_spec import ProcessSpecCollection + from citrine.resources.process_template import ProcessTemplateCollection + from citrine.resources.property_template import PropertyTemplateCollection + _collection_list = [ - ConditionTemplateCollection, ParameterTemplateCollection, PropertyTemplateCollection, - MaterialTemplateCollection, MeasurementTemplateCollection, ProcessTemplateCollection, - IngredientSpecCollection, MaterialSpecCollection, MeasurementSpecCollection, - ProcessSpecCollection, IngredientRunCollection, MaterialRunCollection, - MeasurementRunCollection, ProcessRunCollection + ConditionTemplateCollection, + ParameterTemplateCollection, + PropertyTemplateCollection, + MaterialTemplateCollection, + MeasurementTemplateCollection, + ProcessTemplateCollection, + IngredientSpecCollection, + MaterialSpecCollection, + MeasurementSpecCollection, + ProcessSpecCollection, + IngredientRunCollection, + MaterialRunCollection, + MeasurementRunCollection, + ProcessRunCollection, ] for collection in _collection_list: DataConcepts.collection_dict[collection._individual_key] = collection @@ -187,11 +199,13 @@ def _make_link_by_uid(gemd_object_rep: str | UUID | BaseEntity | LinkByUID) -> L scope = CITRINE_SCOPE return LinkByUID(scope, uid) else: - raise TypeError("Link can only be created from a GEMD object, LinkByUID, str, or UUID." - "Instead got {}.".format(gemd_object_rep)) + raise TypeError( + "Link can only be created from a GEMD object, LinkByUID, str, or UUID." + f"Instead got {gemd_object_rep}." + ) -ResourceType = TypeVar('ResourceType', bound='DataConcepts') +ResourceType = TypeVar("ResourceType", bound="DataConcepts") class DataConceptsCollection(Collection[ResourceType], ABC): @@ -228,11 +242,11 @@ def _path_collection_key(self): @property def _path_template(self): - return f'teams/{self.team_id}/datasets/{self.dataset_id}/{self._path_collection_key}' + return f"teams/{self.team_id}/datasets/{self.dataset_id}/{self._path_collection_key}" @property def _dataset_agnostic_path_template(self): - return f'teams/{self.team_id}/{self._path_collection_key}' + return f"teams/{self.team_id}/{self._path_collection_key}" def build(self, data: dict) -> ResourceType: """ @@ -253,9 +267,7 @@ def build(self, data: dict) -> ResourceType: """ return self.get_type().build(data) - def list(self, *, - per_page: int | None = 100, - forward: bool = True) -> Iterator[ResourceType]: + def list(self, *, per_page: int | None = 100, forward: bool = True) -> Iterator[ResourceType]: """ Get all visible elements of the collection. @@ -280,13 +292,14 @@ def list(self, *, """ params = {} if self.dataset_id is not None: - params['dataset_id'] = str(self.dataset_id) + params["dataset_id"] = str(self.dataset_id) raw_objects = self.session.cursor_paged_resource( self.session.get_resource, self._get_path(ignore_dataset=True), forward=forward, per_page=per_page, - params=params) + params=params, + ) return (self.build(raw) for raw in raw_objects) def register(self, model: ResourceType, *, dry_run=False): @@ -321,7 +334,7 @@ def register(self, model: ResourceType, *, dry_run=False): if self.dataset_id is None: raise RuntimeError("Must specify a dataset in order to register a data model object.") path = self._get_path() - params = {'dry_run': dry_run} + params = {"dry_run": dry_run} temp_scope = str(uuid4()) scope = temp_scope if dry_run else CITRINE_SCOPE @@ -338,27 +351,31 @@ def register(self, model: ResourceType, *, dry_run=False): if registered.tags is not None: if model.tags is None: # This is somehow hit by nextgen-devkit tests model.tags = list() # pragma: no cover - model.tags.extend([tag for tag in registered.tags - if re.match(f"^{CITRINE_TAG_PREFIX}::", tag)]) + model.tags.extend( + [tag for tag in registered.tags if re.match(f"^{CITRINE_TAG_PREFIX}::", tag)] + ) else: # Remove of the tags/uids the platform spuriously added # this might leave objects with just the temp ids, which we want to strip later if CITRINE_SCOPE not in model.uids: registered.uids.pop(CITRINE_SCOPE, None) if registered.tags is not None: - todo = [tag for tag in registered.tags - if re.match(f"^{CITRINE_TAG_PREFIX}::", tag)] + todo = [ + tag for tag in registered.tags if re.match(f"^{CITRINE_TAG_PREFIX}::", tag) + ] for tag in todo: # Covering this block would require dark art if tag not in model.tags: registered.tags.remove(tag) return registered - def register_all(self, - models: Iterable[ResourceType], - *, - dry_run: bool = False, - status_bar: bool = False, - include_nested: bool = False) -> List[ResourceType]: + def register_all( + self, + models: Iterable[ResourceType], + *, + dry_run: bool = False, + status_bar: bool = False, + include_nested: bool = False, + ) -> builtins.list[ResourceType]: """ Register multiple GEMD objects to each of their appropriate collections. @@ -401,14 +418,12 @@ def register_all(self, """ # avoiding a circular import from citrine.resources.gemd_resource import GEMDResourceCollection - gemd_collection = GEMDResourceCollection(team_id=self.team_id, - dataset_id=self.dataset_id, - session=self.session) + + gemd_collection = GEMDResourceCollection( + team_id=self.team_id, dataset_id=self.dataset_id, session=self.session + ) return gemd_collection.register_all( - models, - dry_run=dry_run, - status_bar=status_bar, - include_nested=include_nested + models, dry_run=dry_run, status_bar=status_bar, include_nested=include_nested ) def update(self, model: ResourceType) -> ResourceType: @@ -426,15 +441,20 @@ def update(self, model: ResourceType) -> ResourceType: return self.register(model, dry_run=False) except BadRequest: # If register() cannot be used because an asynchronous check is required - return self.async_update(model, dry_run=False, - wait_for_response=True, return_model=True) - - def async_update(self, model: ResourceType, *, - dry_run: bool = False, - wait_for_response: bool = True, - timeout: float = 2 * 60, - polling_delay: float = 1.0, - return_model: bool = False) -> UUID | ResourceType | None: + return self.async_update( + model, dry_run=False, wait_for_response=True, return_model=True + ) + + def async_update( + self, + model: ResourceType, + *, + dry_run: bool = False, + wait_for_response: bool = True, + timeout: float = 2 * 60, + polling_delay: float = 1.0, + return_model: bool = False, + ) -> UUID | ResourceType | None: """ Update a particular element of the collection with data validation. @@ -484,19 +504,20 @@ def async_update(self, model: ResourceType, *, recursive_foreach(model, lambda x: x.uids.pop(temp_scope, None)) # Strip temp uids scope = CITRINE_SCOPE - id = dumped_data['uids'][scope] + id = dumped_data["uids"][scope] if self.dataset_id is None: - raise RuntimeError("Must specify a dataset in order to update " - "a data model object with data validation.") + raise RuntimeError( + "Must specify a dataset in order to update " + "a data model object with data validation." + ) url = self._get_path(action=[scope, id, "async"]) - response_json = self.session.put_resource(url, dumped_data, params={'dry_run': dry_run}) + response_json = self.session.put_resource(url, dumped_data, params={"dry_run": dry_run}) job_id = response_json["job_id"] if wait_for_response: - self.poll_async_update_job(job_id=job_id, timeout=timeout, - polling_delay=polling_delay) + self.poll_async_update_job(job_id=job_id, timeout=timeout, polling_delay=polling_delay) # That worked, return nothing or return the object if return_model: @@ -507,8 +528,9 @@ def async_update(self, model: ResourceType, *, # TODO: use JobSubmissionResponse here instead return job_id - def poll_async_update_job(self, job_id: UUID, *, timeout: float = 2 * 60, - polling_delay: float = 1.0) -> None: + def poll_async_update_job( + self, job_id: UUID, *, timeout: float = 2 * 60, polling_delay: float = 1.0 + ) -> None: """ Poll for the result of the async_update call. @@ -541,11 +563,12 @@ def poll_async_update_job(self, job_id: UUID, *, timeout: float = 2 * 60, _poll_for_job_completion( session=self.session, team_id=self.team_id, - job=job_id, timeout=timeout, - polling_delay=polling_delay) + job=job_id, + timeout=timeout, + polling_delay=polling_delay, + ) # That worked, nothing returned in this case - return None def get(self, uid: UUID | str | LinkByUID | BaseEntity) -> ResourceType: """ @@ -567,8 +590,9 @@ def get(self, uid: UUID | str | LinkByUID | BaseEntity) -> ResourceType: data = self.session.get_resource(path) return self.build(data) - def list_by_name(self, name: str, *, exact: bool = False, - forward: bool = True, per_page: int = 100) -> Iterator[ResourceType]: + def list_by_name( + self, name: str, *, exact: bool = False, forward: bool = True, per_page: int = 100 + ) -> Iterator[ResourceType]: """ Get all objects with specified name in this dataset. @@ -594,14 +618,15 @@ def list_by_name(self, name: str, *, exact: bool = False, """ if self.dataset_id is None: raise RuntimeError("Must specify a dataset to filter by name.") - params = {'dataset_id': str(self.dataset_id), 'name': name, 'exact': exact} + params = {"dataset_id": str(self.dataset_id), "name": name, "exact": exact} raw_objects = self.session.cursor_paged_resource( self.session.get_resource, # "Ignoring" dataset because it is in the query params (and required) self._get_path(ignore_dataset=True, action="filter-by-name"), forward=forward, per_page=per_page, - params=params) + params=params, + ) return (self.build(raw) for raw in raw_objects) def list_by_tag(self, tag: str, *, per_page: int = 100) -> Iterator[ResourceType]: @@ -630,14 +655,15 @@ def list_by_tag(self, tag: str, *, per_page: int = 100) -> Iterator[ResourceType Every object in this collection. """ - params = {'tags': [tag]} + params = {"tags": [tag]} if self.dataset_id is not None: - params['dataset_id'] = str(self.dataset_id) + params["dataset_id"] = str(self.dataset_id) raw_objects = self.session.cursor_paged_resource( self.session.get_resource, self._get_path(ignore_dataset=True), per_page=per_page, - params=params) + params=params, + ) return (self.build(raw) for raw in raw_objects) def delete(self, uid: UUID | str | LinkByUID | BaseEntity, *, dry_run: bool = False): @@ -655,12 +681,17 @@ def delete(self, uid: UUID | str | LinkByUID | BaseEntity, *, dry_run: bool = Fa """ link = _make_link_by_uid(uid) path = self._get_path(action=[link.scope, link.id]) - params = {'dry_run': dry_run} + params = {"dry_run": dry_run} self.session.delete_resource(path, params=params) return Response(status_code=200) # delete succeeded - def _get_relation(self, relation: str, uid: UUID | str | LinkByUID | BaseEntity, - forward: bool = True, per_page: int = 100) -> Iterator[ResourceType]: + def _get_relation( + self, + relation: str, + uid: UUID | str | LinkByUID | BaseEntity, + forward: bool = True, + per_page: int = 100, + ) -> Iterator[ResourceType]: """ Generic method for searching this collection by relation to another object. @@ -687,19 +718,21 @@ def _get_relation(self, relation: str, uid: UUID | str | LinkByUID | BaseEntity, """ params = {} if self.dataset_id is not None: - params['dataset_id'] = str(self.dataset_id) + params["dataset_id"] = str(self.dataset_id) link = _make_link_by_uid(uid) raw_objects = self.session.cursor_paged_resource( self.session.get_resource, - format_escaped_url('teams/{}/{}/{}/{}/{}', - self.team_id, - relation, - link.scope, - link.id, - self._collection_key.replace('_', '-') - ), + format_escaped_url( + "teams/{}/{}/{}/{}/{}", + self.team_id, + relation, + link.scope, + link.id, + self._collection_key.replace("_", "-"), + ), forward=forward, per_page=per_page, params=params, - version='v1') + version="v1", + ) return (self.build(raw) for raw in raw_objects) diff --git a/src/citrine/resources/data_objects.py b/src/citrine/resources/data_objects.py index a6a6f7d2d..d999e3ad6 100644 --- a/src/citrine/resources/data_objects.py +++ b/src/citrine/resources/data_objects.py @@ -1,24 +1,25 @@ """Top-level class for all data object (i.e., spec and run) objects and collections thereof.""" + from abc import ABC from collections.abc import Iterator from typing import TypeVar from uuid import uuid4 +from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.base_object import BaseObject +from gemd.entity.template.attribute_template import AttributeTemplate from gemd.json import GEMDJson from gemd.util import recursive_foreach -from citrine._utils.functions import get_object_id, replace_objects_with_links, scrub_none from citrine._serialization.properties import List, Object, Optional, String -from gemd.entity.file_link import FileLink +from citrine._utils.functions import get_object_id, replace_objects_with_links, scrub_none from citrine.exceptions import BadRequest from citrine.resources.api_error import ValidationError from citrine.resources.data_concepts import DataConcepts, DataConceptsCollection from citrine.resources.object_templates import ObjectTemplateResourceType from citrine.resources.process_template import ProcessTemplate -from gemd.entity.object.base_object import BaseObject -from gemd.entity.bounds.base_bounds import BaseBounds -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.template.attribute_template import AttributeTemplate class DataObject(DataConcepts, BaseObject, ABC): @@ -28,8 +29,8 @@ class DataObject(DataConcepts, BaseObject, ABC): DataObject must be extended along with `Resource` """ - notes = Optional(String(), 'notes') - file_links = Optional(List(Object(FileLink)), 'file_links', override=True) + notes = Optional(String(), "notes") + file_links = Optional(List(Object(FileLink)), "file_links", override=True) DataObjectResourceType = TypeVar("DataObjectResourceType", bound="DataObject") @@ -39,9 +40,12 @@ class DataObjectCollection(DataConceptsCollection[DataObjectResourceType], ABC): """A collection of one kind of data object object.""" def list_by_attribute_bounds( - self, - attribute_bounds: dict[AttributeTemplate | LinkByUID, BaseBounds], *, - forward: bool = True, per_page: int = 100) -> Iterator[DataObject]: + self, + attribute_bounds: dict[AttributeTemplate | LinkByUID, BaseBounds], + *, + forward: bool = True, + per_page: int = 100, + ) -> Iterator[DataObject]: """ Get all objects in the collection with attributes within certain bounds. @@ -79,7 +83,7 @@ def list_by_attribute_bounds( body = self._get_attribute_bounds_search_body(attribute_bounds) params = {} if self.dataset_id is not None: - params['dataset_id'] = str(self.dataset_id) + params["dataset_id"] = str(self.dataset_id) raw_objects = self.session.cursor_paged_resource( self.session.post_resource, # "Ignoring" dataset because it is in the query params (and required) @@ -87,30 +91,36 @@ def list_by_attribute_bounds( json=body, forward=forward, per_page=per_page, - params=params) + params=params, + ) return (self.build(raw) for raw in raw_objects) @staticmethod def _get_attribute_bounds_search_body(attribute_bounds): if not isinstance(attribute_bounds, dict): - raise TypeError('attribute_bounds must be a dict mapping template to bounds; ' - 'got {}'.format(attribute_bounds)) + raise TypeError( + "attribute_bounds must be a dict mapping template to bounds; " + f"got {attribute_bounds}" + ) if len(attribute_bounds) != 1: - raise NotImplementedError('Currently, only searches with exactly one template ' - 'to bounds mapping are supported; got {}' - .format(attribute_bounds)) + raise NotImplementedError( + "Currently, only searches with exactly one template " + f"to bounds mapping are supported; got {attribute_bounds}" + ) return { - 'attribute_bounds': { + "attribute_bounds": { get_object_id(templ): bounds.as_dict() for templ, bounds in attribute_bounds.items() } } - def validate_templates(self, *, - model: DataObjectResourceType, - object_template: ObjectTemplateResourceType | None = None, - ingredient_process_template: ProcessTemplate | None = None)\ - -> list[ValidationError]: + def validate_templates( + self, + *, + model: DataObjectResourceType, + object_template: ObjectTemplateResourceType | None = None, + ingredient_process_template: ProcessTemplate | None = None, + ) -> list[ValidationError]: """ Validate a data object against its templates. @@ -132,11 +142,13 @@ def validate_templates(self, *, request_data = {"dataObject": dumped_data} if object_template is not None: - request_data["objectTemplate"] = \ - replace_objects_with_links(scrub_none(object_template.dump())) + request_data["objectTemplate"] = replace_objects_with_links( + scrub_none(object_template.dump()) + ) if ingredient_process_template is not None: - request_data["ingredientProcessTemplate"] = \ - replace_objects_with_links(scrub_none(ingredient_process_template.dump())) + request_data["ingredientProcessTemplate"] = replace_objects_with_links( + scrub_none(ingredient_process_template.dump()) + ) try: self.session.put_resource(path, request_data) return [] diff --git a/src/citrine/resources/data_version_update.py b/src/citrine/resources/data_version_update.py index 1de1588e3..3cd902aa0 100644 --- a/src/citrine/resources/data_version_update.py +++ b/src/citrine/resources/data_version_update.py @@ -5,37 +5,31 @@ from citrine._serialization.serializable import Serializable -class DataVersionUpdate(Serializable['DataVersionUpdate']): +class DataVersionUpdate(Serializable["DataVersionUpdate"]): """Container for data updates.""" - current = properties.String('current') - latest = properties.String('latest') + current = properties.String("current") + latest = properties.String("latest") - def __init__(self, - *, - current: str, - latest: str): + def __init__(self, *, current: str, latest: str): self.current = current self.latest = latest - typ = properties.String('type', default='DataVersionUpdate') + typ = properties.String("type", default="DataVersionUpdate") -class BranchDataUpdate(Resource['BranchDataUpdate']): +class BranchDataUpdate(Resource["BranchDataUpdate"]): """Branch data updates with predictors using the versions indicated.""" data_updates = properties.List(properties.Object(DataVersionUpdate), "data_updates") predictors = properties.List(properties.Object(PredictorRef), "predictors") - def __init__(self, - *, - data_updates: list[DataVersionUpdate], - predictors: list[PredictorRef]): + def __init__(self, *, data_updates: list[DataVersionUpdate], predictors: list[PredictorRef]): self.data_updates = data_updates self.predictors = predictors -class NextBranchVersionRequest(Resource['NextBranchVersionRequest']): +class NextBranchVersionRequest(Resource["NextBranchVersionRequest"]): """ Instructions for how the next version of a branch should handle its predictors. @@ -48,9 +42,8 @@ class NextBranchVersionRequest(Resource['NextBranchVersionRequest']): data_updates = properties.List(properties.Object(DataVersionUpdate), "data_updates") use_predictors = properties.List(properties.Object(PredictorRef), "use_predictors") - def __init__(self, - *, - data_updates: list[DataVersionUpdate], - use_predictors: list[PredictorRef]): + def __init__( + self, *, data_updates: list[DataVersionUpdate], use_predictors: list[PredictorRef] + ): self.data_updates = data_updates self.use_predictors = use_predictors diff --git a/src/citrine/resources/dataset.py b/src/citrine/resources/dataset.py index 7e5de13f8..ea5bbef5c 100644 --- a/src/citrine/resources/dataset.py +++ b/src/citrine/resources/dataset.py @@ -1,24 +1,24 @@ """Resources that represent both individual and collections of datasets.""" -from collections.abc import Iterator, Iterable + +from collections.abc import Iterable, Iterator from uuid import UUID from gemd.entity.base_entity import BaseEntity from gemd.entity.link_by_uid import LinkByUID -from citrine._utils.functions import format_escaped_url from citrine._rest.collection import Collection from citrine._rest.resource import Resource, ResourceTypeEnum from citrine._serialization import properties from citrine._session import Session -from citrine._utils.functions import scrub_none +from citrine._utils.functions import format_escaped_url, scrub_none from citrine.exceptions import NotFound from citrine.resources.api_error import ApiError from citrine.resources.condition_template import ConditionTemplateCollection from citrine.resources.data_concepts import DataConcepts from citrine.resources.delete import _poll_for_async_batch_delete_result from citrine.resources.file_link import FileCollection -from citrine.resources.ingestion import IngestionCollection from citrine.resources.gemd_resource import GEMDResourceCollection +from citrine.resources.ingestion import IngestionCollection from citrine.resources.ingredient_run import IngredientRunCollection from citrine.resources.ingredient_spec import IngredientSpecCollection from citrine.resources.material_run import MaterialRunCollection @@ -34,7 +34,7 @@ from citrine.resources.property_template import PropertyTemplateCollection -class Dataset(Resource['Dataset']): +class Dataset(Resource["Dataset"]): """ A collection of data objects. @@ -55,38 +55,46 @@ class Dataset(Resource['Dataset']): """ - _response_key = 'dataset' + _response_key = "dataset" _resource_type = ResourceTypeEnum.DATASET - uid = properties.Optional(properties.UUID(), 'id') + 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.Optional(properties.String, 'summary') - description = properties.Optional(properties.String, 'description') - deleted = properties.Optional(properties.Boolean(), 'deleted') + name = properties.String("name") + unique_name = properties.Optional(properties.String(), "unique_name") + summary = properties.Optional(properties.String, "summary") + description = properties.Optional(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') + 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') + 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') + 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') + 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') + 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') + 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') + public = properties.Optional(properties.Boolean(), "public") """bool: Flag indicating whether the dataset is publicly readable.""" - team_id = properties.Optional(properties.UUID(), 'team_id', - serializable=False, deserializable=False) - session = properties.Optional(properties.Object(Session), 'session', - serializable=False, deserializable=False) - - def __init__(self, name: str, *, summary: str | None = None, - description: str | None = None, unique_name: str | None = None): + team_id = properties.Optional( + properties.UUID(), "team_id", serializable=False, deserializable=False + ) + session = properties.Optional( + properties.Object(Session), "session", serializable=False, deserializable=False + ) + + def __init__( + self, + name: str, + *, + summary: str | None = None, + description: str | None = None, + unique_name: str | None = None, + ): self.name: str = name self.summary: str | None = summary self.description: str | None = description @@ -107,97 +115,112 @@ def __init__(self, name: str, *, summary: str | None = None, self.session = None def __str__(self): - return ''.format(self.name) + return f"" @property def property_templates(self) -> PropertyTemplateCollection: """Return a resource representing all property templates in this dataset.""" - return PropertyTemplateCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return PropertyTemplateCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def condition_templates(self) -> ConditionTemplateCollection: """Return a resource representing all condition templates in this dataset.""" - return ConditionTemplateCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return ConditionTemplateCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def parameter_templates(self) -> ParameterTemplateCollection: """Return a resource representing all parameter templates in this dataset.""" - return ParameterTemplateCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return ParameterTemplateCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def material_templates(self) -> MaterialTemplateCollection: """Return a resource representing all material templates in this dataset.""" - return MaterialTemplateCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return MaterialTemplateCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def measurement_templates(self) -> MeasurementTemplateCollection: """Return a resource representing all measurement templates in this dataset.""" - return MeasurementTemplateCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return MeasurementTemplateCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def process_templates(self) -> ProcessTemplateCollection: """Return a resource representing all process templates in this dataset.""" - return ProcessTemplateCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return ProcessTemplateCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def process_runs(self) -> ProcessRunCollection: """Return a resource representing all process runs in this dataset.""" - return ProcessRunCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return ProcessRunCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def measurement_runs(self) -> MeasurementRunCollection: """Return a resource representing all measurement runs in this dataset.""" - return MeasurementRunCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return MeasurementRunCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def material_runs(self) -> MaterialRunCollection: """Return a resource representing all material runs in this dataset.""" - return MaterialRunCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return MaterialRunCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def ingredient_runs(self) -> IngredientRunCollection: """Return a resource representing all ingredient runs in this dataset.""" - return IngredientRunCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return IngredientRunCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def process_specs(self) -> ProcessSpecCollection: """Return a resource representing all process specs in this dataset.""" - return ProcessSpecCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return ProcessSpecCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def measurement_specs(self) -> MeasurementSpecCollection: """Return a resource representing all measurement specs in this dataset.""" - return MeasurementSpecCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return MeasurementSpecCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def material_specs(self) -> MaterialSpecCollection: """Return a resource representing all material specs in this dataset.""" - return MaterialSpecCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return MaterialSpecCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def ingredient_specs(self) -> IngredientSpecCollection: """Return a resource representing all ingredient specs in this dataset.""" - return IngredientSpecCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return IngredientSpecCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def gemd(self) -> GEMDResourceCollection: """Return a resource representing all GEMD objects/templates in this dataset.""" - return GEMDResourceCollection(team_id=self.team_id, dataset_id=self.uid, - session=self.session) + return GEMDResourceCollection( + team_id=self.team_id, dataset_id=self.uid, session=self.session + ) @property def files(self) -> FileCollection: @@ -213,12 +236,14 @@ def register(self, model: DataConcepts, *, dry_run=False) -> DataConcepts: """Register a data model object to the appropriate collection.""" return self.gemd._collection_for(model).register(model, dry_run=dry_run) - def register_all(self, - models: Iterable[DataConcepts], - *, - dry_run: bool = False, - status_bar: bool = False, - include_nested: bool = False) -> list[DataConcepts]: + def register_all( + self, + models: Iterable[DataConcepts], + *, + dry_run: bool = False, + status_bar: bool = False, + include_nested: bool = False, + ) -> list[DataConcepts]: """ Register multiple GEMD objects to each of their appropriate collections. @@ -256,10 +281,7 @@ def register_all(self, """ return self.gemd.register_all( - models, - dry_run=dry_run, - status_bar=status_bar, - include_nested=include_nested + models, dry_run=dry_run, status_bar=status_bar, include_nested=include_nested ) def update(self, model: DataConcepts) -> DataConcepts: @@ -286,12 +308,12 @@ def delete(self, uid: UUID | str | LinkByUID | DataConcepts, *, dry_run=False): return collection.delete(uid=uid, dry_run=dry_run) def delete_contents( - self, - *, - prompt_to_confirm: bool = True, - remove_templates: bool = True, - timeout: float = 2 * 60, - polling_delay: float = 1.0 + self, + *, + prompt_to_confirm: bool = True, + remove_templates: bool = True, + timeout: float = 2 * 60, + polling_delay: float = 1.0, ): """ Delete all the GEMD objects from within a single Dataset. @@ -320,17 +342,20 @@ def delete_contents( deleted. """ - path = format_escaped_url('teams/{team_id}/datasets/{dataset_uid}/contents', - dataset_uid=self.uid, - team_id=self.team_id) + path = format_escaped_url( + "teams/{team_id}/datasets/{dataset_uid}/contents", + dataset_uid=self.uid, + team_id=self.team_id, + ) while prompt_to_confirm: - print(f"Confirm you want to delete the contents of " - f"Dataset {self.name} {self.uid} [Y/N]") + print( + f"Confirm you want to delete the contents of Dataset {self.name} {self.uid} [Y/N]" + ) user_response = input() - if user_response.lower() in {'y', 'yes'}: + if user_response.lower() in {"y", "yes"}: break # return to main flow - elif user_response.lower() in {'n', 'no'}: + elif user_response.lower() in {"n", "no"}: raise RuntimeError("delete_contents was invoked unintentionally") else: print(f'"{user_response}" is not a valid response') @@ -339,18 +364,20 @@ def delete_contents( response = self.session.delete_resource(path, params=params) job_id = response["job_id"] - return _poll_for_async_batch_delete_result(team_id=self.team_id, - session=self.session, - job_id=job_id, - timeout=timeout, - polling_delay=polling_delay) + return _poll_for_async_batch_delete_result( + team_id=self.team_id, + session=self.session, + job_id=job_id, + timeout=timeout, + polling_delay=polling_delay, + ) def gemd_batch_delete( - self, - id_list: list[LinkByUID | UUID | str | BaseEntity], - *, - timeout: float = 2 * 60, - polling_delay: float = 1.0 + self, + id_list: list[LinkByUID | UUID | str | BaseEntity], + *, + timeout: float = 2 * 60, + polling_delay: float = 1.0, ) -> list[tuple[LinkByUID, ApiError]]: """ Remove a set of GEMD objects. @@ -394,9 +421,9 @@ def gemd_batch_delete( deleted. """ - return self.gemd.batch_delete(id_list=id_list, - timeout=timeout, - polling_delay=polling_delay) + return self.gemd.batch_delete( + id_list=id_list, timeout=timeout, polling_delay=polling_delay + ) class DatasetCollection(Collection[Dataset]): @@ -415,7 +442,7 @@ class DatasetCollection(Collection[Dataset]): _individual_key = None _collection_key = None _resource = Dataset - _path_template = 'teams/{team_id}/datasets' + _path_template = "teams/{team_id}/datasets" def __init__(self, *, session: Session, team_id: UUID): self.session = session @@ -474,7 +501,6 @@ def register(self, model: Dataset) -> Dataset: # Leverage the create-or-update endpoint if we've got a unique name data = self.session.put_resource(path, scrub_none(dumped_dataset)) else: - if model.uid is None: # POST to create a new one if a UID is not assigned data = self.session.post_resource(path, scrub_none(dumped_dataset)) @@ -482,7 +508,8 @@ def register(self, model: Dataset) -> Dataset: else: # Otherwise PUT to update it data = self.session.put_resource( - self._get_path(model.uid), scrub_none(dumped_dataset)) + self._get_path(model.uid), scrub_none(dumped_dataset) + ) full_model = self.build(data) full_model.team_id = self.team_id diff --git a/src/citrine/resources/delete.py b/src/citrine/resources/delete.py index 51c1a82d2..e91469076 100644 --- a/src/citrine/resources/delete.py +++ b/src/citrine/resources/delete.py @@ -6,18 +6,18 @@ from citrine._session import Session from citrine._utils.functions import format_escaped_url -from citrine.resources.api_error import ApiError from citrine.jobs.job import _poll_for_job_completion +from citrine.resources.api_error import ApiError from citrine.resources.data_concepts import _make_link_by_uid def _async_gemd_batch_delete( - id_list: list[LinkByUID | UUID | str | BaseEntity], - team_id: UUID, - session: Session, - dataset_id: UUID | None = None, - timeout: float = 2 * 60, - polling_delay: float = 1.0 + id_list: list[LinkByUID | UUID | str | BaseEntity], + team_id: UUID, + session: Session, + dataset_id: UUID | None = None, + timeout: float = 2 * 60, + polling_delay: float = 1.0, ) -> list[tuple[LinkByUID, ApiError]]: """ Shared implementation of Async GEMD Batch deletion. @@ -64,15 +64,14 @@ def _async_gemd_batch_delete( scoped_uids = [] for uid in id_list: # And now normalize to id/scope pairs link_by_uid = _make_link_by_uid(uid) - scoped_uids.append({'scope': link_by_uid.scope, 'id': link_by_uid.id}) + scoped_uids.append({"scope": link_by_uid.scope, "id": link_by_uid.id}) - body = {'ids': scoped_uids} + body = {"ids": scoped_uids} if dataset_id is not None: - body.update({'dataset_id': str(dataset_id)}) + body.update({"dataset_id": str(dataset_id)}) if team_id is not None: - path = format_escaped_url('/teams/{team_id}/gemd/async-batch-delete', - team_id=team_id) + path = format_escaped_url("/teams/{team_id}/gemd/async-batch-delete", team_id=team_id) else: raise TypeError("Missing one required argument: team_id") response = session.post_resource(path, body) @@ -84,15 +83,12 @@ def _async_gemd_batch_delete( session=session, job_id=job_id, timeout=timeout, - polling_delay=polling_delay) + polling_delay=polling_delay, + ) def _poll_for_async_batch_delete_result( - team_id: UUID, - session: Session, - job_id: str, - timeout: float, - polling_delay: float + team_id: UUID, session: Session, job_id: str, timeout: float, polling_delay: float ) -> list[tuple[LinkByUID, ApiError]]: """ Poll for the result of an asynchronous batch delete (or a deletion of dataset contents). @@ -125,11 +121,10 @@ def _poll_for_async_batch_delete_result( """ response = _poll_for_job_completion( - session=session, - team_id=team_id, - job=job_id, - timeout=timeout, - polling_delay=polling_delay) + session=session, team_id=team_id, job=job_id, timeout=timeout, polling_delay=polling_delay + ) - return [(LinkByUID(f['id']['scope'], f['id']['id']), ApiError.build(f['cause'])) - for f in json.loads(response.output.get('failures', '[]'))] + return [ + (LinkByUID(f["id"]["scope"], f["id"]["id"]), ApiError.build(f["cause"])) + for f in json.loads(response.output.get("failures", "[]")) + ] diff --git a/src/citrine/resources/descriptors.py b/src/citrine/resources/descriptors.py index f5e9569f2..65522fa82 100644 --- a/src/citrine/resources/descriptors.py +++ b/src/citrine/resources/descriptors.py @@ -4,7 +4,7 @@ from citrine._utils.functions import format_escaped_url from citrine.informatics.data_sources import DataSource from citrine.informatics.descriptors import Descriptor -from citrine.informatics.predictors import PredictorNode, GraphPredictor +from citrine.informatics.predictors import GraphPredictor, PredictorNode # Not a full Collection since CRUD operations are not valid for Descriptors @@ -15,8 +15,9 @@ def __init__(self, project_id: UUID, session: Session): self.project_id = project_id self.session: Session = session - def from_predictor_responses(self, *, predictor: GraphPredictor | PredictorNode, - inputs: list[Descriptor]) -> list[Descriptor]: + def from_predictor_responses( + self, *, predictor: GraphPredictor | PredictorNode, inputs: list[Descriptor] + ) -> list[Descriptor]: """ Get responses for a predictor, given an input space. @@ -41,14 +42,12 @@ def from_predictor_responses(self, *, predictor: GraphPredictor | PredictorNode, predictor_data = predictor.dump() response = self.session.post_resource( - path=format_escaped_url('/projects/{}/material-descriptors/predictor-responses', - self.project_id), - json={ - 'predictor': predictor_data, - 'inputs': [i.dump() for i in inputs] - } + path=format_escaped_url( + "/projects/{}/material-descriptors/predictor-responses", self.project_id + ), + json={"predictor": predictor_data, "inputs": [i.dump() for i in inputs]}, ) - return [Descriptor.build(r) for r in response['responses']] + return [Descriptor.build(r) for r in response["responses"]] def from_data_source(self, *, data_source: DataSource) -> list[Descriptor]: """ @@ -66,10 +65,9 @@ def from_data_source(self, *, data_source: DataSource) -> list[Descriptor]: """ response = self.session.post_resource( - path=format_escaped_url('/projects/{}/material-descriptors/from-data-source', - self.project_id), - json={ - 'data_source': data_source.dump() - } + path=format_escaped_url( + "/projects/{}/material-descriptors/from-data-source", self.project_id + ), + json={"data_source": data_source.dump()}, ) - return [Descriptor.build(r) for r in response['descriptors']] + return [Descriptor.build(r) for r in response["descriptors"]] diff --git a/src/citrine/resources/design_execution.py b/src/citrine/resources/design_execution.py index cfa325737..25541b4e1 100644 --- a/src/citrine/resources/design_execution.py +++ b/src/citrine/resources/design_execution.py @@ -1,4 +1,5 @@ """Resources that represent both individual and collections of design workflow executions.""" + from collections.abc import Iterator from uuid import UUID @@ -12,15 +13,12 @@ class DesignExecutionCollection(Collection["DesignExecution"]): """A collection of DesignExecutions.""" - _path_template = '/projects/{project_id}/design-workflows/{workflow_id}/executions' # noqa + _path_template = "/projects/{project_id}/design-workflows/{workflow_id}/executions" # noqa _individual_key = None - _collection_key = 'response' + _collection_key = "response" _resource = executions.DesignExecution - def __init__(self, - project_id: UUID, - session: Session, - workflow_id: UUID | None = None): + def __init__(self, project_id: UUID, session: Session, workflow_id: UUID | None = None): self.project_id: UUID = project_id self.session: Session = session self.workflow_id: UUID = workflow_id @@ -35,7 +33,7 @@ def build(self, data: dict) -> executions.DesignExecution: def trigger(self, execution_input: Score, *, max_candidates: int | None = None): """Trigger a Design Workflow execution given a score and a maximum number of candidates.""" path = self._get_path() - json = {'score': execution_input.dump(), "max_candidates": max_candidates} + json = {"score": execution_input.dump(), "max_candidates": max_candidates} data = self.session.post_resource(path, json) return self.build(data) @@ -56,8 +54,7 @@ def archive(self, uid: UUID | str): Unique identifier of the execution to archive """ - raise NotImplementedError( - "Design Executions cannot be archived") + raise NotImplementedError("Design Executions cannot be archived") def restore(self, uid: UUID): """Restore an archived Design Workflow execution. @@ -68,8 +65,7 @@ def restore(self, uid: UUID): Unique identifier of the execution to restore """ - raise NotImplementedError( - "Design Executions cannot be restored") + raise NotImplementedError("Design Executions cannot be restored") def list(self, *, per_page: int = 100) -> Iterator[executions.DesignExecution]: """ @@ -91,11 +87,12 @@ def list(self, *, per_page: int = 100) -> Iterator[executions.DesignExecution]: Resources in this collection. """ - return self._paginator.paginate(page_fetcher=self._fetch_page, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=self._fetch_page, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def delete(self, uid: UUID | str) -> Response: """Design Workflow Executions cannot be deleted or archived.""" - raise NotImplementedError( - "Design Executions cannot be deleted") + raise NotImplementedError("Design Executions cannot be deleted") diff --git a/src/citrine/resources/design_space.py b/src/citrine/resources/design_space.py index 8d0da2d4d..a0df82104 100644 --- a/src/citrine/resources/design_space.py +++ b/src/citrine/resources/design_space.py @@ -1,14 +1,18 @@ """Resources that represent collections of design spaces.""" + from collections.abc import Iterable from functools import partial from uuid import UUID - -from citrine._utils.functions import format_escaped_url -from citrine.informatics.design_spaces import DefaultDesignSpaceMode, DesignSpaceSettings, \ - HierarchicalDesignSpace, TopLevelDesignSpace from citrine._rest.collection import Collection from citrine._session import Session +from citrine._utils.functions import format_escaped_url +from citrine.informatics.design_spaces import ( + DefaultDesignSpaceMode, + DesignSpaceSettings, + HierarchicalDesignSpace, + TopLevelDesignSpace, +) class DesignSpaceCollection(Collection[TopLevelDesignSpace]): @@ -21,11 +25,11 @@ class DesignSpaceCollection(Collection[TopLevelDesignSpace]): """ - _api_version = 'v3' - _path_template = '/projects/{project_id}/design-spaces' + _api_version = "v3" + _path_template = "/projects/{project_id}/design-spaces" _individual_key = None _resource = TopLevelDesignSpace - _collection_key = 'response' + _collection_key = "response" _enumerated_cell_limit = 128 * 2000 def __init__(self, project_id: UUID, session: Session): @@ -100,9 +104,11 @@ def _list_base(self, *, per_page: int = 100, archived: bool | None = None): filters["archived"] = archived fetcher = partial(self._fetch_page, additional_params=filters, version="v4") - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def list_all(self, *, per_page: int = 20) -> Iterable[TopLevelDesignSpace]: """List all design spaces.""" @@ -116,15 +122,17 @@ def list_archived(self, *, per_page: int = 20) -> Iterable[TopLevelDesignSpace]: """List archived design spaces.""" return self._list_base(per_page=per_page, archived=True) - def create_default(self, - *, - predictor_id: UUID | str, - predictor_version: int | str | None = None, - mode: DefaultDesignSpaceMode = DefaultDesignSpaceMode.ATTRIBUTE, - include_ingredient_fraction_constraints: bool = False, - include_label_fraction_constraints: bool = False, - include_label_count_constraints: bool = False, - include_parameter_constraints: bool = False) -> TopLevelDesignSpace: + def create_default( + self, + *, + predictor_id: UUID | str, + predictor_version: int | str | None = None, + mode: DefaultDesignSpaceMode = DefaultDesignSpaceMode.ATTRIBUTE, + include_ingredient_fraction_constraints: bool = False, + include_label_fraction_constraints: bool = False, + include_label_count_constraints: bool = False, + include_parameter_constraints: bool = False, + ) -> TopLevelDesignSpace: """Create a default design space for a predictor. This method will return an unregistered design space for all inputs @@ -172,7 +180,7 @@ def create_default(self, Default design space """ - path = f'projects/{self.project_id}/design-spaces/default' + path = f"projects/{self.project_id}/design-spaces/default" settings = DesignSpaceSettings( predictor_id=predictor_id, predictor_version=predictor_version, @@ -180,7 +188,7 @@ def create_default(self, include_ingredient_fraction_constraints=include_ingredient_fraction_constraints, include_label_fraction_constraints=include_label_fraction_constraints, include_label_count_constraints=include_label_count_constraints, - include_parameter_constraints=include_parameter_constraints + include_parameter_constraints=include_parameter_constraints, ) data = self.session.post_resource(path, json=settings.dump(), version=self._api_version) @@ -189,11 +197,11 @@ def create_default(self, return ds def convert_to_hierarchical( - self, - uid: UUID | str, - *, - predictor_id: UUID | str, - predictor_version: int | str | None = None + self, + uid: UUID | str, + *, + predictor_id: UUID | str, + predictor_version: int | str | None = None, ) -> HierarchicalDesignSpace: """Convert an existing ProductDesignSpace into an equivalent HierarchicalDesignSpace. @@ -221,11 +229,9 @@ def convert_to_hierarchical( path = format_escaped_url( "projects/{project_id}/design-spaces/{design_space_id}/convert-hierarchical", project_id=self.project_id, - design_space_id=uid + design_space_id=uid, ) - payload = { - "predictor_id": str(predictor_id), - } + payload = {"predictor_id": str(predictor_id)} if predictor_version: payload["predictor_version"] = predictor_version data = self.session.post_resource(path, json=payload, version=self._api_version) diff --git a/src/citrine/resources/design_workflow.py b/src/citrine/resources/design_workflow.py index cf729dc03..fde0b95f2 100644 --- a/src/citrine/resources/design_workflow.py +++ b/src/citrine/resources/design_workflow.py @@ -1,29 +1,31 @@ from collections.abc import Callable, Iterable from copy import deepcopy +from functools import partial from uuid import UUID from citrine._rest.collection import Collection from citrine._session import Session from citrine.informatics.workflows import DesignWorkflow from citrine.resources.response import Response -from functools import partial class DesignWorkflowCollection(Collection[DesignWorkflow]): """A collection of DesignWorkflows.""" - _path_template = '/projects/{project_id}/design-workflows' + _path_template = "/projects/{project_id}/design-workflows" _individual_key = None - _collection_key = 'response' + _collection_key = "response" _resource = DesignWorkflow _api_version = "v2" - def __init__(self, - project_id: UUID, - session: Session, - *, - branch_root_id: UUID | None = None, - branch_version: int | None = None): + def __init__( + self, + project_id: UUID, + session: Session, + *, + branch_root_id: UUID | None = None, + branch_version: int | None = None, + ): self.project_id: UUID = project_id self.session: Session = session @@ -52,9 +54,11 @@ def register(self, model: DesignWorkflow) -> DesignWorkflow: if self.branch_root_id is None or self.branch_version 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 disallowed. - msg = ('A design workflow must be created with a branch. Please use ' - 'branch.design_workflows.register() instead of ' - 'project.design_workflows.register().') + 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_root_id and branch_version are in the body of design workflow endpoints, so @@ -107,20 +111,28 @@ def update(self, model: DesignWorkflow) -> DesignWorkflow: """ if self.branch_root_id is not None or self.branch_version is not None: - if self.branch_root_id != model.branch_root_id or \ - self.branch_version != model.branch_version: - raise ValueError('To move a design workflow to another branch, please use ' - 'Project.design_workflows.update') + if ( + self.branch_root_id != model.branch_root_id + or self.branch_version != model.branch_version + ): + raise ValueError( + "To move a design workflow to another branch, please use " + "Project.design_workflows.update" + ) 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.') + raise ValueError( + "Cannot update a design workflow unless its branch_root_id and " + "branch_version are set." + ) # If executions have already been done, warn about future behavior change executions = model.design_executions.list() if next(executions, None) is not None: - raise RuntimeError("Cannot update a design workflow after candidate generation, " - "please register a new design workflow instead") + raise RuntimeError( + "Cannot update a design workflow after candidate generation, " + "please register a new design workflow instead" + ) return super().update(model) @@ -151,29 +163,35 @@ def restore(self, uid: UUID | str): def delete(self, uid: UUID | str) -> Response: """Design Workflows cannot be deleted; they can be archived instead.""" raise NotImplementedError( - "Design Workflows cannot be deleted; they can be archived instead.") + "Design Workflows cannot be deleted; they can be archived instead." + ) def list_archived(self, *, per_page: int = 500) -> Iterable[DesignWorkflow]: """List archived Design Workflows.""" fetcher = partial(self._fetch_page, additional_params={"filter": "archived eq 'true'"}) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) - - def _fetch_page(self, - path: str | None = None, - fetch_func: Callable[..., dict] | None = None, - page: int | None = None, - per_page: int | None = None, - json_body: dict | None = None, - additional_params: dict | None = None, - ) -> tuple[Iterable[dict], str]: + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) + + def _fetch_page( + self, + path: str | None = None, + fetch_func: Callable[..., dict] | None = None, + page: int | None = None, + per_page: int | None = None, + json_body: dict | None = None, + additional_params: dict | None = None, + ) -> tuple[Iterable[dict], str]: params = additional_params or {} params["branch_root_id"] = self.branch_root_id params["branch_version"] = self.branch_version - return super()._fetch_page(path=path, - fetch_func=fetch_func, - page=page, - per_page=per_page, - json_body=json_body, - additional_params=params) + return super()._fetch_page( + path=path, + fetch_func=fetch_func, + page=page, + per_page=per_page, + json_body=json_body, + additional_params=params, + ) diff --git a/src/citrine/resources/file_link.py b/src/citrine/resources/file_link.py index 0348fa122..5c4ebaeae 100644 --- a/src/citrine/resources/file_link.py +++ b/src/citrine/resources/file_link.py @@ -1,4 +1,5 @@ """A collection of FileLink objects.""" + import mimetypes import os from collections.abc import Iterable, Sequence @@ -8,6 +9,15 @@ from urllib.request import url2pathname from uuid import UUID +import requests +from boto3 import client as boto3_client +from boto3.session import Config +from botocore.exceptions import ClientError +from gemd.entity.bounds.base_bounds import BaseBounds +from gemd.entity.dict_serializable import DictSerializableMeta +from gemd.entity.file_link import FileLink as GEMDFileLink +from gemd.enumeration.base_enumeration import BaseEnumeration + from citrine._rest.collection import Collection from citrine._rest.resource import GEMDResource from citrine._serialization import properties @@ -15,15 +25,6 @@ from citrine._session import Session from citrine._utils.functions import rewrite_s3_links_locally, write_file_locally from citrine.resources.response import Response -from gemd.entity.dict_serializable import DictSerializableMeta -from gemd.entity.bounds.base_bounds import BaseBounds -from gemd.entity.file_link import FileLink as GEMDFileLink -from gemd.enumeration.base_enumeration import BaseEnumeration - -import requests -from boto3 import client as boto3_client -from boto3.session import Config -from botocore.exceptions import ClientError class SearchFileFilterTypeEnum(BaseEnumeration): @@ -49,31 +50,32 @@ class _Uploader: """Holds the many parameters that are generated and used during file upload.""" def __init__(self): - self.bucket = '' - self.object_key = '' - self.upload_id = '' - self.region_name = '' - self.aws_access_key_id = '' - self.aws_secret_access_key = '' - self.aws_session_token = '' - self.s3_version = '' + self.bucket = "" + self.object_key = "" + self.upload_id = "" + self.region_name = "" + self.aws_access_key_id = "" + self.aws_secret_access_key = "" + self.aws_session_token = "" + self.s3_version = "" self.s3_endpoint_url = None self.s3_use_ssl = True - self.s3_addressing_style = 'auto' + self.s3_addressing_style = "auto" class CsvColumnInfo(Serializable): """The info for a CSV Column, contains the name, recommended and exact bounds.""" - name = properties.String('name') + name = properties.String("name") """:str: name of the column""" - bounds = properties.Object(BaseBounds, 'bounds') + bounds = properties.Object(BaseBounds, "bounds") """:BaseBounds: recommended bounds of the column (might include some padding)""" - exact_range_bounds = properties.Object(BaseBounds, 'exact_range_bounds') + exact_range_bounds = properties.Object(BaseBounds, "exact_range_bounds") """:BaseBounds: exact bounds of the column""" - def __init__(self, name: str, bounds: BaseBounds, - exact_range_bounds: BaseBounds): # pragma: no cover + def __init__( + self, name: str, bounds: BaseBounds, exact_range_bounds: BaseBounds + ): # pragma: no cover self.name = name self.bounds = bounds self.exact_range_bounds = exact_range_bounds @@ -84,7 +86,7 @@ class FileLinkMeta(DictSerializableMeta): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) - cls.typ = properties.String('type', default="file_link", deserializable=False) + cls.typ = properties.String("type", default="file_link", deserializable=False) def _get_ids_from_url(url: str) -> tuple[UUID | None, UUID | None]: @@ -93,11 +95,11 @@ def _get_ids_from_url(url: str) -> tuple[UUID | None, UUID | None]: if len(parsed.query) > 0 or len(parsed.fragment) > 0: # Illegal modifiers return None, None - split_path = urlparse(url).path.split('/') - if len(split_path) >= 4 and split_path[-4] == 'files' and split_path[-2] == 'versions': + split_path = urlparse(url).path.split("/") + if len(split_path) >= 4 and split_path[-4] == "files" and split_path[-2] == "versions": file_id = split_path[-3] version_id = split_path[-1] - elif len(split_path) >= 2 and split_path[-2] == 'files': + elif len(split_path) >= 2 and split_path[-2] == "files": file_id = split_path[-1] version_id = None else: @@ -114,10 +116,7 @@ def _get_ids_from_url(url: str) -> tuple[UUID | None, UUID | None]: class FileLink( - GEMDResource['FileLink'], - GEMDFileLink, - metaclass=FileLinkMeta, - typ=GEMDFileLink.typ + GEMDResource["FileLink"], GEMDFileLink, metaclass=FileLinkMeta, typ=GEMDFileLink.typ ): """ Resource that stores the name and url of an external file. @@ -134,23 +133,23 @@ class FileLink( # NOTE: skipping the "metadata" field since it appears to be unused # NOTE: skipping the "versioned_url" field since it is redundant # NOTE: skipping the "unversioned_url" field since it is redundant - filename = properties.String('filename') - url = properties.String('url') - uid = properties.Optional(properties.UUID, 'id', serializable=False) + 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) + 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) + 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) + 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) + 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) + size = properties.Optional(properties.Integer, "size", serializable=False) """int: Size in bytes of the file.""" - description = properties.Optional(properties.String, 'description', serializable=False) + 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) + 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.""" @@ -177,23 +176,23 @@ def name(self): @classmethod def _pre_build(cls, data: dict) -> dict: """Run data modification before building.""" - if 'url' in data and 'id' not in data: - uid, version = _get_ids_from_url(data['url']) + if "url" in data and "id" not in data: + uid, version = _get_ids_from_url(data["url"]) if uid is not None: - data['id'] = str(uid) + data["id"] = str(uid) if version is not None: - data['version'] = str(version) + data["version"] = str(version) return data def __str__(self): - return f'' + return f"" class FileCollection(Collection[FileLink]): """Represents the collection of all file links associated with a dataset.""" - _path_template = 'teams/{team_id}/datasets/{dataset_id}/files' - _collection_key = 'files' + _path_template = "teams/{team_id}/datasets/{dataset_id}/files" + _collection_key = "files" _resource = FileLink def __init__(self, *, session: Session, dataset_id: UUID, team_id: UUID): @@ -201,21 +200,21 @@ def __init__(self, *, session: Session, dataset_id: UUID, team_id: UUID): self.session = session self.team_id = team_id - def _get_path(self, - uid: UUID | str | None = None, - *, - ignore_dataset: bool | None = False, - version: str | UUID = None, - action: str | Sequence[str] = [], - query_terms: dict[str, str] = {},) -> str: + def _get_path( + self, + uid: UUID | str | None = None, + *, + ignore_dataset: bool | None = False, + version: str | UUID = None, + action: str | Sequence[str] = [], + query_terms: dict[str, str] = {}, + ) -> str: """Build the path for taking an action with a particular file version.""" if version is not None: - action = ['versions', version] + ([action] if isinstance(action, str) else action) + action = ["versions", version] + ([action] if isinstance(action, str) else action) return super()._get_path(uid=uid, ignore_dataset=ignore_dataset, action=action) - def _get_path_from_file_link(self, file_link: FileLink, - *, - action: str = None) -> str: + def _get_path_from_file_link(self, file_link: FileLink, *, action: str = None) -> str: """Build the platform path for taking an action with a particular file link.""" if not self._is_on_platform_url(file_link.url) or file_link.uid is None: raise ValueError("FileLink did not contain a Citrine platform file URL.") @@ -224,15 +223,12 @@ def _get_path_from_file_link(self, file_link: FileLink, def build(self, data: dict) -> FileLink: """Build an instance of FileLink.""" # Use this chance to construct a URL from platform metadata - if 'url' not in data: - data['url'] = self._get_path(uid=data['id'], version=data['version']) + if "url" not in data: + data["url"] = self._get_path(uid=data["id"], version=data["version"]) return FileLink.build(data) - def get(self, - uid: UUID | str, - *, - version: UUID | str | int | None = None) -> FileLink: + def get(self, uid: UUID | str, *, version: UUID | str | int | None = None) -> FileLink: """ Retrieve an on-platform FileLink from its filename or file uuid. @@ -252,11 +248,14 @@ def get(self, """ if not isinstance(uid, (str, UUID)): - raise TypeError(f"File Link can only be resolved from str or UUID." - f"Instead got {type(uid)} {uid}.") + raise TypeError( + f"File Link can only be resolved from str or UUID.Instead got {type(uid)} {uid}." + ) if version is not None and not isinstance(version, (str, UUID, int)): - raise TypeError(f"Version can only be resolved from str, int or UUID." - f"Instead got {type(uid)} {uid}.") + raise TypeError( + f"Version can only be resolved from str, int or UUID." + f"Instead got {type(uid)} {uid}." + ) if isinstance(uid, str): try: # Check if the uid string is actually a UUID @@ -278,18 +277,18 @@ def get(self, if isinstance(uid, str): # Assume it's the filename on platform; if version is None or isinstance(version, int): - file = self._search_by_file_name(dset_id=self.dataset_id, - file_name=uid, - file_version_number=version) + file = self._search_by_file_name( + dset_id=self.dataset_id, file_name=uid, file_version_number=version + ) else: # We did our type checks earlier; version is a UUID file = self._search_by_file_version_id(file_version_id=version) else: # We did our type checks earlier; uid is a UUID if isinstance(version, UUID): file = self._search_by_file_version_id(file_version_id=version) else: # We did our type checks earlier; version is an int or None - file = self._search_by_dataset_file_id(dataset_file_id=uid, - dset_id=self.dataset_id, - file_version_number=version) + file = self._search_by_dataset_file_id( + dataset_file_id=uid, dset_id=self.dataset_id, file_version_number=version + ) return file @@ -350,13 +349,7 @@ def _make_upload_request(self, file_path: Path, dest_name: str): file_size = file_path.stat().st_size assert isinstance(file_size, int) upload_json = { - 'files': [ - { - 'file_name': dest_name, - 'mime_type': mime_type, - 'size': file_size - } - ] + "files": [{"file_name": dest_name, "mime_type": mime_type, "size": file_size}] } # POST request creates space in S3 for the file and returns AWS-related information # (such as temporary credentials) that allow the file to be uploaded. @@ -365,29 +358,28 @@ def _make_upload_request(self, file_path: Path, dest_name: str): # Extract all relevant information from the upload request try: - - uploader.region_name = upload_request['s3_region'] - uploader.aws_access_key_id = upload_request['temporary_credentials']['access_key_id'] - uploader.aws_secret_access_key = \ - upload_request['temporary_credentials']['secret_access_key'] - uploader.aws_session_token = upload_request['temporary_credentials']['session_token'] - uploader.bucket = upload_request['s3_bucket'] - uploader.object_key = upload_request['uploads'][0]['s3_key'] - uploader.upload_id = upload_request['uploads'][0]['upload_id'] + uploader.region_name = upload_request["s3_region"] + uploader.aws_access_key_id = upload_request["temporary_credentials"]["access_key_id"] + uploader.aws_secret_access_key = upload_request["temporary_credentials"][ + "secret_access_key" + ] + uploader.aws_session_token = upload_request["temporary_credentials"]["session_token"] + uploader.bucket = upload_request["s3_bucket"] + uploader.object_key = upload_request["uploads"][0]["s3_key"] + uploader.upload_id = upload_request["uploads"][0]["upload_id"] uploader.s3_endpoint_url = self.session.s3_endpoint_url uploader.s3_use_ssl = self.session.s3_use_ssl uploader.s3_addressing_style = self.session.s3_addressing_style except KeyError: - raise RuntimeError("Upload initiation response is missing some fields: " - "{}".format(upload_request)) + raise RuntimeError( + f"Upload initiation response is missing some fields: {upload_request}" + ) return uploader - def _search_by_file_name(self, - file_name: str, - dset_id: UUID, - file_version_number: int | None = None - ) -> FileLink | None: + def _search_by_file_name( + self, file_name: str, dset_id: UUID, file_version_number: int | None = None + ) -> FileLink | None: """ Make a request to the backend to search a file by name. @@ -412,22 +404,19 @@ def _search_by_file_name(self, path = self._get_path(action="search") search_json = { - 'fileSearchFilter': - { - 'type': SearchFileFilterTypeEnum.NAME_SEARCH.value, - 'datasetId': str(dset_id), - 'fileName': file_name, - 'fileVersionNumber': file_version_number - } + "fileSearchFilter": { + "type": SearchFileFilterTypeEnum.NAME_SEARCH.value, + "datasetId": str(dset_id), + "fileName": file_name, + "fileVersionNumber": file_version_number, + } } data = self.session.post_resource(path=path, json=search_json) - return self.build(data['files'][0]) + return self.build(data["files"][0]) - def _search_by_file_version_id(self, - file_version_id: UUID - ) -> FileLink | None: + def _search_by_file_version_id(self, file_version_id: UUID) -> FileLink | None: """ Make a request to the backend to search a file by file version id. @@ -445,21 +434,19 @@ def _search_by_file_version_id(self, path = self._get_path(action="search") search_json = { - 'fileSearchFilter': { - 'type': SearchFileFilterTypeEnum.VERSION_ID_SEARCH.value, - 'fileVersionUuid': str(file_version_id) + "fileSearchFilter": { + "type": SearchFileFilterTypeEnum.VERSION_ID_SEARCH.value, + "fileVersionUuid": str(file_version_id), } } data = self.session.post_resource(path=path, json=search_json) - return self.build(data['files'][0]) + return self.build(data["files"][0]) - def _search_by_dataset_file_id(self, - dataset_file_id: UUID, - dset_id: UUID, - file_version_number: int | None = None - ) -> FileLink | None: + def _search_by_dataset_file_id( + self, dataset_file_id: UUID, dset_id: UUID, file_version_number: int | None = None + ) -> FileLink | None: """ Make a request to the backend to search a file by dataset file id. @@ -484,17 +471,17 @@ def _search_by_dataset_file_id(self, path = self._get_path(action="search") search_json = { - 'fileSearchFilter': { - 'type': SearchFileFilterTypeEnum.DATASET_FILE_ID_SEARCH.value, - 'datasetId': str(dset_id), - 'datasetFileId': str(dataset_file_id), - 'fileVersionNumber': file_version_number + "fileSearchFilter": { + "type": SearchFileFilterTypeEnum.DATASET_FILE_ID_SEARCH.value, + "datasetId": str(dset_id), + "datasetFileId": str(dataset_file_id), + "fileVersionNumber": file_version_number, } } data = self.session.post_resource(path=path, json=search_json) - return self.build(data['files'][0]) + return self.build(data["files"][0]) @staticmethod def _mime_type(file_path: Path): @@ -522,20 +509,22 @@ def _upload_file(file_path: Path, uploader: _Uploader): """ additional_s3_opts = { - 'use_ssl': uploader.s3_use_ssl, - 'config': Config(s3={'addressing_style': uploader.s3_addressing_style}) + "use_ssl": uploader.s3_use_ssl, + "config": Config(s3={"addressing_style": uploader.s3_addressing_style}), } if uploader.s3_endpoint_url is not None: - additional_s3_opts['endpoint_url'] = uploader.s3_endpoint_url - - s3_client = boto3_client('s3', - region_name=uploader.region_name, - aws_access_key_id=uploader.aws_access_key_id, - aws_secret_access_key=uploader.aws_secret_access_key, - aws_session_token=uploader.aws_session_token, - **additional_s3_opts) - with file_path.open(mode='rb') as f: + additional_s3_opts["endpoint_url"] = uploader.s3_endpoint_url + + s3_client = boto3_client( + "s3", + region_name=uploader.region_name, + aws_access_key_id=uploader.aws_access_key_id, + aws_secret_access_key=uploader.aws_secret_access_key, + aws_session_token=uploader.aws_session_token, + **additional_s3_opts, + ) + with file_path.open(mode="rb") as f: try: # NOTE: This is only using the simple PUT logic, not the more sophisticated # multipart upload approach that is also available (providing parallel @@ -544,11 +533,13 @@ def _upload_file(file_path: Path, uploader: _Uploader): Bucket=uploader.bucket, Key=uploader.object_key, Body=f, - Metadata={"X-Citrine-Upload-Id": uploader.upload_id}) + Metadata={"X-Citrine-Upload-Id": uploader.upload_id}, + ) except ClientError as e: - raise RuntimeError(f"Upload of file {file_path} failed with the following " - f"exception: {e}") - uploader.s3_version = upload_response['VersionId'] + raise RuntimeError( + f"Upload of file {file_path} failed with the following exception: {e}" + ) + uploader.s3_version = upload_response["VersionId"] return uploader def _complete_upload(self, dest_name: str, uploader: _Uploader): @@ -569,15 +560,17 @@ def _complete_upload(self, dest_name: str, uploader: _Uploader): """ url = self._get_path(action=["uploads", uploader.upload_id, "complete"]) - complete_response = self.session.put_resource(path=url, - json={'s3_version': uploader.s3_version}) + complete_response = self.session.put_resource( + path=url, json={"s3_version": uploader.s3_version} + ) try: - file_id = complete_response['file_info']['file_id'] - version_id = complete_response['file_info']['version'] + file_id = complete_response["file_info"]["file_id"] + version_id = complete_response["file_info"]["version"] except KeyError: - raise RuntimeError("Upload completion response is missing some " - "fields: {}".format(complete_response)) + raise RuntimeError( + f"Upload completion response is missing some fields: {complete_response}" + ) return self.build({"filename": dest_name, "id": file_id, "version": version_id}) @@ -629,36 +622,37 @@ def read(self, *, file_link: str | UUID | FileLink) -> bytes: if self._is_local_url(file_link.url): # Read the local file parsed_url = urlparse(file_link.url) - if parsed_url.netloc not in {'', '.', 'localhost'}: + if parsed_url.netloc not in {"", ".", "localhost"}: raise ValueError("Non-local UNCs (e.g., Windows network paths) are not supported.") # Space should have been encoded as %20, but just in case it was a + - path = Path(url2pathname(parsed_url.path.replace('+', '%20'))) + path = Path(url2pathname(parsed_url.path.replace("+", "%20"))) return path.read_bytes() if self._is_external_url(file_link.url): # Pull it from where ever it lives final_url = file_link.url else: # The "/content-link" route returns a pre-signed url to download the file. - content_link = self._get_path_from_file_link(file_link, action='content-link') + content_link = self._get_path_from_file_link(file_link, action="content-link") content_link_response = self.session.get_resource(content_link) - pre_signed_url = content_link_response['pre_signed_read_link'] + pre_signed_url = content_link_response["pre_signed_read_link"] final_url = rewrite_s3_links_locally(pre_signed_url, self.session.s3_endpoint_url) download_response = requests.get(final_url) return download_response.content - def ingest(self, - files: Iterable[FileLink | Path | str], - *, - upload: bool = False, - raise_errors: bool = True, - build_table: bool = False, - delete_dataset_contents: bool = False, - delete_templates: bool = True, - timeout: float = None, - polling_delay: float | None = None, - project: "Project | UUID | str | None" = None, # noqa: F821 - ) -> "IngestionStatus": # noqa: F821 + def ingest( + self, + files: Iterable[FileLink | Path | str], + *, + upload: bool = False, + raise_errors: bool = True, + build_table: bool = False, + delete_dataset_contents: bool = False, + delete_templates: bool = True, + timeout: float = None, + polling_delay: float | None = None, + project: "Project | UUID | str | None" = None, # noqa: F821 + ) -> "IngestionStatus": # noqa: F821 """ [ALPHA] Ingest a set of CSVs and/or Excel Workbooks formatted per the gemd-ingest protocol. @@ -740,15 +734,16 @@ def resolve_with_local(candidate: FileLink | Path | str) -> FileLink: self.download(file_link=file_link, local_path=path) onplatform.append(self.upload(file_path=path, dest_name=file_link.filename)) elif len(offplatform) > 0: - raise ValueError(f"All files must be on-platform to load them. " - f"The following are not: {offplatform}") + raise ValueError( + f"All files must be on-platform to load them. " + f"The following are not: {offplatform}" + ) - ingestion_collection = IngestionCollection(team_id=self.team_id, - dataset_id=self.dataset_id, - session=self.session) + ingestion_collection = IngestionCollection( + team_id=self.team_id, dataset_id=self.dataset_id, session=self.session + ) ingestion = ingestion_collection.build_from_file_links( - file_links=onplatform, - raise_errors=raise_errors + file_links=onplatform, raise_errors=raise_errors ) return ingestion.build_objects( build_table=build_table, @@ -756,7 +751,7 @@ def resolve_with_local(candidate: FileLink | Path | str) -> FileLink: delete_dataset_contents=delete_dataset_contents, delete_templates=delete_templates, timeout=timeout, - polling_delay=polling_delay + polling_delay=polling_delay, ) def delete(self, file_link: FileLink): @@ -785,8 +780,9 @@ def _resolve_file_link(self, identifier: str | UUID | FileLink) -> FileLink: if self._is_on_platform_url(update.url): if update.uid is None: - raise ValueError(f"URL was malformed for platform resources; " - f"passed URL {update.url}") + raise ValueError( + f"URL was malformed for platform resources; passed URL {update.url}" + ) else: # Validate that it's a real record update = self.get(uid=update.uid, version=update.version) if update.filename != identifier.filename: @@ -805,13 +801,15 @@ def _resolve_file_link(self, identifier: str | UUID | FileLink) -> FileLink: else: # We got a file UID (and possibly a version UID) from a URL return self.get(uid=file_id, version=version_id) else: # Assume it's an absolute URL - filename = urlparse(identifier).path.split('/')[-1] + filename = urlparse(identifier).path.split("/")[-1] return FileLink(filename=filename, url=identifier) elif isinstance(identifier, UUID): # File UID return self.get(uid=identifier) else: - raise TypeError(f"File Link can only be resolved from str, or UUID." - f"Instead got {type(identifier)} {identifier}.") + raise TypeError( + f"File Link can only be resolved from str, or UUID." + f"Instead got {type(identifier)} {identifier}." + ) def _is_external_url(self, url: str): """Check if the URL is absolute and not associated with this platform instance.""" diff --git a/src/citrine/resources/gemd_resource.py b/src/citrine/resources/gemd_resource.py index b364b6302..2c872e236 100644 --- a/src/citrine/resources/gemd_resource.py +++ b/src/citrine/resources/gemd_resource.py @@ -1,22 +1,31 @@ """Collection class for generic GEMD objects and templates.""" + import re from collections.abc import Iterable from uuid import UUID, uuid4 from gemd.entity.base_entity import BaseEntity from gemd.entity.link_by_uid import LinkByUID -from gemd.util import recursive_flatmap, recursive_foreach, set_uuids, \ - make_index, substitute_objects +from gemd.util import ( + make_index, + recursive_flatmap, + recursive_foreach, + set_uuids, + substitute_objects, +) from tqdm.auto import tqdm -from citrine.resources.api_error import ApiError -from citrine.resources.data_concepts import DataConcepts, DataConceptsCollection, \ - CITRINE_SCOPE, CITRINE_TAG_PREFIX -from citrine.resources.delete import _async_gemd_batch_delete from citrine._session import Session from citrine._utils.batcher import Batcher from citrine._utils.functions import replace_objects_with_links, scrub_none - +from citrine.resources.api_error import ApiError +from citrine.resources.data_concepts import ( + CITRINE_SCOPE, + CITRINE_TAG_PREFIX, + DataConcepts, + DataConceptsCollection, +) +from citrine.resources.delete import _async_gemd_batch_delete BATCH_SIZE = 50 @@ -24,7 +33,7 @@ class GEMDResourceCollection(DataConceptsCollection[DataConcepts]): """A collection of any kind of GEMD objects/templates.""" - _collection_key = 'storables' + _collection_key = "storables" def __init__(self, *, dataset_id: UUID, session: Session, team_id: UUID): super().__init__(team_id=team_id, dataset_id=dataset_id, session=session) @@ -60,12 +69,14 @@ def build(self, data: dict) -> DataConcepts: """ return super().build(data) - def register_all(self, - models: Iterable[DataConcepts], - *, - dry_run=False, - status_bar=False, - include_nested=False) -> list[DataConcepts]: + def register_all( + self, + models: Iterable[DataConcepts], + *, + dry_run=False, + status_bar=False, + include_nested=False, + ) -> list[DataConcepts]: """ Register multiple GEMD objects to each of their appropriate collections. @@ -109,7 +120,7 @@ def register_all(self, if self.dataset_id is None: raise RuntimeError("Must specify a dataset in order to register a data model object.") path = self._get_path() - params = {'dry_run': dry_run} + params = {"dry_run": dry_run} if include_nested: models = recursive_flatmap(models, lambda o: [o], unidirectional=False) @@ -134,11 +145,9 @@ def register_all(self, for batch in iterator: objects = [replace_objects_with_links(scrub_none(model.dump())) for model in batch] response_data = self.session.put_resource( - path + '/batch', - json={'objects': objects}, - params=params + path + "/batch", json={"objects": objects}, params=params ) - registered = [self.build(obj) for obj in response_data['objects']] + registered = [self.build(obj) for obj in response_data["objects"]] result_index.update(make_index(registered)) substitute_objects(registered, result_index, inplace=True) @@ -158,8 +167,9 @@ def register_all(self, citr_id = result.uids.pop(CITRINE_SCOPE, None) result_index.pop(LinkByUID(scope=CITRINE_SCOPE, id=citr_id), None) if result.tags is not None: - todo = [tag for tag in result.tags - if re.match(f"^{CITRINE_TAG_PREFIX}::", tag)] + todo = [ + tag for tag in result.tags if re.match(f"^{CITRINE_TAG_PREFIX}::", tag) + ] for tag in todo: # Covering this block would require dark art if tag not in obj.tags: result.tags.remove(tag) @@ -167,16 +177,17 @@ def register_all(self, resources.extend(registered) if dry_run: # No-op if not dry-run - recursive_foreach(list(models) + list(resources), - lambda x: x.uids.pop(temp_scope, None)) # Strip temp uids + recursive_foreach( + list(models) + list(resources), lambda x: x.uids.pop(temp_scope, None) + ) # Strip temp uids return resources def batch_delete( - self, - id_list: list[LinkByUID | UUID | str | BaseEntity], - *, - timeout: float = 2 * 60, - polling_delay: float = 1.0 + self, + id_list: list[LinkByUID | UUID | str | BaseEntity], + *, + timeout: float = 2 * 60, + polling_delay: float = 1.0, ) -> list[tuple[LinkByUID, ApiError]]: """ Remove a set of GEMD objects. @@ -218,4 +229,5 @@ def batch_delete( session=self.session, dataset_id=self.dataset_id, timeout=timeout, - polling_delay=polling_delay) + polling_delay=polling_delay, + ) diff --git a/src/citrine/resources/gemtables.py b/src/citrine/resources/gemtables.py index 2104b9ea2..c99c1253c 100644 --- a/src/citrine/resources/gemtables.py +++ b/src/citrine/resources/gemtables.py @@ -11,15 +11,18 @@ from citrine._serialization import properties from citrine._serialization.properties import UUID from citrine._session import Session -from citrine._utils.functions import format_escaped_url, rewrite_s3_links_locally, \ - write_file_locally +from citrine._utils.functions import ( + format_escaped_url, + rewrite_s3_links_locally, + write_file_locally, +) from citrine.jobs.job import JobSubmissionResponse, _poll_for_job_completion from citrine.resources.table_config import TableConfig, TableConfigCollection logger = getLogger(__name__) -class GemTable(Resource['Table']): +class GemTable(Resource["Table"]): """A 2-dimensional projection of data. GEM Tables are the basic unit used to flatten and manipulate data objects. @@ -28,15 +31,15 @@ class GemTable(Resource['Table']): can be used to 'flatten' data objects into useful projections. """ - _response_key = 'table' + _response_key = "table" _resource_type = ResourceTypeEnum.TABLE - uid = properties.UUID('id') + uid = properties.UUID("id") """:UUID: unique Citrine id of this GEM Table""" - version = properties.Integer('version') + version = properties.Integer("version") """:int: Version number of the GEM Table. The first table built from a given config is version 1.""" - download_url = properties.String('signed_download_url') + download_url = properties.String("signed_download_url") """:str: URL pointing to the location of the GEM Table's contents. This is an expiring download link and is not unique.""" @@ -50,15 +53,15 @@ def __init__(self): self._session = None def __str__(self): - return ''.format(self.uid, self.version) + return f"" @property def config(self) -> TableConfig: """Configuration used to build the table.""" if self._config is None: - config_collection = TableConfigCollection(team_id=self._team_id, - project_id=self._project_id, - session=self._session) + config_collection = TableConfigCollection( + team_id=self._team_id, project_id=self._project_id, session=self._session + ) self._config = config_collection.get_for_table(self) return self._config @@ -92,8 +95,8 @@ def _comparison_fields(self, entity: GemTable) -> Any: class GemTableCollection(Collection[GemTable]): """Represents the collection of all tables associated with a project.""" - _path_template = 'projects/{project_id}/display-tables' - _collection_key: str = 'tables' + _path_template = "projects/{project_id}/display-tables" + _collection_key: str = "tables" _paginator: Paginator = GemTableVersionPaginator() _resource = GemTable @@ -113,10 +116,7 @@ def get(self, uid: UUID | str, *, version: int | None = None) -> GemTable: newest_table = max(tables, key=lambda x: x.version or 0) return newest_table - def list_versions(self, - uid: UUID, - *, - per_page: int = 100) -> Iterable[GemTable]: + def list_versions(self, uid: UUID, *, per_page: int = 100) -> Iterable[GemTable]: """ List the versions of a table given a specific Table UID. @@ -127,11 +127,12 @@ def list_versions(self, :param per_page: The number of items to fetch per-page. :return: An iterable of the versions of the Tables (as Table objects). """ - def _fetch_versions(page: int | None, - per_page: int) -> tuple[Iterable[dict], str]: - data = self.session.get_resource(self._get_path(uid), - params=self._page_params(page, per_page)) - return data[self._collection_key], data.get('next', "") + + def _fetch_versions(page: int | None, per_page: int) -> tuple[Iterable[dict], str]: + data = self.session.get_resource( + self._get_path(uid), params=self._page_params(page, per_page) + ) + return data[self._collection_key], data.get("next", "") def _build_versions(collection: Iterable[dict]) -> Iterable[GemTable]: for item in collection: @@ -139,12 +140,13 @@ def _build_versions(collection: Iterable[dict]) -> Iterable[GemTable]: return self._paginator.paginate( # Don't deduplicate on uid since uids are shared between versions - _fetch_versions, _build_versions, per_page, deduplicate=False) + _fetch_versions, + _build_versions, + per_page, + deduplicate=False, + ) - def list_by_config(self, - table_config_uid: UUID, - *, - per_page: int = 100) -> Iterable[GemTable]: + def list_by_config(self, table_config_uid: UUID, *, per_page: int = 100) -> Iterable[GemTable]: """ List the versions of a table associated with a given Table Config UID. @@ -155,18 +157,16 @@ def list_by_config(self, :param per_page: The number of items to fetch per-page. :return: An iterable of the versions of the Tables (as Table objects). """ - def _fetch_versions(page: int | None, - per_page: int) -> tuple[Iterable[dict], str]: - path_params = {'table_config_uid_str': str(table_config_uid)} + + def _fetch_versions(page: int | None, per_page: int) -> tuple[Iterable[dict], str]: + path_params = {"table_config_uid_str": str(table_config_uid)} path_params.update(self.__dict__) path = format_escaped_url( - 'projects/{project_id}/table-configs/{table_config_uid_str}/gem-tables', - **path_params + "projects/{project_id}/table-configs/{table_config_uid_str}/gem-tables", + **path_params, ) - data = self.session.get_resource( - path, - params=self._page_params(page, per_page)) - return data[self._collection_key], data.get('next', "") + data = self.session.get_resource(path, params=self._page_params(page, per_page)) + return data[self._collection_key], data.get("next", "") def _build_versions(collection: Iterable[dict]) -> Iterable[GemTable]: for item in collection: @@ -174,10 +174,15 @@ def _build_versions(collection: Iterable[dict]) -> Iterable[GemTable]: return self._paginator.paginate( # Don't deduplicate on uid since uids are shared between versions - _fetch_versions, _build_versions, per_page, deduplicate=False) + _fetch_versions, + _build_versions, + per_page, + deduplicate=False, + ) - def initiate_build(self, config: TableConfig | str | UUID, *, - version: str | UUID = None) -> JobSubmissionResponse: + def initiate_build( + self, config: TableConfig | str | UUID, *, version: str | UUID = None + ) -> JobSubmissionResponse: """ Initiates tables build with provided config. @@ -200,20 +205,24 @@ def initiate_build(self, config: TableConfig | str | UUID, *, """ if isinstance(config, TableConfig): if version is not None: - logger.warning('Ignoring version %s since config object was provided.', version) + logger.warning("Ignoring version %s since config object was provided.", version) if config.version_number is None: - raise ValueError('Cannot build table from config which has no version. ' - 'Try registering the config before building.') + raise ValueError( + "Cannot build table from config which has no version. " + "Try registering the config before building." + ) if config.uid is None: - raise ValueError('Cannot build table from config which has no uid. ' - 'Try registering the config before building.') + raise ValueError( + "Cannot build table from config which has no uid. " + "Try registering the config before building." + ) uid = config.uid version = config.version_number else: if version is None: - raise ValueError('Version must be specified when building by config uid.') + raise ValueError("Version must be specified when building by config uid.") uid = config - logger.info(f'Submitting table build for config {uid} version {version}...') + logger.info(f"Submitting table build for config {uid} version {version}...") path = format_escaped_url( "teams/{}/projects/{}/table-configs/{}/versions/{}/build", self.team_id, @@ -224,13 +233,14 @@ def initiate_build(self, config: TableConfig | str | UUID, *, response = self.session.post_resource(path=path, json={}) submission = JobSubmissionResponse.build(response) logger.info( - f'Table build job submitted from config {uid} ' - f'version {version} with job ID {submission.job_id}' + f"Table build job submitted from config {uid} " + f"version {version} with job ID {submission.job_id}" ) return submission - def get_by_build_job(self, job: JobSubmissionResponse | UUID, *, - timeout: float = 15 * 60) -> GemTable: + def get_by_build_job( + self, job: JobSubmissionResponse | UUID, *, timeout: float = 15 * 60 + ) -> GemTable: """ Gets table by build job, waiting for it to complete if necessary. @@ -250,29 +260,31 @@ def get_by_build_job(self, job: JobSubmissionResponse | UUID, *, """ status = _poll_for_job_completion( - session=self.session, - team_id=self.team_id, - job=job, - timeout=timeout) - - table_id = status.output['display_table_id'] - table_version = status.output['display_table_version'] - warning_blob = status.output.get('table_warnings') + session=self.session, team_id=self.team_id, job=job, timeout=timeout + ) + + table_id = status.output["display_table_id"] + table_version = status.output["display_table_version"] + warning_blob = status.output.get("table_warnings") warnings = json.loads(warning_blob) if warning_blob is not None else [] if warnings: - warn_lines = ['Table build completed with warnings:'] + warn_lines = ["Table build completed with warnings:"] for warning in warnings: - limited_results = warning.get('limited_results', []) + limited_results = warning.get("limited_results", []) warn_lines.extend(limited_results) - total_count = warning.get('total_count', 0) + total_count = warning.get("total_count", 0) if total_count > len(limited_results): - warn_lines.append(f'and {total_count - len(limited_results)} more similar.') - logger.warning('\n\t'.join(warn_lines)) + warn_lines.append(f"and {total_count - len(limited_results)} more similar.") + logger.warning("\n\t".join(warn_lines)) return self.get(table_id, version=table_version) - def build_from_config(self, config: TableConfig | str | UUID, *, - version: str | int = None, - timeout: float = 15 * 60) -> GemTable: + def build_from_config( + self, + config: TableConfig | str | UUID, + *, + version: str | int = None, + timeout: float = 15 * 60, + ) -> GemTable: """ Builds table from table config, waiting for build job to complete. @@ -306,7 +318,7 @@ def build(self, data: dict) -> GemTable: def register(self, model: GemTable) -> GemTable: """Tables cannot be created at this time.""" - raise RuntimeError('Creating Tables is not supported at this time.') + raise RuntimeError("Creating Tables is not supported at this time.") def update(self, model: GemTable) -> GemTable: """Tables cannot be updated.""" diff --git a/src/citrine/resources/generative_design_execution.py b/src/citrine/resources/generative_design_execution.py index 3330f82fb..07ffd6339 100644 --- a/src/citrine/resources/generative_design_execution.py +++ b/src/citrine/resources/generative_design_execution.py @@ -1,4 +1,5 @@ """Resources that represent both individual and collections of design workflow executions.""" + from collections.abc import Iterator from uuid import UUID @@ -12,9 +13,9 @@ class GenerativeDesignExecutionCollection(Collection["GenerativeDesignExecution"]): """A collection of GenerativeDesignExecutions.""" - _path_template = '/projects/{project_id}/generative-design/executions' + _path_template = "/projects/{project_id}/generative-design/executions" _individual_key = None - _collection_key = 'response' + _collection_key = "response" _resource = GenerativeDesignExecution def __init__(self, project_id: UUID, session: Session): @@ -65,12 +66,12 @@ def list(self, *, per_page: int = 10) -> Iterator[GenerativeDesignExecution]: Resources in this collection. """ - return self._paginator.paginate(page_fetcher=self._fetch_page, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=self._fetch_page, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def delete(self, uid: UUID | str) -> Response: """Generative Design Executions cannot be deleted or archived.""" - raise NotImplementedError( - "Generative Design Executions cannot be deleted" - ) + raise NotImplementedError("Generative Design Executions cannot be deleted") diff --git a/src/citrine/resources/ingestion.py b/src/citrine/resources/ingestion.py index e9bc9aa4d..424e1a196 100644 --- a/src/citrine/resources/ingestion.py +++ b/src/citrine/resources/ingestion.py @@ -1,4 +1,5 @@ -from collections.abc import Collection as TypingCollection, Iterator, Iterable +from collections.abc import Collection as TypingCollection +from collections.abc import Iterable, Iterator from uuid import UUID from gemd.enumeration.base_enumeration import BaseEnumeration @@ -7,8 +8,8 @@ from citrine._rest.resource import Resource from citrine._serialization import properties from citrine._session import Session -from citrine.exceptions import CitrineException, BadRequest -from citrine.jobs.job import JobSubmissionResponse, JobFailureError, _poll_for_job_completion +from citrine.exceptions import BadRequest, CitrineException +from citrine.jobs.job import JobFailureError, JobSubmissionResponse, _poll_for_job_completion from citrine.resources.api_error import ApiError, ValidationError from citrine.resources.file_link import FileLink @@ -62,7 +63,7 @@ class IngestionErrorLevel(BaseEnumeration): INFO = "info" -class IngestionErrorTrace(Resource['IngestionErrorTrace']): +class IngestionErrorTrace(Resource["IngestionErrorTrace"]): """[ALPHA] Detailed information about an ingestion issue.""" family = properties.Enumeration(IngestionErrorFamily, "family") @@ -74,17 +75,18 @@ class IngestionErrorTrace(Resource['IngestionErrorTrace']): row_number = properties.Optional(properties.Integer(), "row_number", default=None) column_number = properties.Optional(properties.Integer(), "column_number", default=None) - def __init__(self, - msg, - level=IngestionErrorLevel.ERROR, - *, - family=IngestionErrorFamily.UNKNOWN, - error_type=IngestionErrorType.UNKNOWN_ERROR, - dataset_file_id=dataset_file_id.default, - file_version_uuid=file_version_uuid.default, - row_number=row_number.default, - column_number=column_number.default, - ): + def __init__( + self, + msg, + level=IngestionErrorLevel.ERROR, + *, + family=IngestionErrorFamily.UNKNOWN, + error_type=IngestionErrorType.UNKNOWN_ERROR, + dataset_file_id=dataset_file_id.default, + file_version_uuid=file_version_uuid.default, + row_number=row_number.default, + column_number=column_number.default, + ): self.msg = msg self.level = level self.family = family @@ -97,10 +99,7 @@ def __init__(self, @classmethod def from_validation_error(cls, source: ValidationError) -> "IngestionErrorTrace": """[ALPHA] Generate an IngestionErrorTrace from a ValidationError.""" - return cls( - msg=source.failure_message, - level=IngestionErrorLevel.ERROR, - ) + return cls(msg=source.failure_message, level=IngestionErrorLevel.ERROR) def __str__(self): return f"{self!r}: {self.msg}" @@ -113,18 +112,15 @@ def __repr__(self): class IngestionException(CitrineException): """[ALPHA] An exception that contains details of a failed ingestion.""" - uid = properties.Optional(properties.UUID(), 'ingestion_id', default=None) + uid = properties.Optional(properties.UUID(), "ingestion_id", default=None) """UUID | None""" status = properties.Enumeration(IngestionStatusType, "status") errors = properties.List(properties.Object(IngestionErrorTrace), "errors") """list[IngestionErrorTrace]""" - def __init__(self, - *, - uid: UUID | None = uid.default, - errors: Iterable[IngestionErrorTrace]): + def __init__(self, *, uid: UUID | None = uid.default, errors: Iterable[IngestionErrorTrace]): errors_ = list(errors) - message = '; '.join(str(e) for e in errors_) + message = "; ".join(str(e) for e in errors_) super().__init__(message) self.uid = uid self.errors = errors_ @@ -139,26 +135,28 @@ def from_api_error(cls, source: ApiError) -> "IngestionException": """[ALPHA] Build an IngestionException from an ApiError.""" if len(source.validation_errors) > 0: return cls(errors=[IngestionErrorTrace.from_validation_error(x) - for x in source.validation_errors]) + for x in source.validation_errors]) # fmt: skip else: return cls(errors=[IngestionErrorTrace(msg=source.message)]) -class IngestionStatus(Resource['IngestionStatus']): +class IngestionStatus(Resource["IngestionStatus"]): """[ALPHA] An object that represents the outcome of an ingestion event.""" - uid = properties.Optional(properties.UUID(), 'ingestion_id', default=None) + 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, - *, - uid: UUID | None = uid.default, - status: IngestionStatusType = IngestionStatusType.INGESTION_CREATED, - errors: Iterable[IngestionErrorTrace]): + def __init__( + self, + *, + uid: UUID | None = uid.default, + status: IngestionStatusType = IngestionStatusType.INGESTION_CREATED, + errors: Iterable[IngestionErrorTrace], + ): self.uid = uid self.status = status self.errors = list(errors) @@ -174,7 +172,7 @@ def from_exception(cls, exception: IngestionException) -> "IngestionStatus": return cls(uid=exception.uid, errors=exception.errors) -class Ingestion(Resource['Ingestion']): +class Ingestion(Resource["Ingestion"]): """ [ALPHA] A job that uploads new information to the platform. @@ -184,22 +182,23 @@ class Ingestion(Resource['Ingestion']): """ - uid = properties.UUID('ingestion_id') + uid = properties.UUID("ingestion_id") """UUID: Unique uuid4 identifier of this ingestion.""" - team_id = properties.Optional(properties.UUID, 'team_id', default=None) - dataset_id = properties.UUID('dataset_id') - session = properties.Object(Session, 'session', serializable=False) - raise_errors = properties.Optional(properties.Boolean(), 'raise_errors', default=True) - - def build_objects(self, - *, - build_table: bool = False, - project: "Project | UUID | str | None" = None, # noqa: F821 - delete_dataset_contents: bool = False, - delete_templates: bool = True, - timeout: float = None, - polling_delay: float | None = None - ) -> IngestionStatus: + team_id = properties.Optional(properties.UUID, "team_id", default=None) + dataset_id = properties.UUID("dataset_id") + session = properties.Object(Session, "session", serializable=False) + raise_errors = properties.Optional(properties.Boolean(), "raise_errors", default=True) + + def build_objects( + self, + *, + build_table: bool = False, + project: "Project | UUID | str | None" = None, # noqa: F821 + delete_dataset_contents: bool = False, + delete_templates: bool = True, + timeout: float = None, + polling_delay: float | None = None, + ) -> IngestionStatus: """ [ALPHA] Perform a complete ingestion operation, from start to finish. @@ -231,10 +230,12 @@ def build_objects(self, """ try: - job = self.build_objects_async(build_table=build_table, - project=project, - delete_dataset_contents=delete_dataset_contents, - delete_templates=delete_templates) + job = self.build_objects_async( + build_table=build_table, + project=project, + delete_dataset_contents=delete_dataset_contents, + delete_templates=delete_templates, + ) except IngestionException as e: if self.raise_errors: raise e @@ -248,12 +249,14 @@ def build_objects(self, return status - def build_objects_async(self, - *, - build_table: bool = False, - project: "Project | UUID | str | None" = None, # noqa: F821 - delete_dataset_contents: bool = False, - delete_templates: bool = True) -> JobSubmissionResponse: + def build_objects_async( + self, + *, + build_table: bool = False, + project: "Project | UUID | str | None" = None, # noqa: F821 + delete_dataset_contents: bool = False, + delete_templates: bool = True, + ) -> JobSubmissionResponse: """ [ALPHA] Begin an async ingestion operation. @@ -276,9 +279,10 @@ def build_objects_async(self, """ from citrine.resources.project import Project - collection = IngestionCollection(team_id=self.team_id, - dataset_id=self.dataset_id, - session=self.session) + + collection = IngestionCollection( + team_id=self.team_id, dataset_id=self.dataset_id, session=self.session + ) path = collection._get_path(uid=self.uid, action="gemd-objects-async") # Project resolution logic @@ -309,12 +313,13 @@ def build_objects_async(self, else: raise e - def poll_for_job_completion(self, - job: JobSubmissionResponse, - *, - timeout: float | None = None, - polling_delay: float | None = None - ) -> IngestionStatus: + def poll_for_job_completion( + self, + job: JobSubmissionResponse, + *, + timeout: float | None = None, + polling_delay: float | None = None, + ) -> IngestionStatus: """ [ALPHA] Repeatedly ask server if a job associated with this ingestion has completed. @@ -347,7 +352,7 @@ def poll_for_job_completion(self, team_id=self.team_id, job=job, raise_errors=False, # JobFailureError doesn't contain the error - **kwargs + **kwargs, ) if build_job_status.output is not None and "table_build_job_id" in build_job_status.output: _poll_for_job_completion( @@ -355,7 +360,7 @@ def poll_for_job_completion(self, team_id=self.team_id, job=build_job_status.output["table_build_job_id"], raise_errors=False, # JobFailureError doesn't contain the error - **kwargs + **kwargs, ) return self.status() @@ -369,9 +374,9 @@ def status(self) -> IngestionStatus: The result of the ingestion attempt """ - collection = IngestionCollection(team_id=self.team_id, - dataset_id=self.dataset_id, - session=self.session) + collection = IngestionCollection( + team_id=self.team_id, dataset_id=self.dataset_id, session=self.session + ) path = collection._get_path(uid=self.uid, action="status") return IngestionStatus.build(self.session.get_resource(path=path)) @@ -383,42 +388,46 @@ def __init__(self, errors: Iterable[IngestionErrorTrace]): self.errors = list(errors) self.raise_errors = False - def build_objects(self, - *, - build_table: bool = False, - project: "Project | UUID | str | None" = None, # noqa: F821 - delete_dataset_contents: bool = False, - delete_templates: bool = True, - timeout: float = None, - polling_delay: float | None = None - ) -> IngestionStatus: + def build_objects( + self, + *, + build_table: bool = False, + project: "Project | UUID | str | None" = None, # noqa: F821 + delete_dataset_contents: bool = False, + delete_templates: bool = True, + timeout: float = None, + polling_delay: float | None = None, + ) -> IngestionStatus: """[ALPHA] Satisfy the required interface for a failed ingestion.""" return self.status() - def build_objects_async(self, - *, - build_table: bool = False, - project: "Project | UUID | str | None" = None, # noqa: F821 - delete_dataset_contents: bool = False, - delete_templates: bool = True) -> JobSubmissionResponse: + def build_objects_async( + self, + *, + build_table: bool = False, + project: "Project | UUID | str | None" = None, # noqa: F821 + delete_dataset_contents: bool = False, + delete_templates: bool = True, + ) -> JobSubmissionResponse: """[ALPHA] Satisfy the required interface for a failed ingestion.""" raise JobFailureError( message=f"Errors: {[e.msg for e in self.errors]}", - job_id=UUID('0' * 32), # Nil UUID - failure_reasons=[e.msg for e in self.errors] + job_id=UUID("0" * 32), # Nil UUID + failure_reasons=[e.msg for e in self.errors], ) - def poll_for_job_completion(self, - job: JobSubmissionResponse, - *, - timeout: float | None = None, - polling_delay: float | None = None - ) -> IngestionStatus: + def poll_for_job_completion( + self, + job: JobSubmissionResponse, + *, + timeout: float | None = None, + polling_delay: float | None = None, + ) -> IngestionStatus: """[ALPHA] Satisfy the required interface for a failed ingestion.""" raise JobFailureError( message=f"Errors: {[e.msg for e in self.errors]}", - job_id=UUID('0' * 32), # Nil UUID - failure_reasons=[e.msg for e in self.errors] + job_id=UUID("0" * 32), # Nil UUID + failure_reasons=[e.msg for e in self.errors], ) def status(self) -> IngestionStatus: @@ -434,14 +443,13 @@ def status(self) -> IngestionStatus: if self.raise_errors: raise JobFailureError( message=f"Ingestion creation failed: {self.errors}", - job_id=UUID('0' * 32), # Nil UUID - failure_reasons=[str(x) for x in self.errors] + job_id=UUID("0" * 32), # Nil UUID + failure_reasons=[str(x) for x in self.errors], ) else: - return IngestionStatus.build({ - "status": IngestionStatusType.INGESTION_CREATED, - "errors": self.errors, - }) + return IngestionStatus.build( + {"status": IngestionStatusType.INGESTION_CREATED, "errors": self.errors} + ) class IngestionCollection(Collection[Ingestion]): @@ -460,17 +468,16 @@ class IngestionCollection(Collection[Ingestion]): _individual_key = None _collection_key = None _resource = Ingestion - _path_template = 'teams/{team_id}/ingestions' + _path_template = "teams/{team_id}/ingestions" def __init__(self, *, session: Session, team_id: UUID, dataset_id: UUID): self.dataset_id = dataset_id self.session = session self.team_id = team_id - def build_from_file_links(self, - file_links: TypingCollection[FileLink], - *, - raise_errors: bool = True) -> Ingestion: + def build_from_file_links( + self, file_links: TypingCollection[FileLink], *, raise_errors: bool = True + ) -> Ingestion: """ [ALPHA] Create an on-platform ingestion event based on the passed FileLink objects. @@ -495,7 +502,7 @@ def build_from_file_links(self, "files": [ {"dataset_file_id": str(f.uid), "file_version_uuid": str(f.version)} for f in file_links - ] + ], } try: @@ -503,8 +510,10 @@ def build_from_file_links(self, except BadRequest as e: if e.api_error is not None: if e.api_error.validation_errors: - errors = [IngestionErrorTrace.from_validation_error(error) - for error in e.api_error.validation_errors] + errors = [ + IngestionErrorTrace.from_validation_error(error) + for error in e.api_error.validation_errors + ] else: errors = [IngestionErrorTrace(msg=e.api_error.message)] if raise_errors: @@ -513,10 +522,7 @@ def build_from_file_links(self, return FailedIngestion(errors=errors) else: raise e - return self.build({ - **response, - "raise_errors": raise_errors - }) + return self.build({**response, "raise_errors": raise_errors}) def build(self, data: dict) -> Ingestion: """Build an instance of an Ingestion.""" diff --git a/src/citrine/resources/ingredient_run.py b/src/citrine/resources/ingredient_run.py index 90bb6667d..c9303f089 100644 --- a/src/citrine/resources/ingredient_run.py +++ b/src/citrine/resources/ingredient_run.py @@ -1,10 +1,8 @@ """Resources that represent ingredient run data objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String -from citrine.resources.object_runs import ObjectRun, ObjectRunCollection from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.ingredient_run import IngredientRun as GEMDIngredientRun @@ -13,12 +11,13 @@ from gemd.entity.object.process_run import ProcessRun as GEMDProcessRun from gemd.entity.value.continuous_value import ContinuousValue +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources.object_runs import ObjectRun, ObjectRunCollection + class IngredientRun( - GEMDResource['IngredientRun'], - ObjectRun, - GEMDIngredientRun, - typ=GEMDIngredientRun.typ + GEMDResource["IngredientRun"], ObjectRun, GEMDIngredientRun, typ=GEMDIngredientRun.typ ): """ An ingredient run. @@ -62,52 +61,62 @@ class IngredientRun( _response_key = GEMDIngredientRun.typ # 'ingredient_run' - material = Optional(LinkOrElse(GEMDMaterialRun), 'material', override=True) - process = Optional(LinkOrElse(GEMDProcessRun), 'process', override=True, use_init=True) - mass_fraction = Optional(Object(ContinuousValue), 'mass_fraction') - volume_fraction = Optional(Object(ContinuousValue), 'volume_fraction') - number_fraction = Optional(Object(ContinuousValue), 'number_fraction') - absolute_quantity = Optional(Object(ContinuousValue), 'absolute_quantity') - spec = Optional(LinkOrElse(GEMDIngredientSpec), 'spec', override=True, use_init=True) + material = Optional(LinkOrElse(GEMDMaterialRun), "material", override=True) + process = Optional(LinkOrElse(GEMDProcessRun), "process", override=True, use_init=True) + mass_fraction = Optional(Object(ContinuousValue), "mass_fraction") + volume_fraction = Optional(Object(ContinuousValue), "volume_fraction") + number_fraction = Optional(Object(ContinuousValue), "number_fraction") + absolute_quantity = Optional(Object(ContinuousValue), "absolute_quantity") + spec = Optional(LinkOrElse(GEMDIngredientSpec), "spec", override=True, use_init=True) """ Intentionally private because they have some unusual dynamics """ - _name = Optional(String(), 'name') - _labels = Optional(List(String()), 'labels') - - def __init__(self, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - material: GEMDMaterialRun | None = None, - process: GEMDProcessRun | None = None, - mass_fraction: ContinuousValue | None = None, - volume_fraction: ContinuousValue | None = None, - number_fraction: ContinuousValue | None = None, - absolute_quantity: ContinuousValue | None = None, - spec: GEMDIngredientSpec | None = None, - file_links: list[FileLink] | None = None): + _name = Optional(String(), "name") + _labels = Optional(List(String()), "labels") + + def __init__( + self, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + material: GEMDMaterialRun | None = None, + process: GEMDProcessRun | None = None, + mass_fraction: ContinuousValue | None = None, + volume_fraction: ContinuousValue | None = None, + number_fraction: ContinuousValue | None = None, + absolute_quantity: ContinuousValue | None = None, + spec: GEMDIngredientSpec | None = None, + file_links: list[FileLink] | None = None, + ): if uids is None: uids = dict() super(ObjectRun, self).__init__() - GEMDIngredientRun.__init__(self, uids=uids, tags=tags, notes=notes, - material=material, process=process, - mass_fraction=mass_fraction, volume_fraction=volume_fraction, - number_fraction=number_fraction, - absolute_quantity=absolute_quantity, - spec=spec, file_links=file_links) + GEMDIngredientRun.__init__( + self, + uids=uids, + tags=tags, + notes=notes, + material=material, + process=process, + mass_fraction=mass_fraction, + volume_fraction=volume_fraction, + number_fraction=number_fraction, + absolute_quantity=absolute_quantity, + spec=spec, + file_links=file_links, + ) def __str__(self): - return ''.format(self.name) + return f"" class IngredientRunCollection(ObjectRunCollection[IngredientRun]): """Represents the collection of all ingredient runs associated with a dataset.""" - _individual_key = 'ingredient_run' - _collection_key = 'ingredient_runs' + _individual_key = "ingredient_run" + _collection_key = "ingredient_runs" _resource = IngredientRun @classmethod @@ -115,9 +124,9 @@ def get_type(cls) -> type[IngredientRun]: """Return the resource type in the collection.""" return IngredientRun - def list_by_spec(self, - uid: UUID | str | LinkByUID | GEMDIngredientSpec - ) -> Iterator[IngredientRun]: + def list_by_spec( + self, uid: UUID | str | LinkByUID | GEMDIngredientSpec + ) -> Iterator[IngredientRun]: """ Get the ingredient runs using the specified ingredient spec. @@ -132,11 +141,11 @@ def list_by_spec(self, The ingredient runs using the specified ingredient spec. """ - return self._get_relation(relation='ingredient-specs', uid=uid) + return self._get_relation(relation="ingredient-specs", uid=uid) - def list_by_process(self, - uid: UUID | str | LinkByUID | GEMDProcessRun - ) -> Iterator[IngredientRun]: + def list_by_process( + self, uid: UUID | str | LinkByUID | GEMDProcessRun + ) -> Iterator[IngredientRun]: """ Get ingredients to a process. @@ -151,11 +160,11 @@ def list_by_process(self, The ingredients to the specified process. """ - return self._get_relation(relation='process-runs', uid=uid) + return self._get_relation(relation="process-runs", uid=uid) - def list_by_material(self, - uid: UUID | str | LinkByUID | GEMDMaterialRun - ) -> Iterator[IngredientRun]: + def list_by_material( + self, uid: UUID | str | LinkByUID | GEMDMaterialRun + ) -> Iterator[IngredientRun]: """ Get ingredients using the specified material. @@ -170,4 +179,4 @@ def list_by_material(self, The ingredients using the specified material """ - return self._get_relation(relation='material-runs', uid=uid) + return self._get_relation(relation="material-runs", uid=uid) diff --git a/src/citrine/resources/ingredient_spec.py b/src/citrine/resources/ingredient_spec.py index 21fc13fdd..2d3348c6d 100644 --- a/src/citrine/resources/ingredient_spec.py +++ b/src/citrine/resources/ingredient_spec.py @@ -1,10 +1,8 @@ """Resources that represent ingredient spec data objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String -from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.ingredient_spec import IngredientSpec as GEMDIngredientSpec @@ -12,12 +10,13 @@ from gemd.entity.object.process_spec import ProcessSpec as GEMDProcessSpec from gemd.entity.value.continuous_value import ContinuousValue +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection + class IngredientSpec( - GEMDResource['IngredientSpec'], - ObjectSpec, - GEMDIngredientSpec, - typ=GEMDIngredientSpec.typ + GEMDResource["IngredientSpec"], ObjectSpec, GEMDIngredientSpec, typ=GEMDIngredientSpec.typ ): """ An ingredient specification. @@ -63,49 +62,60 @@ class IngredientSpec( _response_key = GEMDIngredientSpec.typ # 'ingredient_spec' - material = Optional(LinkOrElse(GEMDMaterialSpec), 'material', override=True) - process = Optional(LinkOrElse(GEMDProcessSpec), 'process', override=True, use_init=True) - mass_fraction = Optional(Object(ContinuousValue), 'mass_fraction', override=True) - volume_fraction = Optional(Object(ContinuousValue), 'volume_fraction', override=True) - number_fraction = Optional(Object(ContinuousValue), 'number_fraction', override=True) - absolute_quantity = Optional(Object(ContinuousValue), 'absolute_quantity', override=True) - name = String('name', override=True, use_init=True) - labels = Optional(List(String()), 'labels', override=True, use_init=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - material: GEMDMaterialSpec | None = None, - process: GEMDProcessSpec | None = None, - mass_fraction: ContinuousValue | None = None, - volume_fraction: ContinuousValue | None = None, - number_fraction: ContinuousValue | None = None, - absolute_quantity: ContinuousValue | None = None, - labels: list[str] | None = None, - file_links: list[FileLink] | None = None): + material = Optional(LinkOrElse(GEMDMaterialSpec), "material", override=True) + process = Optional(LinkOrElse(GEMDProcessSpec), "process", override=True, use_init=True) + mass_fraction = Optional(Object(ContinuousValue), "mass_fraction", override=True) + volume_fraction = Optional(Object(ContinuousValue), "volume_fraction", override=True) + number_fraction = Optional(Object(ContinuousValue), "number_fraction", override=True) + absolute_quantity = Optional(Object(ContinuousValue), "absolute_quantity", override=True) + name = String("name", override=True, use_init=True) + labels = Optional(List(String()), "labels", override=True, use_init=True) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + material: GEMDMaterialSpec | None = None, + process: GEMDProcessSpec | None = None, + mass_fraction: ContinuousValue | None = None, + volume_fraction: ContinuousValue | None = None, + number_fraction: ContinuousValue | None = None, + absolute_quantity: ContinuousValue | None = None, + labels: list[str] | None = None, + file_links: list[FileLink] | None = None, + ): if uids is None: uids = dict() super(ObjectSpec, self).__init__() - GEMDIngredientSpec.__init__(self, uids=uids, tags=tags, notes=notes, - material=material, process=process, - mass_fraction=mass_fraction, volume_fraction=volume_fraction, - number_fraction=number_fraction, - absolute_quantity=absolute_quantity, labels=labels, - name=name, file_links=file_links) + GEMDIngredientSpec.__init__( + self, + uids=uids, + tags=tags, + notes=notes, + material=material, + process=process, + mass_fraction=mass_fraction, + volume_fraction=volume_fraction, + number_fraction=number_fraction, + absolute_quantity=absolute_quantity, + labels=labels, + name=name, + file_links=file_links, + ) def __str__(self): - return ''.format(self.name) + return f"" class IngredientSpecCollection(ObjectSpecCollection[IngredientSpec]): """Represents the collection of all ingredient specs associated with a dataset.""" - _individual_key = 'ingredient_spec' - _collection_key = 'ingredient_specs' + _individual_key = "ingredient_spec" + _collection_key = "ingredient_specs" _resource = IngredientSpec @classmethod @@ -113,9 +123,9 @@ def get_type(cls) -> type[IngredientSpec]: """Return the resource type in the collection.""" return IngredientSpec - def list_by_process(self, - uid: UUID | str | LinkByUID | GEMDProcessSpec - ) -> Iterator[IngredientSpec]: + def list_by_process( + self, uid: UUID | str | LinkByUID | GEMDProcessSpec + ) -> Iterator[IngredientSpec]: """ Get ingredients to a process. @@ -130,11 +140,11 @@ def list_by_process(self, The ingredients to the specified process. """ - return self._get_relation(relation='process-specs', uid=uid) + return self._get_relation(relation="process-specs", uid=uid) - def list_by_material(self, - uid: UUID | str | LinkByUID | GEMDMaterialSpec - ) -> Iterator[IngredientSpec]: + def list_by_material( + self, uid: UUID | str | LinkByUID | GEMDMaterialSpec + ) -> Iterator[IngredientSpec]: """ Get ingredients using the specified material. @@ -149,4 +159,4 @@ def list_by_material(self, The ingredients using the specified material """ - return self._get_relation(relation='material-specs', uid=uid) + return self._get_relation(relation="material-specs", uid=uid) diff --git a/src/citrine/resources/material_run.py b/src/citrine/resources/material_run.py index d31cc25ab..e94d146b5 100644 --- a/src/citrine/resources/material_run.py +++ b/src/citrine/resources/material_run.py @@ -1,7 +1,15 @@ """Resources that represent material run data objects.""" + from collections.abc import Iterator from uuid import UUID +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object.material_run import MaterialRun as GEMDMaterialRun +from gemd.entity.object.material_spec import MaterialSpec as GEMDMaterialSpec +from gemd.entity.object.process_run import ProcessRun as GEMDProcessRun +from gemd.entity.template.material_template import MaterialTemplate as GEMDMaterialTemplate + from citrine._rest.resource import GEMDResource from citrine._serialization.properties import LinkOrElse, Optional, String from citrine._utils.functions import format_escaped_url @@ -9,19 +17,10 @@ from citrine.resources.data_concepts import _make_link_by_uid from citrine.resources.material_spec import MaterialSpecCollection from citrine.resources.object_runs import ObjectRun, ObjectRunCollection -from gemd.entity.file_link import FileLink -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.object.material_run import MaterialRun as GEMDMaterialRun -from gemd.entity.object.material_spec import MaterialSpec as GEMDMaterialSpec -from gemd.entity.template.material_template import MaterialTemplate as GEMDMaterialTemplate -from gemd.entity.object.process_run import ProcessRun as GEMDProcessRun class MaterialRun( - GEMDResource['MaterialRun'], - ObjectRun, - GEMDMaterialRun, - typ=GEMDMaterialRun.typ + GEMDResource["MaterialRun"], ObjectRun, GEMDMaterialRun, typ=GEMDMaterialRun.typ ): """ A material run. @@ -60,40 +59,49 @@ class MaterialRun( _response_key = GEMDMaterialRun.typ # 'material_run' - name = String('name', override=True, use_init=True) - process = Optional(LinkOrElse(GEMDProcessRun), 'process', override=True, use_init=True) - sample_type = Optional(String, 'sample_type', override=True) - spec = Optional(LinkOrElse(GEMDMaterialSpec), 'spec', override=True, use_init=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - process: GEMDProcessRun | None = None, - sample_type: str | None = "unknown", - spec: GEMDMaterialSpec | None = None, - file_links: list[FileLink] | None = None, - default_labels: list[str] | None = None): + name = String("name", override=True, use_init=True) + process = Optional(LinkOrElse(GEMDProcessRun), "process", override=True, use_init=True) + sample_type = Optional(String, "sample_type", override=True) + spec = Optional(LinkOrElse(GEMDMaterialSpec), "spec", override=True, use_init=True) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + process: GEMDProcessRun | None = None, + sample_type: str | None = "unknown", + spec: GEMDMaterialSpec | None = None, + file_links: list[FileLink] | None = None, + default_labels: list[str] | None = None, + ): if uids is None: uids = dict() all_tags = _inject_default_label_tags(tags, default_labels) super(ObjectRun, self).__init__() - GEMDMaterialRun.__init__(self, name=name, uids=uids, - tags=all_tags, process=process, - sample_type=sample_type, spec=spec, - file_links=file_links, notes=notes) + GEMDMaterialRun.__init__( + self, + name=name, + uids=uids, + tags=all_tags, + process=process, + sample_type=sample_type, + spec=spec, + file_links=file_links, + notes=notes, + ) def __str__(self): - return ''.format(self.name) + return f"" class MaterialRunCollection(ObjectRunCollection[MaterialRun]): """Represents the collection of all material runs associated with a dataset.""" - _individual_key = 'material_run' - _collection_key = 'material_runs' + _individual_key = "material_run" + _collection_key = "material_runs" _resource = MaterialRun @classmethod @@ -124,19 +132,14 @@ def get_history(self, id: str | UUID | LinkByUID | MaterialRun) -> MaterialRun: """ link = _make_link_by_uid(id) path = format_escaped_url( - "teams/{}/gemd/query/material-histories?filter_nonroot_materials=true", - self.team_id) + "teams/{}/gemd/query/material-histories?filter_nonroot_materials=true", self.team_id + ) query = { "criteria": [ { "datasets": [str(self.dataset_id)], "type": "terminal_material_run_identifiers_criteria", - "terminal_material_ids": [ - { - "scope": link.scope, - "id": link.id - } - ] + "terminal_material_ids": [{"scope": link.scope, "id": link.id}], } ] } @@ -152,9 +155,7 @@ def get_history(self, id: str | UUID | LinkByUID | MaterialRun) -> MaterialRun: else: return None - def get_by_process(self, - uid: UUID | str | LinkByUID | GEMDProcessRun - ) -> MaterialRun | None: + def get_by_process(self, uid: UUID | str | LinkByUID | GEMDProcessRun) -> MaterialRun | None: """ Get output material of a process. @@ -169,14 +170,11 @@ def get_by_process(self, The output material of the specified process, or None if no such material exists. """ - return next( - self._get_relation(relation='process-runs', uid=uid, per_page=1), - None - ) + return next(self._get_relation(relation="process-runs", uid=uid, per_page=1), None) - def list_by_spec(self, - uid: UUID | str | LinkByUID | GEMDMaterialSpec - ) -> Iterator[MaterialRun]: + def list_by_spec( + self, uid: UUID | str | LinkByUID | GEMDMaterialSpec + ) -> Iterator[MaterialRun]: """ Get the material runs using the specified material spec. @@ -191,11 +189,11 @@ def list_by_spec(self, The material runs using the specified material spec. """ - return self._get_relation('material-specs', uid=uid) + return self._get_relation("material-specs", uid=uid) - def list_by_template(self, - uid: UUID | str | LinkByUID | GEMDMaterialTemplate - ) -> Iterator[MaterialRun]: + def list_by_template( + self, uid: UUID | str | LinkByUID | GEMDMaterialTemplate + ) -> Iterator[MaterialRun]: """ Get the material runs using the specified material template. @@ -211,10 +209,7 @@ def list_by_template(self, """ spec_collection = MaterialSpecCollection( - team_id=self.team_id, - dataset_id=self.dataset_id, - session=self.session + team_id=self.team_id, dataset_id=self.dataset_id, session=self.session ) specs = spec_collection.list_by_template(uid=_make_link_by_uid(uid)) - return (run for runs in (self.list_by_spec(spec) for spec in specs) - for run in runs) + return (run for runs in (self.list_by_spec(spec) for spec in specs) for run in runs) diff --git a/src/citrine/resources/material_spec.py b/src/citrine/resources/material_spec.py index 7ea74f7ca..ffb165c7b 100644 --- a/src/citrine/resources/material_spec.py +++ b/src/citrine/resources/material_spec.py @@ -1,11 +1,8 @@ """Resources that represent material spec data objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String -from citrine.resources._default_labels import _inject_default_label_tags -from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection from gemd.entity.attribute.property_and_conditions import PropertyAndConditions from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID @@ -13,12 +10,14 @@ from gemd.entity.object.process_spec import ProcessSpec as GEMDProcessSpec from gemd.entity.template.material_template import MaterialTemplate as GEMDMaterialTemplate +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources._default_labels import _inject_default_label_tags +from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection + class MaterialSpec( - GEMDResource['MaterialSpec'], - ObjectSpec, - GEMDMaterialSpec, - typ=GEMDMaterialSpec.typ + GEMDResource["MaterialSpec"], ObjectSpec, GEMDMaterialSpec, typ=GEMDMaterialSpec.typ ): """ A material specification. @@ -55,39 +54,49 @@ class MaterialSpec( _response_key = GEMDMaterialSpec.typ # 'material_spec' - name = String('name', override=True, use_init=True) - process = Optional(LinkOrElse(GEMDProcessSpec), 'process', override=True, use_init=True) - properties = Optional(List(Object(PropertyAndConditions)), 'properties', override=True) - template = Optional(LinkOrElse(GEMDMaterialTemplate), 'template', override=True, use_init=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - process: GEMDProcessSpec | None = None, - properties: list[PropertyAndConditions] | None = None, - template: GEMDMaterialTemplate | None = None, - file_links: list[FileLink] | None = None, - default_labels: list[str] | None = None): + name = String("name", override=True, use_init=True) + process = Optional(LinkOrElse(GEMDProcessSpec), "process", override=True, use_init=True) + properties = Optional(List(Object(PropertyAndConditions)), "properties", override=True) + template = Optional(LinkOrElse(GEMDMaterialTemplate), "template", override=True, use_init=True) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + process: GEMDProcessSpec | None = None, + properties: list[PropertyAndConditions] | None = None, + template: GEMDMaterialTemplate | None = None, + file_links: list[FileLink] | None = None, + default_labels: list[str] | None = None, + ): if uids is None: uids = dict() all_tags = _inject_default_label_tags(tags, default_labels) super(ObjectSpec, self).__init__() - GEMDMaterialSpec.__init__(self, name=name, uids=uids, - tags=all_tags, process=process, properties=properties, - template=template, file_links=file_links, notes=notes) + GEMDMaterialSpec.__init__( + self, + name=name, + uids=uids, + tags=all_tags, + process=process, + properties=properties, + template=template, + file_links=file_links, + notes=notes, + ) def __str__(self): - return ''.format(self.name) + return f"" class MaterialSpecCollection(ObjectSpecCollection[MaterialSpec]): """Represents the collection of all material specs associated with a dataset.""" - _individual_key = 'material_spec' - _collection_key = 'material_specs' + _individual_key = "material_spec" + _collection_key = "material_specs" _resource = MaterialSpec @classmethod @@ -95,9 +104,9 @@ def get_type(cls) -> type[MaterialSpec]: """Return the resource type in the collection.""" return MaterialSpec - def list_by_template(self, - uid: UUID | str | LinkByUID | GEMDMaterialTemplate - ) -> Iterator[MaterialSpec]: + def list_by_template( + self, uid: UUID | str | LinkByUID | GEMDMaterialTemplate + ) -> Iterator[MaterialSpec]: """ Get the material specs using the specified material template. @@ -112,11 +121,9 @@ def list_by_template(self, The material specs using the specified material template. """ - return self._get_relation('material-templates', uid=uid) + return self._get_relation("material-templates", uid=uid) - def get_by_process(self, - uid: UUID | str | LinkByUID | GEMDProcessSpec - ) -> MaterialSpec | None: + def get_by_process(self, uid: UUID | str | LinkByUID | GEMDProcessSpec) -> MaterialSpec | None: """ Get output material of a process. @@ -131,4 +138,4 @@ def get_by_process(self, The output material of the specified process, or None if no such material exists. """ - return next(self._get_relation(relation='process-specs', uid=uid, per_page=1), None) + return next(self._get_relation(relation="process-specs", uid=uid, per_page=1), None) diff --git a/src/citrine/resources/material_template.py b/src/citrine/resources/material_template.py index 604c6d3c1..c90ef2e96 100644 --- a/src/citrine/resources/material_template.py +++ b/src/citrine/resources/material_template.py @@ -1,22 +1,30 @@ """Resources that represent material templates.""" + from collections.abc import Sequence -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, \ - SpecifiedMixedList, Union -from citrine.resources.object_templates import ObjectTemplateCollection, ObjectTemplate -from citrine.resources.property_template import PropertyTemplate from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.link_by_uid import LinkByUID from gemd.entity.template.material_template import MaterialTemplate as GEMDMaterialTemplate from gemd.entity.template.property_template import PropertyTemplate as GEMDPropertyTemplate +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import ( + LinkOrElse, + List, + Object, + Optional, + SpecifiedMixedList, + Union, +) +from citrine.resources.object_templates import ObjectTemplate, ObjectTemplateCollection +from citrine.resources.property_template import PropertyTemplate + class MaterialTemplate( - GEMDResource['MaterialTemplate'], + GEMDResource["MaterialTemplate"], ObjectTemplate, GEMDMaterialTemplate, - typ=GEMDMaterialTemplate.typ + typ=GEMDMaterialTemplate.typ, ): """ A material template. @@ -49,39 +57,54 @@ class MaterialTemplate( _response_key = GEMDMaterialTemplate.typ # 'material_template' - properties = Optional(List(Union([LinkOrElse(GEMDPropertyTemplate), - SpecifiedMixedList([LinkOrElse(GEMDPropertyTemplate), - Optional(Object(BaseBounds))])])), - 'properties', override=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - properties: Sequence[PropertyTemplate | LinkByUID - | Sequence[PropertyTemplate | LinkByUID | BaseBounds | None] - ] | None = None, - description: str | None = None, - tags: list[str] | None = None): + properties = Optional( + List( + Union( + [ + LinkOrElse(GEMDPropertyTemplate), + SpecifiedMixedList( + [LinkOrElse(GEMDPropertyTemplate), Optional(Object(BaseBounds))] + ), + ] + ) + ), + "properties", + override=True, + ) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + properties: Sequence[ + PropertyTemplate + | LinkByUID + | Sequence[PropertyTemplate | LinkByUID | BaseBounds | None] + ] + | None = None, + description: str | None = None, + tags: list[str] | None = None, + ): # properties is a list, each element of which is a PropertyTemplate OR is a list with # 2 entries: [PropertyTemplate, BaseBounds]. Python typing is not expressive enough, so # the typing above is more general. if uids is None: uids = dict() super(ObjectTemplate, self).__init__() - GEMDMaterialTemplate.__init__(self, name=name, properties=properties, - uids=uids, tags=tags, - description=description) + GEMDMaterialTemplate.__init__( + self, name=name, properties=properties, uids=uids, tags=tags, description=description + ) def __str__(self): - return ''.format(self.name) + return f"" class MaterialTemplateCollection(ObjectTemplateCollection[MaterialTemplate]): """A collection of material templates.""" - _individual_key = 'material_template' - _collection_key = 'material_templates' + _individual_key = "material_template" + _collection_key = "material_templates" _resource = MaterialTemplate @classmethod diff --git a/src/citrine/resources/measurement_run.py b/src/citrine/resources/measurement_run.py index e7e7704f3..ae0c872a1 100644 --- a/src/citrine/resources/measurement_run.py +++ b/src/citrine/resources/measurement_run.py @@ -1,10 +1,8 @@ """Resources that represent measurement run data objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String -from citrine.resources.object_runs import ObjectRun, ObjectRunCollection from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.attribute.property import Property @@ -15,12 +13,13 @@ from gemd.entity.object.measurement_spec import MeasurementSpec as GEMDMeasurementSpec from gemd.entity.source.performed_source import PerformedSource +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources.object_runs import ObjectRun, ObjectRunCollection + class MeasurementRun( - GEMDResource['MeasurementRun'], - ObjectRun, - GEMDMeasurementRun, - typ=GEMDMeasurementRun.typ + GEMDResource["MeasurementRun"], ObjectRun, GEMDMeasurementRun, typ=GEMDMeasurementRun.typ ): """ A measurement run. @@ -60,45 +59,56 @@ class MeasurementRun( _response_key = GEMDMeasurementRun.typ # 'measurement_run' - name = String('name', override=True, use_init=True) - conditions = Optional(List(Object(Condition)), 'conditions', override=True) - parameters = Optional(List(Object(Parameter)), 'parameters', override=True) - properties = Optional(List(Object(Property)), 'properties', override=True) - spec = Optional(LinkOrElse(GEMDMeasurementSpec), 'spec', override=True, use_init=True,) + name = String("name", override=True, use_init=True) + conditions = Optional(List(Object(Condition)), "conditions", override=True) + parameters = Optional(List(Object(Parameter)), "parameters", override=True) + properties = Optional(List(Object(Property)), "properties", override=True) + spec = Optional(LinkOrElse(GEMDMeasurementSpec), "spec", override=True, use_init=True) material = Optional(LinkOrElse(GEMDMaterialRun), "material", override=True, use_init=True) source = Optional(Object(PerformedSource), "source", override=True) - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - conditions: list[Condition] | None = None, - properties: list[Property] | None = None, - parameters: list[Parameter] | None = None, - spec: GEMDMeasurementSpec | None = None, - material: GEMDMaterialRun | None = None, - file_links: list[FileLink] | None = None, - source: PerformedSource | None = None): + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + conditions: list[Condition] | None = None, + properties: list[Property] | None = None, + parameters: list[Parameter] | None = None, + spec: GEMDMeasurementSpec | None = None, + material: GEMDMaterialRun | None = None, + file_links: list[FileLink] | None = None, + source: PerformedSource | None = None, + ): if uids is None: uids = dict() super(ObjectRun, self).__init__() - GEMDMeasurementRun.__init__(self, name=name, uids=uids, - material=material, - tags=tags, conditions=conditions, properties=properties, - parameters=parameters, spec=spec, - file_links=file_links, notes=notes, source=source) + GEMDMeasurementRun.__init__( + self, + name=name, + uids=uids, + material=material, + tags=tags, + conditions=conditions, + properties=properties, + parameters=parameters, + spec=spec, + file_links=file_links, + notes=notes, + source=source, + ) def __str__(self): - return ''.format(self.name) + return f"" class MeasurementRunCollection(ObjectRunCollection[MeasurementRun]): """Represents the collection of all measurement runs associated with a dataset.""" - _individual_key = 'measurement_run' - _collection_key = 'measurement_runs' + _individual_key = "measurement_run" + _collection_key = "measurement_runs" _resource = MeasurementRun @classmethod @@ -106,9 +116,9 @@ def get_type(cls) -> type[MeasurementRun]: """Return the resource type in the collection.""" return MeasurementRun - def list_by_spec(self, - uid: UUID | str | LinkByUID | GEMDMeasurementSpec - ) -> Iterator[MeasurementRun]: + def list_by_spec( + self, uid: UUID | str | LinkByUID | GEMDMeasurementSpec + ) -> Iterator[MeasurementRun]: """ Get the measurement runs using the specified measurement spec. @@ -123,11 +133,11 @@ def list_by_spec(self, The measurement runs using the specified measurement spec. """ - return self._get_relation('measurement-specs', uid=uid) + return self._get_relation("measurement-specs", uid=uid) - def list_by_material(self, - uid: UUID | str | LinkByUID | GEMDMaterialRun - ) -> Iterator[MeasurementRun]: + def list_by_material( + self, uid: UUID | str | LinkByUID | GEMDMaterialRun + ) -> Iterator[MeasurementRun]: """ Get measurements of the specified material. @@ -142,4 +152,4 @@ def list_by_material(self, The measurements of the specified material """ - return self._get_relation(relation='material-runs', uid=uid) + return self._get_relation(relation="material-runs", uid=uid) diff --git a/src/citrine/resources/measurement_spec.py b/src/citrine/resources/measurement_spec.py index 31412261d..5f4d27030 100644 --- a/src/citrine/resources/measurement_spec.py +++ b/src/citrine/resources/measurement_spec.py @@ -1,24 +1,24 @@ """Resources that represent measurement spec data objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String -from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.file_link import FileLink from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.measurement_spec import MeasurementSpec as GEMDMeasurementSpec -from gemd.entity.template.measurement_template import \ - MeasurementTemplate as GEMDMeasurementTemplate +from gemd.entity.template.measurement_template import ( + MeasurementTemplate as GEMDMeasurementTemplate, +) + +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection class MeasurementSpec( - GEMDResource['MeasurementSpec'], - ObjectSpec, - GEMDMeasurementSpec, - typ=GEMDMeasurementSpec.typ + GEMDResource["MeasurementSpec"], ObjectSpec, GEMDMeasurementSpec, typ=GEMDMeasurementSpec.typ ): """ A measurement specification. @@ -51,38 +51,49 @@ class MeasurementSpec( _response_key = GEMDMeasurementSpec.typ # 'measurement_spec' - name = String('name', override=True, use_init=True) - conditions = Optional(List(Object(Condition)), 'conditions', override=True) - parameters = Optional(List(Object(Parameter)), 'parameters', override=True) - template = Optional(LinkOrElse(GEMDMeasurementTemplate), 'template', override=True, - use_init=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - conditions: list[Condition] | None = None, - parameters: list[Parameter] | None = None, - template: GEMDMeasurementTemplate | None = None, - file_links: list[FileLink] | None = None): + name = String("name", override=True, use_init=True) + conditions = Optional(List(Object(Condition)), "conditions", override=True) + parameters = Optional(List(Object(Parameter)), "parameters", override=True) + template = Optional( + LinkOrElse(GEMDMeasurementTemplate), "template", override=True, use_init=True + ) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + conditions: list[Condition] | None = None, + parameters: list[Parameter] | None = None, + template: GEMDMeasurementTemplate | None = None, + file_links: list[FileLink] | None = None, + ): if uids is None: uids = dict() super(ObjectSpec, self).__init__() - GEMDMeasurementSpec.__init__(self, name=name, uids=uids, - tags=tags, conditions=conditions, parameters=parameters, - template=template, file_links=file_links, notes=notes) + GEMDMeasurementSpec.__init__( + self, + name=name, + uids=uids, + tags=tags, + conditions=conditions, + parameters=parameters, + template=template, + file_links=file_links, + notes=notes, + ) def __str__(self): - return ''.format(self.name) + return f"" class MeasurementSpecCollection(ObjectSpecCollection[MeasurementSpec]): """Represents the collection of all measurement specs associated with a dataset.""" - _individual_key = 'measurement_spec' - _collection_key = 'measurement_specs' + _individual_key = "measurement_spec" + _collection_key = "measurement_specs" _resource = MeasurementSpec @classmethod @@ -90,9 +101,9 @@ def get_type(cls) -> type[MeasurementSpec]: """Return the resource type in the collection.""" return MeasurementSpec - def list_by_template(self, - uid: UUID | str | LinkByUID | GEMDMeasurementTemplate - ) -> Iterator[MeasurementSpec]: + def list_by_template( + self, uid: UUID | str | LinkByUID | GEMDMeasurementTemplate + ) -> Iterator[MeasurementSpec]: """ Get the measurement specs using the specified measurement template. @@ -108,4 +119,4 @@ def list_by_template(self, The measurement specs using the specified measurement template. """ - return self._get_relation('measurement-templates', uid=uid) + return self._get_relation("measurement-templates", uid=uid) diff --git a/src/citrine/resources/measurement_template.py b/src/citrine/resources/measurement_template.py index a48d2deac..468ebc663 100644 --- a/src/citrine/resources/measurement_template.py +++ b/src/citrine/resources/measurement_template.py @@ -1,27 +1,36 @@ """Resources that represent measurement templates.""" + from collections.abc import Sequence -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, \ - SpecifiedMixedList, Union -from citrine.resources.condition_template import ConditionTemplate -from citrine.resources.object_templates import ObjectTemplate, ObjectTemplateCollection -from citrine.resources.parameter_template import ParameterTemplate -from citrine.resources.property_template import PropertyTemplate from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.template.measurement_template \ - import MeasurementTemplate as GEMDMeasurementTemplate from gemd.entity.template.condition_template import ConditionTemplate as GEMDConditionTemplate +from gemd.entity.template.measurement_template import ( + MeasurementTemplate as GEMDMeasurementTemplate, +) from gemd.entity.template.parameter_template import ParameterTemplate as GEMDParameterTemplate from gemd.entity.template.property_template import PropertyTemplate as GEMDPropertyTemplate +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import ( + LinkOrElse, + List, + Object, + Optional, + SpecifiedMixedList, + Union, +) +from citrine.resources.condition_template import ConditionTemplate +from citrine.resources.object_templates import ObjectTemplate, ObjectTemplateCollection +from citrine.resources.parameter_template import ParameterTemplate +from citrine.resources.property_template import PropertyTemplate + class MeasurementTemplate( - GEMDResource['MeasurementTemplate'], + GEMDResource["MeasurementTemplate"], ObjectTemplate, GEMDMeasurementTemplate, - typ=GEMDMeasurementTemplate.typ + typ=GEMDMeasurementTemplate.typ, ): """ A measurement template. @@ -64,53 +73,98 @@ class MeasurementTemplate( _response_key = GEMDMeasurementTemplate.typ # 'measurement_template' - properties = Optional(List(Union([LinkOrElse(GEMDPropertyTemplate), - SpecifiedMixedList([LinkOrElse(GEMDPropertyTemplate), - Optional(Object(BaseBounds))])])), - 'properties', - override=True) - conditions = Optional(List(Union([LinkOrElse(GEMDConditionTemplate), - SpecifiedMixedList([LinkOrElse(GEMDConditionTemplate), - Optional(Object(BaseBounds))])])), - 'conditions', - override=True) - parameters = Optional(List(Union([LinkOrElse(GEMDParameterTemplate), - SpecifiedMixedList([LinkOrElse(GEMDParameterTemplate), - Optional(Object(BaseBounds))])])), - 'parameters', - override=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - properties: Sequence[PropertyTemplate | LinkByUID - | Sequence[PropertyTemplate | LinkByUID | BaseBounds | None] - ] | None = None, - conditions: Sequence[ConditionTemplate | LinkByUID - | Sequence[ConditionTemplate | LinkByUID | BaseBounds | None] - ] | None = None, - parameters: Sequence[ParameterTemplate | LinkByUID - | Sequence[ParameterTemplate | LinkByUID | BaseBounds | None] - ] | None = None, - description: str | None = None, - tags: list[str] | None = None): + properties = Optional( + List( + Union( + [ + LinkOrElse(GEMDPropertyTemplate), + SpecifiedMixedList( + [LinkOrElse(GEMDPropertyTemplate), Optional(Object(BaseBounds))] + ), + ] + ) + ), + "properties", + override=True, + ) + conditions = Optional( + List( + Union( + [ + LinkOrElse(GEMDConditionTemplate), + SpecifiedMixedList( + [LinkOrElse(GEMDConditionTemplate), Optional(Object(BaseBounds))] + ), + ] + ) + ), + "conditions", + override=True, + ) + parameters = Optional( + List( + Union( + [ + LinkOrElse(GEMDParameterTemplate), + SpecifiedMixedList( + [LinkOrElse(GEMDParameterTemplate), Optional(Object(BaseBounds))] + ), + ] + ) + ), + "parameters", + override=True, + ) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + properties: Sequence[ + PropertyTemplate + | LinkByUID + | Sequence[PropertyTemplate | LinkByUID | BaseBounds | None] + ] + | None = None, + conditions: Sequence[ + ConditionTemplate + | LinkByUID + | Sequence[ConditionTemplate | LinkByUID | BaseBounds | None] + ] + | None = None, + parameters: Sequence[ + ParameterTemplate + | LinkByUID + | Sequence[ParameterTemplate | LinkByUID | BaseBounds | None] + ] + | None = None, + description: str | None = None, + tags: list[str] | None = None, + ): if uids is None: uids = dict() super(ObjectTemplate, self).__init__() - GEMDMeasurementTemplate.__init__(self, name=name, properties=properties, - conditions=conditions, parameters=parameters, tags=tags, - uids=uids, description=description) + GEMDMeasurementTemplate.__init__( + self, + name=name, + properties=properties, + conditions=conditions, + parameters=parameters, + tags=tags, + uids=uids, + description=description, + ) def __str__(self): - return ''.format(self.name) + return f"" class MeasurementTemplateCollection(ObjectTemplateCollection[MeasurementTemplate]): """A collection of measurement templates.""" - _individual_key = 'measurement_template' - _collection_key = 'measurement_templates' + _individual_key = "measurement_template" + _collection_key = "measurement_templates" _resource = MeasurementTemplate @classmethod diff --git a/src/citrine/resources/object_runs.py b/src/citrine/resources/object_runs.py index 79c90b84d..56c214187 100644 --- a/src/citrine/resources/object_runs.py +++ b/src/citrine/resources/object_runs.py @@ -1,10 +1,12 @@ """Top-level class for all object run objects and collections thereof.""" + from abc import ABC from typing import TypeVar -from citrine.resources.data_objects import DataObject, DataObjectCollection from gemd.entity.object.has_spec import HasSpec +from citrine.resources.data_objects import DataObject, DataObjectCollection + class ObjectRun(DataObject, HasSpec, ABC): """ diff --git a/src/citrine/resources/object_specs.py b/src/citrine/resources/object_specs.py index 893492355..a01ad9b82 100644 --- a/src/citrine/resources/object_specs.py +++ b/src/citrine/resources/object_specs.py @@ -1,4 +1,5 @@ """Top-level class for all object spec objects and collections thereof.""" + from abc import ABC from typing import TypeVar diff --git a/src/citrine/resources/object_templates.py b/src/citrine/resources/object_templates.py index 54e46485e..71adab4f9 100644 --- a/src/citrine/resources/object_templates.py +++ b/src/citrine/resources/object_templates.py @@ -1,12 +1,13 @@ """Top-level class for all object template objects and collections thereof.""" + from abc import ABC from typing import TypeVar -from citrine._serialization.properties import Optional -from citrine._serialization.properties import String -from citrine.resources.templates import Template, TemplateCollection from gemd.entity.template.base_template import BaseTemplate as GEMDTemplate +from citrine._serialization.properties import Optional, String +from citrine.resources.templates import Template, TemplateCollection + class ObjectTemplate(Template, GEMDTemplate, ABC): """ @@ -15,8 +16,8 @@ class ObjectTemplate(Template, GEMDTemplate, ABC): ObjectTemplate must be extended along with `Resource` """ - name = String('name') - description = Optional(String(), 'description') + name = String("name") + description = Optional(String(), "description") ObjectTemplateResourceType = TypeVar("ObjectTemplateResourceType", bound="ObjectTemplate") diff --git a/src/citrine/resources/parameter_template.py b/src/citrine/resources/parameter_template.py index 379cc5013..ebf765667 100644 --- a/src/citrine/resources/parameter_template.py +++ b/src/citrine/resources/parameter_template.py @@ -1,16 +1,17 @@ """Resources that represent parameter templates.""" -from citrine._rest.resource import GEMDResource -from citrine.resources.attribute_templates import AttributeTemplate, AttributeTemplateCollection from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.template.parameter_template import ParameterTemplate as GEMDParameterTemplate +from citrine._rest.resource import GEMDResource +from citrine.resources.attribute_templates import AttributeTemplate, AttributeTemplateCollection + class ParameterTemplate( - GEMDResource['ParameterTemplate'], + GEMDResource["ParameterTemplate"], AttributeTemplate, GEMDParameterTemplate, - typ=GEMDParameterTemplate.typ + typ=GEMDParameterTemplate.typ, ): """ A parameter template. @@ -36,28 +37,31 @@ class ParameterTemplate( _response_key = GEMDParameterTemplate.typ # 'parameter_template' - def __init__(self, - name: str, - *, - bounds: BaseBounds, - uids: dict[str, str] | None = None, - description: str | None = None, - tags: list[str] | None = None): + def __init__( + self, + name: str, + *, + bounds: BaseBounds, + uids: dict[str, str] | None = None, + description: str | None = None, + tags: list[str] | None = None, + ): if uids is None: uids = dict() super(AttributeTemplate, self).__init__() - GEMDParameterTemplate.__init__(self, name=name, bounds=bounds, tags=tags, - uids=uids, description=description) + GEMDParameterTemplate.__init__( + self, name=name, bounds=bounds, tags=tags, uids=uids, description=description + ) def __str__(self): - return ''.format(self.name) + return f"" class ParameterTemplateCollection(AttributeTemplateCollection[ParameterTemplate]): """A collection of parameter templates.""" - _individual_key = 'parameter_template' - _collection_key = 'parameter_templates' + _individual_key = "parameter_template" + _collection_key = "parameter_templates" _resource = ParameterTemplate @classmethod diff --git a/src/citrine/resources/predictor.py b/src/citrine/resources/predictor.py index fb7f58766..f5aa49041 100644 --- a/src/citrine/resources/predictor.py +++ b/src/citrine/resources/predictor.py @@ -1,4 +1,5 @@ """Resources that represent collections of predictors.""" + from collections.abc import Iterable from functools import partial from typing import Any @@ -7,8 +8,8 @@ from gemd.enumeration.base_enumeration import BaseEnumeration from citrine._rest.collection import Collection -from citrine._rest.resource import Resource from citrine._rest.paginator import Paginator +from citrine._rest.resource import Resource from citrine._serialization import properties from citrine._session import Session from citrine.informatics.data_sources import DataSource @@ -16,7 +17,6 @@ from citrine.informatics.predictors import GraphPredictor from citrine.resources.status_detail import StatusDetail - # Refers to the most recently edited prediction version. Could be a draft. MOST_RECENT_VER = "most_recent" LATEST_VER = "latest" # Refers to the highest saved predictor version. @@ -25,17 +25,18 @@ class AsyncDefaultPredictor(Resource["AsyncDefaultPredictor"]): """Return type for async default predictor generation and retrieval.""" - uid = properties.UUID('id', serializable=False) + uid = properties.UUID("id", serializable=False) """:UUID: Citrine Platform unique identifier for this task.""" - predictor = properties.Optional(properties.Object(GraphPredictor), 'data', serializable=False) + predictor = properties.Optional(properties.Object(GraphPredictor), "data", serializable=False) """:GraphPredictor | None:""" - status = properties.String('metadata.status', serializable=False) + status = properties.String("metadata.status", serializable=False) """:str: short description of the resource's status""" - status_detail = properties.List(properties.Object(StatusDetail), 'metadata.status_detail', - default=[], serializable=False) + status_detail = properties.List( + properties.Object(StatusDetail), "metadata.status_detail", default=[], serializable=False + ) """:list[StatusDetail]: a list of structured status info, containing the message and level""" @classmethod @@ -54,9 +55,9 @@ class AutoConfigureMode(BaseEnumeration): * INFER auto-detects the GEM table and predictor type """ - PLAIN = 'PLAIN' - FORMULATION = 'FORMULATION' - INFER = 'INFER' + PLAIN = "PLAIN" + FORMULATION = "FORMULATION" + INFER = "INFER" class _PredictorVersionPaginator(Paginator): @@ -71,11 +72,11 @@ def paginate(self, *args, **kwargs) -> Iterable[GraphPredictor]: class _PredictorVersionCollection(Collection[GraphPredictor]): - _api_version = 'v3' - _path_template = '/projects/{project_id}/predictors/{uid}/versions' + _api_version = "v3" + _path_template = "/projects/{project_id}/predictors/{uid}/versions" _individual_key = None _resource = GraphPredictor - _collection_key = 'response' + _collection_key = "response" _paginator: Paginator = _PredictorVersionPaginator() _SPECIAL_VERSIONS = [LATEST_VER, MOST_RECENT_VER] @@ -84,17 +85,19 @@ def __init__(self, project_id: UUID, session: Session): self.project_id = project_id self.session: Session = session - def _construct_path(self, - uid: UUID | str, - version: int | str | None = None, - action: str = None) -> str: + def _construct_path( + self, uid: UUID | str, version: int | str | None = None, action: str = None + ) -> str: path = self._path_template.format(project_id=self.project_id, uid=str(uid)) if version is not None: version_str = str(version) - if version_str not in self._SPECIAL_VERSIONS \ - and (not version_str.isdecimal() or int(version_str) <= 0): - raise ValueError("A predictor version must either be a positive integer, " - f"\"{LATEST_VER}\", or \"{MOST_RECENT_VER}\".") + if version_str not in self._SPECIAL_VERSIONS and ( + not version_str.isdecimal() or int(version_str) <= 0 + ): + raise ValueError( + "A predictor version must either be a positive integer, " + f'"{LATEST_VER}", or "{MOST_RECENT_VER}".' + ) path += f"/{version_str}" path += f"/{action}" if action else "" @@ -103,7 +106,7 @@ def _construct_path(self, def _page_fetcher(self, *, uid: UUID | str, **additional_params): fetcher_params = { "path": self._construct_path(uid), - "additional_params": additional_params + "additional_params": additional_params, } return partial(self._fetch_page, **fetcher_params) @@ -114,85 +117,68 @@ def build(self, data: dict) -> GraphPredictor: predictor._project_id = self.project_id return predictor - def get(self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER) -> GraphPredictor: + def get(self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER) -> GraphPredictor: path = self._construct_path(uid, version) entity = self.session.get_resource(path, version=self._api_version) predictor = self.build(entity) return predictor def get_featurized_training_data( - self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER + self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER ) -> list[HierarchicalDesignMaterial]: version_path = self._construct_path(uid, version) full_path = f"{version_path}/featurized-training-data" payload = self.session.get_resource(full_path, version=self._api_version) return [HierarchicalDesignMaterial.build(x) for x in payload] - def list(self, - uid: UUID | str, - *, - per_page: int = 100) -> Iterable[GraphPredictor]: + def list(self, uid: UUID | str, *, per_page: int = 100) -> Iterable[GraphPredictor]: """List non-archived versions of the given predictor.""" page_fetcher = self._page_fetcher(uid=uid) - return self._paginator.paginate(page_fetcher=page_fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) - - def list_archived(self, - uid: UUID | str, - *, - per_page: int = 20) -> Iterable[GraphPredictor]: + return self._paginator.paginate( + page_fetcher=page_fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) + + def list_archived(self, uid: UUID | str, *, per_page: int = 20) -> Iterable[GraphPredictor]: """List archived versions of the given predictor.""" page_fetcher = self._page_fetcher(uid=uid, filter="archived eq 'true'") - return self._paginator.paginate(page_fetcher=page_fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) - - def archive(self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER) -> GraphPredictor: + return self._paginator.paginate( + page_fetcher=page_fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) + + def archive(self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER) -> GraphPredictor: url = self._construct_path(uid, version, "archive") entity = self.session.put_resource(url, {}, version=self._api_version) return self.build(entity) - def restore(self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER) -> GraphPredictor: + def restore(self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER) -> GraphPredictor: url = self._construct_path(uid, version, "restore") entity = self.session.put_resource(url, {}, version=self._api_version) return self.build(entity) - def is_stale(self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER) -> bool: + def is_stale(self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER) -> bool: path = self._construct_path(uid, version, "is-stale") response = self.session.get_resource(path, version=self._api_version) return response["is_stale"] - def retrain_stale(self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER) -> GraphPredictor: + def retrain_stale( + self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER + ) -> GraphPredictor: path = self._construct_path(uid, version, "retrain-stale") entity = self.session.put_resource(path, {}, version=self._api_version) return self.build(entity) - def rename(self, - uid: UUID | str, - *, - version: int | str, - name: str | None = None, - description: str | None = None - ) -> GraphPredictor: + def rename( + self, + uid: UUID | str, + *, + version: int | str, + name: str | None = None, + description: str | None = None, + ) -> GraphPredictor: path = self._construct_path(uid, version, "rename") json = {"name": name, "description": description} entity = self.session.put_resource(path, json, version=self._api_version) @@ -214,11 +200,11 @@ class PredictorCollection(Collection[GraphPredictor]): """ - _api_version = 'v3' - _path_template = '/projects/{project_id}/predictors' + _api_version = "v3" + _path_template = "/projects/{project_id}/predictors" _individual_key = None _resource = GraphPredictor - _collection_key = 'response' + _collection_key = "response" def __init__(self, project_id: UUID, session: Session): self.project_id = project_id @@ -232,10 +218,7 @@ def build(self, data: dict) -> GraphPredictor: predictor._project_id = self.project_id return predictor - def get(self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER) -> GraphPredictor: + def get(self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER) -> GraphPredictor: """Get a predictor by ID and (optionally) version. If version is omitted, the most recent version will be retrieved. @@ -245,10 +228,7 @@ def get(self, return self._versions_collection.get(uid=uid, version=version) def get_featurized_training_data( - self, - uid: UUID | str, - *, - version: int | str = MOST_RECENT_VER + self, uid: UUID | str, *, version: int | str = MOST_RECENT_VER ) -> list[HierarchicalDesignMaterial]: """Retrieve a list of featurized materials for a trained predictor. @@ -312,21 +292,11 @@ def train(self, uid: UUID | str) -> GraphPredictor: entity = self.session.put_resource(path, {}, params=params, version=self._api_version) return self.build(entity) - def archive_version( - self, - uid: UUID | str, - *, - version: int | str - ) -> GraphPredictor: + def archive_version(self, uid: UUID | str, *, version: int | str) -> GraphPredictor: """Archive a predictor version.""" return self._versions_collection.archive(uid, version=version) - def restore_version( - self, - uid: UUID | str, - *, - version: int | str - ) -> GraphPredictor: + def restore_version(self, uid: UUID | str, *, version: int | str) -> GraphPredictor: """Restore a predictor version.""" return self._versions_collection.restore(uid, version=version) @@ -361,25 +331,29 @@ def root_is_archived(self, uid: UUID | str) -> bool: def archive(self, uid: UUID | str): """[UNSUPPORTED] Use archive_root or archive_version instead.""" - raise NotImplementedError("The archive() method is no longer supported. You most likely " - "want archive_root(), or possibly archive_version().") + raise NotImplementedError( + "The archive() method is no longer supported. You most likely " + "want archive_root(), or possibly archive_version()." + ) def restore(self, uid: UUID | str): """[UNSUPPORTED] Use restore_root or restore_version instead.""" - raise NotImplementedError("The restore() method is no longer supported. You most likely " - "want restore_root(), or possibly restore_version().") + raise NotImplementedError( + "The restore() method is no longer supported. You most likely " + "want restore_root(), or possibly restore_version()." + ) def _list_base(self, *, per_page: int = 100, archived: bool | None = None): filters = {} if archived is not None: filters["archived"] = archived - fetcher = partial(self._fetch_page, - additional_params=filters, - version="v4") - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) + fetcher = partial(self._fetch_page, additional_params=filters, version="v4") + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def list_all(self, *, per_page: int = 20) -> Iterable[GraphPredictor]: """List the most recent version of all predictors.""" @@ -393,17 +367,15 @@ def list_archived(self, *, per_page: int = 20) -> Iterable[GraphPredictor]: """List the most recent version of all archived predictors.""" return self._list_base(per_page=per_page, archived=True) - def list_versions(self, - uid: UUID | str = None, - *, - per_page: int = 100) -> Iterable[GraphPredictor]: + def list_versions( + self, uid: UUID | str = None, *, per_page: int = 100 + ) -> Iterable[GraphPredictor]: """List all non-archived versions of the given Predictor.""" return self._versions_collection.list(uid, per_page=per_page) - def list_archived_versions(self, - uid: UUID | str = None, - *, - per_page: int = 20) -> Iterable[GraphPredictor]: + def list_archived_versions( + self, uid: UUID | str = None, *, per_page: int = 20 + ) -> Iterable[GraphPredictor]: """List all archived versions of the given Predictor.""" return self._versions_collection.list_archived(uid, per_page=per_page) @@ -437,11 +409,13 @@ def check_for_update(self, uid: UUID | str) -> GraphPredictor | None: else: return None - def create_default(self, - *, - training_data: DataSource, - pattern: str | AutoConfigureMode = AutoConfigureMode.INFER, - prefer_valid: bool = True) -> GraphPredictor: + def create_default( + self, + *, + training_data: DataSource, + pattern: str | AutoConfigureMode = AutoConfigureMode.INFER, + prefer_valid: bool = True, + ) -> GraphPredictor: """Create a default predictor for some training data. This method will return an unregistered predictor generated by inspecting the @@ -485,11 +459,13 @@ def create_default(self, data = self.session.post_resource(path, json=payload, version=self._api_version) return self.build(GraphPredictor.wrap_instance(data["instance"])) - def create_default_async(self, - *, - training_data: DataSource, - pattern: str | AutoConfigureMode = AutoConfigureMode.INFER, - prefer_valid: bool = True) -> AsyncDefaultPredictor: + def create_default_async( + self, + *, + training_data: DataSource, + pattern: str | AutoConfigureMode = AutoConfigureMode.INFER, + prefer_valid: bool = True, + ) -> AsyncDefaultPredictor: """Similar to PredictorCollection.create_default, except asynchronous. This begins a long-running task to generate the predictor. The returned object contains an @@ -527,14 +503,19 @@ def create_default_async(self, return AsyncDefaultPredictor.build(data) @staticmethod - def _create_default_payload(training_data: DataSource, - pattern: str | AutoConfigureMode = AutoConfigureMode.INFER, - prefer_valid: bool = True) -> dict: + def _create_default_payload( + training_data: DataSource, + pattern: str | AutoConfigureMode = AutoConfigureMode.INFER, + prefer_valid: bool = True, + ) -> dict: # Continue handling string pattern inputs pattern = AutoConfigureMode.from_str(pattern, exception=True) - return {"data_source": training_data.dump(), "pattern": pattern, - "prefer_valid": prefer_valid} + return { + "data_source": training_data.dump(), + "pattern": pattern, + "prefer_valid": prefer_valid, + } def get_default_async(self, *, task_id: UUID | str) -> AsyncDefaultPredictor: """Get the current async default predictor generation result. @@ -565,12 +546,14 @@ def retrain_stale(self, uid: UUID | str, *, version: int | str) -> GraphPredicto """ return self._versions_collection.retrain_stale(uid, version=version) - def rename(self, - uid: UUID | str, - *, - version: int | str, - name: str | None = None, - description: str | None = None) -> GraphPredictor: + def rename( + self, + uid: UUID | str, + *, + version: int | str, + name: str | None = None, + description: str | None = None, + ) -> GraphPredictor: """Rename an existing predictor. Both the name and description can be changed. This does not trigger retraining. diff --git a/src/citrine/resources/predictor_evaluation.py b/src/citrine/resources/predictor_evaluation.py index 9840b836d..22bb94c10 100644 --- a/src/citrine/resources/predictor_evaluation.py +++ b/src/citrine/resources/predictor_evaluation.py @@ -1,16 +1,19 @@ +import builtins from collections.abc import Iterable, Iterator from functools import partial -from typing import List from uuid import UUID -from citrine.informatics.executions.predictor_evaluation import PredictorEvaluation, \ - PredictorEvaluationRequest, PredictorEvaluatorsResponse -from citrine.informatics.predictor_evaluator import PredictorEvaluator -from citrine.informatics.predictors import GraphPredictor -from citrine.resources.predictor import LATEST_VER as LATEST_PRED_VER from citrine._rest.collection import Collection from citrine._rest.resource import PredictorRef from citrine._session import Session +from citrine.informatics.executions.predictor_evaluation import ( + PredictorEvaluation, + PredictorEvaluationRequest, + PredictorEvaluatorsResponse, +) +from citrine.informatics.predictor_evaluator import PredictorEvaluator +from citrine.informatics.predictors import GraphPredictor +from citrine.resources.predictor import LATEST_VER as LATEST_PRED_VER class PredictorEvaluationCollection(Collection[PredictorEvaluation]): @@ -23,11 +26,11 @@ class PredictorEvaluationCollection(Collection[PredictorEvaluation]): """ - _api_version = 'v1' - _path_template = '/projects/{project_id}/predictor-evaluations' + _api_version = "v1" + _path_template = "/projects/{project_id}/predictor-evaluations" _individual_key = None _resource = PredictorEvaluation - _collection_key = 'response' + _collection_key = "response" def __init__(self, project_id: UUID, session: Session): self.project_id = project_id @@ -40,13 +43,14 @@ def build(self, data: dict) -> PredictorEvaluation: evaluation.project_id = self.project_id return evaluation - def _list_base(self, - *, - per_page: int = 100, - predictor_id: UUID | None = None, - predictor_version: int | str | None = None, - archived: bool | None = None - ) -> Iterator[PredictorEvaluation]: + def _list_base( + self, + *, + per_page: int = 100, + predictor_id: UUID | None = None, + predictor_version: int | str | None = None, + archived: bool | None = None, + ) -> Iterator[PredictorEvaluation]: params = {"archived": archived} if predictor_id is not None: params["predictor_id"] = str(predictor_id) @@ -54,44 +58,53 @@ def _list_base(self, params["predictor_version"] = predictor_version fetcher = partial(self._fetch_page, additional_params=params) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) - - def list_all(self, - *, - per_page: int = 100, - predictor_id: UUID | None = None, - predictor_version: int | str | None = None - ) -> Iterable[PredictorEvaluation]: + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) + + def list_all( + self, + *, + per_page: int = 100, + predictor_id: UUID | None = None, + predictor_version: int | str | None = None, + ) -> Iterable[PredictorEvaluation]: """List all predictor evaluations.""" - return self._list_base(per_page=per_page, - predictor_id=predictor_id, - predictor_version=predictor_version) - - def list(self, - *, - per_page: int = 100, - predictor_id: UUID | None = None, - predictor_version: int | str | None = None - ) -> Iterable[PredictorEvaluation]: + return self._list_base( + per_page=per_page, predictor_id=predictor_id, predictor_version=predictor_version + ) + + def list( + self, + *, + per_page: int = 100, + predictor_id: UUID | None = None, + predictor_version: int | str | None = None, + ) -> Iterable[PredictorEvaluation]: """List non-archived predictor evaluations.""" - return self._list_base(per_page=per_page, - predictor_id=predictor_id, - predictor_version=predictor_version, - archived=False) - - def list_archived(self, - *, - per_page: int = 100, - predictor_id: UUID | None = None, - predictor_version: int | str | None = None - ) -> Iterable[PredictorEvaluation]: + return self._list_base( + per_page=per_page, + predictor_id=predictor_id, + predictor_version=predictor_version, + archived=False, + ) + + def list_archived( + self, + *, + per_page: int = 100, + predictor_id: UUID | None = None, + predictor_version: int | str | None = None, + ) -> Iterable[PredictorEvaluation]: """List archived predictor evaluations.""" - return self._list_base(per_page=per_page, - predictor_id=predictor_id, - predictor_version=predictor_version, - archived=True) + return self._list_base( + per_page=per_page, + predictor_id=predictor_id, + predictor_version=predictor_version, + archived=True, + ) def archive(self, uid: UUID | str): """Archive an evaluation.""" @@ -105,7 +118,7 @@ def restore(self, uid: UUID | str): result = self.session.put_resource(url, {}, version=self._api_version) return self.build(result) - def default_from_config(self, config: GraphPredictor) -> List[PredictorEvaluator]: + def default_from_config(self, config: GraphPredictor) -> builtins.list[PredictorEvaluator]: """Retrieve the default evaluators for an arbitrary (but valid) predictor config. See :func:`~citrine.resources.PredictorEvaluationCollection.default` for details @@ -116,11 +129,9 @@ def default_from_config(self, config: GraphPredictor) -> List[PredictorEvaluator result = self.session.post_resource(path, json=payload, version=self._api_version) return PredictorEvaluatorsResponse.build(result).evaluators - def default(self, - *, - predictor_id: UUID | str, - predictor_version: int | str = LATEST_PRED_VER - ) -> List[PredictorEvaluator]: + def default( + self, *, predictor_id: UUID | str, predictor_version: int | str = LATEST_PRED_VER + ) -> builtins.list[PredictorEvaluator]: """Retrieve the default evaluators for a stored predictor. The current default evaluators perform 5-fold, 3-trial cross-validation on all valid @@ -153,11 +164,13 @@ def default(self, result = self.session.post_resource(path, json=payload, version=self._api_version) return PredictorEvaluatorsResponse.build(result).evaluators - def trigger(self, - *, - predictor_id: UUID | str, - predictor_version: int | str = LATEST_PRED_VER, - evaluators: List[PredictorEvaluator]) -> PredictorEvaluation: + def trigger( + self, + *, + predictor_id: UUID | str, + predictor_version: int | str = LATEST_PRED_VER, + evaluators: builtins.list[PredictorEvaluator], + ) -> PredictorEvaluation: """Evaluate a predictor using the provided evaluators. Parameters @@ -175,17 +188,15 @@ def trigger(self, """ path = self._get_path("trigger") - payload = PredictorEvaluationRequest(evaluators=evaluators, - predictor_id=predictor_id, - predictor_version=predictor_version).dump() + payload = PredictorEvaluationRequest( + evaluators=evaluators, predictor_id=predictor_id, predictor_version=predictor_version + ).dump() result = self.session.post_resource(path, payload, version=self._api_version) return self.build(result) - def trigger_default(self, - *, - predictor_id: UUID | str, - predictor_version: int | str = LATEST_PRED_VER - ) -> PredictorEvaluation: + def trigger_default( + self, *, predictor_id: UUID | str, predictor_version: int | str = LATEST_PRED_VER + ) -> PredictorEvaluation: """Evaluate a predictor using the default evaluators. See :func:`~citrine.resources.PredictorCollection.default` for details on the evaluators. diff --git a/src/citrine/resources/process_run.py b/src/citrine/resources/process_run.py index 17d3e9971..c8a29dfd1 100644 --- a/src/citrine/resources/process_run.py +++ b/src/citrine/resources/process_run.py @@ -1,10 +1,8 @@ """Resources that represent process run data objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String -from citrine.resources.object_runs import ObjectRun, ObjectRunCollection from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.file_link import FileLink @@ -13,8 +11,12 @@ from gemd.entity.object.process_spec import ProcessSpec as GEMDProcessSpec from gemd.entity.source.performed_source import PerformedSource +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources.object_runs import ObjectRun, ObjectRunCollection + -class ProcessRun(GEMDResource['ProcessRun'], ObjectRun, GEMDProcessRun, typ=GEMDProcessRun.typ): +class ProcessRun(GEMDResource["ProcessRun"], ObjectRun, GEMDProcessRun, typ=GEMDProcessRun.typ): """ A process run. @@ -49,39 +51,50 @@ class ProcessRun(GEMDResource['ProcessRun'], ObjectRun, GEMDProcessRun, typ=GEMD _response_key = GEMDProcessRun.typ # 'process_run' - name = String('name', override=True, use_init=True) - conditions = Optional(List(Object(Condition)), 'conditions', override=True) - parameters = Optional(List(Object(Parameter)), 'parameters', override=True) - spec = Optional(LinkOrElse(GEMDProcessSpec), 'spec', override=True, use_init=True,) + name = String("name", override=True, use_init=True) + conditions = Optional(List(Object(Condition)), "conditions", override=True) + parameters = Optional(List(Object(Parameter)), "parameters", override=True) + spec = Optional(LinkOrElse(GEMDProcessSpec), "spec", override=True, use_init=True) source = Optional(Object(PerformedSource), "source", override=True) - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - conditions: list[Condition] | None = None, - parameters: list[Parameter] | None = None, - spec: GEMDProcessSpec | None = None, - file_links: list[FileLink] | None = None, - source: PerformedSource | None = None): + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + conditions: list[Condition] | None = None, + parameters: list[Parameter] | None = None, + spec: GEMDProcessSpec | None = None, + file_links: list[FileLink] | None = None, + source: PerformedSource | None = None, + ): if uids is None: uids = dict() super(ObjectRun, self).__init__() - GEMDProcessRun.__init__(self, name=name, uids=uids, - tags=tags, conditions=conditions, parameters=parameters, - spec=spec, file_links=file_links, notes=notes, source=source) + GEMDProcessRun.__init__( + self, + name=name, + uids=uids, + tags=tags, + conditions=conditions, + parameters=parameters, + spec=spec, + file_links=file_links, + notes=notes, + source=source, + ) def __str__(self): - return ''.format(self.name) + return f"" class ProcessRunCollection(ObjectRunCollection[ProcessRun]): """Represents the collection of all process runs associated with a dataset.""" - _individual_key = 'process_run' - _collection_key = 'process_runs' + _individual_key = "process_run" + _collection_key = "process_runs" _resource = ProcessRun @classmethod @@ -89,9 +102,7 @@ def get_type(cls) -> type[ProcessRun]: """Return the resource type in the collection.""" return ProcessRun - def list_by_spec(self, - uid: UUID | str | LinkByUID | GEMDProcessSpec - ) -> Iterator[ProcessRun]: + def list_by_spec(self, uid: UUID | str | LinkByUID | GEMDProcessSpec) -> Iterator[ProcessRun]: """ Get the process runs using the specified process spec. @@ -106,4 +117,4 @@ def list_by_spec(self, The process runs using the specified process spec. """ - return self._get_relation('process-specs', uid=uid) + return self._get_relation("process-specs", uid=uid) diff --git a/src/citrine/resources/process_spec.py b/src/citrine/resources/process_spec.py index 63a8233c4..0af1834fd 100644 --- a/src/citrine/resources/process_spec.py +++ b/src/citrine/resources/process_spec.py @@ -1,10 +1,8 @@ """Resources that represent process spec objects.""" + from collections.abc import Iterator from uuid import UUID -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import List, LinkOrElse, Object, Optional, String -from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.parameter import Parameter from gemd.entity.file_link import FileLink @@ -12,12 +10,13 @@ from gemd.entity.object.process_spec import ProcessSpec as GEMDProcessSpec from gemd.entity.template.process_template import ProcessTemplate as GEMDProcessTemplate +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import LinkOrElse, List, Object, Optional, String +from citrine.resources.object_specs import ObjectSpec, ObjectSpecCollection + class ProcessSpec( - GEMDResource['ProcessSpec'], - ObjectSpec, - GEMDProcessSpec, - typ=GEMDProcessSpec.typ + GEMDResource["ProcessSpec"], ObjectSpec, GEMDProcessSpec, typ=GEMDProcessSpec.typ ): """ A process specification. @@ -51,38 +50,47 @@ class ProcessSpec( _response_key = GEMDProcessSpec.typ # 'process_spec' - name = String('name', override=True, use_init=True) - conditions = Optional(List(Object(Condition)), 'conditions', override=True) - parameters = Optional(List(Object(Parameter)), 'parameters', override=True) - template = Optional(LinkOrElse(GEMDProcessTemplate), 'template', override=True, use_init=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - tags: list[str] | None = None, - notes: str | None = None, - conditions: list[Condition] | None = None, - parameters: list[Parameter] | None = None, - template: GEMDProcessTemplate | None = None, - file_links: list[FileLink] | None = None - ): + name = String("name", override=True, use_init=True) + conditions = Optional(List(Object(Condition)), "conditions", override=True) + parameters = Optional(List(Object(Parameter)), "parameters", override=True) + template = Optional(LinkOrElse(GEMDProcessTemplate), "template", override=True, use_init=True) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + tags: list[str] | None = None, + notes: str | None = None, + conditions: list[Condition] | None = None, + parameters: list[Parameter] | None = None, + template: GEMDProcessTemplate | None = None, + file_links: list[FileLink] | None = None, + ): if uids is None: uids = dict() super(ObjectSpec, self).__init__() - GEMDProcessSpec.__init__(self, name=name, uids=uids, - tags=tags, conditions=conditions, parameters=parameters, - template=template, file_links=file_links, notes=notes) + GEMDProcessSpec.__init__( + self, + name=name, + uids=uids, + tags=tags, + conditions=conditions, + parameters=parameters, + template=template, + file_links=file_links, + notes=notes, + ) def __str__(self): - return ''.format(self.name) + return f"" class ProcessSpecCollection(ObjectSpecCollection[ProcessSpec]): """Represents the collection of all process specs associated with a dataset.""" - _individual_key = 'process_spec' - _collection_key = 'process_specs' + _individual_key = "process_spec" + _collection_key = "process_specs" _resource = ProcessSpec @classmethod @@ -90,9 +98,9 @@ def get_type(cls) -> type[ProcessSpec]: """Return the resource type in the collection.""" return ProcessSpec - def list_by_template(self, - uid: UUID | str | LinkByUID | GEMDProcessTemplate - ) -> Iterator[ProcessSpec]: + def list_by_template( + self, uid: UUID | str | LinkByUID | GEMDProcessTemplate + ) -> Iterator[ProcessSpec]: """ Get the process specs using the specified process template. @@ -107,4 +115,4 @@ def list_by_template(self, The process specs using the specified process template """ - return self._get_relation('process-templates', uid=uid) + return self._get_relation("process-templates", uid=uid) diff --git a/src/citrine/resources/process_template.py b/src/citrine/resources/process_template.py index 0a98c64e7..eb1e22c2a 100644 --- a/src/citrine/resources/process_template.py +++ b/src/citrine/resources/process_template.py @@ -1,24 +1,33 @@ """Resources that represent process templates.""" + from collections.abc import Sequence -from citrine._rest.resource import GEMDResource -from citrine._serialization.properties import LinkOrElse, List, Object, Optional, \ - SpecifiedMixedList, String, Union -from citrine.resources.condition_template import ConditionTemplate -from citrine.resources.object_templates import ObjectTemplate, ObjectTemplateCollection -from citrine.resources.parameter_template import ParameterTemplate from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.template.process_template import ProcessTemplate as GEMDProcessTemplate from gemd.entity.template.condition_template import ConditionTemplate as GEMDConditionTemplate from gemd.entity.template.parameter_template import ParameterTemplate as GEMDParameterTemplate +from gemd.entity.template.process_template import ProcessTemplate as GEMDProcessTemplate + +from citrine._rest.resource import GEMDResource +from citrine._serialization.properties import ( + LinkOrElse, + List, + Object, + Optional, + SpecifiedMixedList, + String, + Union, +) +from citrine.resources.condition_template import ConditionTemplate +from citrine.resources.object_templates import ObjectTemplate, ObjectTemplateCollection +from citrine.resources.parameter_template import ParameterTemplate class ProcessTemplate( - GEMDResource['ProcessTemplate'], + GEMDResource["ProcessTemplate"], ObjectTemplate, GEMDProcessTemplate, - typ=GEMDProcessTemplate.typ + typ=GEMDProcessTemplate.typ, ): """ A process template. @@ -56,50 +65,84 @@ class ProcessTemplate( _response_key = GEMDProcessTemplate.typ # 'process_template' - conditions = Optional(List(Union([LinkOrElse(GEMDConditionTemplate), - SpecifiedMixedList([LinkOrElse(GEMDConditionTemplate), - Optional(Object(BaseBounds))])])), - 'conditions', - override=True) - parameters = Optional(List(Union([LinkOrElse(GEMDParameterTemplate), - SpecifiedMixedList([LinkOrElse(GEMDParameterTemplate), - Optional(Object(BaseBounds))])])), - 'parameters', - override=True) - allowed_labels = Optional(List(String()), 'allowed_labels', override=True) - allowed_names = Optional(List(String()), 'allowed_names', override=True) - - def __init__(self, - name: str, - *, - uids: dict[str, str] | None = None, - conditions: Sequence[ConditionTemplate | LinkByUID - | Sequence[ConditionTemplate | LinkByUID | BaseBounds | None] - ] | None = None, - parameters: Sequence[ParameterTemplate | LinkByUID - | Sequence[ParameterTemplate | LinkByUID | BaseBounds | None] - ] | None = None, - allowed_labels: list[str] | None = None, - allowed_names: list[str] | None = None, - description: str | None = None, - tags: list[str] | None = None): + conditions = Optional( + List( + Union( + [ + LinkOrElse(GEMDConditionTemplate), + SpecifiedMixedList( + [LinkOrElse(GEMDConditionTemplate), Optional(Object(BaseBounds))] + ), + ] + ) + ), + "conditions", + override=True, + ) + parameters = Optional( + List( + Union( + [ + LinkOrElse(GEMDParameterTemplate), + SpecifiedMixedList( + [LinkOrElse(GEMDParameterTemplate), Optional(Object(BaseBounds))] + ), + ] + ) + ), + "parameters", + override=True, + ) + + allowed_labels = Optional(List(String()), "allowed_labels", override=True) + allowed_names = Optional(List(String()), "allowed_names", override=True) + + def __init__( + self, + name: str, + *, + uids: dict[str, str] | None = None, + conditions: Sequence[ + ConditionTemplate + | LinkByUID + | Sequence[ConditionTemplate | LinkByUID | BaseBounds | None] + ] + | None = None, + parameters: Sequence[ + ParameterTemplate + | LinkByUID + | Sequence[ParameterTemplate | LinkByUID | BaseBounds | None] + ] + | None = None, + allowed_labels: list[str] | None = None, + allowed_names: list[str] | None = None, + description: str | None = None, + tags: list[str] | None = None, + ): if uids is None: uids = dict() super(ObjectTemplate, self).__init__() - GEMDProcessTemplate.__init__(self, name=name, uids=uids, - conditions=conditions, parameters=parameters, tags=tags, - description=description, allowed_labels=allowed_labels, - allowed_names=allowed_names) + GEMDProcessTemplate.__init__( + self, + name=name, + uids=uids, + conditions=conditions, + parameters=parameters, + tags=tags, + description=description, + allowed_labels=allowed_labels, + allowed_names=allowed_names, + ) def __str__(self): - return ''.format(self.name) + return f"" class ProcessTemplateCollection(ObjectTemplateCollection[ProcessTemplate]): """A collection of process templates.""" - _individual_key = 'process_template' - _collection_key = 'process_templates' + _individual_key = "process_template" + _collection_key = "process_templates" _resource = ProcessTemplate @classmethod diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index 8c24de12f..9e2c572db 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -1,4 +1,5 @@ """Resources that represent both individual and collections of projects.""" + from collections.abc import Iterable, Iterator from functools import partial from uuid import UUID @@ -13,16 +14,15 @@ from citrine.resources.design_space import DesignSpaceCollection from citrine.resources.design_workflow import DesignWorkflowCollection from citrine.resources.gemtables import GemTableCollection +from citrine.resources.generative_design_execution import GenerativeDesignExecutionCollection from citrine.resources.predictor import PredictorCollection from citrine.resources.predictor_evaluation import PredictorEvaluationCollection -from citrine.resources.generative_design_execution import \ - GenerativeDesignExecutionCollection from citrine.resources.project_member import ProjectMember from citrine.resources.response import Response from citrine.resources.table_config import TableConfigCollection -class Project(Resource['Project']): +class Project(Resource["Project"]): """ A Citrine Project. @@ -40,27 +40,29 @@ class Project(Resource['Project']): """ - _response_key = 'project' + _response_key = "project" _resource_type = ResourceTypeEnum.PROJECT - name = properties.String('name') - description = properties.Optional(properties.String(), 'description') - uid = properties.Optional(properties.UUID(), 'id') + 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') + status = properties.Optional(properties.String(), "status") """str: Status of the project.""" - created_at = properties.Optional(properties.Datetime(), 'created_at') + created_at = properties.Optional(properties.Datetime(), "created_at") """int: Time the project was created, in seconds since epoch.""" - archived = properties.Optional(properties.Boolean, 'archived') + archived = properties.Optional(properties.Boolean, "archived") """bool: Whether the project is archived.""" _team_id = properties.Optional(properties.UUID, "team.id", serializable=False) - def __init__(self, - name: str, - *, - description: str | None = None, - session: Session | None = None, - team_id: UUID | None = None): + def __init__( + self, + name: str, + *, + description: str | None = None, + session: Session | None = None, + team_id: UUID | None = None, + ): self.name: str = name self.description: str | None = description self.session: Session = session @@ -70,18 +72,17 @@ def _post_dump(self, data: dict) -> dict: return {key: value for key, value in data.items() if value is not None} def __str__(self): - return ''.format(self.name) + return f"" def _path(self): - return format_escaped_url('/projects/{project_id}', project_id=self.uid) + return format_escaped_url("/projects/{project_id}", project_id=self.uid) @property def team_id(self): """Returns the Team's id-scoped UUID.""" if self._team_id is None: self._team_id = self.get_team_id_from_project_id( - session=self.session, - project_id=self.uid + session=self.session, project_id=self.uid ) return self._team_id @@ -92,8 +93,8 @@ def team_id(self, value: UUID | None): @classmethod def get_team_id_from_project_id(cls, session: Session, project_id: UUID): """Returns the UUID of the Team that owns the project with the provided project_id.""" - response = session.get_resource(path=f'projects/{project_id}', version="v3") - return response['project']['team']['id'] + response = session.get_resource(path=f"projects/{project_id}", version="v3") + return response["project"]["team"]["id"] @property def branches(self) -> BranchCollection: @@ -138,9 +139,9 @@ def tables(self) -> GemTableCollection: @property def table_configs(self) -> TableConfigCollection: """Return a resource representing all Table Configs in the project.""" - return TableConfigCollection(team_id=self.team_id, - project_id=self.uid, - session=self.session) + return TableConfigCollection( + team_id=self.team_id, project_id=self.uid, session=self.session + ) def publish(self, *, resource: Resource): """ @@ -168,8 +169,9 @@ def publish(self, *, resource: Resource): self.session.checked_post( f"{self._path()}/published-resources/{resource_type}/batch-publish", - version='v3', - json={'ids': [resource_access["id"]]}) + version="v3", + json={"ids": [resource_access["id"]]}, + ) return True def un_publish(self, *, resource: Resource): @@ -194,8 +196,9 @@ def un_publish(self, *, resource: Resource): self.session.checked_post( f"{self._path()}/published-resources/{resource_type}/batch-un-publish", - version='v3', - json={'ids': [resource_access["id"]]}) + version="v3", + json={"ids": [resource_access["id"]]}, + ) return True def pull_in_resource(self, *, resource: Resource): @@ -218,11 +221,12 @@ def pull_in_resource(self, *, resource: Resource): if resource_type == ResourceTypeEnum.DATASET: raise ValueError("Pulling a dataset into a project is unnecessary.") - base_url = f'/teams/{self.team_id}{self._path()}' + base_url = f"/teams/{self.team_id}{self._path()}" self.session.checked_post( - f'{base_url}/outside-resources/{resource_type}/batch-pull-in', - version='v3', - json={'ids': [resource_access["id"]]}) + f"{base_url}/outside-resources/{resource_type}/batch-pull-in", + version="v3", + json={"ids": [resource_access["id"]]}, + ) return True def list_members(self) -> "list[ProjectMember] | list[TeamMember]": # noqa: F821 @@ -258,13 +262,14 @@ class ProjectCollection(Collection[Project]): @property def _path_template(self): if self.team_id is None: - return '/projects' + return "/projects" else: - return '/teams/{team_id}/projects' - _individual_key = 'project' - _collection_key = 'projects' + return "/teams/{team_id}/projects" + + _individual_key = "project" + _collection_key = "projects" _resource = Project - _api_version = 'v3' + _api_version = "v3" def __init__(self, session: Session, *, team_id: UUID | None = None): self.session = session @@ -325,8 +330,9 @@ def register(self, name: str, *, description: str | None = None) -> Project: """ if self.team_id is None: - raise NotImplementedError("Cannot register a project without a team ID. " - "Use team.projects.register.") + raise NotImplementedError( + "Cannot register a project without a team ID. Use team.projects.register." + ) project = Project(name, description=description) return super().register(project) @@ -337,9 +343,11 @@ def _list_base(self, *, per_page: int = 1000, archived: bool | None = None): filters["archived"] = str(archived).lower() fetcher = partial(self._fetch_page, additional_params=filters, version=self._api_version) - return self._paginator.paginate(page_fetcher=fetcher, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def list(self, *, per_page: int = 1000) -> Iterator[Project]: """ @@ -440,24 +448,25 @@ def search_all(self, search_params: dict | None) -> Iterable[dict]: """ collections = [] - query_params = {'userId': ""} + query_params = {"userId": ""} - json = {} if search_params is None else {'search_params': search_params} + json = {} if search_params is None else {"search_params": search_params} - data = self.session.post_resource(self._get_path(action="search"), - params=query_params, - json=json, - version=self._api_version) + data = self.session.post_resource( + self._get_path(action="search"), + params=query_params, + json=json, + version=self._api_version, + ) if self._collection_key is not None: collections = data[self._collection_key] return collections - def search(self, - *, - search_params: dict | None = None, - per_page: int = 1000) -> Iterable[Project]: + def search( + self, *, search_params: dict | None = None, per_page: int = 1000 + ) -> Iterable[Project]: """ Search for projects matching the desired name or description. diff --git a/src/citrine/resources/project_member.py b/src/citrine/resources/project_member.py index 06b6e0066..ec910eac6 100644 --- a/src/citrine/resources/project_member.py +++ b/src/citrine/resources/project_member.py @@ -5,17 +5,20 @@ class ProjectMember: """A Member of a Project.""" - def __init__(self, - *, - user: User, - project: 'Project', # noqa: F821 - role: ROLES): + def __init__( + self, + *, + user: User, + project: "Project", # noqa: F821 + role: ROLES, + ): self.user: User = user # To avoid circular dependency, use forward-reference for type definition # https://www.python.org/dev/peps/pep-0484/#forward-references - self.project: 'Project' = project # noqa: F821 + self.project: Project = project # noqa: F821 self.role: ROLES = role def __str__(self): - return ''\ - .format(self.user.screen_name, self.role, self.project.name) + return ( + f"" + ) diff --git a/src/citrine/resources/property_template.py b/src/citrine/resources/property_template.py index dc3d5ab15..0f116e5a6 100644 --- a/src/citrine/resources/property_template.py +++ b/src/citrine/resources/property_template.py @@ -1,16 +1,17 @@ """Resources that represent property templates.""" -from citrine._rest.resource import GEMDResource -from citrine.resources.attribute_templates import AttributeTemplate, AttributeTemplateCollection from gemd.entity.bounds.base_bounds import BaseBounds from gemd.entity.template.property_template import PropertyTemplate as GEMDPropertyTemplate +from citrine._rest.resource import GEMDResource +from citrine.resources.attribute_templates import AttributeTemplate, AttributeTemplateCollection + class PropertyTemplate( - GEMDResource['PropertyTemplate'], + GEMDResource["PropertyTemplate"], AttributeTemplate, GEMDPropertyTemplate, - typ=GEMDPropertyTemplate.typ + typ=GEMDPropertyTemplate.typ, ): """ A property template. @@ -36,28 +37,31 @@ class PropertyTemplate( _response_key = GEMDPropertyTemplate.typ # 'property_template' - def __init__(self, - name: str, - *, - bounds: BaseBounds, - uids: dict[str, str] | None = None, - description: str | None = None, - tags: list[str] | None = None): + def __init__( + self, + name: str, + *, + bounds: BaseBounds, + uids: dict[str, str] | None = None, + description: str | None = None, + tags: list[str] | None = None, + ): if uids is None: uids = dict() super(AttributeTemplate, self).__init__() - GEMDPropertyTemplate.__init__(self, name=name, bounds=bounds, tags=tags, - uids=uids, description=description) + GEMDPropertyTemplate.__init__( + self, name=name, bounds=bounds, tags=tags, uids=uids, description=description + ) def __str__(self): - return ''.format(self.name) + return f"" class PropertyTemplateCollection(AttributeTemplateCollection[PropertyTemplate]): """A collection of property templates.""" - _individual_key = 'property_template' - _collection_key = 'property_templates' + _individual_key = "property_template" + _collection_key = "property_templates" _resource = PropertyTemplate @classmethod diff --git a/src/citrine/resources/report.py b/src/citrine/resources/report.py index df643bcf6..425416769 100644 --- a/src/citrine/resources/report.py +++ b/src/citrine/resources/report.py @@ -1,4 +1,5 @@ """A resource that represents a single module report.""" + from uuid import UUID from citrine._rest.resource import Resource @@ -7,7 +8,7 @@ from citrine.informatics.reports import Report -class ReportResource(Resource['ReportResource']): +class ReportResource(Resource["ReportResource"]): """Defines a resource for fetching reports from a module. Parameters @@ -17,24 +18,25 @@ class ReportResource(Resource['ReportResource']): """ - _path_template = '/projects/{project_id}/predictors/{predictor_id}/versions/{version}/report' - _api_version = 'v3' + _path_template = "/projects/{project_id}/predictors/{predictor_id}/versions/{version}/report" + _api_version = "v3" def __init__(self, project_id: UUID, session: Session): self.project_id = project_id self.session = session - def get(self, - *, - predictor_id: UUID | str, - predictor_version: int | str | None = None) -> Report: + def get( + self, *, predictor_id: UUID | str, predictor_version: int | str | None = None + ) -> Report: """Gets a single report keyed on the predictor ID and (optionally) version.""" version = predictor_version or "most_recent" - url_path = format_escaped_url(self._path_template, - project_id=self.project_id, - predictor_id=str(predictor_id), - version=version) + url_path = format_escaped_url( + self._path_template, + project_id=self.project_id, + predictor_id=str(predictor_id), + version=version, + ) data = self.session.get_resource(url_path, version=self._api_version) report = Report.build(data) diff --git a/src/citrine/resources/response.py b/src/citrine/resources/response.py index b0bb25ea5..8aeaac6cf 100644 --- a/src/citrine/resources/response.py +++ b/src/citrine/resources/response.py @@ -22,7 +22,7 @@ def _get_body_string(self): return "No body available" def __repr__(self): - return f'Response({self._get_status_string()!r}, {self._get_body_string()!r})' + return f"Response({self._get_status_string()!r}, {self._get_body_string()!r})" def __str__(self): - return f'' + return f"" diff --git a/src/citrine/resources/sample_design_space_execution.py b/src/citrine/resources/sample_design_space_execution.py index 033baf1dc..f5bfd241f 100644 --- a/src/citrine/resources/sample_design_space_execution.py +++ b/src/citrine/resources/sample_design_space_execution.py @@ -1,21 +1,22 @@ """Resources that represent both individual and collections of sample design space executions.""" + from collections.abc import Iterator from uuid import UUID from citrine._rest.collection import Collection from citrine._session import Session -from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution from citrine.informatics.design_spaces.sample_design_space import SampleDesignSpaceInput +from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution from citrine.resources.response import Response class SampleDesignSpaceExecutionCollection(Collection["SampleDesignSpaceExecution"]): """A collection of SampleDesignSpaceExecutions.""" - _api_version = 'v3' - _path_template = '/projects/{project_id}/design-spaces/{design_space_id}/sample' + _api_version = "v3" + _path_template = "/projects/{project_id}/design-spaces/{design_space_id}/sample" _individual_key = None - _collection_key = 'response' + _collection_key = "response" _resource = SampleDesignSpaceExecution def __init__(self, project_id: UUID, design_space_id: UUID, session: Session): @@ -48,9 +49,7 @@ def update(self, model: SampleDesignSpaceExecution) -> SampleDesignSpaceExecutio """Cannot update an execution.""" raise NotImplementedError("Cannot update a SampleDesignSpaceExecution.") - def list(self, *, - per_page: int = 10, - ) -> Iterator[SampleDesignSpaceExecution]: + def list(self, *, per_page: int = 10) -> Iterator[SampleDesignSpaceExecution]: """ Paginate over the elements of the collection. @@ -70,12 +69,12 @@ def list(self, *, Resources in this collection. """ - return self._paginator.paginate(page_fetcher=self._fetch_page, - collection_builder=self._build_collection_elements, - per_page=per_page) + return self._paginator.paginate( + page_fetcher=self._fetch_page, + collection_builder=self._build_collection_elements, + per_page=per_page, + ) def delete(self, uid: UUID | str) -> Response: """Sample Design Space Executions cannot be deleted or archived.""" - raise NotImplementedError( - "Sample Design Space Executions cannot be deleted" - ) + raise NotImplementedError("Sample Design Space Executions cannot be deleted") diff --git a/src/citrine/resources/status_detail.py b/src/citrine/resources/status_detail.py index 197a7878c..47b918891 100644 --- a/src/citrine/resources/status_detail.py +++ b/src/citrine/resources/status_detail.py @@ -1,12 +1,11 @@ from typing import TypeVar -from citrine._serialization.serializable import Serializable -from citrine._serialization import properties - from gemd.enumeration.base_enumeration import BaseEnumeration +from citrine._serialization import properties +from citrine._serialization.serializable import Serializable -StatusDetailType = TypeVar('StatusDetailType', bound='StatusDetail') +StatusDetailType = TypeVar("StatusDetailType", bound="StatusDetail") class StatusLevelEnum(BaseEnumeration): diff --git a/src/citrine/resources/table_config.py b/src/citrine/resources/table_config.py index 086628836..7ea05f85f 100644 --- a/src/citrine/resources/table_config.py +++ b/src/citrine/resources/table_config.py @@ -1,9 +1,9 @@ from copy import copy +from typing import TYPE_CHECKING from uuid import UUID -from gemd.entity.object import MaterialRun - from gemd.entity.link_by_uid import LinkByUID +from gemd.entity.object import MaterialRun from gemd.enumeration.base_enumeration import BaseEnumeration from citrine._rest.collection import Collection @@ -11,21 +11,30 @@ from citrine._serialization import properties from citrine._session import Session from citrine._utils.functions import format_escaped_url -from citrine.resources.dataset import DatasetCollection -from citrine.resources.data_concepts import CITRINE_SCOPE, _make_link_by_uid -from citrine.resources.process_template import ProcessTemplate from citrine.gemd_queries.gemd_query import GemdQuery -from citrine.gemtables.columns import Column, MeanColumn, IdentityColumn, OriginalUnitsColumn, \ - ConcatColumn +from citrine.gemtables.columns import ( + Column, + ConcatColumn, + IdentityColumn, + MeanColumn, + OriginalUnitsColumn, +) from citrine.gemtables.rows import Row from citrine.gemtables.variables import ( - Variable, IngredientIdentifierByProcessTemplateAndName, IngredientQuantityByProcessAndName, - IngredientQuantityDimension, IngredientIdentifierInOutput, IngredientQuantityInOutput, - IngredientLabelsSetByProcessAndName, IngredientLabelsSetInOutput + IngredientIdentifierByProcessTemplateAndName, + IngredientIdentifierInOutput, + IngredientLabelsSetByProcessAndName, + IngredientLabelsSetInOutput, + IngredientQuantityByProcessAndName, + IngredientQuantityDimension, + IngredientQuantityInOutput, + Variable, ) +from citrine.resources.data_concepts import CITRINE_SCOPE, _make_link_by_uid +from citrine.resources.dataset import DatasetCollection +from citrine.resources.process_template import ProcessTemplate -from typing import TYPE_CHECKING -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from citrine.resources.team import Team @@ -97,12 +106,12 @@ def _get_dups(lst: list) -> list: # Hmmn, this looks like a potentially costly operation?! return [x for x in lst if lst.count(x) > 1] - config_uid = properties.Optional(properties.UUID(), 'definition_id') + config_uid = properties.Optional(properties.UUID(), "definition_id") """:UUID | None: Unique ID of the table config, independent of its version.""" - version_number = properties.Optional(properties.Integer, 'version_number') + version_number = properties.Optional(properties.Integer, "version_number") """:int | None: The version of the table config, starting from 1. It increases every time the table config is updated.""" - version_uid = properties.Optional(properties.UUID(), 'id') + version_uid = properties.Optional(properties.UUID(), "id") """:UUID | None: Unique ID that specifies one version of one table config.""" name = properties.String("name") @@ -116,15 +125,18 @@ def _get_dups(lst: list) -> list: properties.Enumeration(TableFromGemdQueryAlgorithm), "generation_algorithm" ) - def __init__(self, name: str, - *, - description: str, - datasets: list[UUID], - variables: list[Variable], - rows: list[Row], - columns: list[Column], - gemd_query: GemdQuery = None, - generation_algorithm: TableFromGemdQueryAlgorithm | None = None): + def __init__( + self, + name: str, + *, + description: str, + datasets: list[UUID], + variables: list[Variable], + rows: list[Row], + columns: list[Column], + gemd_query: GemdQuery = None, + generation_algorithm: TableFromGemdQueryAlgorithm | None = None, + ): self.name = name self.description = description self.datasets = datasets @@ -140,18 +152,22 @@ def __init__(self, name: str, names = [x.name for x in variables] dup_names = self._get_dups(names) if len(dup_names) > 0: - raise ValueError("Multiple variables defined these names," - " which much be unique: {}".format(dup_names)) + raise ValueError( + f"Multiple variables defined these names, which much be unique: {dup_names}" + ) headers = [x.headers for x in variables] dup_headers = self._get_dups(headers) if len(dup_headers) > 0: - raise ValueError("Multiple variables defined these headers," - " which much be unique: {}".format(dup_headers)) + raise ValueError( + f"Multiple variables defined these headers, which much be unique: {dup_headers}" + ) missing_variables = [x.data_source for x in columns if x.data_source not in names] if len(missing_variables) > 0: - raise ValueError("The data_source of the columns must match one of the variable names," - " but {} were missing".format(missing_variables)) + raise ValueError( + "The data_source of the columns must match one of the variable names," + f" but {missing_variables} were missing" + ) @property def uid(self) -> UUID: @@ -163,12 +179,14 @@ def uid(self, new_uid: str | UUID) -> None: """Set the unique ID of the table config, independent of its version.""" self.config_uid = new_uid - def add_columns(self, *, - variable: Variable, - columns: list[Column], - name: str | None = None, - description: str | None = None - ) -> 'TableConfig': + def add_columns( + self, + *, + variable: Variable, + columns: list[Column], + name: str | None = None, + description: str | None = None, + ) -> "TableConfig": """Add a variable and one or more columns to this TableConfig (out-of-place). This method checks that the variable name is not already in use and that the columns @@ -188,12 +206,13 @@ def add_columns(self, *, """ if variable.name in [x.name for x in self.variables]: - raise ValueError("The variable name {} is already used".format(variable.name)) + raise ValueError(f"The variable name {variable.name} is already used") mismatched_data_source = [x for x in columns if x.data_source != variable.name] if len(mismatched_data_source): - raise ValueError("Column.data_source must be {} but found {}" - .format(variable.name, mismatched_data_source)) + raise ValueError( + f"Column.data_source must be {variable.name} but found {mismatched_data_source}" + ) new_config = TableConfig( name=name or self.name, @@ -201,20 +220,22 @@ def add_columns(self, *, datasets=copy(self.datasets), rows=copy(self.rows), variables=copy(self.variables) + [variable], - columns=copy(self.columns) + columns + columns=copy(self.columns) + columns, ) new_config.version_number = copy(self.version_number) new_config.config_uid = copy(self.config_uid) new_config.version_uid = copy(self.version_uid) return new_config - def add_all_ingredients(self, *, - process_template: LinkByUID | ProcessTemplate | str | UUID, - team: 'Team', - quantity_dimension: IngredientQuantityDimension, - scope: str = CITRINE_SCOPE, - unit: str | None = None - ): + def add_all_ingredients( + self, + *, + process_template: LinkByUID | ProcessTemplate | str | UUID, + team: "Team", + quantity_dimension: IngredientQuantityDimension, + scope: str = CITRINE_SCOPE, + unit: str | None = None, + ): """Add variables and columns for all of the possible ingredients in a process. For each allowed ingredient name in the process template there is a column for the id of @@ -239,38 +260,43 @@ def add_all_ingredients(self, *, IngredientQuantityDimension.ABSOLUTE: "absolute quantity", IngredientQuantityDimension.MASS: "mass fraction", IngredientQuantityDimension.VOLUME: "volume fraction", - IngredientQuantityDimension.NUMBER: "number fraction" + IngredientQuantityDimension.NUMBER: "number fraction", } link = _make_link_by_uid(process_template) process: ProcessTemplate = team.process_templates.get(uid=link) if not process.allowed_names: raise RuntimeError( - "Cannot add ingredients for process template \'{}\' because it has no defined " - "ingredients (allowed_names is not defined).".format(process.name)) + f"Cannot add ingredients for process template '{process.name}' because it " + "has no defined ingredients (allowed_names is not defined)." + ) new_variables = [] new_columns = [] for name in process.allowed_names: identifier_variable = IngredientIdentifierByProcessTemplateAndName( - name='_'.join([process.name, name, str(hash(link.id + name + scope))]), + name="_".join([process.name, name, str(hash(link.id + name + scope))]), headers=[process.name, name, scope], process_template=link, ingredient_name=name, - scope=scope + scope=scope, ) quantity_variable = IngredientQuantityByProcessAndName( - name='_'.join([process.name, name, str(hash( - link.id + name + dimension_display[quantity_dimension]))]), + name="_".join( + [ + process.name, + name, + str(hash(link.id + name + dimension_display[quantity_dimension])), + ] + ), headers=[process.name, name, dimension_display[quantity_dimension]], process_template=link, ingredient_name=name, quantity_dimension=quantity_dimension, - unit=unit + unit=unit, ) label_variable = IngredientLabelsSetByProcessAndName( - name='_'.join([process.name, name, str(hash( - link.id + name + 'Labels'))]), - headers=[process.name, name, 'Labels'], + name="_".join([process.name, name, str(hash(link.id + name + "Labels"))]), + headers=[process.name, name, "Labels"], process_template=link, ingredient_name=name, ) @@ -287,7 +313,7 @@ def add_all_ingredients(self, *, new_columns.append( ConcatColumn( data_source=label_variable.name, - subcolumn=IdentityColumn(data_source=label_variable.name) + subcolumn=IdentityColumn(data_source=label_variable.name), ) ) @@ -297,20 +323,22 @@ def add_all_ingredients(self, *, datasets=copy(self.datasets), rows=copy(self.rows), variables=copy(self.variables) + new_variables, - columns=copy(self.columns) + new_columns + columns=copy(self.columns) + new_columns, ) new_config.version_number = copy(self.version_number) new_config.config_uid = copy(self.config_uid) new_config.version_uid = copy(self.version_uid) return new_config - def add_all_ingredients_in_output(self, *, - process_templates: list[LinkByUID], - team: 'Team', - quantity_dimension: IngredientQuantityDimension, - scope: str = CITRINE_SCOPE, - unit: str | None = None - ): + def add_all_ingredients_in_output( + self, + *, + process_templates: list[LinkByUID], + team: "Team", + quantity_dimension: IngredientQuantityDimension, + scope: str = CITRINE_SCOPE, + unit: str | None = None, + ): """Add variables and columns for all possible ingredients in a list of processes. For each allowed ingredient name in the union of all passed process templates there is a @@ -338,7 +366,7 @@ def add_all_ingredients_in_output(self, *, IngredientQuantityDimension.ABSOLUTE: "absolute quantity", IngredientQuantityDimension.MASS: "mass fraction", IngredientQuantityDimension.VOLUME: "volume fraction", - IngredientQuantityDimension.NUMBER: "number fraction" + IngredientQuantityDimension.NUMBER: "number fraction", } union_allowed_names = [] for process_template_link in process_templates: @@ -355,23 +383,23 @@ def add_all_ingredients_in_output(self, *, new_columns = [] for name in union_allowed_names: identifier_variable = IngredientIdentifierInOutput( - name='_'.join([name, str(hash(name + scope))]), + name="_".join([name, str(hash(name + scope))]), headers=[name, scope], process_templates=process_templates, ingredient_name=name, - scope=scope + scope=scope, ) quantity_variable = IngredientQuantityInOutput( - name='_'.join([name, str(hash(name + dimension_display[quantity_dimension]))]), + name="_".join([name, str(hash(name + dimension_display[quantity_dimension]))]), headers=[name, dimension_display[quantity_dimension]], process_templates=process_templates, ingredient_name=name, quantity_dimension=quantity_dimension, - unit=unit + unit=unit, ) label_variable = IngredientLabelsSetInOutput( - name='_'.join([name, str(hash(name + 'Labels'))]), - headers=[name, 'Labels'], + name="_".join([name, str(hash(name + "Labels"))]), + headers=[name, "Labels"], process_templates=process_templates, ingredient_name=name, ) @@ -388,7 +416,7 @@ def add_all_ingredients_in_output(self, *, new_columns.append( ConcatColumn( data_source=label_variable.name, - subcolumn=IdentityColumn(data_source=label_variable.name) + subcolumn=IdentityColumn(data_source=label_variable.name), ) ) @@ -398,7 +426,7 @@ def add_all_ingredients_in_output(self, *, datasets=copy(self.datasets), rows=copy(self.rows), variables=copy(self.variables) + new_variables, - columns=copy(self.columns) + new_columns + columns=copy(self.columns) + new_columns, ) new_config.version_number = copy(self.version_number) new_config.config_uid = copy(self.config_uid) @@ -410,8 +438,8 @@ class TableConfigCollection(Collection[TableConfig]): """Represents the collection of all Table Configs associated with a project.""" # FIXME (DML): use newly named properties when they're available - _path_template = 'projects/{project_id}/ara-definitions' - _collection_key = 'definitions' + _path_template = "projects/{project_id}/ara-definitions" + _collection_key = "definitions" _resource = TableConfig # NOTE: This isn't actually an 'individual key' - both parts (version and @@ -437,9 +465,9 @@ def get(self, uid: UUID | str, *, version: int | None = None): else: path = self._get_path(uid) data = self.session.get_resource(path) - version_numbers = [version_data['version_number'] for version_data in data['versions']] + version_numbers = [version_data["version_number"] for version_data in data["versions"]] index = version_numbers.index(max(version_numbers)) - data['version'] = data['versions'][index] + data["version"] = data["versions"][index] return self.build(data) def get_for_table(self, table: "GemTable") -> TableConfig: # noqa: F821 @@ -459,29 +487,33 @@ def get_for_table(self, table: "GemTable") -> TableConfig: # noqa: F821 """ # the route to fetch the config is built off the display table route tree path = format_escaped_url( - 'projects/{}/display-tables/{}/versions/{}/definition', - self.project_id, table.uid, table.version) + "projects/{}/display-tables/{}/versions/{}/definition", + self.project_id, + table.uid, + table.version, + ) data = self.session.get_resource(path) return self.build(data) def build(self, data: dict) -> TableConfig: """Build an individual Table Config from a dictionary.""" - version_data = data['version'] - table_config = TableConfig.build(version_data['ara_definition']) - table_config.version_number = version_data['version_number'] - table_config.version_uid = version_data['id'] - table_config.config_uid = data['definition']['id'] + version_data = data["version"] + table_config = TableConfig.build(version_data["ara_definition"]) + table_config.version_number = version_data["version_number"] + table_config.version_uid = version_data["id"] + table_config.config_uid = data["definition"]["id"] table_config.team_id = self.team_id table_config.project_id = self.project_id table_config.session = self.session return table_config def default_for_material( - self, *, - material: MaterialRun | LinkByUID | str | UUID, - name: str, - description: str = None, - algorithm: TableBuildAlgorithm | None = None + self, + *, + material: MaterialRun | LinkByUID | str | UUID, + name: str, + description: str = None, + algorithm: TableBuildAlgorithm | None = None, ) -> tuple[TableConfig, list[tuple[Variable, Column]]]: """ Build best-guess default table config for provided terminal material's history. @@ -516,34 +548,29 @@ def default_for_material( """ link = _make_link_by_uid(material) - params = { - 'id': link.id, - 'scope': link.scope, - 'name': name, - } + params = {"id": link.id, "scope": link.scope, "name": name} if description is not None: - params['description'] = description + params["description"] = description if algorithm is not None: if isinstance(algorithm, TableBuildAlgorithm): - params['algorithm'] = algorithm.value + params["algorithm"] = algorithm.value else: # Not per spec, but be forgiving - params['algorithm'] = str(algorithm) + params["algorithm"] = str(algorithm) data = self.session.get_resource( - format_escaped_url('teams/{}/table-configs/default', self.team_id), - params=params, + format_escaped_url("teams/{}/table-configs/default", self.team_id), params=params ) - config = TableConfig.build(data['config']) - ambiguous = [(Variable.build(v), Column.build(c)) for v, c in data['ambiguous']] + config = TableConfig.build(data["config"]) + ambiguous = [(Variable.build(v), Column.build(c)) for v, c in data["ambiguous"]] return config, ambiguous def from_query( - self, - gemd_query: GemdQuery, - *, - name: str = None, - description: str = None, - algorithm: TableFromGemdQueryAlgorithm | None = None, - register_config: bool = False + self, + gemd_query: GemdQuery, + *, + name: str = None, + description: str = None, + algorithm: TableFromGemdQueryAlgorithm | None = None, + register_config: bool = False, ) -> tuple[TableConfig, list[tuple[Variable, Column]]]: """ Build a TableConfig based on the results of a database query. @@ -570,35 +597,33 @@ def from_query( """ if name is None: - collection = DatasetCollection( - session=self.session, - team_id=self.team_id + collection = DatasetCollection(session=self.session, team_id=self.team_id) + name = ( + f"Automatic Table for Dataset: " + f"{', '.join([collection.get(x).name for x in gemd_query.datasets])}" ) - name = (f"Automatic Table for Dataset: " - f"{', '.join([collection.get(x).name for x in gemd_query.datasets])}") params = {"name": name} if description is not None: - params['description'] = description + params["description"] = description if algorithm is not None: - params['algorithm'] = algorithm + params["algorithm"] = algorithm data = self.session.post_resource( - format_escaped_url('teams/{}/table-configs/from-query', self.team_id), + format_escaped_url("teams/{}/table-configs/from-query", self.team_id), params=params, - json=gemd_query.dump() + json=gemd_query.dump(), ) - config = TableConfig.build(data['config']) - ambiguous = [(Variable.build(v), Column.build(c)) for v, c in data['ambiguous']] + config = TableConfig.build(data["config"]) + ambiguous = [(Variable.build(v), Column.build(c)) for v, c in data["ambiguous"]] if register_config: return self.register(config), ambiguous else: return config, ambiguous - def preview(self, *, - table_config: TableConfig, - preview_materials: list[LinkByUID] = None - ) -> dict: + def preview( + self, *, table_config: TableConfig, preview_materials: list[LinkByUID] = None + ) -> dict: """Preview a Table Config on an explicit set of terminal materials. Parameters @@ -609,13 +634,10 @@ def preview(self, *, List of links to the material runs to use as terminal materials in the preview """ - path = format_escaped_url( - "teams/{}/ara-definitions/preview", - self.team_id - ) + path = format_escaped_url("teams/{}/ara-definitions/preview", self.team_id) body = { "definition": table_config.dump(), - "rows": [x.as_dict() for x in preview_materials] + "rows": [x.as_dict() for x in preview_materials], } return self.session.post_resource(path, body) @@ -665,10 +687,12 @@ def update(self, table_config: TableConfig) -> TableConfig: :return: The updated Table Config with updated metadata """ if table_config.config_uid is None: - raise ValueError("Cannot update Table Config without a config_uid." - " Please either use register() to initially register this" - " Table Config or retrieve the registered details before calling" - " update()") + raise ValueError( + "Cannot update Table Config without a config_uid." + " Please either use register() to initially register this" + " Table Config or retrieve the registered details before calling" + " update()" + ) return self.register(table_config) def delete(self, uid: UUID | str): diff --git a/src/citrine/resources/team.py b/src/citrine/resources/team.py index 084360dc7..4b72d5c02 100644 --- a/src/citrine/resources/team.py +++ b/src/citrine/resources/team.py @@ -1,4 +1,5 @@ """Resources that represent both individual and collections of teams.""" + from typing import Union from uuid import UUID @@ -32,7 +33,6 @@ from citrine.resources.property_template import PropertyTemplateCollection from citrine.resources.user import User, UserCollection - WRITE = "WRITE" READ = "READ" SHARE = "SHARE" @@ -42,18 +42,19 @@ class TeamMember: """A Member of a Team.""" - def __init__(self, - *, - user: User, - team: 'Team', # noqa: F821 - actions: list[TEAM_ACTIONS]): + def __init__( + self, + *, + user: User, + team: "Team", # noqa: F821 + actions: list[TEAM_ACTIONS], + ): self.user = user - self.team: 'Team' = team # noqa: F821 + self.team: Team = team # noqa: F821 self.actions: list[TEAM_ACTIONS] = actions def __str__(self): - return '' \ - .format(self.user.screen_name, self.actions, self.team.name) + return f"" class TeamResourceIDs: @@ -75,22 +76,19 @@ class TeamResourceIDs: _api_version = "v3" - def __init__(self, - session: Session, - team_id: str | UUID, - resource_type: str) -> None: + def __init__(self, session: Session, team_id: str | UUID, resource_type: str) -> None: self.session = session self.team_id = team_id self.resource_type = resource_type def _path(self) -> str: - return format_escaped_url(f'/teams/{self.team_id}') + return format_escaped_url(f"/teams/{self.team_id}") def _list_ids(self, action: str) -> list[str]: query_params = {"domain": self._path(), "action": action} - return self.session.get_resource(f"/{self.resource_type}/authorized-ids", - params=query_params, - version=self._api_version)['ids'] + return self.session.get_resource( + f"/{self.resource_type}/authorized-ids", params=query_params, version=self._api_version + )["ids"] def list_readable(self): """ @@ -129,7 +127,7 @@ def list_shareable(self): return self._list_ids(action=SHARE) -class Team(Resource['Team']): +class Team(Resource["Team"]): """ A Citrine Team. @@ -146,33 +144,29 @@ class Team(Resource['Team']): """ - _response_key = 'team' + _response_key = "team" _resource_type = ResourceTypeEnum.TEAM _api_version = "v3" - name = properties.String('name') + name = properties.String("name") """str: Name of the Team""" - description = properties.Optional(properties.String(), 'description') + description = properties.Optional(properties.String(), "description") """str: Description of the Team""" - uid = properties.Optional(properties.UUID(), 'id') + uid = properties.Optional(properties.UUID(), "id") """UUID: Unique uuid4 identifier of this team.""" - created_at = properties.Optional(properties.Datetime(), 'created_at') + created_at = properties.Optional(properties.Datetime(), "created_at") """int: Time the team was created, in seconds since epoch.""" - def __init__(self, - name: str, - *, - description: str = "", - session: Session | None = None): + def __init__(self, name: str, *, description: str = "", session: Session | None = None): self.name: str = name self.description: str = description self.session: Session = session def __str__(self): - return ''.format(self.name) + return f"" def _path(self): - return format_escaped_url('/teams/{team_id}', team_id=self.uid) + return format_escaped_url("/teams/{team_id}", team_id=self.uid) def list_members(self) -> list[TeamMember]: """ @@ -209,7 +203,7 @@ def get_member(self, user_id: str | UUID | User) -> TeamMember: """ if isinstance(user_id, User): user_id = user_id.uid - path = self._path() + format_escaped_url('/users/{user_id}', user_id=user_id) + path = self._path() + format_escaped_url("/users/{user_id}", user_id=user_id) member = self.session.get_resource(path=path, version=self._api_version)["user"] return TeamMember(user=User.build(member), team=self, actions=member["actions"]) @@ -245,14 +239,16 @@ def remove_user(self, user_id: str | UUID | User) -> bool: """ if isinstance(user_id, User): user_id = user_id.uid - self.session.checked_post(self._path() + "/users/batch-remove", - json={"ids": [str(user_id)]}, version=self._api_version) + self.session.checked_post( + self._path() + "/users/batch-remove", + json={"ids": [str(user_id)]}, + version=self._api_version, + ) return True # note: only get here if checked_post doesn't raise error - def add_user(self, - user_id: str | UUID | User, - *, - actions: list[TEAM_ACTIONS] | None = None) -> bool: + def add_user( + self, user_id: str | UUID | User, *, actions: list[TEAM_ACTIONS] | None = None + ) -> bool: """ Add a User to a Team. @@ -283,10 +279,9 @@ def add_user(self, actions = [READ] return self.update_user_action(user_id, actions=actions) - def update_user_action(self, - user_id: str | UUID | User, - *, - actions: list[TEAM_ACTIONS]) -> bool: + def update_user_action( + self, user_id: str | UUID | User, *, actions: list[TEAM_ACTIONS] + ) -> bool: """ Overwrites a User's action permissions in the Team. @@ -308,14 +303,14 @@ def update_user_action(self, """ if isinstance(user_id, User): user_id = user_id.uid - self.session.checked_put(self._path() + "/users", version=self._api_version, - json={'id': str(user_id), "actions": actions}) + self.session.checked_put( + self._path() + "/users", + version=self._api_version, + json={"id": str(user_id), "actions": actions}, + ) return True - def share(self, - *, - resource: Resource, - target_team_id: "str | UUID | Team") -> bool: + def share(self, *, resource: Resource, target_team_id: "str | UUID | Team") -> bool: """ Share a resource with another team. @@ -340,10 +335,11 @@ def share(self, payload = { "resource_type": resource_access["type"], "resource_id": resource_access["id"], - "target_team_id": str(target_team_id) + "target_team_id": str(target_team_id), } - self.session.checked_post(self._path() + "/shared-resources", - version=self._api_version, json=payload) + self.session.checked_post( + self._path() + "/shared-resources", version=self._api_version, json=payload + ) return True def un_share(self, *, resource: Resource, target_team_id: "str | UUID | Team") -> bool: @@ -372,7 +368,7 @@ def un_share(self, *, resource: Resource, target_team_id: "str | UUID | Team") - self.session.checked_delete( self._path() + f"/shared-resources/{resource_type}/{resource_id}", version=self._api_version, - json={"target_team_id": str(target_team_id)} + json={"target_team_id": str(target_team_id)}, ) return True @@ -387,10 +383,10 @@ def owned_dataset_ids(self) -> list[str]: """ query_params = {"userId": "", "domain": self._path(), "action": "WRITE"} - response = self.session.get_resource("/DATASET/authorized-ids", - params=query_params, - version="v3") - return response['ids'] + response = self.session.get_resource( + "/DATASET/authorized-ids", params=query_params, version="v3" + ) + return response["ids"] @property def projects(self) -> ProjectCollection: @@ -405,9 +401,9 @@ def analyses(self) -> AnalysisWorkflowCollection: @property def dataset_ids(self) -> TeamResourceIDs: """Return a TeamResourceIDs instance for listing published dataset IDs.""" - return TeamResourceIDs(session=self.session, - team_id=self.uid, - resource_type=ResourceTypeEnum.DATASET.value) + return TeamResourceIDs( + session=self.session, team_id=self.uid, resource_type=ResourceTypeEnum.DATASET.value + ) @property def datasets(self) -> DatasetCollection: @@ -417,23 +413,25 @@ def datasets(self) -> DatasetCollection: @property def module_ids(self) -> TeamResourceIDs: """Return a TeamResourceIDs instance for listing published module IDs.""" - return TeamResourceIDs(session=self.session, - team_id=self.uid, - resource_type=ResourceTypeEnum.MODULE.value) + return TeamResourceIDs( + session=self.session, team_id=self.uid, resource_type=ResourceTypeEnum.MODULE.value + ) @property def table_ids(self) -> TeamResourceIDs: """Return a TeamResourceIDs instance for listing published table IDs.""" - return TeamResourceIDs(session=self.session, - team_id=self.uid, - resource_type=ResourceTypeEnum.TABLE.value) + return TeamResourceIDs( + session=self.session, team_id=self.uid, resource_type=ResourceTypeEnum.TABLE.value + ) @property def table_definition_ids(self) -> TeamResourceIDs: """Return a TeamResourceIDs instance for listing published table definition IDs.""" - return TeamResourceIDs(session=self.session, - team_id=self.uid, - resource_type=ResourceTypeEnum.TABLE_DEFINITION.value) + return TeamResourceIDs( + session=self.session, + team_id=self.uid, + resource_type=ResourceTypeEnum.TABLE_DEFINITION.value, + ) @property def property_templates(self) -> PropertyTemplateCollection: @@ -458,9 +456,9 @@ def material_templates(self) -> MaterialTemplateCollection: @property def measurement_templates(self) -> MeasurementTemplateCollection: """Return a resource representing all measurement templates in this dataset.""" - return MeasurementTemplateCollection(team_id=self.uid, - dataset_id=None, - session=self.session) + return MeasurementTemplateCollection( + team_id=self.uid, dataset_id=None, session=self.session + ) @property def process_templates(self) -> ProcessTemplateCollection: @@ -512,11 +510,13 @@ def gemd(self) -> GEMDResourceCollection: """Return a resource representing all GEMD objects/templates in this dataset.""" return GEMDResourceCollection(team_id=self.uid, dataset_id=None, session=self.session) - def gemd_batch_delete(self, - id_list: list[LinkByUID | UUID | str | BaseEntity], - *, - timeout: float = 2 * 60, - polling_delay: float = 1.0) -> list[tuple[LinkByUID, ApiError]]: + def gemd_batch_delete( + self, + id_list: list[LinkByUID | UUID | str | BaseEntity], + *, + timeout: float = 2 * 60, + polling_delay: float = 1.0, + ) -> list[tuple[LinkByUID, ApiError]]: """ Remove a set of GEMD objects. @@ -550,12 +550,14 @@ def gemd_batch_delete(self, deleted. """ - return _async_gemd_batch_delete(id_list=id_list, - team_id=self.uid, - session=self.session, - dataset_id=None, - timeout=timeout, - polling_delay=polling_delay) + return _async_gemd_batch_delete( + id_list=id_list, + team_id=self.uid, + session=self.session, + dataset_id=None, + timeout=timeout, + polling_delay=polling_delay, + ) class TeamCollection(AdminCollection[Team]): @@ -569,9 +571,9 @@ class TeamCollection(AdminCollection[Team]): """ - _path_template = '/teams' - _individual_key = 'team' - _collection_key = 'teams' + _path_template = "/teams" + _individual_key = "team" + _collection_key = "teams" _resource = Team _api_version = "v3" diff --git a/src/citrine/resources/templates.py b/src/citrine/resources/templates.py index 4faf81223..4eaafc1ec 100644 --- a/src/citrine/resources/templates.py +++ b/src/citrine/resources/templates.py @@ -1,4 +1,5 @@ """Top-level class for all template objects and collections thereof.""" + from abc import ABC from typing import TypeVar diff --git a/src/citrine/resources/user.py b/src/citrine/resources/user.py index ffc6ae7cd..1ab70a8ef 100644 --- a/src/citrine/resources/user.py +++ b/src/citrine/resources/user.py @@ -6,7 +6,7 @@ from citrine._session import Session -class User(Resource['User']): +class User(Resource["User"]): """ A Citrine User. @@ -28,18 +28,13 @@ class User(Resource['User']): _resource_type = ResourceTypeEnum.USER _session: Session | None = None - uid = properties.Optional(properties.UUID, 'id') - screen_name = properties.String('screen_name') - position = properties.Optional(properties.String(), 'position') - email = properties.String('email') - is_admin = properties.Boolean('is_admin') - - def __init__(self, - *, - screen_name: str, - email: str, - position: str | None, - is_admin: bool): + uid = properties.Optional(properties.UUID, "id") + screen_name = properties.String("screen_name") + position = properties.Optional(properties.String(), "position") + email = properties.String("email") + is_admin = properties.Boolean("is_admin") + + def __init__(self, *, screen_name: str, email: str, position: str | None, is_admin: bool): self.email: str = email self.position: str | None = position self.screen_name: str = screen_name @@ -51,7 +46,7 @@ def is_internal(self) -> bool: return self.email.split("@")[-1] == "citrine.io" def __str__(self): - return ''.format(self.screen_name) + return f"" def get(self): """Retrieve a specific user from the database.""" @@ -61,9 +56,9 @@ def get(self): class UserCollection(AdminCollection[User]): """Represents the collection of all users.""" - _path_template = '/users' - _collection_key = 'users' - _individual_key = 'user' + _path_template = "/users" + _collection_key = "users" + _individual_key = "user" _resource = User def __init__(self, session: Session): @@ -71,7 +66,7 @@ def __init__(self, session: Session): def me(self): """Get information about the current user.""" - data = self.session.get_resource(self._path_template + '/me') + data = self.session.get_resource(self._path_template + "/me") return self.build(data) def build(self, data): @@ -93,15 +88,8 @@ def build(self, data): user._session = self.session return user - def register(self, - *, - screen_name: str, - email: str, - position: str, - is_admin: bool) -> User: + def register(self, *, screen_name: str, email: str, position: str, is_admin: bool) -> User: """Register a User.""" - return super().register(User( - screen_name=screen_name, - email=email, - position=position, - is_admin=is_admin)) + return super().register( + User(screen_name=screen_name, email=email, position=position, is_admin=is_admin) + ) diff --git a/src/citrine/seeding/find_or_create.py b/src/citrine/seeding/find_or_create.py index e548d8fd1..09e0b0dea 100644 --- a/src/citrine/seeding/find_or_create.py +++ b/src/citrine/seeding/find_or_create.py @@ -3,15 +3,15 @@ from logging import getLogger from typing import TypeVar +from citrine._rest.collection import Collection, CreationType from citrine.exceptions import NotFound from citrine.informatics.workflows.design_workflow import DesignWorkflow -from citrine.resources.dataset import DatasetCollection, Dataset -from citrine.resources.project import ProjectCollection, Project -from citrine.resources.team import TeamCollection, Team -from citrine._rest.collection import CreationType, Collection +from citrine.resources.dataset import Dataset, DatasetCollection +from citrine.resources.project import Project, ProjectCollection +from citrine.resources.team import Team, TeamCollection logger = getLogger(__name__) -T = TypeVar('T') +T = TypeVar("T") def find_collection(*, collection: Collection[T], name: str) -> T | None: @@ -25,12 +25,11 @@ def find_collection(*, collection: Collection[T], name: str) -> T | None: # try to use search if it is available # call list() to collapse the iterator, otherwise the NotFound # won't show up until collection_list is used - collection_list = list(collection.search(search_params={ - "name": { - "value": name, - "search_method": "EXACT" - } - })) + collection_list = list( + collection.search( + search_params={"name": {"value": name, "search_method": "EXACT"}} + ) + ) except (NotFound, NotImplementedError): # Search must not be available yet or any more collection_list = collection.list() @@ -39,19 +38,18 @@ def find_collection(*, collection: Collection[T], name: str) -> T | None: matching_resources = [resource for resource in collection_list if resource.name == name] if len(matching_resources) > 1: - raise ValueError("Found multiple collections with name '{}'".format(name)) + raise ValueError(f"Found multiple collections with name '{name}'") if len(matching_resources) == 1: result = matching_resources.pop() - logger.info('Found existing: {}'.format(result)) + logger.info(f"Found existing: {result}") return result else: return None -def get_by_name_or_create(*, - collection: Collection[T], - name: str, - default_provider: Callable[..., T]) -> T: +def get_by_name_or_create( + *, collection: Collection[T], name: str, default_provider: Callable[..., T] +) -> T: """ Tries to find a collection by its name (returns first hit). @@ -61,7 +59,7 @@ def get_by_name_or_create(*, if found: return found else: - logger.info('Failed to find resource with name {}, creating one instead.'.format(name)) + logger.info(f"Failed to find resource with name {name}, creating one instead.") return default_provider() @@ -75,21 +73,21 @@ def get_by_name_or_raise_error(*, collection: Collection[T], name: str) -> T: if found: return found else: - raise ValueError("Did not find resource with the given name: {}".format(name)) + raise ValueError(f"Did not find resource with the given name: {name}") -def find_or_create_project(*, - project_collection: ProjectCollection, - project_name: str, - raise_error: bool = False) -> Project: +def find_or_create_project( + *, project_collection: ProjectCollection, project_name: str, raise_error: bool = False +) -> Project: """ Tries to find a project by name (returns first hit). If not found, creates a new project with the given name """ if project_collection.team_id is None: - raise NotImplementedError("Collection must have a team ID, such as when retrieved with " - "find_or_create_team.") + 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) @@ -97,15 +95,14 @@ def find_or_create_project(*, project = get_by_name_or_create( collection=project_collection, name=project_name, - default_provider=lambda: project_collection.register(project_name) + default_provider=lambda: project_collection.register(project_name), ) return project -def find_or_create_team(*, - team_collection: TeamCollection, - team_name: str, - raise_error: bool = False) -> Team: +def find_or_create_team( + *, team_collection: TeamCollection, team_name: str, raise_error: bool = False +) -> Team: """ Tries to find a team by name (returns first hit). @@ -117,15 +114,14 @@ def find_or_create_team(*, team = get_by_name_or_create( collection=team_collection, name=team_name, - default_provider=lambda: team_collection.register(team_name) + default_provider=lambda: team_collection.register(team_name), ) return team -def find_or_create_dataset(*, - dataset_collection: DatasetCollection, - dataset_name: str, - raise_error: bool = False) -> Dataset: +def find_or_create_dataset( + *, dataset_collection: DatasetCollection, dataset_name: str, raise_error: bool = False +) -> Dataset: """ Tries to find a dataset by name (returns first hit). @@ -139,14 +135,14 @@ def find_or_create_dataset(*, name=dataset_name, default_provider=lambda: dataset_collection.register( Dataset(dataset_name, summary="seed summ.", description="seed desc.") - ) + ), ) return dataset -def create_or_update(*, - collection: Collection[CreationType], - resource: CreationType) -> CreationType: +def create_or_update( + *, collection: Collection[CreationType], resource: CreationType +) -> CreationType: """ Update a resource of a given name belonging to a collection. @@ -169,7 +165,7 @@ def create_or_update(*, """ old_resource = find_collection(collection=collection, name=resource.name) if old_resource: - logger.info("Updating module: {}".format(resource.name)) + logger.info(f"Updating module: {resource.name}") # Copy so that passed-in resource is unaffected new_resource = deepcopy(resource) new_resource.uid = old_resource.uid @@ -180,5 +176,5 @@ def create_or_update(*, new_resource.branch_version = old_resource.branch_version return collection.update(new_resource) else: - logger.info("Registering new module: {}".format(resource.name)) + logger.info(f"Registering new module: {resource.name}") return collection.register(resource) diff --git a/tests/_serialization/_data.py b/tests/_serialization/_data.py index 132231eb3..68347c584 100644 --- a/tests/_serialization/_data.py +++ b/tests/_serialization/_data.py @@ -1,18 +1,23 @@ import uuid + import arrow -from citrine._serialization import properties +from citrine._serialization import properties VALID_SERIALIZATIONS = [ (properties.Integer, 5, 5), (properties.Float, 3.0, 3.0), (properties.Raw, 1234, 1234), - (properties.String, 'foo', 'foo'), + (properties.String, "foo", "foo"), (properties.Boolean, True, True), (properties.Boolean, False, False), - (properties.UUID, uuid.UUID('284e6cec-dd05-4f8e-9a94-4abb298bde82'), '284e6cec-dd05-4f8e-9a94-4abb298bde82'), + ( + properties.UUID, + uuid.UUID("284e6cec-dd05-4f8e-9a94-4abb298bde82"), + "284e6cec-dd05-4f8e-9a94-4abb298bde82", + ), (properties.Datetime, arrow.get(269815509154).datetime, 269815509154), - (properties.Datetime, arrow.get('2019-07-19T10:46:08+00:00').datetime, 1563533168000), + (properties.Datetime, arrow.get("2019-07-19T10:46:08+00:00").datetime, 1563533168000), ] @@ -21,13 +26,14 @@ (properties.Float, object()), (properties.String, 1), (properties.Boolean, 3), - (properties.Boolean, 'False'), - (properties.UUID, '284e6cec'), + (properties.Boolean, "False"), + (properties.UUID, "284e6cec"), ] class DummyProperty(properties.Property): """This is a concrete sublcass that does not overwrite __str__ for base Property testing""" + @property def underlying_types(self): return None @@ -44,12 +50,12 @@ def _deserialize(self, value): VALID_STRINGS = [ - (DummyProperty, 'hi', ""), - (properties.Raw, 'hi', ""), - (properties.Integer, 'foo', ""), - (properties.Float, 'bar', ""), - (properties.String, 'foobar', ""), - (properties.Boolean, 'what', ""), + (DummyProperty, "hi", ""), + (properties.Raw, "hi", ""), + (properties.Integer, "foo", ""), + (properties.Float, "bar", ""), + (properties.String, "foobar", ""), + (properties.Boolean, "what", ""), ] INVALID_INSTANCES = [ @@ -57,35 +63,35 @@ def _deserialize(self, value): (properties.Integer, "1"), (properties.Integer, complex(1, 2)), (properties.Integer, True), - (properties.Integer, 'asdf'), + (properties.Integer, "asdf"), (properties.Float, complex(1, 2)), (properties.Float, True), - (properties.Float, 'asdf'), + (properties.Float, "asdf"), (properties.String, 1), (properties.String, dict()), (properties.Boolean, 1), (properties.Boolean, 1.0), - (properties.Boolean, 'asdf'), + (properties.Boolean, "asdf"), (properties.UUID, str(uuid.uuid4())), # string(uuid) != uuid (properties.UUID, 1.0), - (properties.Datetime, '2019-07-19T10:46:08.949682+00:00'), # str(datetime) != datetime - (properties.LinkOrElse, object()) + (properties.Datetime, "2019-07-19T10:46:08.949682+00:00"), # str(datetime) != datetime + (properties.LinkOrElse, object()), ] INVALID_SERIALIZED_INSTANCES = [ - (properties.Integer, '1.0'), + (properties.Integer, "1.0"), (properties.Integer, str(complex(1, 2))), (properties.Integer, True), - (properties.Integer, 'asdf'), + (properties.Integer, "asdf"), (properties.Integer, 14.4), - (properties.Float, str(complex(1,2))), + (properties.Float, str(complex(1, 2))), (properties.Float, True), - (properties.Float, 'asdf'), + (properties.Float, "asdf"), (properties.String, 1), (properties.String, dict()), (properties.Boolean, 1), (properties.Boolean, 1.0), - (properties.Boolean, 'asdf'), - (properties.UUID, 'wrong-number-of-chars'), - (properties.Datetime, '2019-07-19T35:46:08.949682+99:99'), # nonsense time + (properties.Boolean, "asdf"), + (properties.UUID, "wrong-number-of-chars"), + (properties.Datetime, "2019-07-19T35:46:08.949682+99:99"), # nonsense time ] diff --git a/tests/_serialization/_utils.py b/tests/_serialization/_utils.py index d8fe3142f..fcf78d963 100644 --- a/tests/_serialization/_utils.py +++ b/tests/_serialization/_utils.py @@ -1,15 +1,21 @@ -from typing import Type, Any, Optional +from typing import Any from citrine._serialization import properties -def make_class_with_property(prop_type: Type[properties.Property], field_name: str, field_path: Optional[str] = None): +def make_class_with_property( + prop_type: type[properties.Property], field_name: str, field_path: str | None = None +): class SampleObject: def __init__(self, field_value: Any): setattr(self, field_name, field_value) def __eq__(self, other): return getattr(self, field_name) == getattr(other, field_name) - setattr(SampleObject, field_name, - prop_type(serialization_path=field_name if field_path is None else field_path)) + + setattr( + SampleObject, + field_name, + prop_type(serialization_path=field_name if field_path is None else field_path), + ) return SampleObject diff --git a/tests/_serialization/test_container_properties.py b/tests/_serialization/test_container_properties.py index 660def198..a2e567b13 100644 --- a/tests/_serialization/test_container_properties.py +++ b/tests/_serialization/test_container_properties.py @@ -1,14 +1,15 @@ -import pytest import unittest +import pytest +from gemd.entity.link_by_uid import LinkByUID + from citrine._serialization import properties from ._data import VALID_SERIALIZATIONS from ._utils import make_class_with_property -from gemd.entity.link_by_uid import LinkByUID -@pytest.mark.parametrize('sub_prop,sub_value,sub_serialized', VALID_SERIALIZATIONS) +@pytest.mark.parametrize("sub_prop,sub_value,sub_serialized", VALID_SERIALIZATIONS) def test_list_property_serde(sub_prop, sub_value, sub_serialized): prop = properties.List(sub_prop) value = [sub_value for _ in range(5)] @@ -17,17 +18,17 @@ def test_list_property_serde(sub_prop, sub_value, sub_serialized): assert prop.serialize(value) == serialized -@pytest.mark.parametrize('sub_prop,sub_value,sub_serialized', VALID_SERIALIZATIONS) +@pytest.mark.parametrize("sub_prop,sub_value,sub_serialized", VALID_SERIALIZATIONS) def test_object_property_serde(sub_prop, sub_value, sub_serialized): - klass = make_class_with_property(sub_prop, 'some_property_name') + klass = make_class_with_property(sub_prop, "some_property_name") prop = properties.Object(klass) instance = klass(sub_value) - serialized = {'some_property_name': sub_serialized} + serialized = {"some_property_name": sub_serialized} assert prop.deserialize(serialized) == instance assert prop.serialize(instance) == serialized -@pytest.mark.parametrize('sub_prop,sub_value,sub_serialized', VALID_SERIALIZATIONS) +@pytest.mark.parametrize("sub_prop,sub_value,sub_serialized", VALID_SERIALIZATIONS) def test_optional_property(sub_prop, sub_value, sub_serialized): prop = properties.Optional(sub_prop) assert prop.deserialize(sub_serialized) == sub_value @@ -36,9 +37,11 @@ def test_optional_property(sub_prop, sub_value, sub_serialized): assert prop.serialize(None) is None -@pytest.mark.parametrize('key_type,key_value,key_serialized', VALID_SERIALIZATIONS) -@pytest.mark.parametrize('value_type,value_value,value_serialized', VALID_SERIALIZATIONS) -def test_mapping_property(key_type, value_type, key_value, value_value, key_serialized, value_serialized): +@pytest.mark.parametrize("key_type,key_value,key_serialized", VALID_SERIALIZATIONS) +@pytest.mark.parametrize("value_type,value_value,value_serialized", VALID_SERIALIZATIONS) +def test_mapping_property( + key_type, value_type, key_value, value_value, key_serialized, value_serialized +): prop = properties.Mapping(key_type, value_type) value = {key_value: value_value} serialized = {key_serialized: value_serialized} @@ -46,31 +49,37 @@ def test_mapping_property(key_type, value_type, key_value, value_value, key_seri assert prop.serialize(value) == serialized -@pytest.mark.parametrize('key_type,key_value,key_serialized', VALID_SERIALIZATIONS) -@pytest.mark.parametrize('value_type,value_value,value_serialized', VALID_SERIALIZATIONS) -def test_mapping_property_list_of_pairs(key_type, value_type, key_value, value_value, key_serialized, value_serialized): - prop = properties.Mapping(key_type, value_type, ser_as_list_of_pairs = True) +@pytest.mark.parametrize("key_type,key_value,key_serialized", VALID_SERIALIZATIONS) +@pytest.mark.parametrize("value_type,value_value,value_serialized", VALID_SERIALIZATIONS) +def test_mapping_property_list_of_pairs( + key_type, value_type, key_value, value_value, key_serialized, value_serialized +): + prop = properties.Mapping(key_type, value_type, ser_as_list_of_pairs=True) value = {key_value: value_value} - serialized = [(key_serialized, value_serialized),] + serialized = [(key_serialized, value_serialized)] assert prop.deserialize(serialized) == value unittest.TestCase().assertCountEqual(prop.serialize(value), serialized) def test_mapping_property_list_of_pairs_multiple(): - prop = properties.Mapping(properties.String, properties.Integer, ser_as_list_of_pairs = True) - value = {'foo': 1, 'bar': 2} - serialized = [('foo', 1), ('bar', 2)] + prop = properties.Mapping(properties.String, properties.Integer, ser_as_list_of_pairs=True) + value = {"foo": 1, "bar": 2} + serialized = [("foo", 1), ("bar", 2)] assert prop.deserialize(serialized) == value unittest.TestCase().assertCountEqual(prop.serialize(value), serialized) -class DummyDescriptor(object): +class DummyDescriptor: dummy_map = properties.Mapping(properties.Float(), properties.String, "dummy_map") dummy_list = properties.List(properties.Float, "dummy_list") dummy_set = properties.Set(type(properties.Float()), "dummy_map") link_or_else = properties.LinkOrElse(serialization_path="link_or_else") - map_collection_key = properties.Mapping(properties.Optional(properties.String), properties.Integer, "map_collection_key") - specified_mixed_list = properties.SpecifiedMixedList([properties.Integer(default=100)], "specified_mixed_list") + map_collection_key = properties.Mapping( + properties.Optional(properties.String), properties.Integer, "map_collection_key" + ) + specified_mixed_list = properties.SpecifiedMixedList( + [properties.Integer(default=100)], "specified_mixed_list" + ) def test_collection_setters(): @@ -78,8 +87,8 @@ def test_collection_setters(): dummy_descriptor.dummy_map = {1: "1"} dummy_descriptor.dummy_set = {1} dummy_descriptor.dummy_list = [1, 2] - dummy_descriptor.map_collection_key = {'foo': 1, 'bar': 2} - dummy_descriptor.link_or_else = {'type': LinkByUID.typ, "scope": "templates", "id": "density"} + dummy_descriptor.map_collection_key = {"foo": 1, "bar": 2} + dummy_descriptor.link_or_else = {"type": LinkByUID.typ, "scope": "templates", "id": "density"} dummy_descriptor.specified_mixed_list = [1] assert 1 in dummy_descriptor.specified_mixed_list @@ -92,6 +101,6 @@ def test_collection_setters(): dummy_descriptor.specified_mixed_list = [1, 2] assert 1.0 in dummy_descriptor.dummy_map - assert 'foo' in dummy_descriptor.map_collection_key + assert "foo" in dummy_descriptor.map_collection_key assert 1.0 in dummy_descriptor.dummy_set assert 1.0 in dummy_descriptor.dummy_list diff --git a/tests/_serialization/test_object_serialization.py b/tests/_serialization/test_object_serialization.py index 638e75a66..054093324 100644 --- a/tests/_serialization/test_object_serialization.py +++ b/tests/_serialization/test_object_serialization.py @@ -1,23 +1,26 @@ -import pytest from typing import Any -from citrine._serialization.serializable import Serializable -from citrine._serialization.properties import String, Object, Optional +import pytest from gemd.entity.value.base_value import BaseValue from gemd.entity.value.nominal_real import NominalReal +from citrine._serialization.properties import Object, Optional, String +from citrine._serialization.serializable import Serializable + class UnserializableClass: """A dummy class that has no clear serialization or deserialization method.""" + def __init__(self, foo): self.foo = foo class SampleClass(Serializable): """A class to stress the deser scheme's ability to handle objects.""" - prop_string = String('prop_string.string', default='default') - prop_value = Object(BaseValue, 'prop_value') - prop_object = Optional(Object(UnserializableClass), 'prop_object') + + prop_string = String("prop_string.string", default="default") + prop_value = Object(BaseValue, "prop_value") + prop_object = Optional(Object(UnserializableClass), "prop_object") def __init__(self, prop_string: str, prop_value: BaseValue, prop_object: Any = None): self.prop_string = prop_string @@ -27,7 +30,7 @@ def __init__(self, prop_string: str, prop_value: BaseValue, prop_object: Any = N def test_gemd_object_serde(): """Test that an unspecified gemd object can be serialized and deserialized.""" - good_obj = SampleClass("Can be serialized", NominalReal(17, '')) + good_obj = SampleClass("Can be serialized", NominalReal(17, "")) copy = SampleClass.build(good_obj.dump()) assert copy.prop_value == good_obj.prop_value assert copy.prop_string == good_obj.prop_string @@ -35,37 +38,38 @@ def test_gemd_object_serde(): def test_default_nested_serde(): """Test that defaults work in nested dictionaries.""" - good_obj = SampleClass("Can be serialized", NominalReal(17, '')) + good_obj = SampleClass("Can be serialized", NominalReal(17, "")) data = good_obj.dump() # If 'prop_string.string' is a non-string, that's an error - data['prop_string']['string'] = 0 + data["prop_string"]["string"] = 0 with pytest.raises(ValueError): SampleClass.build(data) # If data['prop_string'] is an empty dictionary, then the default is used - data['prop_string'] = dict() - assert SampleClass.build(data).prop_string == 'default' + data["prop_string"] = dict() + assert SampleClass.build(data).prop_string == "default" # If `data` does not even have a 'prop_string' key, then the default is used - del data['prop_string'] - assert SampleClass.build(data).prop_string == 'default' + del data["prop_string"] + assert SampleClass.build(data).prop_string == "default" def test_bad_object_serde(): """Test that a 'mystery' object cannot be serialized.""" - bad_obj = SampleClass("Cannot be serialized", NominalReal(34, ''), UnserializableClass(1)) + bad_obj = SampleClass("Cannot be serialized", NominalReal(34, ""), UnserializableClass(1)) with pytest.raises(AttributeError): bad_obj.dump() def test_object_str_representation(): - assert "" == str(Object(NominalReal, 'foo')) + assert "" == str(Object(NominalReal, "foo")) def test_override_configurations(): """Check that weird override cases get caught.""" - class OverrideTestClass(Serializable['OverrideTestClass']): + + class OverrideTestClass(Serializable["OverrideTestClass"]): overridden_value = String("overridden_value", override=True) overridden_option = Optional(String(), "overridden_option", override=True) @@ -98,7 +102,7 @@ def initable(self): def required(self): return self._required - class OverrideTestClass(Serializable['OverrideTestClass'], BaseTestClass): + class OverrideTestClass(Serializable["OverrideTestClass"], BaseTestClass): no_key = Optional(String(), "no_key", override=True) initable = Optional(String(), "initable", override=True, use_init=True) required = String("required", override=True, use_init=True) @@ -135,11 +139,11 @@ def required(self, value): raise TypeError("magic_value") self._required = value - class BadClass(Serializable['BadClass'], TestClass): + class BadClass(Serializable["BadClass"], TestClass): required = String("required", override=True) optional = Optional(String(), "optional", use_init=True) - class GoodClass(Serializable['BadClass'], TestClass): + class GoodClass(Serializable["BadClass"], TestClass): required = Optional(String(), "required", override=True, use_init=True) optional = Optional(String(), "optional", use_init=True) diff --git a/tests/_serialization/test_simple_properties.py b/tests/_serialization/test_simple_properties.py index a5d916642..9b9c85304 100644 --- a/tests/_serialization/test_simple_properties.py +++ b/tests/_serialization/test_simple_properties.py @@ -2,95 +2,100 @@ import arrow import pytest -from gemd.enumeration.base_enumeration import BaseEnumeration +from gemd.entity.attribute.condition import Condition from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object import ProcessSpec -from gemd.entity.attribute.condition import Condition +from gemd.enumeration.base_enumeration import BaseEnumeration from citrine._serialization.properties import ( + UUID, Datetime, Enumeration, Float, Integer, LinkOrElse, - Set, - SpecifiedMixedList, Object, Optional, + Set, + SpecifiedMixedList, String, Union, - UUID ) -from citrine.informatics.predictor_evaluation_metrics import PredictorEvaluationMetric, RMSE, CoverageProbability +from citrine.informatics.predictor_evaluation_metrics import ( + RMSE, + CoverageProbability, + PredictorEvaluationMetric, +) from citrine.resources.dataset import Dataset + from ._data import ( - VALID_SERIALIZATIONS, - VALID_STRINGS, INVALID_DESERIALIZATION_TYPES, INVALID_INSTANCES, INVALID_SERIALIZED_INSTANCES, + VALID_SERIALIZATIONS, + VALID_STRINGS, ) -@pytest.mark.parametrize('prop_type,value,serialized', VALID_SERIALIZATIONS) +@pytest.mark.parametrize("prop_type,value,serialized", VALID_SERIALIZATIONS) def test_simple_property_serde(prop_type, value, serialized): prop = prop_type() assert prop.deserialize(serialized) == value assert prop.serialize(value) == serialized -@pytest.mark.parametrize('prop_type,value', INVALID_INSTANCES) +@pytest.mark.parametrize("prop_type,value", INVALID_INSTANCES) def test_invalid_property_serialization(prop_type, value): prop = prop_type() with pytest.raises(Exception): prop.serialize(value) -@pytest.mark.parametrize('prop_type,serialized', INVALID_SERIALIZED_INSTANCES) +@pytest.mark.parametrize("prop_type,serialized", INVALID_SERIALIZED_INSTANCES) def test_invalid_property_deserialization(prop_type, serialized): prop = prop_type() with pytest.raises(Exception): prop.deserialize(serialized) -@pytest.mark.parametrize('prop_type,serialized', INVALID_DESERIALIZATION_TYPES) +@pytest.mark.parametrize("prop_type,serialized", INVALID_DESERIALIZATION_TYPES) def test_invalid_deserialization_type(prop_type, serialized): prop = prop_type() with pytest.raises(ValueError): prop.deserialize(serialized) -@pytest.mark.parametrize('prop_type,serialized', INVALID_DESERIALIZATION_TYPES) +@pytest.mark.parametrize("prop_type,serialized", INVALID_DESERIALIZATION_TYPES) def test_invalid_deserialization_type_with_base_class(prop_type, serialized): class BaseTest: pass prop = prop_type() - prop.serialization_path = 'ser_path' + prop.serialization_path = "ser_path" with pytest.raises(ValueError) as excinfo: prop.deserialize(serialized, base_class=BaseTest().__class__) # Check that the exception includes the calling class name and argument if not isinstance(prop, UUID): - assert 'BaseTest:ser_path' in str(excinfo.value) + assert "BaseTest:ser_path" in str(excinfo.value) -@pytest.mark.parametrize('prop_type,serialized', INVALID_DESERIALIZATION_TYPES) +@pytest.mark.parametrize("prop_type,serialized", INVALID_DESERIALIZATION_TYPES) def test_invalid_deserialization_type_with_dataset(prop_type, serialized): # Supplying a Daatset instance as the base_class should include it's # name in the exception value string (UUIDs are a special case) dset = Dataset(name="dset", summary="test dataset", description="description") prop = prop_type() - prop.serialization_path = 'ser_path' + prop.serialization_path = "ser_path" with pytest.raises(ValueError) as excinfo: prop.deserialize(serialized, base_class=dset.__class__) if not isinstance(prop, UUID): - assert 'Dataset:ser_path' in str(excinfo.value) + assert "Dataset:ser_path" in str(excinfo.value) -@pytest.mark.parametrize('prop_type,path,expected', VALID_STRINGS) +@pytest.mark.parametrize("prop_type,path,expected", VALID_STRINGS) def test_valid_property_deserialization(prop_type, path, expected): assert expected == str(prop_type(path)) @@ -101,19 +106,19 @@ def test_serialize_to_dict_error(): def test_valid_serialize_to_dict(): - assert {'my_foo': 100} == Integer('my_foo').serialize_to_dict({}, 100) + assert {"my_foo": 100} == Integer("my_foo").serialize_to_dict({}, 100) def test_serialize_dot_value_to_dict(): - assert {'my': {'foo': 100}} == Integer('my.foo').serialize_to_dict({}, 100) + assert {"my": {"foo": 100}} == Integer("my.foo").serialize_to_dict({}, 100) def test_set_int_property_from_string(): class Foo: - bar = Integer('bar') + bar = Integer("bar") f = Foo() - f.bar = '12' + f.bar = "12" assert 12 == f.bar @@ -134,7 +139,9 @@ def test_float_cannot_deserialize_bool(): def test_deserialize_string_datetime(): - assert arrow.get('2019-07-19T10:46:08+00:00').datetime == Datetime().deserialize('2019-07-19T10:46:08+00:00') + assert arrow.get("2019-07-19T10:46:08+00:00").datetime == Datetime().deserialize( + "2019-07-19T10:46:08+00:00" + ) def test_datetime_cannot_deserialize_float(): @@ -149,14 +156,14 @@ def test_mixed_list_requires_property_list(): def test_deserialize_mixed_list(): ml = SpecifiedMixedList([Integer, String]) - assert [1, '2'] == ml.deserialize([1, '2']) + assert [1, "2"] == ml.deserialize([1, "2"]) assert [1, None] == ml.deserialize([1]) def test_mixed_list_cannot_deserialize_larger_lists(): ml = SpecifiedMixedList([Integer]) with pytest.raises(ValueError): - ml.deserialize([1, '2']) + ml.deserialize([1, "2"]) with pytest.raises(ValueError): ml.deserialize([1, 2]) @@ -164,7 +171,7 @@ def test_mixed_list_cannot_deserialize_larger_lists(): def test_mixed_list_cannot_serialize_larger_lists(): ml = SpecifiedMixedList([Integer]) with pytest.raises(ValueError): - ml.serialize([1, '2']) + ml.serialize([1, "2"]) with pytest.raises(ValueError): ml.serialize([1, 2]) @@ -231,7 +238,7 @@ class Foo: obj = Object(Foo) with pytest.raises(AttributeError): - obj.deserialize({'key': 'value'}) + obj.deserialize({"key": "value"}) def test_linkorelse_deserialize_requires_serializable(): @@ -243,28 +250,30 @@ def test_linkorelse_deserialize_requires_serializable(): def test_linkorelse_deserialize_requires_scope_and_id(): loe = LinkOrElse() with pytest.raises(ValueError, match=r"missing.+required"): - loe.deserialize({'type': LinkByUID.typ}) + loe.deserialize({"type": LinkByUID.typ}) def test_linkorelse_raises_deep_errors(): loe = LinkOrElse() with pytest.raises(TypeError): - loe.deserialize({ - 'type': ProcessSpec.typ, - 'name': 'Badly structured', - 'conditions': [{'type': Condition.typ, "value": 'invalid structure'}], - }) + loe.deserialize( + { + "type": ProcessSpec.typ, + "name": "Badly structured", + "conditions": [{"type": Condition.typ, "value": "invalid structure"}], + } + ) def test_linkorelse_deserialize(): loe = LinkOrElse() - lbu = loe.deserialize({'type': LinkByUID.typ, 'scope': 'foo', 'id': str(uuid.uuid4())}) + lbu = loe.deserialize({"type": LinkByUID.typ, "scope": "foo", "id": str(uuid.uuid4())}) assert isinstance(lbu, LinkByUID) def test_optional_repr(): opt = Optional(String) - assert '] None>' == str(opt) + assert "] None>" == str(opt) def test_set_serialize_sortable(): diff --git a/tests/_serialization/test_taurus_interop.py b/tests/_serialization/test_taurus_interop.py index bb5e02d0d..26237a79d 100644 --- a/tests/_serialization/test_taurus_interop.py +++ b/tests/_serialization/test_taurus_interop.py @@ -1,12 +1,11 @@ import pytest +from gemd.entity.bounds.categorical_bounds import CategoricalBounds +from gemd.util import flatten from citrine.resources.condition_template import ConditionTemplate from citrine.resources.process_spec import ProcessSpec from citrine.resources.process_template import ProcessTemplate -from gemd.entity.bounds.categorical_bounds import CategoricalBounds -from gemd.util import flatten - def test_flatten(): """Test that gemd utility methods can be applied to citrine-python objects. @@ -16,12 +15,11 @@ def test_flatten(): bounds = CategoricalBounds(categories=["foo", "bar"]) template = ProcessTemplate( - "spam", - conditions=[(ConditionTemplate(name="eggs", bounds=bounds), bounds)] + "spam", conditions=[(ConditionTemplate(name="eggs", bounds=bounds), bounds)] ) spec = ProcessSpec(name="spec", template=template) - flat = flatten(spec, scope='testing') + flat = flatten(spec, scope="testing") assert len(flat) == 3, "Expected 3 flattened objects" diff --git a/tests/_util/source_mod.py b/tests/_util/source_mod.py index 3651138b7..aaecd1781 100644 --- a/tests/_util/source_mod.py +++ b/tests/_util/source_mod.py @@ -1,2 +1,2 @@ -class ExampleClass(): - pass \ No newline at end of file +class ExampleClass: + pass diff --git a/tests/_util/test_batcher.py b/tests/_util/test_batcher.py index 1018bd2c0..c53e705b6 100644 --- a/tests/_util/test_batcher.py +++ b/tests/_util/test_batcher.py @@ -1,13 +1,12 @@ import pytest - -from citrine._utils.batcher import Batcher - from gemd.demo.cake import make_cake from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object import * from gemd.entity.template import * from gemd.util import flatten, writable_sort_order +from citrine._utils.batcher import Batcher + def test_by_type(): """Test type batching.""" @@ -16,8 +15,9 @@ def test_by_type(): first = batcher.batch(flatten(cake), batch_size=10) assert all(len(batch) <= 10 for batch in first), "A batch was too long" for i in range(len(first) - 1): - assert max(writable_sort_order(x) for x in first[i]) \ - <= min(writable_sort_order(x) for x in first[i+1]), "Load order violated" + assert max(writable_sort_order(x) for x in first[i]) <= min( + writable_sort_order(x) for x in first[i + 1] + ), "Load order violated" assert len(flatten(cake)) == len({y for x in first for y in x}), "Object missing" assert len(flatten(cake)) == len([y for x in first for y in x]), "Object repeated" @@ -30,7 +30,7 @@ def test_by_type(): with pytest.raises(ValueError): bad = [ ProcessSpec(name="One", uids={"bad": "id"}), - ProcessSpec(name="Two", uids={"bad": "id"}) + ProcessSpec(name="Two", uids={"bad": "id"}), ] batcher.batch(bad, batch_size=10) @@ -77,9 +77,9 @@ def test_by_dependency(): elif isinstance(obj, ProcessRun): assert obj.spec in derefs, "Spec wasn't in batch" for x in obj.parameters: - assert(x.template in derefs), "Referenced parameter wasn't in batch" + assert x.template in derefs, "Referenced parameter wasn't in batch" for x in obj.conditions: - assert(x.template in derefs), "Referenced condition wasn't in batch" + assert x.template in derefs, "Referenced condition wasn't in batch" elif isinstance(obj, MaterialSpec): assert obj.template in derefs, "Template wasn't in batch" assert obj.process in derefs, "Process wasn't in batch" diff --git a/tests/_util/test_functions.py b/tests/_util/test_functions.py index 803c99484..0fba51f9b 100644 --- a/tests/_util/test_functions.py +++ b/tests/_util/test_functions.py @@ -1,68 +1,72 @@ -from pathlib import Path -import pytest import uuid import warnings +from pathlib import Path +from urllib.parse import urlparse +import pytest +from gemd.entity.attribute.property import Property from gemd.entity.bounds.real_bounds import RealBounds 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, migrate_deprecated_argument, format_escaped_url, \ - MigratedClassMeta, generate_shared_meta -from gemd.entity.attribute.property import Property +from citrine._utils.functions import ( + MigratedClassMeta, + format_escaped_url, + generate_shared_meta, + get_object_id, + migrate_deprecated_argument, + object_to_link_by_uid, + rewrite_s3_links_locally, + validate_type, + write_file_locally, +) from citrine.resources.condition_template import ConditionTemplate def test_get_object_id_from_base_attribute(): with pytest.raises(ValueError): - get_object_id(Property('some property')) + get_object_id(Property("some property")) def test_get_object_id_from_data_concepts(): uid = str(uuid.uuid4()) - template = ConditionTemplate( - name='test', - bounds=RealBounds(0.0, 1.0, ''), - uids={'id': uid} - ) + template = ConditionTemplate(name="test", bounds=RealBounds(0.0, 1.0, ""), uids={"id": uid}) assert uid == get_object_id(template) def test_get_object_id_from_data_concepts_id_is_none(): - template = ConditionTemplate( - name='test', - bounds=RealBounds(0.0, 1.0, '') - ) + template = ConditionTemplate(name="test", bounds=RealBounds(0.0, 1.0, "")) with pytest.raises(ValueError): - template.uids = {'id': None} + template.uids = {"id": None} def test_get_object_id_link_by_uid_bad_scope(): with pytest.raises(ValueError): - get_object_id(LinkByUID('bad_scope', '123')) + get_object_id(LinkByUID("bad_scope", "123")) def test_get_object_id_wrong_type(): with pytest.raises(TypeError): - get_object_id('no id here') + get_object_id("no id here") def test_validate_type_wrong_type(): - with pytest.raises(Exception): - validate_type({'type': 'int'}, 'foo') + with pytest.raises(ValueError): + validate_type({"type": "int"}, "foo") def test_validate_type_set_type(): - assert {'type': 'int'} == validate_type({}, 'int') + assert {"type": "int"} == validate_type({}, "int") def test_object_to_link_by_uid_missing_uids(): - assert {'foo': 'bar'} == object_to_link_by_uid({'foo': 'bar'}) + assert {"foo": "bar"} == object_to_link_by_uid({"foo": "bar"}) def test_rewrite_s3_links_locally(): - assert "http://localhost:9566" == rewrite_s3_links_locally("http://localstack:4566", "http://localhost:9566") + assert "http://localhost:9566" == rewrite_s3_links_locally( + "http://localstack:4566", "http://localhost:9566" + ) def test_write_file_locally(tmpdir): @@ -101,16 +105,19 @@ def test_migrated_class(): with warnings.catch_warnings(): warnings.simplefilter("error") - class MigratedProperty(Property, - deprecated_in="1.2.3", - removed_in="2.0.0", - metaclass=generate_shared_meta(Property)): + class MigratedProperty( + Property, + deprecated_in="1.2.3", + removed_in="2.0.0", + metaclass=generate_shared_meta(Property), + ): pass with pytest.deprecated_call(): MigratedProperty(name="I'm a property!") with pytest.deprecated_call(): + class DerivedProperty(MigratedProperty): pass @@ -132,13 +139,13 @@ class IndependentProperty(Property): assert isinstance(Property("Property Name"), MigratedProperty) with pytest.raises(TypeError, match="deprecated_in"): + class NoVersionInfo(Property, metaclass=generate_shared_meta(Property)): pass with pytest.raises(TypeError, match="precisely"): - class NoParent(deprecated_in="1.2.3", - removed_in="2.0.0", - metaclass=MigratedClassMeta): + + class NoParent(deprecated_in="1.2.3", removed_in="2.0.0", metaclass=MigratedClassMeta): pass assert generate_shared_meta(dict) is MigratedClassMeta @@ -156,10 +163,9 @@ def test_recursive_subtype_recovery(): class Simple(abc.ABC): pass - class MigratedProperty(Simple, - deprecated_in="1.2.3", - removed_in="2.0.0", - metaclass=MigratedClassMeta): + class MigratedProperty( + Simple, deprecated_in="1.2.3", removed_in="2.0.0", metaclass=MigratedClassMeta + ): pass assert not issubclass(dict, Simple) @@ -170,10 +176,9 @@ def test_migrate_deprecated_argument(): # ValueError if neither argument is specified migrate_deprecated_argument(None, "new name", None, "old name") - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - # ValueError if both arguments are specified - migrate_deprecated_argument("something", "new name", "something else", "old name") + with pytest.warns(DeprecationWarning), pytest.raises(ValueError): + # ValueError if both arguments are specified + migrate_deprecated_argument("something", "new name", "something else", "old name") # Return the value if the new argument is specified assert migrate_deprecated_argument(14, "new name", None, "old name") == 14 @@ -181,15 +186,19 @@ def test_migrate_deprecated_argument(): with pytest.warns(DeprecationWarning) as caught: # If the old argument is specified, return the value and throw a deprecation warning assert migrate_deprecated_argument(None, "new name", 15, "old name") == 15 - msg = str(caught[0].message) - assert "old name" in msg and "new name" in msg + msg = str(caught[0].message) + assert "old name" in msg and "new name" in msg def test_format_escaped_url(): - url = format_escaped_url('http://base.com/{}/{}/{word1}/{word2}', 1, '&', word1='fine', word2='+/?#') - assert 'http://base.com/' in url - assert 'fine' in url - assert '1' in url - for c in '&' + '+?#': + url = format_escaped_url( + "http://base.com/{}/{}/{word1}/{word2}", 1, "&", word1="fine", word2="+/?#" + ) + parsed = urlparse(url) + assert parsed.scheme == "http" + assert parsed.netloc == "base.com" + assert "fine" in url + assert "1" in url + for c in "&" + "+?#": assert c not in url - assert 6 == sum(c == '/' for c in url) + assert 6 == sum(c == "/" for c in url) diff --git a/tests/_util/test_replace_object_with_link.py b/tests/_util/test_replace_object_with_link.py index 09f34059e..89a91fc4c 100644 --- a/tests/_util/test_replace_object_with_link.py +++ b/tests/_util/test_replace_object_with_link.py @@ -1,54 +1,45 @@ """Tests of the functions that replace objects with Links.""" + from citrine._utils.functions import replace_objects_with_links def test_simple_replacement(): """A top-level object should turn into a link-by-uid.""" - json = dict( - key='value', - object=dict( - type='material_run', - uids={'my_id': '1', 'id': '17'} - ) - ) + json = dict(key="value", object=dict(type="material_run", uids={"my_id": "1", "id": "17"})) replaced_json = replace_objects_with_links(json) - assert replaced_json == {'key': 'value', - 'object': {'type': 'link_by_uid', 'scope': 'id', 'id': '17'}} + assert replaced_json == { + "key": "value", + "object": {"type": "link_by_uid", "scope": "id", "id": "17"}, + } def test_nested_replacement(): """A list of objects should turn into a list of link-by-uids.""" json = dict( - object=[dict(type='material_run', uids={'my_id': '1'}), - dict(type='material_run', uids={'my_id': '2'})] + object=[ + dict(type="material_run", uids={"my_id": "1"}), + dict(type="material_run", uids={"my_id": "2"}), + ] ) replaced_json = replace_objects_with_links(json) - assert replaced_json == {'object': [{'type': 'link_by_uid', 'scope': 'my_id', 'id': '1'}, - {'type': 'link_by_uid', 'scope': 'my_id', 'id': '2'}]} + assert replaced_json == { + "object": [ + {"type": "link_by_uid", "scope": "my_id", "id": "1"}, + {"type": "link_by_uid", "scope": "my_id", "id": "2"}, + ] + } def test_failed_replacement(): """An object that does not have a type and a uids dictionary should not be replaced.""" - json = dict(object=dict( - some_field='material_run', - uids={'my_id': '1', 'id': '17'} - )) + json = dict(object=dict(some_field="material_run", uids={"my_id": "1", "id": "17"})) assert json == replace_objects_with_links(json) # no type field - json = dict(object=dict( - type='material_run', - uids='a uid string' - )) + json = dict(object=dict(type="material_run", uids="a uid string")) assert json == replace_objects_with_links(json) # uids is not a dictionary - json = dict(object=dict( - type='material_run', - some_field={'my_id': '1', 'id': '17'} - )) + json = dict(object=dict(type="material_run", some_field={"my_id": "1", "id": "17"})) assert json == replace_objects_with_links(json) # no uids field - json = dict(object=dict( - type='material_run', - uids={} - )) + json = dict(object=dict(type="material_run", uids={})) assert json == replace_objects_with_links(json) # uids is an empty dictionary diff --git a/tests/_util/test_scrub_none.py b/tests/_util/test_scrub_none.py index b693f3426..eccf3c305 100644 --- a/tests/_util/test_scrub_none.py +++ b/tests/_util/test_scrub_none.py @@ -1,42 +1,26 @@ """Tests of the method that removes None values from object dictionaries.""" + from citrine._utils.functions import scrub_none def test_scrub_none(): """Test that scrub_none() when applied to some examples yields expected results.""" - json = dict( - key1=1, - key2=None - ) + json = dict(key1=1, key2=None) scrub_none(json) assert json == dict(key1=1) json = dict( - key1=dict( - key11='foo', - key12=None - ), - key2=[ - dict(key21=None, key22=17), - dict(key23=None), - dict(key24=34, key25=51) - ] + key1=dict(key11="foo", key12=None), + key2=[dict(key21=None, key22=17), dict(key23=None), dict(key24=34, key25=51)], ) scrub_none(json) assert json == dict( - key1=dict(key11='foo'), - key2=[dict(key22=17), dict(), dict(key24=34, key25=51)] + key1=dict(key11="foo"), key2=[dict(key22=17), dict(), dict(key24=34, key25=51)] ) - json = dict( - key1=1, - key2=[None, 'foo', None, None, 'bar', None], - key3=[None, None, None] - ) + json = dict(key1=1, key2=[None, "foo", None, None, "bar", None], key3=[None, None, None]) scrub_none(json) # None should not be removed from lists assert json == dict( - key1=1, - key2=[None, 'foo', None, None, 'bar', None], - key3=[None, None, None] + key1=1, key2=[None, "foo", None, None, "bar", None], key3=[None, None, None] ) diff --git a/tests/_util/test_template_util.py b/tests/_util/test_template_util.py index c0d869d78..efa08285d 100644 --- a/tests/_util/test_template_util.py +++ b/tests/_util/test_template_util.py @@ -1,138 +1,94 @@ -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 +from gemd.entity.object import * +from gemd.entity.value import * + +from citrine._utils.template_util import make_attribute_table + def _make_list_of_gems(): faux_gems = [ ProcessSpec( - name = "hello world", - parameters = [ + name="hello world", + parameters=[ + Parameter(name="param 1", value=NominalReal(nominal=4.2, units="g")), + Parameter(name="param 2", value=NominalCategorical(category="foo")), Parameter( - name = "param 1", - value = NominalReal(nominal=4.2, units="g") - ), - Parameter( - name = "param 2", - value = NominalCategorical(category="foo") + name="attr 1", + value=InChI( + inchi="InChI=1S/C8H10N4O2/c1-10-4-9-6-5(10)7(13)12(3)8(14)11(6)2/h4H,1-3H3" + ), ), - Parameter( - name = "attr 1", - value = InChI(inchi="InChI=1S/C8H10N4O2/c1-10-4-9-6-5(10)7(13)12(3)8(14)11(6)2/h4H,1-3H3") - ) ], - conditions = [ - Condition( - name = "cond 1", - value = NormalReal(mean=4, std=0.5, units="") - ) - ] + conditions=[Condition(name="cond 1", value=NormalReal(mean=4, std=0.5, units=""))], ), IngredientSpec( - name = "I shouldn't be a row", - material=LinkByUID(scope = "faux", id = "abcde"), - process=LinkByUID(scope = "foo", id = "bar") + name="I shouldn't be a row", + material=LinkByUID(scope="faux", id="abcde"), + process=LinkByUID(scope="foo", id="bar"), ), ProcessRun( - name = "process 1", - spec = ProcessSpec( - name = "nestled Spec", - conditions=[ - Condition( - name = "cond 1", - value = NormalReal(mean=6, std=0.3, units="") - ), - ] + name="process 1", + spec=ProcessSpec( + name="nestled Spec", + conditions=[Condition(name="cond 1", value=NormalReal(mean=6, std=0.3, units=""))], ), - parameters = [ - Parameter( - name = "param 1", - value = NormalReal(mean=4.2, std = 0.1, units="g") - ), - Parameter( - name = "param 3", - value = NominalCategorical(category="bar") - ) + parameters=[ + Parameter(name="param 1", value=NormalReal(mean=4.2, std=0.1, units="g")), + Parameter(name="param 3", value=NominalCategorical(category="bar")), ], - conditions = [ + conditions=[ + Condition(name="cond 1", value=NormalReal(mean=4, std=0.5, units="")), + Condition(name="cond 2", value=NominalCategorical(category="hi")), Condition( - name = "cond 1", - value = NormalReal(mean=4, std=0.5, units="") - ), - Condition( - name = "cond 2", - value = NominalCategorical(category="hi") - ), - Condition( - name = "attr 1", - value = InChI(inchi="InChI=1S/C34H34N4O4.Fe/c1-7-21-17(3)25-13-26-19(5)23(9-11-33(39)40)31(37-26)16-32-24(10-12-34(41)42)20(6)28(38-32)15-30-22(8-2)18(4)27(36-30)14-29(21)35-25;/h7-8,13-16H,1-2,9-12H2,3-6H3,(H4,35,36,37,38,39,40,41,42);/q;+2/p-2") + name="attr 1", + value=InChI( + inchi="InChI=1S/C34H34N4O4.Fe/c1-7-21-17(3)25-13-26-19(5)23(9-11-33(39)40)31(37-26)16-32-24(10-12-34(41)42)20(6)28(38-32)15-30-22(8-2)18(4)27(36-30)14-29(21)35-25;/h7-8,13-16H,1-2,9-12H2,3-6H3,(H4,35,36,37,38,39,40,41,42);/q;+2/p-2" + ), ), - ] + ], ), MaterialSpec( - name = "material 1", - process = LinkByUID(scope = "faux 2", id = "id2"), + name="material 1", + process=LinkByUID(scope="faux 2", id="id2"), properties=[ PropertyAndConditions( property=Property( - name = "prop 1", - value = NormalReal(mean=100, std=10, units="g/cm**3") + name="prop 1", value=NormalReal(mean=100, std=10, units="g/cm**3") ), - conditions=[ - Condition( - name = "cond 2", - value = NominalCategorical(category="hi") - ) - ] + conditions=[Condition(name="cond 2", value=NominalCategorical(category="hi"))], ), PropertyAndConditions( - property=Property( - name = "prop 2", - value = NominalReal(nominal=33, units="1/lb") - ), + property=Property(name="prop 2", value=NominalReal(nominal=33, units="1/lb")), conditions=[ - Condition( - name = "cond 3", - value = NominalCategorical(category="citrine") - ) - ] + Condition(name="cond 3", value=NominalCategorical(category="citrine")) + ], ), - ] + ], ), MeasurementSpec( - name = "meas spec 1", - parameters = [ - Parameter( - name = "param 1", - value = NominalReal(nominal=2.2, units="kg") - ), - Parameter( - name = "param 2", - value = NominalCategorical(category="bar") - ) + name="meas spec 1", + parameters=[ + Parameter(name="param 1", value=NominalReal(nominal=2.2, units="kg")), + Parameter(name="param 2", value=NominalCategorical(category="bar")), ], ), MeasurementRun( - name = "meas run 1", - spec = LinkByUID(scope="another fake scope", id = "another fake id"), - properties=[ - Property( - name = "prop 1", - value=NominalReal(nominal=4.1, units="") - ) - ] - ) + name="meas run 1", + spec=LinkByUID(scope="another fake scope", id="another fake id"), + properties=[Property(name="prop 1", value=NominalReal(nominal=4.1, units=""))], + ), ] return faux_gems + def test_attribute_alignment(): """Tests the make_attribute_table() method on a list of GEMD objects including nestled objects, confirming the expected values are being returned in the correct locations """ info_dict = make_attribute_table(_make_list_of_gems()) - assert(isinstance(info_dict, list)) - assert(isinstance(info_dict[0], dict)) + assert isinstance(info_dict, list) + assert isinstance(info_dict[0], dict) 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) diff --git a/tests/conftest.py b/tests/conftest.py index 09468f5fc..77add5893 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,39 +1,30 @@ -import random import uuid -from copy import deepcopy import pytest from citrine.informatics.predictors import AutoMLEstimator from citrine.resources.status_detail import StatusDetail, StatusLevelEnum -from tests.utils.factories import (PredictorEntityDataFactory, PredictorDataDataFactory, - PredictorMetadataDataFactory, StatusDataFactory) +from tests.utils.factories import ( + PredictorDataDataFactory, + PredictorEntityDataFactory, + PredictorMetadataDataFactory, + StatusDataFactory, +) def build_predictor_entity(instance, status_name="READY", status_detail=[]): user = str(uuid.uuid4()) - time = '2020-04-23T15:46:26Z' + time = "2020-04-23T15:46:26Z" return dict( id=str(uuid.uuid4()), data=dict( - name=instance.get("name"), - description=instance.get("description"), - instance=instance + name=instance.get("name"), description=instance.get("description"), instance=instance ), metadata=dict( - status=dict( - name=status_name, - detail=status_detail - ), - created=dict( - user=user, - time=time - ), - updated=dict( - user=user, - time=time - ) - ) + status=dict(name=status_name, detail=status_detail), + created=dict(user=user, time=time), + updated=dict(user=user, time=time), + ), ) @@ -41,80 +32,72 @@ def build_predictor_entity(instance, status_name="READY", status_detail=[]): def valid_product_design_space_data(): """Produce valid product design space data.""" from citrine.informatics.descriptors import FormulationDescriptor + user = str(uuid.uuid4()) - time = '2020-04-23T15:46:26Z' + time = "2020-04-23T15:46:26Z" return dict( id=str(uuid.uuid4()), data=dict( - name='my design space', - description='does some things', + name="my design space", + description="does some things", instance=dict( - type='ProductDesignSpace', - name='my design space', - description='does some things', + type="ProductDesignSpace", + name="my design space", + description="does some things", subspaces=[ dict( - type='FormulationDesignSpace', - name='first subspace', - description='', + type="FormulationDesignSpace", + name="first subspace", + description="", formulation_descriptor=FormulationDescriptor.hierarchical().dump(), - ingredients=['foo'], - labels={'bar': ['foo']}, - untested_ingredients=['qux'], + ingredients=["foo"], + labels={"bar": ["foo"]}, + untested_ingredients=["qux"], constraints=[], - resolution=0.1 + resolution=0.1, ), dict( - type='FormulationDesignSpace', - name='second subspace', - description='formulates some things', + type="FormulationDesignSpace", + name="second subspace", + description="formulates some things", formulation_descriptor=FormulationDescriptor.hierarchical().dump(), - ingredients=['baz'], + ingredients=["baz"], labels={}, untested_ingredients=None, constraints=[], - resolution=0.1 - ) + resolution=0.1, + ), ], dimensions=[ dict( - type='ContinuousDimension', + type="ContinuousDimension", descriptor=dict( - type='Real', - descriptor_key='alpha', - units='', + type="Real", + descriptor_key="alpha", + units="", lower_bound=5.0, upper_bound=10.0, ), lower_bound=6.0, - upper_bound=7.0 + upper_bound=7.0, ), dict( - type='EnumeratedDimension', + type="EnumeratedDimension", descriptor=dict( - type='Categorical', - descriptor_key='color', - descriptor_values=['blue', 'green', 'red'], + type="Categorical", + descriptor_key="color", + descriptor_values=["blue", "green", "red"], ), - list=['red'] - ) - ] - ) + list=["red"], + ), + ], + ), ), metadata=dict( - created=dict( - user=user, - time=time - ), - updated=dict( - user=user, - time=time - ), - status=dict( - name='VALIDATING', - detail=[] - ) - ) + created=dict(user=user, time=time), + updated=dict(user=user, time=time), + status=dict(name="VALIDATING", detail=[]), + ), ) @@ -123,103 +106,86 @@ def valid_formulation_design_space_data(): """Produce valid formulation design space data.""" from citrine.informatics.constraints import IngredientCountConstraint from citrine.informatics.descriptors import FormulationDescriptor + descriptor = FormulationDescriptor.hierarchical() constraint = IngredientCountConstraint(formulation_descriptor=descriptor, min=0, max=1) return dict( - type='FormulationDesignSpace', - name='formulation design space', - description='formulates some things', + type="FormulationDesignSpace", + name="formulation design space", + description="formulates some things", formulation_descriptor=descriptor.dump(), - ingredients=['foo'], - labels={'bar': ['foo']}, - untested_ingredients=['qux'], + ingredients=["foo"], + labels={"bar": ["foo"]}, + untested_ingredients=["qux"], constraints=[constraint.dump()], - resolution=0.1 + resolution=0.1, ) @pytest.fixture def valid_hierarchical_design_space_data( - valid_material_node_definition_data, - valid_gem_data_source_dict + valid_material_node_definition_data, valid_gem_data_source_dict ): """Produce valid hierarchical design space data.""" import copy - name = 'hierarchical design space' - description = 'does things but in levels' + + name = "hierarchical design space" + description = "does things but in levels" user = str(uuid.uuid4()) - time = '2020-04-23T15:46:26Z' + time = "2020-04-23T15:46:26Z" return dict( id=str(uuid.uuid4()), data=dict( name=name, description=description, instance=dict( - type='HierarchicalDesignSpace', + type="HierarchicalDesignSpace", name=name, description=description, root=copy.deepcopy(valid_material_node_definition_data), subspaces=[copy.deepcopy(valid_material_node_definition_data)], - data_sources=[valid_gem_data_source_dict] - ) + data_sources=[valid_gem_data_source_dict], + ), ), metadata=dict( - created=dict( - user=user, - time=time - ), - updated=dict( - user=user, - time=time - ), - archived=dict( - user=user, - time=time - ), - status=dict( - name='VALIDATING', - detail=[] - ) - ) + created=dict(user=user, time=time), + updated=dict(user=user, time=time), + archived=dict(user=user, time=time), + status=dict(name="VALIDATING", detail=[]), + ), ) @pytest.fixture def valid_material_node_definition_data(valid_formulation_design_space_data): return dict( - identifier=dict( - id=f"Material Node-{uuid.uuid4()}", - scope="Custom Scope" - ), + identifier=dict(id=f"Material Node-{uuid.uuid4()}", scope="Custom Scope"), attributes=[ dict( - type='ContinuousDimension', + type="ContinuousDimension", descriptor=dict( - type='Real', - descriptor_key='alpha', - units='', + type="Real", + descriptor_key="alpha", + units="", lower_bound=5.0, upper_bound=10.0, ), lower_bound=6.0, - upper_bound=7.0 + upper_bound=7.0, ), dict( - type='EnumeratedDimension', + type="EnumeratedDimension", descriptor=dict( - type='Categorical', - descriptor_key='color', - descriptor_values=['blue', 'green', 'red'], + type="Categorical", + descriptor_key="color", + descriptor_values=["blue", "green", "red"], ), - list=['red'] - ) + list=["red"], + ), ], formulation=valid_formulation_design_space_data, - template=dict( - material_template=str(uuid.uuid4()), - process_template=str(uuid.uuid4()), - ), - display_name="Material Node" + template=dict(material_template=str(uuid.uuid4()), process_template=str(uuid.uuid4())), + display_name="Material Node", ) @@ -227,8 +193,8 @@ def valid_material_node_definition_data(valid_formulation_design_space_data): def valid_gem_data_source_dict(): return { "type": "hosted_table_data_source", - "table_id": 'e5c51369-8e71-4ec6-b027-1f92bdc14762', - "table_version": 2 + "table_id": "e5c51369-8e71-4ec6-b027-1f92bdc14762", + "table_version": 2, } @@ -236,39 +202,41 @@ def valid_gem_data_source_dict(): def valid_auto_ml_predictor_data(valid_gem_data_source_dict): """Produce valid data used for tests.""" from citrine.informatics.descriptors import RealDescriptor + x = RealDescriptor("x", lower_bound=0, upper_bound=100, units="") z = RealDescriptor("z", lower_bound=0, upper_bound=100, units="") return dict( - type='AutoML', - name='AutoML predictor', - description='Predicts z from input x', + type="AutoML", + name="AutoML predictor", + description="Predicts z from input x", inputs=[x.dump()], outputs=[z.dump()], - estimators=[AutoMLEstimator.RANDOM_FOREST.value] + estimators=[AutoMLEstimator.RANDOM_FOREST.value], ) @pytest.fixture def valid_graph_predictor_data( - valid_simple_mixture_predictor_data, - valid_label_fractions_predictor_data, - valid_expression_predictor_data, - valid_mean_property_predictor_data, - valid_auto_ml_predictor_data + valid_simple_mixture_predictor_data, + valid_label_fractions_predictor_data, + valid_expression_predictor_data, + valid_mean_property_predictor_data, + valid_auto_ml_predictor_data, ): """Produce valid data used for tests.""" from citrine.informatics.data_sources import GemTableDataSource + instance = dict( - name='Graph predictor', - description='description', + name="Graph predictor", + description="description", predictors=[ valid_simple_mixture_predictor_data, valid_label_fractions_predictor_data, valid_expression_predictor_data, valid_mean_property_predictor_data, - valid_auto_ml_predictor_data + valid_auto_ml_predictor_data, ], - training_data=[GemTableDataSource(table_id=uuid.uuid4(), table_version=0).dump()] + training_data=[GemTableDataSource(table_id=uuid.uuid4(), table_version=0).dump()], ) return PredictorEntityDataFactory(data=PredictorDataDataFactory(instance=instance)) @@ -277,11 +245,11 @@ def valid_graph_predictor_data( def valid_graph_predictor_data_empty(): """Another predictor valid data used for tests.""" instance = dict( - type='Graph', - name='Empty Graph predictor', - description='description', + type="Graph", + name="Empty Graph predictor", + description="description", predictors=[], - training_data=[] + training_data=[], ) return PredictorEntityDataFactory(data=PredictorDataDataFactory(instance=instance)) @@ -290,19 +258,23 @@ def valid_graph_predictor_data_empty(): def valid_expression_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import RealDescriptor - shear_modulus = RealDescriptor('Property~Shear modulus', lower_bound=0, upper_bound=100, units='GPa') - youngs_modulus = RealDescriptor('Property~Young\'s modulus', lower_bound=0, upper_bound=100, units='GPa') - poissons_ratio = RealDescriptor('Property~Poisson\'s ratio', lower_bound=-1, upper_bound=0.5, units='') + + shear_modulus = RealDescriptor( + "Property~Shear modulus", lower_bound=0, upper_bound=100, units="GPa" + ) + youngs_modulus = RealDescriptor( + "Property~Young's modulus", lower_bound=0, upper_bound=100, units="GPa" + ) + poissons_ratio = RealDescriptor( + "Property~Poisson's ratio", lower_bound=-1, upper_bound=0.5, units="" + ) return dict( - type='AnalyticExpression', - name='Expression predictor', - description='Computes shear modulus from Youngs modulus and Poissons ratio', - expression='Y / (2 * (1 + v))', + type="AnalyticExpression", + name="Expression predictor", + description="Computes shear modulus from Youngs modulus and Poissons ratio", + expression="Y / (2 * (1 + v))", output=shear_modulus.dump(), - aliases={ - 'Y': youngs_modulus.dump(), - 'v': poissons_ratio.dump(), - } + aliases={"Y": youngs_modulus.dump(), "v": poissons_ratio.dump()}, ) @@ -310,40 +282,37 @@ def valid_expression_predictor_data(): def valid_predictor_report_data(example_categorical_pva_metrics, example_f1_metrics): """Produce valid data used for tests.""" from citrine.informatics.descriptors import RealDescriptor + x = RealDescriptor("x", lower_bound=0, upper_bound=1, units="") y = RealDescriptor("y", lower_bound=0, upper_bound=100, units="") z = RealDescriptor("z", lower_bound=0, upper_bound=101, units="") return dict( - id='7c2dda5d-675a-41b6-829c-e485163f0e43', - module_id='31c7f311-6f3d-4a93-9387-94cc877f170c', - status='OK', - create_time='2020-04-23T15:46:26Z', - update_time='2020-04-23T15:46:26Z', + id="7c2dda5d-675a-41b6-829c-e485163f0e43", + module_id="31c7f311-6f3d-4a93-9387-94cc877f170c", + status="OK", + create_time="2020-04-23T15:46:26Z", + update_time="2020-04-23T15:46:26Z", report=dict( models=[ dict( - name='GeneralLoloModel_1', - type='ML Model', + name="GeneralLoloModel_1", + type="ML Model", inputs=[x.key], outputs=[y.key], - display_name='ML Model', + display_name="ML Model", model_settings=[ dict( - name='Algorithm', - value='Ensemble of non-linear estimators', + name="Algorithm", + value="Ensemble of non-linear estimators", children=[ - dict(name='Number of estimators', value=64, children=[]), - dict(name='Leaf model', value='Mean', children=[]), - dict(name='Use jackknife', value=True, children=[]) - ] + dict(name="Number of estimators", value=64, children=[]), + dict(name="Leaf model", value="Mean", children=[]), + dict(name="Use jackknife", value=True, children=[]), + ], ) ], feature_importances=[ - dict( - response_key='y', - importances=dict(x=1.00), - top_features=5 - ) + dict(response_key="y", importances=dict(x=1.00), top_features=5) ], selection_summary=dict( n_folds=4, @@ -351,48 +320,44 @@ def valid_predictor_report_data(example_categorical_pva_metrics, example_f1_metr dict( model_settings=[ dict( - name='Algorithm', - value='Ensemble of non-linear estimators', + name="Algorithm", + value="Ensemble of non-linear estimators", children=[ - dict(name='Number of estimators', value=64, children=[]), - dict(name='Leaf model', value='Mean', children=[]), - dict(name='Use jackknife', value=True, children=[]) - ] + dict( + name="Number of estimators", value=64, children=[] + ), + dict(name="Leaf model", value="Mean", children=[]), + dict(name="Use jackknife", value=True, children=[]), + ], ) ], response_results=dict( response_name=dict( metrics=dict( predicted_vs_actual=example_categorical_pva_metrics, - f1=example_f1_metrics + f1=example_f1_metrics, ) ) - ) + ), ) - ] + ], ), - predictor_configuration_name="Predict y from x with ML" + predictor_configuration_name="Predict y from x with ML", ), dict( - name='GeneralLosslessModel_2', - type='Analytic Model', + name="GeneralLosslessModel_2", + type="Analytic Model", inputs=[x.key, y.key], outputs=[z.key], - display_name='GeneralLosslessModel_2', - model_settings=[ - dict( - name="Expression", - value="(z) <- (x + y)", - children=[] - ) - ], + display_name="GeneralLosslessModel_2", + model_settings=[dict(name="Expression", value="(z) <- (x + y)", children=[])], feature_importances=[], predictor_configuration_name="Expression for z", - predictor_configuration_uid="249bf32c-6f3d-4a93-9387-94cc877f170c" - ) + predictor_configuration_uid="249bf32c-6f3d-4a93-9387-94cc877f170c", + ), ], - descriptors=[x.dump(), y.dump(), z.dump()] - ) + descriptors=[x.dump(), y.dump(), z.dump()], + ), ) @@ -400,18 +365,18 @@ def valid_predictor_report_data(example_categorical_pva_metrics, example_f1_metr def valid_ing_formulation_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import RealDescriptor + return dict( - type='IngredientsToSimpleMixture', - name='Ingredients to formulation predictor', - description='Constructs mixtures from ingredients', + type="IngredientsToSimpleMixture", + name="Ingredients to formulation predictor", + description="Constructs mixtures from ingredients", id_to_quantity={ - 'water': RealDescriptor('water quantity', lower_bound=0, upper_bound=1, units="").dump(), - 'salt': RealDescriptor('salt quantity', lower_bound=0, upper_bound=1, units="").dump() + "water": RealDescriptor( + "water quantity", lower_bound=0, upper_bound=1, units="" + ).dump(), + "salt": RealDescriptor("salt quantity", lower_bound=0, upper_bound=1, units="").dump(), }, - labels={ - 'solvent': ['water'], - 'solute': ['salt'], - } + labels={"solvent": ["water"], "solute": ["salt"]}, ) @@ -419,17 +384,18 @@ def valid_ing_formulation_predictor_data(): def valid_generalized_mean_property_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import FormulationDescriptor + formulation_descriptor = FormulationDescriptor.hierarchical() return dict( - type='GeneralizedMeanProperty', - name='Mean property predictor', - description='Computes mean ingredient properties', + type="GeneralizedMeanProperty", + name="Mean property predictor", + description="Computes mean ingredient properties", input=formulation_descriptor.dump(), - properties=['density'], + properties=["density"], p=2, impute_properties=True, - default_properties={'density': 1.0}, - label='solvent' + default_properties={"density": 1.0}, + label="solvent", ) @@ -437,18 +403,19 @@ def valid_generalized_mean_property_predictor_data(): def valid_mean_property_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor + formulation_descriptor = FormulationDescriptor.flat() - density = RealDescriptor(key='density', lower_bound=0, upper_bound=100, units='g/cm^3') + density = RealDescriptor(key="density", lower_bound=0, upper_bound=100, units="g/cm^3") return dict( - type='MeanProperty', - name='Mean property predictor', - description='Computes mean ingredient properties', + type="MeanProperty", + name="Mean property predictor", + description="Computes mean ingredient properties", input=formulation_descriptor.dump(), properties=[density.dump()], p=2.0, impute_properties=True, - default_properties={'density': 1.0}, - label='solvent' + default_properties={"density": 1.0}, + label="solvent", ) @@ -456,12 +423,13 @@ def valid_mean_property_predictor_data(): def valid_label_fractions_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import FormulationDescriptor + return dict( - type='LabelFractions', - name='Label fractions predictor', - description='Computes relative proportions of labeled ingredients', + type="LabelFractions", + name="Label fractions predictor", + description="Computes relative proportions of labeled ingredients", input=FormulationDescriptor.hierarchical().dump(), - labels=['solvent'] + labels=["solvent"], ) @@ -469,12 +437,13 @@ def valid_label_fractions_predictor_data(): def valid_ingredient_fractions_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import FormulationDescriptor + return dict( - type='IngredientFractions', - name='Ingredient fractions predictor', - description='Computes ingredient fractions', + type="IngredientFractions", + name="Ingredient fractions predictor", + description="Computes ingredient fractions", input=FormulationDescriptor.hierarchical().dump(), - ingredients=['Blue dye', 'Red dye'] + ingredients=["Blue dye", "Red dye"], ) @@ -484,7 +453,7 @@ def valid_data_source_design_space_dict(valid_gem_data_source_dict): type="DataSourceDesignSpace", name="Example valid data source design space", description="Example valid data source design space based on a GEM Table Data Source.", - data_source=valid_gem_data_source_dict + data_source=valid_gem_data_source_dict, ) @@ -492,15 +461,16 @@ def valid_data_source_design_space_dict(valid_gem_data_source_dict): def invalid_predictor_node_data(): """Produce invalid valid data used for tests.""" from citrine.informatics.descriptors import RealDescriptor + x = RealDescriptor("x", lower_bound=0, upper_bound=100, units="") y = RealDescriptor("y", lower_bound=0, upper_bound=100, units="") z = RealDescriptor("z", lower_bound=0, upper_bound=100, units="") return dict( - type='invalid', - name='my predictor', - description='does some things', + type="invalid", + name="my predictor", + description="does some things", inputs=[x.dump(), y.dump()], - output=z.dump() + output=z.dump(), ) @@ -508,23 +478,24 @@ def invalid_predictor_node_data(): def invalid_graph_predictor_data(): """Produce valid data used for tests.""" from citrine.informatics.descriptors import RealDescriptor + x = RealDescriptor("x", lower_bound=0, upper_bound=100, units="") y = RealDescriptor("y", lower_bound=0, upper_bound=100, units="") z = RealDescriptor("z", lower_bound=0, upper_bound=100, units="") instance = dict( - type='invalid', - name='my predictor', - description='does some things badly', + type="invalid", + name="my predictor", + description="does some things badly", predictors=[x.dump(), y.dump()], ) detail = [ - StatusDetail(level=StatusLevelEnum.WARNING, msg='Something is wrong'), - StatusDetail(level="Error", msg='Very wrong') + StatusDetail(level=StatusLevelEnum.WARNING, msg="Something is wrong"), + StatusDetail(level="Error", msg="Very wrong"), ] - status = StatusDataFactory(name='INVALID', detail=detail) + status = StatusDataFactory(name="INVALID", detail=detail) return PredictorEntityDataFactory( data=PredictorDataDataFactory(instance=instance), - meatadata=PredictorMetadataDataFactory(status=status) + meatadata=PredictorMetadataDataFactory(status=status), ) @@ -532,11 +503,11 @@ def invalid_graph_predictor_data(): def invalid_design_subspace_data(): """Produce invalid valid data used for tests.""" return dict( - type='invalid', - name='my design space', - description='does some things', + type="invalid", + name="my design space", + description="does some things", subspaces=[], - dimensions=[] + dimensions=[], ) @@ -544,9 +515,9 @@ def invalid_design_subspace_data(): def valid_simple_mixture_predictor_data(): """Produce valid data used for tests.""" return dict( - type='SimpleMixture', - name='Simple mixture predictor', - description='simple mixture description' + type="SimpleMixture", + name="Simple mixture predictor", + description="simple mixture description", ) @@ -559,10 +530,8 @@ def example_cv_evaluator_dict(): "responses": ["salt?", "saltiness"], "n_folds": 6, "n_trials": 8, - "metrics": [ - {"type": "PVA"}, {"type": "RMSE"}, {"type": "F1"} - ], - "ignore_when_grouping": ["temperature"] + "metrics": [{"type": "PVA"}, {"type": "RMSE"}, {"type": "F1"}], + "ignore_when_grouping": ["temperature"], } @@ -574,24 +543,18 @@ def example_holdout_evaluator_dict(valid_gem_data_source_dict): "description": "", "responses": ["sweetness"], "data_source": valid_gem_data_source_dict, - "metrics": [{"type": "RMSE"}] + "metrics": [{"type": "RMSE"}], } + @pytest.fixture() def example_rmse_metrics(): - return { - "type": "RealMetricValue", - "mean": 0.4, - "standard_error": 0.12 - } + return {"type": "RealMetricValue", "mean": 0.4, "standard_error": 0.12} @pytest.fixture def example_f1_metrics(): - return { - "type": "RealMetricValue", - "mean": 0.3 - } + return {"type": "RealMetricValue", "mean": 0.3} @pytest.fixture @@ -604,18 +567,10 @@ def example_real_pva_metrics(): "identifiers": ["Foo", "Bar"], "trial": 1, "fold": 3, - "predicted": { - "type": "RealMetricValue", - "mean": 1.0, - "standard_error": 0.12 - }, - "actual": { - "type": "RealMetricValue", - "mean": 1.2, - "standard_error": 0.0 - } + "predicted": {"type": "RealMetricValue", "mean": 1.0, "standard_error": 0.12}, + "actual": {"type": "RealMetricValue", "mean": 1.2, "standard_error": 0.0}, } - ] + ], } @@ -629,20 +584,21 @@ def example_categorical_pva_metrics(): "identifiers": ["Foo", "Bar"], "trial": 1, "fold": 3, - "predicted": { - "salt": 0.3, - "not salt": 0.7 - }, - "actual": { - "not salt": 1.0 - } + "predicted": {"salt": 0.3, "not salt": 0.7}, + "actual": {"not salt": 1.0}, } - ] + ], } @pytest.fixture() -def example_cv_result_dict(example_cv_evaluator_dict, example_rmse_metrics, example_categorical_pva_metrics, example_f1_metrics, example_real_pva_metrics): +def example_cv_result_dict( + example_cv_evaluator_dict, + example_rmse_metrics, + example_categorical_pva_metrics, + example_f1_metrics, + example_real_pva_metrics, +): return { "type": "CrossValidationResult", "evaluator": example_cv_evaluator_dict, @@ -650,16 +606,16 @@ def example_cv_result_dict(example_cv_evaluator_dict, example_rmse_metrics, exam "salt?": { "metrics": { "predicted_vs_actual": example_categorical_pva_metrics, - "f1": example_f1_metrics + "f1": example_f1_metrics, } }, "saltiness": { "metrics": { "predicted_vs_actual": example_real_pva_metrics, - "rmse": example_rmse_metrics + "rmse": example_rmse_metrics, } - } - } + }, + }, } @@ -668,13 +624,7 @@ def example_holdout_result_dict(example_holdout_evaluator_dict, example_rmse_met return { "type": "HoldoutSetResult", "evaluator": example_holdout_evaluator_dict, - "response_results": { - "sweetness": { - "metrics": { - "rmse": example_rmse_metrics - } - } - } + "response_results": {"sweetness": {"metrics": {"rmse": example_rmse_metrics}}}, } @@ -687,8 +637,8 @@ def sample_design_space_execution_dict(generic_entity): "status": { "major": ret.get("status"), "minor": ret.get("status_description"), - "detail": ret.get("status_detail") - } + "detail": ret.get("status_detail"), + }, } ) return ret @@ -697,30 +647,28 @@ def sample_design_space_execution_dict(generic_entity): @pytest.fixture() def example_design_material(): return { - 'vars': { - 'Temperature': {'type': 'R', 'm': 475.8, 's': 0}, - 'Flour': {'type': 'C', 'cp': {'flour': 100.0}}, - 'Water': {'type': 'M', 'q': {'water': 72.5}, 'l': {}}, - 'Salt': {'type': 'F', 'f': 'NaCl'}, - 'Yeast': {'type': 'S', 's': 'O1C=2C=C(C=3SC=C4C=CNC43)CC2C=5C=CC=6C=CNC6C15'} + "vars": { + "Temperature": {"type": "R", "m": 475.8, "s": 0}, + "Flour": {"type": "C", "cp": {"flour": 100.0}}, + "Water": {"type": "M", "q": {"water": 72.5}, "l": {}}, + "Salt": {"type": "F", "f": "NaCl"}, + "Yeast": {"type": "S", "s": "O1C=2C=C(C=3SC=C4C=CNC43)CC2C=5C=CC=6C=CNC6C15"}, + }, + "identifiers": { + "id": str(uuid.uuid4()), + "identifiers": [], + "material_template": str(uuid.uuid4()), + "process_template": str(uuid.uuid4()), }, - 'identifiers': { - 'id': str(uuid.uuid4()), - 'identifiers': [], - 'material_template': str(uuid.uuid4()), - 'process_template': str(uuid.uuid4()) - } } @pytest.fixture() def example_hierarchical_design_material(example_design_material): return { - 'terminal': example_design_material, - 'sub_materials': [example_design_material], - 'mixtures': { - str(uuid.uuid4()): {'q': {'A': 0.5, 'B': 0.5}, 'l': {}} - } + "terminal": example_design_material, + "sub_materials": [example_design_material], + "mixtures": {str(uuid.uuid4()): {"q": {"A": 0.5, "B": 0.5}, "l": {}}}, } @@ -729,48 +677,47 @@ def example_hierarchical_candidates(example_hierarchical_design_material): return { "page": 2, "per_page": 4, - "response": [{ - "id": str(uuid.uuid4()), - "primary_score": 0, - "rank": 1, - "material": example_hierarchical_design_material, - "name": "Example candidate", - "hidden": True, - "comments": [ - { - "message": "a message", - "created": { - "user": str(uuid.uuid4()), - "time": '2025-02-20T10:46:26Z' + "response": [ + { + "id": str(uuid.uuid4()), + "primary_score": 0, + "rank": 1, + "material": example_hierarchical_design_material, + "name": "Example candidate", + "hidden": True, + "comments": [ + { + "message": "a message", + "created": {"user": str(uuid.uuid4()), "time": "2025-02-20T10:46:26Z"}, } - } - ] - }] + ], + } + ], } + @pytest.fixture() def example_candidates(example_design_material): return { "page": 2, "per_page": 4, - "response": [{ - "id": str(uuid.uuid4()), - "material_id": str(uuid.uuid4()), - "identifiers": [], - "primary_score": 0, - "material": example_design_material, - "name": "Example candidate", - "hidden": True, - "comments": [ - { - "message": "a message", - "created": { - "user": str(uuid.uuid4()), - "time": '2025-02-20T10:46:26Z' + "response": [ + { + "id": str(uuid.uuid4()), + "material_id": str(uuid.uuid4()), + "identifiers": [], + "primary_score": 0, + "material": example_design_material, + "name": "Example candidate", + "hidden": True, + "comments": [ + { + "message": "a message", + "created": {"user": str(uuid.uuid4()), "time": "2025-02-20T10:46:26Z"}, } - } - ] - }] + ], + } + ], } @@ -778,15 +725,16 @@ def example_candidates(example_design_material): def example_sample_design_space_response(example_hierarchical_design_material): return { "per_page": 4, - "response": [{ - "id": str(uuid.uuid4()), - "execution_id": str(uuid.uuid4()), - "material": example_hierarchical_design_material - }] + "response": [ + { + "id": str(uuid.uuid4()), + "execution_id": str(uuid.uuid4()), + "material": example_hierarchical_design_material, + } + ], } - @pytest.fixture def generic_entity(): user = str(uuid.uuid4()) @@ -795,8 +743,8 @@ def generic_entity(): "status": "INPROGRESS", "status_description": "VALIDATING", "status_detail": [{"level": "Info", "msg": "System processing"}], - "create_time": '2020-04-23T15:46:26Z', - "update_time": '2020-04-23T15:46:26Z', + "create_time": "2020-04-23T15:46:26Z", + "update_time": "2020-04-23T15:46:26Z", "created_by": user, "updated_by": user, } @@ -805,19 +753,21 @@ def generic_entity(): @pytest.fixture def design_execution_dict(generic_entity): ret = generic_entity.copy() - ret.update({ - "workflow_id": str(uuid.uuid4()), - "version_number": 2, - "score": { - "type": "MLI", - "baselines": [], - "constraints": [], - "objectives": [], - "name": "score", - "description": "" - }, - "descriptors": [] - }) + ret.update( + { + "workflow_id": str(uuid.uuid4()), + "version_number": 2, + "score": { + "type": "MLI", + "baselines": [], + "constraints": [], + "objectives": [], + "name": "score", + "description": "", + }, + "descriptors": [], + } + ) return ret @@ -832,14 +782,16 @@ def example_generation_results(): return { "page": 1, "per_page": 4, - "response": [{ - "id": str(uuid.uuid4()), - "execution_id": str(uuid.uuid4()), - "result": { - "seed": "CCCCO", - "mutated": "CCCN", - "fingerprint_similarity": 0.41, - "fingerprint_type": "ECFP4", + "response": [ + { + "id": str(uuid.uuid4()), + "execution_id": str(uuid.uuid4()), + "result": { + "seed": "CCCCO", + "mutated": "CCCN", + "fingerprint_similarity": 0.41, + "fingerprint_type": "ECFP4", + }, } - }] + ], } diff --git a/tests/gemd_query/test_gemd_query.py b/tests/gemd_query/test_gemd_query.py index b302da482..52e9b198a 100644 --- a/tests/gemd_query/test_gemd_query.py +++ b/tests/gemd_query/test_gemd_query.py @@ -1,10 +1,10 @@ from uuid import uuid4 + import pytest from citrine.gemd_queries.criteria import PropertiesCriteria from citrine.gemd_queries.filter import AllRealFilter from citrine.gemd_queries.gemd_query import GemdQuery - from tests.utils.factories import GemdQueryDataFactory @@ -13,14 +13,14 @@ def test_gemd_query_version(): assert GemdQuery.build(valid) is not None invalid = GemdQueryDataFactory() - invalid['schema_version'] = 2 + invalid["schema_version"] = 2 with pytest.raises(ValueError): GemdQuery.build(invalid) def test_criteria_rebuild(): value_filter = AllRealFilter() - value_filter.unit = 'm' + value_filter.unit = "m" value_filter.lower = 0 value_filter.upper = 1 @@ -31,15 +31,18 @@ def test_criteria_rebuild(): query = GemdQuery() query.criteria.append(crit) query.datasets.add(uuid4()) - query.object_types = {'material_run'} + query.object_types = {"material_run"} query_copy = GemdQuery.build(query.dump()) assert len(query.criteria) == len(query_copy.criteria) - assert query.criteria[0].property_templates_filter == query_copy.criteria[0].property_templates_filter - assert query.criteria[0].value_type_filter.unit == query_copy.criteria[0].value_type_filter.unit - assert query.criteria[0].value_type_filter.lower == query_copy.criteria[0].value_type_filter.lower - assert query.criteria[0].value_type_filter.upper == query_copy.criteria[0].value_type_filter.upper + for field in ( + lambda x: x.property_templates_filter, + lambda x: x.value_type_filter.unit, + lambda x: x.value_type_filter.lower, + lambda x: x.value_type_filter.upper, + ): + assert field(query.criteria[0]) == field(query_copy.criteria[0]) assert query.datasets == query_copy.datasets assert query.object_types == query_copy.object_types assert query.schema_version == query_copy.schema_version diff --git a/tests/gemtable/test_columns.py b/tests/gemtable/test_columns.py index 4dbb268d3..b054d1f4f 100644 --- a/tests/gemtable/test_columns.py +++ b/tests/gemtable/test_columns.py @@ -1,25 +1,28 @@ """Tests for citrine.informatics.columns.""" + import pytest from citrine.gemtables.columns import * from citrine.gemtables.variables import TerminalMaterialInfo -@pytest.fixture(params=[ - IdentityColumn(data_source="terminal name"), - MeanColumn(data_source="density", target_units="g/cm^3"), - StdDevColumn(data_source="density", target_units="g/cm^3"), - QuantileColumn(data_source="density", quantile=0.95), - OriginalUnitsColumn(data_source="density"), - MostLikelyCategoryColumn(data_source="color"), - MostLikelyProbabilityColumn(data_source="color"), - FlatCompositionColumn(data_source="formula", sort_order=CompositionSortOrder.QUANTITY), - ComponentQuantityColumn(data_source="formula", component_name="Si", normalize=True), - NthBiggestComponentNameColumn(data_source="formula", n=1), - NthBiggestComponentQuantityColumn(data_source="formula", n=2), - MolecularStructureColumn(data_source="molecule", format=ChemicalDisplayFormat.SMILES), - ConcatColumn(data_source="labels", subcolumn=IdentityColumn(data_source="terminal name")) -]) +@pytest.fixture( + params=[ + IdentityColumn(data_source="terminal name"), + MeanColumn(data_source="density", target_units="g/cm^3"), + StdDevColumn(data_source="density", target_units="g/cm^3"), + QuantileColumn(data_source="density", quantile=0.95), + OriginalUnitsColumn(data_source="density"), + MostLikelyCategoryColumn(data_source="color"), + MostLikelyProbabilityColumn(data_source="color"), + FlatCompositionColumn(data_source="formula", sort_order=CompositionSortOrder.QUANTITY), + ComponentQuantityColumn(data_source="formula", component_name="Si", normalize=True), + NthBiggestComponentNameColumn(data_source="formula", n=1), + NthBiggestComponentQuantityColumn(data_source="formula", n=2), + MolecularStructureColumn(data_source="molecule", format=ChemicalDisplayFormat.SMILES), + ConcatColumn(data_source="labels", subcolumn=IdentityColumn(data_source="terminal name")), + ] +) def column(request): return request.param @@ -46,10 +49,7 @@ def test_invalid_deser(): def test_data_source_args(): terminal_name = "terminal name" - var = TerminalMaterialInfo(name=terminal_name, - headers=[terminal_name], - field='NAME' - ) + var = TerminalMaterialInfo(name=terminal_name, headers=[terminal_name], field="NAME") IdentityColumn(data_source=terminal_name) IdentityColumn(data_source=var) with pytest.raises(TypeError): diff --git a/tests/gemtable/test_rows.py b/tests/gemtable/test_rows.py index 47006415d..12cfe2d5a 100644 --- a/tests/gemtable/test_rows.py +++ b/tests/gemtable/test_rows.py @@ -1,22 +1,28 @@ """Tests for citrine.informatics.rows.""" + import pytest +from gemd.entity.link_by_uid import LinkByUID from citrine.gemtables.rows import MaterialRunByTemplate, Row -from gemd.entity.link_by_uid import LinkByUID -@pytest.fixture(params=[ - MaterialRunByTemplate(templates=[ - LinkByUID(scope="templates", id="density"), LinkByUID(scope="templates", id="ingredients") - ]), - MaterialRunByTemplate(templates=[ - LinkByUID(scope="templates", id="density"), LinkByUID(scope="templates", id="ingredients") - ], - tags=[ - "foo::bar", "some::tag" - ] - ), -]) +@pytest.fixture( + params=[ + MaterialRunByTemplate( + templates=[ + LinkByUID(scope="templates", id="density"), + LinkByUID(scope="templates", id="ingredients"), + ] + ), + MaterialRunByTemplate( + templates=[ + LinkByUID(scope="templates", id="density"), + LinkByUID(scope="templates", id="ingredients"), + ], + tags=["foo::bar", "some::tag"], + ), + ] +) def row(request): return request.param diff --git a/tests/gemtable/test_variables.py b/tests/gemtable/test_variables.py index beb28cac6..64211250d 100644 --- a/tests/gemtable/test_variables.py +++ b/tests/gemtable/test_variables.py @@ -1,32 +1,134 @@ """Tests for citrine.informatics.variables.""" + import pytest from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.link_by_uid import LinkByUID from citrine.gemtables.variables import * -from gemd.entity.link_by_uid import LinkByUID -@pytest.fixture(params=[ - TerminalMaterialInfo(name="terminal name", headers=["Root", "Name"], field="name"), - XOR(name="terminal name or sample_type", headers=["Root", "Info"], variables=[TerminalMaterialInfo(name="terminal name", headers=["Root", "Name"], field="name"), TerminalMaterialInfo(name="terminal name", headers=["Root", "Sample Type"], field="sample_type")]), - AttributeByTemplate(name="density", headers=["density"], template=LinkByUID(scope="templates", id="density"), attribute_constraints=[[LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3")]]), - AttributeByTemplateAfterProcessTemplate(name="density", headers=["density"], attribute_template=LinkByUID(scope="template", id="density"), process_template=LinkByUID(scope="template", id="process")), - AttributeByTemplateAndObjectTemplate(name="density", headers=["density"], attribute_template=LinkByUID(scope="template", id="density"), object_template=LinkByUID(scope="template", id="object")), - AttributeInOutput(name="density", headers=["density"], attribute_template=LinkByUID(scope="template", id="density"), process_templates=[LinkByUID(scope="template", id="object")]), - LocalAttribute(name="density", headers=["density"], template=LinkByUID(scope="templates", id="density"), attribute_constraints=[[LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3")]]), - LocalAttributeAndObject(name="density", headers=["density"], template=LinkByUID(scope="templates", id="density"), object_template=LinkByUID(scope="templates", id="object"), attribute_constraints=[[LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3")]]), - IngredientIdentifierByProcessTemplateAndName(name="ingredient id", headers=["density"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", scope="scope"), - IngredientIdentifierInOutput(name="ingredient id", headers=["ingredient id"], ingredient_name="ingredient", process_templates=[LinkByUID(scope="template", id="object")], scope="scope"), - LocalIngredientIdentifier(name="ingredient id", headers=["ingredient id"], ingredient_name="ingredient", scope="scope"), - IngredientLabelByProcessAndName(name="ingredient label", headers=["label"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", label="label"), - IngredientLabelsSetByProcessAndName(name="ingredient label", headers=["label"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient"), - IngredientLabelsSetInOutput(name="ingredient label", headers=["label"], process_templates=[LinkByUID(scope="template", id="process")], ingredient_name="ingredient"), - LocalIngredientLabelsSet(name="ingredient label", headers=["label"], ingredient_name="ingredient"), - IngredientQuantityByProcessAndName(name="ingredient quantity dimension", headers=["quantity"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.ABSOLUTE, unit='kg'), - IngredientQuantityInOutput(name="ingredient quantity", headers=["ingredient quantity"], ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.MASS, process_templates=[LinkByUID(scope="template", id="object")]), - LocalIngredientQuantity(name="ingredient quantity", headers=["ingredient quantity"], ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.MASS), - TerminalMaterialIdentifier(name="terminal id", headers=["id"], scope="scope") -]) +@pytest.fixture( + params=[ + TerminalMaterialInfo(name="terminal name", headers=["Root", "Name"], field="name"), + XOR( + name="terminal name or sample_type", + headers=["Root", "Info"], + variables=[ + TerminalMaterialInfo(name="terminal name", headers=["Root", "Name"], field="name"), + TerminalMaterialInfo( + name="terminal name", headers=["Root", "Sample Type"], field="sample_type" + ), + ], + ), + AttributeByTemplate( + name="density", + headers=["density"], + template=LinkByUID(scope="templates", id="density"), + attribute_constraints=[ + [LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3")] + ], + ), + AttributeByTemplateAfterProcessTemplate( + name="density", + headers=["density"], + attribute_template=LinkByUID(scope="template", id="density"), + process_template=LinkByUID(scope="template", id="process"), + ), + AttributeByTemplateAndObjectTemplate( + name="density", + headers=["density"], + attribute_template=LinkByUID(scope="template", id="density"), + object_template=LinkByUID(scope="template", id="object"), + ), + AttributeInOutput( + name="density", + headers=["density"], + attribute_template=LinkByUID(scope="template", id="density"), + process_templates=[LinkByUID(scope="template", id="object")], + ), + LocalAttribute( + name="density", + headers=["density"], + template=LinkByUID(scope="templates", id="density"), + attribute_constraints=[ + [LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3")] + ], + ), + LocalAttributeAndObject( + name="density", + headers=["density"], + template=LinkByUID(scope="templates", id="density"), + object_template=LinkByUID(scope="templates", id="object"), + attribute_constraints=[ + [LinkByUID(scope="templates", id="density"), RealBounds(0, 100, "g/cm**3")] + ], + ), + IngredientIdentifierByProcessTemplateAndName( + name="ingredient id", + headers=["density"], + process_template=LinkByUID(scope="template", id="process"), + ingredient_name="ingredient", + scope="scope", + ), + IngredientIdentifierInOutput( + name="ingredient id", + headers=["ingredient id"], + ingredient_name="ingredient", + process_templates=[LinkByUID(scope="template", id="object")], + scope="scope", + ), + LocalIngredientIdentifier( + name="ingredient id", + headers=["ingredient id"], + ingredient_name="ingredient", + scope="scope", + ), + IngredientLabelByProcessAndName( + name="ingredient label", + headers=["label"], + process_template=LinkByUID(scope="template", id="process"), + ingredient_name="ingredient", + label="label", + ), + IngredientLabelsSetByProcessAndName( + name="ingredient label", + headers=["label"], + process_template=LinkByUID(scope="template", id="process"), + ingredient_name="ingredient", + ), + IngredientLabelsSetInOutput( + name="ingredient label", + headers=["label"], + process_templates=[LinkByUID(scope="template", id="process")], + ingredient_name="ingredient", + ), + LocalIngredientLabelsSet( + name="ingredient label", headers=["label"], ingredient_name="ingredient" + ), + IngredientQuantityByProcessAndName( + name="ingredient quantity dimension", + headers=["quantity"], + process_template=LinkByUID(scope="template", id="process"), + ingredient_name="ingredient", + quantity_dimension=IngredientQuantityDimension.ABSOLUTE, + unit="kg", + ), + IngredientQuantityInOutput( + name="ingredient quantity", + headers=["ingredient quantity"], + ingredient_name="ingredient", + quantity_dimension=IngredientQuantityDimension.MASS, + process_templates=[LinkByUID(scope="template", id="object")], + ), + LocalIngredientQuantity( + name="ingredient quantity", + headers=["ingredient quantity"], + ingredient_name="ingredient", + quantity_dimension=IngredientQuantityDimension.MASS, + ), + TerminalMaterialIdentifier(name="terminal id", headers=["id"], scope="scope"), + ] +) def variable(request): return request.param @@ -57,7 +159,7 @@ def test_quantity_dimension_serializes_to_string(): headers=["quantity"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.NUMBER + quantity_dimension=IngredientQuantityDimension.NUMBER, ) variable_data = variable.dump() assert variable_data["quantity_dimension"] == "number" @@ -69,7 +171,7 @@ def test_absolute_units(): headers=["quantity"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.NUMBER + quantity_dimension=IngredientQuantityDimension.NUMBER, ) IngredientQuantityByProcessAndName( name="This should be fine, too", @@ -77,7 +179,7 @@ def test_absolute_units(): process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.ABSOLUTE, - unit='kg' + unit="kg", ) with pytest.raises(ValueError): IngredientQuantityByProcessAndName( @@ -85,7 +187,7 @@ def test_absolute_units(): headers=["quantity"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", - quantity_dimension="bunk" + quantity_dimension="bunk", ) with pytest.raises(ValueError): IngredientQuantityByProcessAndName( @@ -93,7 +195,7 @@ def test_absolute_units(): headers=["quantity"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.ABSOLUTE + quantity_dimension=IngredientQuantityDimension.ABSOLUTE, ) with pytest.raises(ValueError): IngredientQuantityByProcessAndName( @@ -102,7 +204,7 @@ def test_absolute_units(): process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.NUMBER, - unit='kg' + unit="kg", ) # And again, for IngredientQuantityInOutput @@ -111,7 +213,7 @@ def test_absolute_units(): headers=["quantity"], process_templates=[LinkByUID(scope="template", id="process")], ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.NUMBER + quantity_dimension=IngredientQuantityDimension.NUMBER, ) IngredientQuantityInOutput( name="This should be fine, too", @@ -119,7 +221,7 @@ def test_absolute_units(): process_templates=[LinkByUID(scope="template", id="process")], ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.ABSOLUTE, - unit='kg' + unit="kg", ) with pytest.raises(ValueError): IngredientQuantityInOutput( @@ -127,7 +229,7 @@ def test_absolute_units(): headers=["quantity"], process_templates=[LinkByUID(scope="template", id="process")], ingredient_name="ingredient", - quantity_dimension="bunk" + quantity_dimension="bunk", ) with pytest.raises(ValueError): IngredientQuantityInOutput( @@ -135,7 +237,7 @@ def test_absolute_units(): headers=["quantity"], process_templates=[LinkByUID(scope="template", id="process")], ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.ABSOLUTE + quantity_dimension=IngredientQuantityDimension.ABSOLUTE, ) with pytest.raises(ValueError): IngredientQuantityInOutput( @@ -144,7 +246,7 @@ def test_absolute_units(): process_templates=[LinkByUID(scope="template", id="process")], ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.NUMBER, - unit='kg' + unit="kg", ) # And again, for LocalIngredientQuantity @@ -152,28 +254,28 @@ def test_absolute_units(): name="This should be fine", headers=["quantity"], ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.NUMBER + quantity_dimension=IngredientQuantityDimension.NUMBER, ) LocalIngredientQuantity( name="This should be fine, too", headers=["quantity"], ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.ABSOLUTE, - unit='kg' + unit="kg", ) with pytest.raises(ValueError): LocalIngredientQuantity( name="Invalid quantity dimension as string", headers=["quantity"], ingredient_name="ingredient", - quantity_dimension="bunk" + quantity_dimension="bunk", ) with pytest.raises(ValueError): LocalIngredientQuantity( name="This needs units", headers=["quantity"], ingredient_name="ingredient", - quantity_dimension=IngredientQuantityDimension.ABSOLUTE + quantity_dimension=IngredientQuantityDimension.ABSOLUTE, ) with pytest.raises(ValueError): LocalIngredientQuantity( @@ -181,5 +283,5 @@ def test_absolute_units(): headers=["quantity"], ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.NUMBER, - unit='kg' + unit="kg", ) diff --git a/tests/informatics/test_constraints.py b/tests/informatics/test_constraints.py index 18d039e81..d5d65a586 100644 --- a/tests/informatics/test_constraints.py +++ b/tests/informatics/test_constraints.py @@ -1,4 +1,5 @@ """Tests for citrine.informatics.constraints.""" + import pytest from citrine.informatics.constraints import * @@ -11,30 +12,20 @@ def scalar_range_constraint() -> ScalarRangeConstraint: """Build a ScalarRangeConstraint.""" return ScalarRangeConstraint( - descriptor_key='z', - lower_bound=1.0, - upper_bound=10.0, - lower_inclusive=False + descriptor_key="z", lower_bound=1.0, upper_bound=10.0, lower_inclusive=False ) @pytest.fixture def integer_range_constraint() -> IntegerRangeConstraint: """Build an IntegerRangeConstraint.""" - return IntegerRangeConstraint( - descriptor_key='integer', - lower_bound=1, - upper_bound=10 - ) + return IntegerRangeConstraint(descriptor_key="integer", lower_bound=1, upper_bound=10) @pytest.fixture def categorical_constraint() -> AcceptableCategoriesConstraint: """Build a CategoricalConstraint.""" - return AcceptableCategoriesConstraint( - descriptor_key='x', - acceptable_categories=['y', 'z'] - ) + return AcceptableCategoriesConstraint(descriptor_key="x", acceptable_categories=["y", "z"]) @pytest.fixture @@ -42,10 +33,10 @@ def ingredient_fraction_constraint() -> IngredientFractionConstraint: """Build an IngredientFractionConstraint.""" return IngredientFractionConstraint( formulation_descriptor=formulation_descriptor, - ingredient='foo', + ingredient="foo", min=0.0, max=1.0, - is_required=False + is_required=False, ) @@ -53,10 +44,7 @@ def ingredient_fraction_constraint() -> IngredientFractionConstraint: def ingredient_count_constraint() -> IngredientCountConstraint: """Build an IngredientCountConstraint.""" return IngredientCountConstraint( - formulation_descriptor=formulation_descriptor, - min=0, - max=1, - label='bar' + formulation_descriptor=formulation_descriptor, min=0, max=1, label="bar" ) @@ -65,10 +53,10 @@ def label_fraction_constraint() -> LabelFractionConstraint: """Build a LabelFractionConstraint.""" return LabelFractionConstraint( formulation_descriptor=formulation_descriptor, - label='bar', + label="bar", min=0.0, max=1.0, - is_required=False + is_required=False, ) @@ -82,13 +70,13 @@ def ingredient_ratio_constraint() -> IngredientRatioConstraint: ingredient=("foo", 1.0), label=("foolabel", 0.5), basis_ingredients=["baz", "bat"], - basis_labels=["bazlabel", "batlabel"] + basis_labels=["bazlabel", "batlabel"], ) def test_scalar_range_initialization(scalar_range_constraint): """Make sure the correct fields go to the correct places.""" - assert scalar_range_constraint.descriptor_key == 'z' + assert scalar_range_constraint.descriptor_key == "z" assert scalar_range_constraint.lower_bound == 1.0 assert scalar_range_constraint.upper_bound == 10.0 assert not scalar_range_constraint.lower_inclusive @@ -97,22 +85,22 @@ def test_scalar_range_initialization(scalar_range_constraint): def test_integer_range_initialization(integer_range_constraint): """Make sure the correct fields go to the correct places.""" - assert integer_range_constraint.descriptor_key == 'integer' + assert integer_range_constraint.descriptor_key == "integer" assert integer_range_constraint.lower_bound == 1 assert integer_range_constraint.upper_bound == 10 def test_categorical_initialization(categorical_constraint): """Make sure the correct fields go to the correct places.""" - assert categorical_constraint.descriptor_key == 'x' - assert categorical_constraint.acceptable_categories == ['y', 'z'] + assert categorical_constraint.descriptor_key == "x" + assert categorical_constraint.acceptable_categories == ["y", "z"] assert "Acceptable" in str(categorical_constraint) def test_ingredient_fraction_initialization(ingredient_fraction_constraint): """Make sure the correct fields go to the correct places.""" assert ingredient_fraction_constraint.formulation_descriptor == formulation_descriptor - assert ingredient_fraction_constraint.ingredient == 'foo' + assert ingredient_fraction_constraint.ingredient == "foo" assert ingredient_fraction_constraint.min == 0.0 assert ingredient_fraction_constraint.max == 1.0 assert not ingredient_fraction_constraint.is_required @@ -123,13 +111,13 @@ def test_ingredient_count_initialization(ingredient_count_constraint): assert ingredient_count_constraint.formulation_descriptor == formulation_descriptor assert ingredient_count_constraint.min == 0 assert ingredient_count_constraint.max == 1 - assert ingredient_count_constraint.label == 'bar' + assert ingredient_count_constraint.label == "bar" def test_label_fraction_initialization(label_fraction_constraint): """Make sure the correct fields go to the correct places.""" assert label_fraction_constraint.formulation_descriptor == formulation_descriptor - assert label_fraction_constraint.label == 'bar' + assert label_fraction_constraint.label == "bar" assert label_fraction_constraint.min == 0.0 assert label_fraction_constraint.max == 1.0 assert not label_fraction_constraint.is_required @@ -150,7 +138,7 @@ def test_ingredient_ratio_interaction(ingredient_ratio_constraint): with pytest.raises(ValueError): ingredient_ratio_constraint.ingredient = ("foo", 2, "bar", 4) with pytest.raises(ValueError): - ingredient_ratio_constraint.ingredient = ("foo", ) + ingredient_ratio_constraint.ingredient = ("foo",) with pytest.raises(TypeError): ingredient_ratio_constraint.ingredient = ("foo", "yup") with pytest.raises(ValueError): @@ -167,7 +155,7 @@ def test_ingredient_ratio_interaction(ingredient_ratio_constraint): with pytest.raises(ValueError): ingredient_ratio_constraint.label = ("foolabel", 2, "barlabel", 4) with pytest.raises(ValueError): - ingredient_ratio_constraint.label = ("foolabel", ) + ingredient_ratio_constraint.label = ("foolabel",) with pytest.raises(TypeError): ingredient_ratio_constraint.label = ("foolabel", "yup") with pytest.raises(ValueError): @@ -197,8 +185,10 @@ def test_range_defaults(): assert ScalarRangeConstraint(descriptor_key="x").lower_inclusive is True assert ScalarRangeConstraint(descriptor_key="x").upper_inclusive is True - assert ScalarRangeConstraint(descriptor_key="x", upper_inclusive=False).upper_inclusive is False - assert ScalarRangeConstraint(descriptor_key="x", lower_inclusive=False).lower_inclusive is False + upper_exclusive = ScalarRangeConstraint(descriptor_key="x", upper_inclusive=False) + assert upper_exclusive.upper_inclusive is False + lower_exclusive = ScalarRangeConstraint(descriptor_key="x", lower_inclusive=False) + assert lower_exclusive.lower_inclusive is False assert ScalarRangeConstraint(descriptor_key="x", lower_bound=0).lower_bound == 0.0 assert ScalarRangeConstraint(descriptor_key="x", upper_bound=0).upper_bound == 0.0 diff --git a/tests/informatics/test_data_source.py b/tests/informatics/test_data_source.py index 823506698..848f914f1 100644 --- a/tests/informatics/test_data_source.py +++ b/tests/informatics/test_data_source.py @@ -1,23 +1,25 @@ """Tests for citrine.informatics.descriptors.""" + import uuid import pytest from citrine.informatics.data_sources import DataSource, GemTableDataSource, SnapshotDataSource -from citrine.informatics.descriptors import RealDescriptor -from citrine.resources.file_link import FileLink from citrine.resources.gemtables import GemTable - from tests.utils.factories import GemTableDataFactory -@pytest.fixture(params=[ - GemTableDataSource(table_id=uuid.uuid4(), table_version=1), - GemTableDataSource(table_id=uuid.uuid4(), table_version="2"), - SnapshotDataSource(snapshot_id=uuid.uuid4()) -]) + +@pytest.fixture( + params=[ + GemTableDataSource(table_id=uuid.uuid4(), table_version=1), + GemTableDataSource(table_id=uuid.uuid4(), table_version="2"), + SnapshotDataSource(snapshot_id=uuid.uuid4()), + ] +) def data_source(request): return request.param + def test_deser_from_parent(data_source): # Serialize and deserialize the descriptors, making sure they are round-trip serializable data = data_source.dump() @@ -41,12 +43,14 @@ def test_invalid_deser(): def test_data_source_id(data_source): assert data_source == DataSource.from_data_source_id(data_source.to_data_source_id()) + def test_from_gem_table(): table = GemTable.build(GemTableDataFactory()) data_source = GemTableDataSource.from_gemtable(table) assert data_source.table_id == table.uid assert data_source.table_version == table.version + def test_invalid_data_source_id(): with pytest.raises(ValueError): DataSource.from_data_source_id(f"Undefined::{uuid.uuid4()}") diff --git a/tests/informatics/test_descriptors.py b/tests/informatics/test_descriptors.py index 8c1744756..e6b846890 100644 --- a/tests/informatics/test_descriptors.py +++ b/tests/informatics/test_descriptors.py @@ -1,4 +1,5 @@ """Tests for citrine.informatics.descriptors.""" + import json import pytest @@ -6,15 +7,17 @@ from citrine.informatics.descriptors import * -@pytest.fixture(params=[ - RealDescriptor('alpha', lower_bound=0, upper_bound=100, units=""), - IntegerDescriptor('count', lower_bound=0, upper_bound=100), - ChemicalFormulaDescriptor('formula'), - MolecularStructureDescriptor("organic"), - CategoricalDescriptor("my categorical", categories=["a", "b"]), - CategoricalDescriptor("categorical", categories=["*"]), - FormulationDescriptor.hierarchical() -]) +@pytest.fixture( + params=[ + RealDescriptor("alpha", lower_bound=0, upper_bound=100, units=""), + IntegerDescriptor("count", lower_bound=0, upper_bound=100), + ChemicalFormulaDescriptor("formula"), + MolecularStructureDescriptor("organic"), + CategoricalDescriptor("my categorical", categories=["a", "b"]), + CategoricalDescriptor("categorical", categories=["*"]), + FormulationDescriptor.hierarchical(), + ] +) def descriptor(request): return request.param diff --git a/tests/informatics/test_design_candidate.py b/tests/informatics/test_design_candidate.py index 57124c814..bb935be45 100644 --- a/tests/informatics/test_design_candidate.py +++ b/tests/informatics/test_design_candidate.py @@ -1,4 +1,5 @@ """Tests for citrine.informatics.design_candidate.""" + from citrine.informatics.design_candidate import DesignVariable diff --git a/tests/informatics/test_design_spaces.py b/tests/informatics/test_design_spaces.py index 3ecd19210..ca08bfeb6 100644 --- a/tests/informatics/test_design_spaces.py +++ b/tests/informatics/test_design_spaces.py @@ -1,29 +1,39 @@ """Tests for citrine.informatics.design_spaces.""" + import uuid import pytest from citrine.informatics.constraints import IngredientCountConstraint from citrine.informatics.data_sources import DataSource, GemTableDataSource -from citrine.informatics.descriptors import FormulationDescriptor, RealDescriptor, \ - CategoricalDescriptor, IntegerDescriptor +from citrine.informatics.descriptors import ( + CategoricalDescriptor, + FormulationDescriptor, + IntegerDescriptor, + RealDescriptor, +) from citrine.informatics.design_spaces import * -from citrine.informatics.dimensions import ContinuousDimension, EnumeratedDimension, \ - IntegerDimension +from citrine.informatics.dimensions import ( + ContinuousDimension, + EnumeratedDimension, + IntegerDimension, +) @pytest.fixture def product_design_space() -> ProductDesignSpace: """Build a ProductDesignSpace for testing.""" - alpha = RealDescriptor('alpha', lower_bound=0, upper_bound=100, units="") - beta = IntegerDescriptor('beta', lower_bound=0, upper_bound=100) - gamma = CategoricalDescriptor('gamma', categories=['a', 'b', 'c']) + alpha = RealDescriptor("alpha", lower_bound=0, upper_bound=100, units="") + beta = IntegerDescriptor("beta", lower_bound=0, upper_bound=100) + gamma = CategoricalDescriptor("gamma", categories=["a", "b", "c"]) dimensions = [ ContinuousDimension(alpha, lower_bound=0, upper_bound=10), IntegerDimension(beta, lower_bound=0, upper_bound=10), - EnumeratedDimension(gamma, values=['a', 'c']) + EnumeratedDimension(gamma, values=["a", "c"]), ] - return ProductDesignSpace(name='my design space', description='does some things', dimensions=dimensions) + return ProductDesignSpace( + name="my design space", description="does some things", dimensions=dimensions + ) @pytest.fixture @@ -36,9 +46,7 @@ def formulation_design_space() -> FormulationDesignSpace: ingredients={"dog", "cat", "bird"}, labels={"canine": {"dog"}, "feline": {"cat"}}, untested_ingredients={"fish", "hamster"}, - constraints={ - IngredientCountConstraint(formulation_descriptor=desc, min=1, max=2) - } + constraints={IngredientCountConstraint(formulation_descriptor=desc, min=1, max=2)}, ) @@ -49,25 +57,23 @@ def hierarchical_design_space(material_node_definition) -> HierarchicalDesignSpa description="Does things in levels", root=material_node_definition, subspaces=[material_node_definition], - data_sources=[ - GemTableDataSource(table_id=uuid.uuid4(), table_version=2) - ] + data_sources=[GemTableDataSource(table_id=uuid.uuid4(), table_version=2)], ) @pytest.fixture def material_node_definition(formulation_design_space) -> MaterialNodeDefinition: - temp = RealDescriptor('temperature', lower_bound=0.0, upper_bound=1.0, units='') + temp = RealDescriptor("temperature", lower_bound=0.0, upper_bound=1.0, units="") temp_dimension = ContinuousDimension(temp, lower_bound=0.1, upper_bound=0.9) - color = CategoricalDescriptor('color', categories={'r', 'g', 'b'}) - color_dimension = EnumeratedDimension(color, values=['g', 'b']) + color = CategoricalDescriptor("color", categories={"r", "g", "b"}) + color_dimension = EnumeratedDimension(color, values=["g", "b"]) link = TemplateLink( material_template=uuid.uuid4(), process_template=uuid.uuid4(), material_template_name="Material Template Name", - process_template_name="Process Template Name" + process_template_name="Process Template Name", ) return MaterialNodeDefinition( @@ -76,18 +82,17 @@ def material_node_definition(formulation_design_space) -> MaterialNodeDefinition formulation_subspace=formulation_design_space, template_link=link, attributes=[temp_dimension, color_dimension], - display_name="Special Material" + display_name="Special Material", ) def test_formulation_initialization(formulation_design_space): """Make sure the correct fields go to the correct places.""" - assert formulation_design_space.name == 'Formulation DS' + assert formulation_design_space.name == "Formulation DS" assert formulation_design_space.ingredients == {"dog", "cat", "bird"} assert formulation_design_space.untested_ingredients == {"fish", "hamster"} # The untested split survives serialization. - assert set(formulation_design_space.dump()["untested_ingredients"]) \ - == {"fish", "hamster"} + assert set(formulation_design_space.dump()["untested_ingredients"]) == {"fish", "hamster"} def test_formulation_untested_ingredients_default(): @@ -98,19 +103,19 @@ def test_formulation_untested_ingredients_default(): description="Does formulations", formulation_descriptor=desc, ingredients={"dog"}, - constraints={IngredientCountConstraint(formulation_descriptor=desc, min=1, max=1)} + constraints={IngredientCountConstraint(formulation_descriptor=desc, min=1, max=1)}, ) assert ds.untested_ingredients is None def test_product_initialization(product_design_space): """Make sure the correct fields go to the correct places.""" - assert product_design_space.name == 'my design space' - assert product_design_space.description == 'does some things' + assert product_design_space.name == "my design space" + assert product_design_space.description == "does some things" assert len(product_design_space.dimensions) == 3 - assert product_design_space.dimensions[0].descriptor.key == 'alpha' - assert product_design_space.dimensions[1].descriptor.key == 'beta' - assert product_design_space.dimensions[2].descriptor.key == 'gamma' + assert product_design_space.dimensions[0].descriptor.key == "alpha" + assert product_design_space.dimensions[1].descriptor.key == "beta" + assert product_design_space.dimensions[2].descriptor.key == "gamma" def test_hierarchical_initialization(hierarchical_design_space): @@ -140,9 +145,9 @@ def test_data_source_build(valid_data_source_design_space_dict): def test_data_source_initialization(valid_data_source_design_space_dict): data = valid_data_source_design_space_dict data_source = DataSource.build(data["data_source"]) - ds = DataSourceDesignSpace(name=data["name"], - description=data["description"], - data_source=data_source) + ds = DataSourceDesignSpace( + name=data["name"], description=data["description"], data_source=data_source + ) assert ds.name == data["name"] assert ds.description == data["description"] assert ds.data_source.dump() == data["data_source"] diff --git a/tests/informatics/test_dimensions.py b/tests/informatics/test_dimensions.py index 1159f959f..337287b12 100644 --- a/tests/informatics/test_dimensions.py +++ b/tests/informatics/test_dimensions.py @@ -1,36 +1,43 @@ """Tests for citrine.informatics.dimensions.""" + import pytest -from citrine.informatics.descriptors import RealDescriptor, CategoricalDescriptor, \ - IntegerDescriptor -from citrine.informatics.dimensions import ContinuousDimension, EnumeratedDimension, \ - IntegerDimension +from citrine.informatics.descriptors import ( + CategoricalDescriptor, + IntegerDescriptor, + RealDescriptor, +) +from citrine.informatics.dimensions import ( + ContinuousDimension, + EnumeratedDimension, + IntegerDimension, +) @pytest.fixture def continuous_dimension() -> ContinuousDimension: """Build a ContinuousDimension.""" - alpha = RealDescriptor('alpha', lower_bound=0, upper_bound=100, units="") + alpha = RealDescriptor("alpha", lower_bound=0, upper_bound=100, units="") return ContinuousDimension(alpha, lower_bound=3, upper_bound=33) @pytest.fixture def enumerated_dimension() -> EnumeratedDimension: """Build an EnumeratedDimension.""" - color = CategoricalDescriptor('color', categories={'red', 'green', 'blue'}) - return EnumeratedDimension(color, values=['red', 'red', 'blue']) + color = CategoricalDescriptor("color", categories={"red", "green", "blue"}) + return EnumeratedDimension(color, values=["red", "red", "blue"]) def test_continuous_initialization(continuous_dimension): """Make sure the correct fields go to the correct places.""" - assert continuous_dimension.descriptor.key == 'alpha' + assert continuous_dimension.descriptor.key == "alpha" assert continuous_dimension.lower_bound == 3 assert continuous_dimension.upper_bound == 33 def test_continuous_bounds(): """Test bounds are assigned correctly, even when bounds are == 0""" - beta = RealDescriptor('beta', lower_bound=-10, upper_bound=10, units="") + beta = RealDescriptor("beta", lower_bound=-10, upper_bound=10, units="") lower_none = ContinuousDimension(beta, upper_bound=0) assert lower_none.lower_bound == -10 assert lower_none.upper_bound == 0 @@ -42,7 +49,7 @@ def test_continuous_bounds(): def test_integer_bounds(): """Test bounds are assigned correctly, even when bounds are == 0""" - beta = IntegerDescriptor('beta', lower_bound=-10, upper_bound=10) + beta = IntegerDescriptor("beta", lower_bound=-10, upper_bound=10) lower_none = IntegerDimension(beta, upper_bound=0) assert lower_none.lower_bound == -10 assert lower_none.upper_bound == 0 @@ -54,6 +61,6 @@ def test_integer_bounds(): def test_enumerated_initialization(enumerated_dimension): """Make sure the correct fields go to the correct places.""" - assert enumerated_dimension.descriptor.key == 'color' - assert enumerated_dimension.descriptor.categories == {'red', 'green', 'blue'} - assert enumerated_dimension.values == ['red', 'red', 'blue'] + assert enumerated_dimension.descriptor.key == "color" + assert enumerated_dimension.descriptor.categories == {"red", "green", "blue"} + assert enumerated_dimension.values == ["red", "red", "blue"] diff --git a/tests/informatics/test_informatics.py b/tests/informatics/test_informatics.py index 0e17e698d..e040ee9bf 100644 --- a/tests/informatics/test_informatics.py +++ b/tests/informatics/test_informatics.py @@ -1,52 +1,82 @@ import pytest +from citrine.informatics.constraints import ( + AcceptableCategoriesConstraint, + IngredientCountConstraint, + IngredientFractionConstraint, + IngredientRatioConstraint, + IntegerRangeConstraint, + LabelFractionConstraint, + ScalarRangeConstraint, +) from citrine.informatics.descriptors import FormulationDescriptor, FormulationKey -from citrine.informatics.constraints import ScalarRangeConstraint, AcceptableCategoriesConstraint, \ - IngredientCountConstraint, IngredientFractionConstraint, IngredientRatioConstraint, \ - LabelFractionConstraint, IntegerRangeConstraint -from citrine.informatics.design_spaces import ProductDesignSpace, FormulationDesignSpace +from citrine.informatics.design_spaces import FormulationDesignSpace, ProductDesignSpace from citrine.informatics.objectives import ScalarMaxObjective, ScalarMinObjective -from citrine.informatics.scores import LIScore, EIScore, EVScore +from citrine.informatics.scores import EIScore, EVScore, LIScore informatics_string_data = [ - (IngredientCountConstraint( - formulation_descriptor=FormulationDescriptor.hierarchical(), - min=0, max=1 - ), f""), - (IngredientFractionConstraint( - formulation_descriptor=FormulationDescriptor.hierarchical(), - ingredient='y', - min=0, - max=1 - ), f""), - (LabelFractionConstraint( - formulation_descriptor=FormulationDescriptor.hierarchical(), - label='y', - min=0, - max=1 - ), f""), - (ScalarRangeConstraint(descriptor_key='z'), ""), - (IntegerRangeConstraint(descriptor_key='w'), ""), - (AcceptableCategoriesConstraint(descriptor_key='x', acceptable_categories=[]), ""), - (IngredientRatioConstraint(formulation_descriptor=FormulationDescriptor('Flat Formulation'), min=0.0, max=1.0, ingredient=("x", 1.5), label=("x'", 0.5), basis_ingredients=["y", "z"], basis_labels=["y'", "z'"]), ""), - (ProductDesignSpace(name='my design space', description='does some things'), - ""), - (FormulationDesignSpace( - name='Formulation', - description='desc', - formulation_descriptor=FormulationDescriptor.hierarchical(), - ingredients={'y'}, - constraints=set(), - labels={} - ), ""), - (ScalarMaxObjective('z'), ""), - (ScalarMinObjective('z'), ""), + ( + IngredientCountConstraint( + formulation_descriptor=FormulationDescriptor.hierarchical(), min=0, max=1 + ), + f"", + ), + ( + IngredientFractionConstraint( + formulation_descriptor=FormulationDescriptor.hierarchical(), + ingredient="y", + min=0, + max=1, + ), + f"", + ), + ( + LabelFractionConstraint( + formulation_descriptor=FormulationDescriptor.hierarchical(), label="y", min=0, max=1 + ), + f"", + ), + (ScalarRangeConstraint(descriptor_key="z"), ""), + (IntegerRangeConstraint(descriptor_key="w"), ""), + ( + AcceptableCategoriesConstraint(descriptor_key="x", acceptable_categories=[]), + "", + ), + ( + IngredientRatioConstraint( + formulation_descriptor=FormulationDescriptor("Flat Formulation"), + min=0.0, + max=1.0, + ingredient=("x", 1.5), + label=("x'", 0.5), + basis_ingredients=["y", "z"], + basis_labels=["y'", "z'"], + ), + "", + ), + ( + ProductDesignSpace(name="my design space", description="does some things"), + "", + ), + ( + FormulationDesignSpace( + name="Formulation", + description="desc", + formulation_descriptor=FormulationDescriptor.hierarchical(), + ingredients={"y"}, + constraints=set(), + labels={}, + ), + "", + ), + (ScalarMaxObjective("z"), ""), + (ScalarMinObjective("z"), ""), (LIScore(objectives=[], baselines=[]), ""), (EIScore(objectives=[], baselines=[], constraints=[]), ""), (EVScore(objectives=[], constraints=[]), ""), ] -@pytest.mark.parametrize('obj,repr', informatics_string_data) +@pytest.mark.parametrize("obj,repr", informatics_string_data) def test_str_representation(obj, repr): assert str(obj) == repr diff --git a/tests/informatics/test_objectives.py b/tests/informatics/test_objectives.py index 56a61a0e8..b8926700f 100644 --- a/tests/informatics/test_objectives.py +++ b/tests/informatics/test_objectives.py @@ -1,4 +1,5 @@ """Tests for citrine.informatics.objectives.""" + import pytest from citrine.informatics.objectives import ScalarMaxObjective, ScalarMinObjective @@ -7,17 +8,13 @@ @pytest.fixture def scalar_max_objective() -> ScalarMaxObjective: """Build a ScalarMaxObjective.""" - return ScalarMaxObjective( - descriptor_key="z", - ) + return ScalarMaxObjective(descriptor_key="z") @pytest.fixture def scalar_min_objective() -> ScalarMinObjective: """Build a ScalarMinObjective.""" - return ScalarMinObjective( - descriptor_key="z", - ) + return ScalarMinObjective(descriptor_key="z") def test_scalar_max_initialization(scalar_max_objective): diff --git a/tests/informatics/test_predictor_evaluation_metrics.py b/tests/informatics/test_predictor_evaluation_metrics.py index 0e024f4d0..5fbffd2f8 100644 --- a/tests/informatics/test_predictor_evaluation_metrics.py +++ b/tests/informatics/test_predictor_evaluation_metrics.py @@ -1,21 +1,29 @@ """Tests for citrine.informatics.descriptors.""" + import json import logging import pytest + from citrine.informatics.predictor_evaluation_metrics import * -@pytest.fixture(params=[ - (RMSE(), "rmse", "RMSE"), - (RSquared(), "R^2", "R^2"), - (NDME(), "ndme", "NDME"), - (StandardRMSE(), "standardized_rmse", "Standardized RMSE"), - (PVA(), "predicted_vs_actual", "Predicted vs Actual"), - (F1(), "f1", "F1 Score"), - (AreaUnderROC(), "area_under_roc", "Area Under the ROC"), - (CoverageProbability(coverage_level=0.123), "coverage_probability_0.123", "Coverage Probability (0.123)") -]) +@pytest.fixture( + params=[ + (RMSE(), "rmse", "RMSE"), + (RSquared(), "R^2", "R^2"), + (NDME(), "ndme", "NDME"), + (StandardRMSE(), "standardized_rmse", "Standardized RMSE"), + (PVA(), "predicted_vs_actual", "Predicted vs Actual"), + (F1(), "f1", "F1 Score"), + (AreaUnderROC(), "area_under_roc", "Area Under the ROC"), + ( + CoverageProbability(coverage_level=0.123), + "coverage_probability_0.123", + "Coverage Probability (0.123)", + ), + ] +) def metric(request): return request.param diff --git a/tests/informatics/test_predictor_evaluation_result.py b/tests/informatics/test_predictor_evaluation_result.py index 0f12c522d..af038f816 100644 --- a/tests/informatics/test_predictor_evaluation_result.py +++ b/tests/informatics/test_predictor_evaluation_result.py @@ -1,10 +1,15 @@ """Tests for citrine.informatics.descriptors.""" + import json + import pytest + from citrine.informatics.predictor_evaluation_metrics import * -from citrine.informatics.predictor_evaluation_result import PredictorEvaluationResult, \ - PredictedVsActualRealPoint, \ - PredictedVsActualCategoricalPoint +from citrine.informatics.predictor_evaluation_result import ( + PredictedVsActualCategoricalPoint, + PredictedVsActualRealPoint, + PredictorEvaluationResult, +) from citrine.informatics.predictor_evaluator import CrossValidationEvaluator, HoldoutSetEvaluator @@ -35,7 +40,9 @@ def test_cv_serde(example_cv_result, example_cv_result_dict): def test_holdout_serde(example_holdout_result, example_holdout_result_dict): - round_trip = PredictorEvaluationResult.build(json.loads(json.dumps(example_holdout_result_dict))) + round_trip = PredictorEvaluationResult.build( + json.loads(json.dumps(example_holdout_result_dict)) + ) assert example_holdout_result.evaluator == round_trip.evaluator @@ -43,7 +50,7 @@ def test_ev_evaluator(example_cv_result, example_cv_evaluator_dict): args = example_cv_evaluator_dict del args["type"] expected = CrossValidationEvaluator(**args) - assert expected.responses == set(example_cv_evaluator_dict['responses']) + assert expected.responses == set(example_cv_evaluator_dict["responses"]) assert example_cv_result.evaluator == expected assert example_cv_result.evaluator != 0 # make sure eq does something for mismatched classes @@ -52,29 +59,34 @@ def test_holdout_set_evaluator(example_holdout_result, example_holdout_evaluator args = example_holdout_evaluator_dict del args["type"] expected = HoldoutSetEvaluator(**args) - assert expected.responses == set(example_holdout_evaluator_dict['responses']) + assert expected.responses == set(example_holdout_evaluator_dict["responses"]) assert example_holdout_result.evaluator == expected - assert example_holdout_result.evaluator != 0 # make sure eq does something for mismatched classes + # make sure eq does something for mismatched classes + assert example_holdout_result.evaluator != 0 def test_check_rmse(example_cv_result, example_rmse_metrics): - assert example_cv_result["saltiness"]["rmse"].mean == example_rmse_metrics["mean"] - assert example_cv_result["saltiness"][RMSE()].standard_error == example_rmse_metrics["standard_error"] - # check eq method does something - assert example_cv_result["saltiness"][RMSE()] != 0 + ex_cv_saltiness = example_cv_result["saltiness"] + assert ex_cv_saltiness["rmse"].mean == example_rmse_metrics["mean"] + assert ex_cv_saltiness[RMSE()].standard_error == example_rmse_metrics["standard_error"] + assert ex_cv_saltiness[RMSE()] != 0 # Verify eq method does something with pytest.raises(TypeError): - _ = example_cv_result["saltiness"][0] + _ = ex_cv_saltiness[0] def test_real_pva(example_cv_result, example_real_pva_metrics): args = example_real_pva_metrics["value"][0] expected = PredictedVsActualRealPoint.build(args) - assert example_cv_result["saltiness"]["predicted_vs_actual"][0].predicted == expected.predicted - assert next(iter(example_cv_result["saltiness"]["predicted_vs_actual"])).actual == expected.actual + pva = example_cv_result["saltiness"]["predicted_vs_actual"] + assert list(pva) == pva.value # __iter__ + assert pva[0].predicted == expected.predicted # __getitem__ + assert pva[0].actual == expected.actual def test_categorical_pva(example_cv_result, example_categorical_pva_metrics): args = example_categorical_pva_metrics["value"][0] expected = PredictedVsActualCategoricalPoint.build(args) - assert example_cv_result["salt?"]["predicted_vs_actual"][0].predicted == expected.predicted - assert next(iter(example_cv_result["salt?"]["predicted_vs_actual"])).actual == expected.actual + pva = example_cv_result["salt?"]["predicted_vs_actual"] + assert list(pva) == pva.value # __iter__ + assert pva[0].predicted == expected.predicted # __getitem__ + assert pva[0].actual == expected.actual diff --git a/tests/informatics/test_predictor_evaluations.py b/tests/informatics/test_predictor_evaluations.py index 1ac41b9ee..32247f520 100644 --- a/tests/informatics/test_predictor_evaluations.py +++ b/tests/informatics/test_predictor_evaluations.py @@ -2,17 +2,23 @@ import pytest -from citrine.informatics.executions.predictor_evaluation import PredictorEvaluation, PredictorEvaluationRequest, PredictorEvaluatorsResponse -from citrine.informatics.predictor_evaluator import CrossValidationEvaluator +from citrine._rest.resource import PredictorRef +from citrine.informatics.executions.predictor_evaluation import ( + PredictorEvaluation, + PredictorEvaluationRequest, + PredictorEvaluatorsResponse, +) from citrine.informatics.predictor_evaluation_metrics import NDME from citrine.informatics.predictor_evaluation_result import PredictorEvaluationResult -from citrine._rest.resource import PredictorRef +from citrine.informatics.predictor_evaluator import CrossValidationEvaluator from tests.utils.session import FakeCall, FakeSession @pytest.fixture def cross_validation_evaluator(): - yield CrossValidationEvaluator("foo", description="desc", responses={"dk"}, n_folds=2, n_trials=5, metrics={NDME()}) + yield CrossValidationEvaluator( + "foo", description="desc", responses={"dk"}, n_folds=2, n_trials=5, metrics={NDME()} + ) @pytest.fixture @@ -27,7 +33,11 @@ def predictor_evaluators_response(cross_validation_evaluator): @pytest.fixture def predictor_evaluation_request(cross_validation_evaluator, predictor_ref): - yield PredictorEvaluationRequest(evaluators=[cross_validation_evaluator], predictor_id=predictor_ref.uid, predictor_version=predictor_ref.version) + yield PredictorEvaluationRequest( + evaluators=[cross_validation_evaluator], + predictor_id=predictor_ref.uid, + predictor_version=predictor_ref.version, + ) @pytest.fixture @@ -37,8 +47,8 @@ def predictor_evaluation(cross_validation_evaluator, predictor_ref): evaluation.evaluators = [cross_validation_evaluator] evaluation.predictor_id = predictor_ref.uid evaluation.predictor_version = predictor_ref.version - evaluation.status = 'SUCCEEDED' - evaluation.status_description = 'COMPLETED' + evaluation.status = "SUCCEEDED" + evaluation.status_description = "COMPLETED" yield evaluation @@ -46,7 +56,9 @@ def test_predictor_evaluator_response(predictor_evaluators_response, cross_valid assert predictor_evaluators_response.evaluators == [cross_validation_evaluator] -def test_predictor_evaluator_request(predictor_evaluation_request, cross_validation_evaluator, predictor_ref): +def test_predictor_evaluator_request( + predictor_evaluation_request, cross_validation_evaluator, predictor_ref +): assert predictor_evaluation_request.evaluators == [cross_validation_evaluator] assert predictor_evaluation_request.predictor.dump() == predictor_ref.dump() @@ -56,8 +68,8 @@ def test_predictor_evaluation(predictor_evaluation, cross_validation_evaluator, assert predictor_evaluation.evaluator_names == [cross_validation_evaluator.name] assert predictor_evaluation.predictor_id == predictor_ref.uid assert predictor_evaluation.predictor_version == predictor_ref.version - assert predictor_evaluation.status == 'SUCCEEDED' - assert predictor_evaluation.status_description == 'COMPLETED' + assert predictor_evaluation.status == "SUCCEEDED" + assert predictor_evaluation.status_description == "COMPLETED" assert predictor_evaluation.status_detail == [] @@ -71,9 +83,9 @@ def test_results(predictor_evaluation, example_cv_result_dict): results = predictor_evaluation["Example Evaluator"] expected_call = FakeCall( - method='GET', - path=f'/projects/{predictor_evaluation.project_id}/predictor-evaluations/{predictor_evaluation.uid}/results', - params={"evaluator_name": "Example Evaluator"} + method="GET", + path=f"/projects/{predictor_evaluation.project_id}/predictor-evaluations/{predictor_evaluation.uid}/results", + params={"evaluator_name": "Example Evaluator"}, ) assert session.last_call == expected_call assert results.evaluator == PredictorEvaluationResult.build(example_cv_result_dict).evaluator diff --git a/tests/informatics/test_predictors.py b/tests/informatics/test_predictors.py index 6761cafe4..4efe1e608 100644 --- a/tests/informatics/test_predictors.py +++ b/tests/informatics/test_predictors.py @@ -1,66 +1,72 @@ """Tests for citrine.informatics.predictors.""" -import mock -import pytest + import uuid -from random import random +from unittest import mock + +import pytest from citrine.informatics.data_sources import GemTableDataSource -from citrine.informatics.descriptors import RealDescriptor, IntegerDescriptor, \ - MolecularStructureDescriptor, FormulationDescriptor, ChemicalFormulaDescriptor, \ - CategoricalDescriptor, FormulationKey +from citrine.informatics.descriptors import ( + CategoricalDescriptor, + ChemicalFormulaDescriptor, + FormulationDescriptor, + FormulationKey, + IntegerDescriptor, + MolecularStructureDescriptor, + RealDescriptor, +) +from citrine.informatics.design_candidate import DesignMaterial from citrine.informatics.predictors import * from citrine.informatics.predictors.single_predict_request import SinglePredictRequest from citrine.informatics.predictors.single_prediction import SinglePrediction -from citrine.informatics.design_candidate import DesignMaterial - from tests.utils.factories import FeatureEffectsResponseFactory from tests.utils.session import FakeCall, FakeSession - w = IntegerDescriptor("w", lower_bound=0, upper_bound=100) x = RealDescriptor("x", lower_bound=0, upper_bound=100, units="") y = RealDescriptor("y", lower_bound=0, upper_bound=100, units="") z = RealDescriptor("z", lower_bound=0, upper_bound=100, units="") -density = RealDescriptor('density', lower_bound=0, upper_bound=100, units='g/cm^3') -shear_modulus = RealDescriptor('Property~Shear modulus', lower_bound=0, upper_bound=100, units='GPa') -youngs_modulus = RealDescriptor('Property~Young\'s modulus', lower_bound=0, upper_bound=100, units='GPa') -poissons_ratio = RealDescriptor('Property~Poisson\'s ratio', lower_bound=-1, upper_bound=0.5, units='') -chain_type = CategoricalDescriptor('Chain Type', categories={'Gaussian Coil', 'Rigid Rod', 'Worm-like'}) +density = RealDescriptor("density", lower_bound=0, upper_bound=100, units="g/cm^3") +shear_modulus = RealDescriptor( + "Property~Shear modulus", lower_bound=0, upper_bound=100, units="GPa" +) +youngs_modulus = RealDescriptor( + "Property~Young's modulus", lower_bound=0, upper_bound=100, units="GPa" +) +poissons_ratio = RealDescriptor( + "Property~Poisson's ratio", lower_bound=-1, upper_bound=0.5, units="" +) +chain_type = CategoricalDescriptor( + "Chain Type", categories={"Gaussian Coil", "Rigid Rod", "Worm-like"} +) flat_formulation = FormulationDescriptor.flat() -water_quantity = RealDescriptor('water quantity', lower_bound=0, upper_bound=1, units="") -salt_quantity = RealDescriptor('salt quantity', lower_bound=0, upper_bound=1, units="") -data_source = GemTableDataSource(table_id=uuid.UUID('e5c51369-8e71-4ec6-b027-1f92bdc14762'), table_version=0) -formulation_data_source = GemTableDataSource(table_id=uuid.UUID('6894a181-81d2-4304-9dfa-a6c5b114d8bc'), table_version=0) +water_quantity = RealDescriptor("water quantity", lower_bound=0, upper_bound=1, units="") +salt_quantity = RealDescriptor("salt quantity", lower_bound=0, upper_bound=1, units="") +data_source = GemTableDataSource( + table_id=uuid.UUID("e5c51369-8e71-4ec6-b027-1f92bdc14762"), table_version=0 +) +formulation_data_source = GemTableDataSource( + table_id=uuid.UUID("6894a181-81d2-4304-9dfa-a6c5b114d8bc"), table_version=0 +) def build_predictor_data(instance): return dict( - name=instance.get("name"), - description=instance.get("description"), - instance=instance + name=instance.get("name"), description=instance.get("description"), instance=instance ) def build_predictor_entity(data): user = str(uuid.uuid4()) - time = '2020-04-23T15:46:26Z' + time = "2020-04-23T15:46:26Z" return dict( id=str(uuid.uuid4()), data=data, metadata=dict( - status=dict( - name='READY', - info=[] - ), - created=dict( - user=user, - time=time - ), - updated=dict( - user=user, - time=time - ) - ) + status=dict(name="READY", info=[]), + created=dict(user=user, time=time), + updated=dict(user=user, time=time), + ), ) @@ -71,7 +77,7 @@ def molecule_featurizer() -> MolecularStructureFeaturizer: description="description", input_descriptor=MolecularStructureDescriptor("SMILES"), features=["all"], - excludes=["standard"] + excludes=["standard"], ) @@ -83,37 +89,37 @@ def chemical_featurizer() -> ChemicalFormulaFeaturizer: input_descriptor=ChemicalFormulaDescriptor("formula"), features=["standard"], excludes=None, - powers=[1.0, 2.0] + powers=[1.0, 2.0], ) @pytest.fixture def auto_ml() -> AutoMLPredictor: return AutoMLPredictor( - name='AutoML Predictor', - description='Predicts z from inputs w and x', + name="AutoML Predictor", + description="Predicts z from inputs w and x", inputs=[w, x], - outputs=[z] + outputs=[z], ) @pytest.fixture def auto_ml_no_outputs() -> AutoMLPredictor: return AutoMLPredictor( - name='AutoML Predictor', - description='Predicts z from inputs w and x', + name="AutoML Predictor", + description="Predicts z from inputs w and x", inputs=[w, x], - outputs=[] + outputs=[], ) @pytest.fixture def auto_ml_multiple_outputs() -> AutoMLPredictor: return AutoMLPredictor( - name='AutoML Predictor', - description='Predicts z from inputs w and x', + name="AutoML Predictor", + description="Predicts z from inputs w and x", inputs=[w, x], - outputs=[z, y] + outputs=[z, y], ) @@ -121,10 +127,10 @@ def auto_ml_multiple_outputs() -> AutoMLPredictor: def graph_predictor(molecule_featurizer, auto_ml) -> GraphPredictor: """Build a GraphPredictor for testing.""" return GraphPredictor( - name='Graph predictor', - description='description', + name="Graph predictor", + description="description", predictors=[molecule_featurizer, auto_ml], - training_data=[data_source, formulation_data_source] + training_data=[data_source, formulation_data_source], ) @@ -132,30 +138,22 @@ def graph_predictor(molecule_featurizer, auto_ml) -> GraphPredictor: def expression_predictor() -> ExpressionPredictor: """Build an ExpressionPredictor for testing.""" return ExpressionPredictor( - name='Expression predictor', - description='Computes shear modulus from Youngs modulus and Poissons ratio', - expression='Y / (2 * (1 + v))', + name="Expression predictor", + description="Computes shear modulus from Youngs modulus and Poissons ratio", + expression="Y / (2 * (1 + v))", output=shear_modulus, - aliases={ - 'Y': youngs_modulus, - 'v': poissons_ratio - }) + aliases={"Y": youngs_modulus, "v": poissons_ratio}, + ) @pytest.fixture def ing_to_formulation_predictor() -> IngredientsToFormulationPredictor: """Build an IngredientsToFormulationPredictor for testing.""" return IngredientsToFormulationPredictor( - name='Ingredients to formulation predictor', - description='Constructs a mixture from ingredient quantities', - id_to_quantity={ - 'water': water_quantity, - 'salt': salt_quantity - }, - labels={ - 'solvent': {'water'}, - 'solute': {'salt'} - } + name="Ingredients to formulation predictor", + description="Constructs a mixture from ingredient quantities", + id_to_quantity={"water": water_quantity, "salt": salt_quantity}, + labels={"solvent": {"water"}, "solute": {"salt"}}, ) @@ -163,14 +161,14 @@ def ing_to_formulation_predictor() -> IngredientsToFormulationPredictor: def mean_property_predictor() -> MeanPropertyPredictor: """Build a mean property predictor for testing.""" return MeanPropertyPredictor( - name='Mean property predictor', - description='Computes mean ingredient properties', + name="Mean property predictor", + description="Computes mean ingredient properties", input_descriptor=flat_formulation, properties=[density, chain_type], p=2.5, impute_properties=True, - default_properties={'density': 1.0, 'Chain Type': 'Gaussian Coil'}, - label='solvent' + default_properties={"density": 1.0, "Chain Type": "Gaussian Coil"}, + label="solvent", ) @@ -178,8 +176,7 @@ def mean_property_predictor() -> MeanPropertyPredictor: def simple_mixture_predictor() -> SimpleMixturePredictor: """Build a simple mixture predictor for testing.""" return SimpleMixturePredictor( - name='Simple mixture predictor', - description='Computes mean ingredient properties', + name="Simple mixture predictor", description="Computes mean ingredient properties" ) @@ -187,10 +184,10 @@ def simple_mixture_predictor() -> SimpleMixturePredictor: def label_fractions_predictor() -> LabelFractionsPredictor: """Build a label fractions predictor for testing""" return LabelFractionsPredictor( - name='Label fractions predictor', - description='Compute relative proportions of labeled ingredients', + name="Label fractions predictor", + description="Compute relative proportions of labeled ingredients", input_descriptor=flat_formulation, - labels={'solvent'} + labels={"solvent"}, ) @@ -198,10 +195,10 @@ def label_fractions_predictor() -> LabelFractionsPredictor: def ingredient_fractions_predictor() -> IngredientFractionsPredictor: """Build a Ingredient Fractions predictor for testing.""" return IngredientFractionsPredictor( - name='Ingredient fractions predictor', - description='Computes total ingredient fractions', + name="Ingredient fractions predictor", + description="Computes total ingredient fractions", input_descriptor=flat_formulation, - ingredients={"Green Paste", "Blue Paste"} + ingredients={"Green Paste", "Blue Paste"}, ) @@ -211,7 +208,7 @@ def attribute_accumulation_predictor() -> AttributeAccumulationPredictor: name="Attribute accumulation predictor", description="Aid training", attributes=[x, y], - sequential=True + sequential=True, ) @@ -221,32 +218,34 @@ def test_simple_report(graph_predictor): # without a project or session, this should error assert graph_predictor.report is None session = mock.Mock() - session.get_resource.return_value = dict(status='OK', report=dict(descriptors=[], models=[]), uid=str(uuid.uuid4())) + session.get_resource.return_value = dict( + status="OK", report=dict(descriptors=[], models=[]), uid=str(uuid.uuid4()) + ) graph_predictor._session = session graph_predictor._project_id = uuid.uuid4() graph_predictor.uid = uuid.uuid4() graph_predictor.version = 2 assert graph_predictor.report is not None assert session.get_resource.call_count == 1 - assert graph_predictor.report.status == 'OK' + assert graph_predictor.report.status == "OK" def test_graph_initialization(graph_predictor): """Make sure the correct fields go to the correct places for a graph predictor.""" - assert graph_predictor.name == 'Graph predictor' - assert graph_predictor.description == 'description' + assert graph_predictor.name == "Graph predictor" + assert graph_predictor.description == "description" assert len(graph_predictor.predictors) == 2 assert graph_predictor.training_data == [data_source, formulation_data_source] - assert str(graph_predictor) == '' + assert str(graph_predictor) == "" def test_expression_initialization(expression_predictor): """Make sure the correct fields go to the correct places for an expression predictor.""" - assert expression_predictor.name == 'Expression predictor' - assert expression_predictor.output.key == 'Property~Shear modulus' - assert expression_predictor.expression == 'Y / (2 * (1 + v))' - assert expression_predictor.aliases == {'Y': youngs_modulus, 'v': poissons_ratio} - assert str(expression_predictor) == '' + assert expression_predictor.name == "Expression predictor" + assert expression_predictor.output.key == "Property~Shear modulus" + assert expression_predictor.expression == "Y / (2 * (1 + v))" + assert expression_predictor.aliases == {"Y": youngs_modulus, "v": poissons_ratio} + assert str(expression_predictor) == "" def test_molecule_featurizer(molecule_featurizer): @@ -259,13 +258,13 @@ def test_molecule_featurizer(molecule_featurizer): assert str(molecule_featurizer) == "" assert molecule_featurizer.dump() == { - 'name': 'Molecule featurizer', - 'description': 'description', - 'descriptor': {'descriptor_key': 'SMILES', 'type': 'Organic'}, - 'features': ['all'], - 'excludes': ['standard'], - 'type': 'MoleculeFeaturizer' - } + "name": "Molecule featurizer", + "description": "description", + "descriptor": {"descriptor_key": "SMILES", "type": "Organic"}, + "features": ["all"], + "excludes": ["standard"], + "type": "MoleculeFeaturizer", + } def test_chemical_featurizer(chemical_featurizer): @@ -279,15 +278,15 @@ def test_chemical_featurizer(chemical_featurizer): assert str(chemical_featurizer) == "" assert chemical_featurizer.dump() == { - 'name': 'Chemical featurizer', - 'description': 'description', - 'input': ChemicalFormulaDescriptor("formula").dump(), - 'features': ['standard'], - 'excludes': [], - 'powers': [1.0, 2.0], - 'type': 'ChemicalFormulaFeaturizer' + "name": "Chemical featurizer", + "description": "description", + "input": ChemicalFormulaDescriptor("formula").dump(), + "features": ["standard"], + "excludes": [], + "powers": [1.0, 2.0], + "type": "ChemicalFormulaFeaturizer", } - + chemical_featurizer.powers = [0.5, -1.0] assert chemical_featurizer.powers == [0.5, -1.0] @@ -296,75 +295,69 @@ def test_auto_ml(auto_ml): assert auto_ml.name == "AutoML Predictor" assert auto_ml.description == "Predicts z from inputs w and x" assert auto_ml.inputs == [w, x] - assert auto_ml.dump()['outputs'] == [z.dump()] + assert auto_ml.dump()["outputs"] == [z.dump()] assert str(auto_ml) == "" built = AutoMLPredictor.build(auto_ml.dump()) assert built.outputs == [z] - assert built.dump()['outputs'] == [z.dump()] + assert built.dump()["outputs"] == [z.dump()] def test_auto_ml_no_outputs(auto_ml_no_outputs): assert auto_ml_no_outputs.outputs == [] - assert auto_ml_no_outputs.dump()['outputs'] == [] + assert auto_ml_no_outputs.dump()["outputs"] == [] built = AutoMLPredictor.build(auto_ml_no_outputs.dump()) assert built.outputs == [] - assert built.dump()['outputs'] == [] + assert built.dump()["outputs"] == [] def test_auto_ml_estimators(): # Check an empty set is coerced to RF default - empty_aml = AutoMLPredictor( - name="", - description="", - inputs=[x], - outputs=[y], - estimators={} - ) + empty_aml = AutoMLPredictor(name="", description="", inputs=[x], outputs=[y], estimators={}) assert empty_aml.estimators == {AutoMLEstimator.RANDOM_FOREST} # Check passing invalid strings leads to an error with pytest.raises(ValueError): - AutoMLPredictor( - name="", - description="", - inputs=[x], - outputs=[y], - estimators={"pancakes"} - ) + AutoMLPredictor(name="", description="", inputs=[x], outputs=[y], estimators={"pancakes"}) def test_auto_ml_multiple_outputs(auto_ml_multiple_outputs): assert auto_ml_multiple_outputs.outputs == [z, y] - assert auto_ml_multiple_outputs.dump()['outputs'] == [z.dump(), y.dump()] + assert auto_ml_multiple_outputs.dump()["outputs"] == [z.dump(), y.dump()] built = AutoMLPredictor.build(auto_ml_multiple_outputs.dump()) assert built.outputs == [z, y] - assert built.dump()['outputs'] == [z.dump(), y.dump()] + assert built.dump()["outputs"] == [z.dump(), y.dump()] def test_ing_to_formulation_initialization(ing_to_formulation_predictor): """Make sure the correct fields go to the correct places for an ingredients to formulation predictor.""" - assert ing_to_formulation_predictor.name == 'Ingredients to formulation predictor' + assert ing_to_formulation_predictor.name == "Ingredients to formulation predictor" assert ing_to_formulation_predictor.output.key == FormulationKey.HIERARCHICAL.value - assert ing_to_formulation_predictor.id_to_quantity == {'water': water_quantity, 'salt': salt_quantity} - assert ing_to_formulation_predictor.labels == {'solvent': {'water'}, 'solute': {'salt'}} - expected_str = f'' + assert ing_to_formulation_predictor.id_to_quantity == { + "water": water_quantity, + "salt": salt_quantity, + } + assert ing_to_formulation_predictor.labels == {"solvent": {"water"}, "solute": {"salt"}} + expected_str = f"" assert str(ing_to_formulation_predictor) == expected_str def test_mean_property_initialization(mean_property_predictor): """Make sure the correct fields go to the correct places for a mean property predictor.""" - assert mean_property_predictor.name == 'Mean property predictor' + assert mean_property_predictor.name == "Mean property predictor" assert mean_property_predictor.input_descriptor.key == FormulationKey.FLAT.value assert mean_property_predictor.properties == [density, chain_type] assert mean_property_predictor.p == 2.5 assert mean_property_predictor.impute_properties == True - assert mean_property_predictor.default_properties == {'density': 1.0, 'Chain Type': 'Gaussian Coil'} - assert mean_property_predictor.label == 'solvent' - expected_str = '' + assert mean_property_predictor.default_properties == { + "density": 1.0, + "Chain Type": "Gaussian Coil", + } + assert mean_property_predictor.label == "solvent" + expected_str = "" assert str(mean_property_predictor) == expected_str @@ -381,44 +374,47 @@ def test_mean_property_round_robin(mean_property_predictor): def test_label_fractions_property_initialization(label_fractions_predictor): """Make sure the correct fields go to the correct places for a label fraction predictor.""" - assert label_fractions_predictor.name == 'Label fractions predictor' + assert label_fractions_predictor.name == "Label fractions predictor" assert label_fractions_predictor.input_descriptor.key == FormulationKey.FLAT.value - assert label_fractions_predictor.labels == {'solvent'} - expected_str = '' + assert label_fractions_predictor.labels == {"solvent"} + expected_str = "" assert str(label_fractions_predictor) == expected_str def test_simple_mixture_predictor_initialization(simple_mixture_predictor): """Make sure the correct fields go to the correct places for a simple mixture predictor.""" - assert simple_mixture_predictor.name == 'Simple mixture predictor' + assert simple_mixture_predictor.name == "Simple mixture predictor" assert simple_mixture_predictor.input_descriptor.key == FormulationKey.HIERARCHICAL.value assert simple_mixture_predictor.output_descriptor.key == FormulationKey.FLAT.value - expected_str = '' + expected_str = "" assert str(simple_mixture_predictor) == expected_str def test_ingredient_fractions_property_initialization(ingredient_fractions_predictor): """Make sure the correct fields go to the correct places for an ingredient fractions predictor.""" - assert ingredient_fractions_predictor.name == 'Ingredient fractions predictor' + assert ingredient_fractions_predictor.name == "Ingredient fractions predictor" assert ingredient_fractions_predictor.input_descriptor.key == FormulationKey.FLAT.value assert ingredient_fractions_predictor.ingredients == {"Green Paste", "Blue Paste"} - expected_str = '' + expected_str = "" assert str(ingredient_fractions_predictor) == expected_str def test_attribute_accumulation_property_initialization(attribute_accumulation_predictor): """Make sure the correct fields go to the correct places for an attribute accumulation predictor.""" - assert attribute_accumulation_predictor.name == 'Attribute accumulation predictor' + assert attribute_accumulation_predictor.name == "Attribute accumulation predictor" assert attribute_accumulation_predictor.attributes == [x, y] assert attribute_accumulation_predictor.sequential is True - expected_str = '' + expected_str = "" assert str(attribute_accumulation_predictor) == expected_str def test_status(graph_predictor, valid_graph_predictor_data): """Ensure we can check the status of predictor validation.""" # A locally built predictor should be "False" for all status checks - assert not graph_predictor.in_progress() and not graph_predictor.failed() and not graph_predictor.succeeded() + assert graph_predictor.in_progress() is False + assert graph_predictor.failed() is False + assert graph_predictor.succeeded() is False + # A deserialized predictor should have the correct status predictor = GraphPredictor.build(valid_graph_predictor_data) assert predictor.succeeded() and not predictor.in_progress() and not predictor.failed() @@ -431,13 +427,8 @@ def test_single_predict(graph_predictor): graph_predictor.uid = uuid.uuid4() graph_predictor.version = 2 material_data = { - 'vars': { - 'X': {'m': 1.1, 's': 0.1, 'type': 'R'}, - 'Y': {'m': 2.2, 's': 0.2, 'type': 'R'} - }, - 'identifiers': { - 'id': str(uuid.uuid4()) - } + "vars": {"X": {"m": 1.1, "s": 0.1, "type": "R"}, "Y": {"m": 2.2, "s": 0.2, "type": "R"}}, + "identifiers": {"id": str(uuid.uuid4())}, } material = DesignMaterial.build(material_data) request = SinglePredictRequest(uuid.uuid4(), list(), material) @@ -455,31 +446,37 @@ def test_feature_effects(graph_predictor): session = FakeSession() session.set_response(feature_effects_response) - + graph_predictor._session = session graph_predictor._project_id = uuid.uuid4() fe = graph_predictor.feature_effects - expected_path = f"/projects/{graph_predictor._project_id}/predictors/{graph_predictor.uid}" + \ - f"/versions/{graph_predictor.version}/shapley/query" - assert session.calls == [FakeCall(method='POST', path=expected_path, json={})] + expected_path = ( + f"/projects/{graph_predictor._project_id}/predictors/{graph_predictor.uid}" + + f"/versions/{graph_predictor.version}/shapley/query" + ) + assert session.calls == [FakeCall(method="POST", path=expected_path, json={})] assert fe.as_dict == feature_effects_as_dict def test_feature_effects_in_progress(graph_predictor): - feature_effects_response = FeatureEffectsResponseFactory(metadata__status="INPROGRESS", result=None) + feature_effects_response = FeatureEffectsResponseFactory( + metadata__status="INPROGRESS", result=None + ) session = FakeSession() session.set_response(feature_effects_response) - + graph_predictor._session = session graph_predictor._project_id = uuid.uuid4() fe = graph_predictor.feature_effects - expected_path = f"/projects/{graph_predictor._project_id}/predictors/{graph_predictor.uid}" + \ - f"/versions/{graph_predictor.version}/shapley/query" - assert session.calls == [FakeCall(method='POST', path=expected_path, json={})] + expected_path = ( + f"/projects/{graph_predictor._project_id}/predictors/{graph_predictor.uid}" + + f"/versions/{graph_predictor.version}/shapley/query" + ) + assert session.calls == [FakeCall(method="POST", path=expected_path, json={})] assert fe.outputs is None assert fe.as_dict == {} diff --git a/tests/informatics/test_reports.py b/tests/informatics/test_reports.py index 6b031c796..2628ddf93 100644 --- a/tests/informatics/test_reports.py +++ b/tests/informatics/test_reports.py @@ -1,6 +1,6 @@ """Tests reports initialization.""" -from citrine.informatics.reports import PredictorReport, ModelSummary, FeatureImportanceReport, Report -from citrine.informatics.descriptors import RealDescriptor + +from citrine.informatics.reports import PredictorReport, Report def test_status(valid_predictor_report_data): diff --git a/tests/informatics/test_scores.py b/tests/informatics/test_scores.py index 7a6cdc8b8..dec8c1ab3 100644 --- a/tests/informatics/test_scores.py +++ b/tests/informatics/test_scores.py @@ -1,35 +1,25 @@ """Tests for citrine.informatics.scores.""" + import pytest from citrine.informatics.constraints import ScalarRangeConstraint from citrine.informatics.objectives import ScalarMaxObjective -from citrine.informatics.scores import LIScore, EIScore, EVScore +from citrine.informatics.scores import EIScore, EVScore, LIScore @pytest.fixture def li_score() -> LIScore: """Build an LIScore.""" - return LIScore( - objectives=[ - ScalarMaxObjective( - descriptor_key="z" - ) - ], - baselines=[10.0] - ) + return LIScore(objectives=[ScalarMaxObjective(descriptor_key="z")], baselines=[10.0]) @pytest.fixture def ei_score() -> EIScore: """Build an EIScore.""" return EIScore( - objectives=[ - ScalarMaxObjective( - descriptor_key="x" - ) - ], + objectives=[ScalarMaxObjective(descriptor_key="x")], baselines=[1.0], - constraints=[ScalarRangeConstraint(descriptor_key='y', lower_bound=0.0, upper_bound=1.0)] + constraints=[ScalarRangeConstraint(descriptor_key="y", lower_bound=0.0, upper_bound=1.0)], ) @@ -37,34 +27,30 @@ def ei_score() -> EIScore: def ev_score() -> EVScore: """Build an MEVScore.""" return EVScore( - objectives=[ - ScalarMaxObjective( - descriptor_key="x" - ) - ], - constraints=[ScalarRangeConstraint(descriptor_key='y', lower_bound=0.0, upper_bound=1.0)] + objectives=[ScalarMaxObjective(descriptor_key="x")], + constraints=[ScalarRangeConstraint(descriptor_key="y", lower_bound=0.0, upper_bound=1.0)], ) def test_li_initialization(li_score): """Make sure the correct fields go to the correct places.""" assert isinstance(li_score.objectives[0], ScalarMaxObjective) - assert li_score.objectives[0].descriptor_key == 'z' + assert li_score.objectives[0].descriptor_key == "z" assert li_score.baselines == [10.0] assert li_score.constraints == [] def test_ei_initialization(ei_score): """Make sure the correct fields go to the correct places.""" - assert ei_score.objectives[0].descriptor_key == 'x' + assert ei_score.objectives[0].descriptor_key == "x" assert ei_score.baselines == [1.0] assert isinstance(ei_score.constraints[0], ScalarRangeConstraint) - assert ei_score.constraints[0].descriptor_key == 'y' + assert ei_score.constraints[0].descriptor_key == "y" def test_ev_initialization(ev_score): """Make sure the correct fields go to the correct places.""" - assert ev_score.objectives[0].descriptor_key == 'x' + assert ev_score.objectives[0].descriptor_key == "x" assert isinstance(ev_score.constraints[0], ScalarRangeConstraint) - assert ev_score.constraints[0].descriptor_key == 'y' + assert ev_score.constraints[0].descriptor_key == "y" assert "EVScore" in str(ev_score) diff --git a/tests/informatics/test_workflows.py b/tests/informatics/test_workflows.py index 7d5fcdcb1..4073aee5d 100644 --- a/tests/informatics/test_workflows.py +++ b/tests/informatics/test_workflows.py @@ -1,45 +1,47 @@ """Tests for citrine.informatics.workflows.""" -from multiprocessing.reduction import register -from uuid import uuid4, UUID + +from uuid import UUID, uuid4 import pytest -from citrine.informatics.design_candidate import DesignMaterial, DesignCandidate, ChemicalFormula, \ - MeanAndStd, TopCategories, Mixture, MolecularStructure +from citrine.informatics.design_candidate import ( + ChemicalFormula, + DesignCandidate, + DesignMaterial, + MeanAndStd, + Mixture, + MolecularStructure, + TopCategories, +) from citrine.informatics.executions import DesignExecution from citrine.informatics.predict_request import PredictRequest from citrine.informatics.workflows import DesignWorkflow from citrine.resources.design_execution import DesignExecutionCollection from citrine.resources.design_workflow import DesignWorkflowCollection - from tests.utils.factories import BranchDataFactory, DesignWorkflowDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture def branch_data(): return BranchDataFactory() + @pytest.fixture def session() -> FakeSession: return FakeSession() + @pytest.fixture def collection(session, branch_data) -> DesignWorkflowCollection: session.set_response(branch_data) - return DesignWorkflowCollection( - project_id=uuid4(), - session=session, - ) + return DesignWorkflowCollection(project_id=uuid4(), session=session) @pytest.fixture def execution_collection(session) -> DesignExecutionCollection: - return DesignExecutionCollection( - project_id=uuid4(), - session=session, - ) + return DesignExecutionCollection(project_id=uuid4(), session=session) PROJECT_ID = uuid4() @@ -49,13 +51,14 @@ def execution_collection(session) -> DesignExecutionCollection: def design_workflow(collection) -> DesignWorkflow: return collection.build(DesignWorkflowDataFactory(register=True)) + @pytest.fixture def design_execution(execution_collection, design_execution_dict) -> DesignExecution: return execution_collection.build(design_execution_dict) def test_d_workflow_str(design_workflow): - assert str(design_workflow) == f'' + assert str(design_workflow) == f"" def test_workflow_executions_with_project(design_workflow): @@ -63,28 +66,18 @@ def test_workflow_executions_with_project(design_workflow): def test_workflow_executions_without_project(): - workflow = DesignWorkflow( - name="workflow", - design_space_id=uuid4(), - predictor_id=uuid4() - ) + workflow = DesignWorkflow(name="workflow", design_space_id=uuid4(), predictor_id=uuid4()) with pytest.raises(AttributeError): workflow.design_executions def test_design_material(): values = { - "RealValue": MeanAndStd(mean=1.4,std=.3), - "Cat": TopCategories(probabilities={ - "Red": 0.85, - "Blue": 0.15 - }), - "Mixture": Mixture(quantities={ - "Water": 0.5, - "Active": 0.5 - }), + "RealValue": MeanAndStd(mean=1.4, std=0.3), + "Cat": TopCategories(probabilities={"Red": 0.85, "Blue": 0.15}), + "Mixture": Mixture(quantities={"Water": 0.5, "Active": 0.5}), "Formula": ChemicalFormula(formula="NaCl"), - "Organic": MolecularStructure(smiles="CCO") + "Organic": MolecularStructure(smiles="CCO"), } material = DesignMaterial(values=values) assert material.values == values @@ -93,30 +86,32 @@ def test_design_material(): def test_predict(design_workflow, design_execution, example_candidates): session = design_execution._session - candidate = DesignCandidate.build(example_candidates['response'][0]) + candidate = DesignCandidate.build(example_candidates["response"][0]) material_id = UUID("9953cc63-5d53-4d0a-884a-a9cff3b7de18") - predict_req = PredictRequest(material_id=material_id, - material=candidate.material, - created_from_id=candidate.uid, - identifiers=candidate.identifiers) + predict_req = PredictRequest( + material_id=material_id, + material=candidate.material, + created_from_id=candidate.uid, + identifiers=candidate.identifiers, + ) session.set_response(candidate.dump()) predict_response = design_execution.predict(predict_request=predict_req) assert session.num_calls == 1 expected_call = FakeCall( - method='POST', + method="POST", path=f"/projects/{design_execution.project_id}/design-workflows/{design_execution.workflow_id}" - + f"/executions/{design_execution.uid}/predict", + + f"/executions/{design_execution.uid}/predict", json={ - 'material_id': str(material_id), - 'identifiers': [], - 'material': candidate.material.dump(), - 'created_from_id': str(candidate.uid), - 'random_seed': None + "material_id": str(material_id), + "identifiers": [], + "material": candidate.material.dump(), + "created_from_id": str(candidate.uid), + "random_seed": None, }, - version="v1" + version="v1", ) assert expected_call == session.last_call diff --git a/tests/jobs/test_job_status.py b/tests/jobs/test_job_status.py index 7bbbba264..6e97b7211 100644 --- a/tests/jobs/test_job_status.py +++ b/tests/jobs/test_job_status.py @@ -1,14 +1,15 @@ -from citrine.jobs.job import JobStatus, JobStatusResponse, TaskNode import pytest -from tests.utils.factories import TaskNodeDataFactory, JobStatusResponseDataFactory +from citrine.jobs.job import JobStatus, JobStatusResponse, TaskNode +from tests.utils.factories import JobStatusResponseDataFactory, TaskNodeDataFactory + def test_status_response_status(): status_response = JobStatusResponse.build(JobStatusResponseDataFactory(failure=True)) assert status_response.status == JobStatus.FAILURE with pytest.raises(ValueError): - status_response.status = 'Failed' + status_response.status = "Failed" assert isinstance(status_response.status, JobStatus) with pytest.raises(ValueError): @@ -18,12 +19,13 @@ def test_status_response_status(): status_response.status = JobStatus.SUCCESS assert status_response.status == JobStatus.SUCCESS + def test_task_node_status(): status_response = TaskNode.build(TaskNodeDataFactory(failure=True)) assert status_response.status == JobStatus.FAILURE with pytest.raises(ValueError): - status_response.status = 'Failed' + status_response.status = "Failed" assert isinstance(status_response.status, JobStatus) status_response.status = JobStatus.SUCCESS diff --git a/tests/jobs/test_waiting.py b/tests/jobs/test_waiting.py index 62ce79f0d..90bb26735 100644 --- a/tests/jobs/test_waiting.py +++ b/tests/jobs/test_waiting.py @@ -1,22 +1,24 @@ """Tests waiting utilities""" -from datetime import datetime + import io -import mock -import pytest import sys import time +from datetime import datetime +from unittest import mock + +import pytest from citrine.informatics.executions.design_execution import DesignExecution from citrine.jobs.waiting import ( + ConditionTimeoutError, wait_for_asynchronous_object, wait_while_executing, wait_while_validating, - ConditionTimeoutError ) from citrine.resources.status_detail import StatusDetail -@mock.patch('time.sleep', return_value=None) +@mock.patch("time.sleep", return_value=None) def test_wait_while_validating(sleep_mock): captured_output = io.StringIO() sys.stdout = captured_output @@ -24,7 +26,9 @@ def test_wait_while_validating(sleep_mock): collection = mock.Mock() module = mock.Mock() statuses = mock.PropertyMock(side_effect=["VALIDATING", "VALID", "VALID"]) - status_detail = mock.PropertyMock(return_value=[StatusDetail(msg="The predictor is now validated.", level="Info")]) + status_detail = mock.PropertyMock( + return_value=[StatusDetail(msg="The predictor is now validated.", level="Info")] + ) in_progress = mock.PropertyMock(side_effect=[True, False, False]) type(module).status = statuses type(module).status_detail = status_detail @@ -33,15 +37,16 @@ def test_wait_while_validating(sleep_mock): wait_while_validating(collection=collection, module=module, print_status_info=True) - assert("Status = VALID" in captured_output.getvalue()) - assert("The predictor is now validated." in captured_output.getvalue()) + assert "Status = VALID" in captured_output.getvalue() + assert "The predictor is now validated." in captured_output.getvalue() + -@mock.patch('time.time') -@mock.patch('time.sleep', return_value=None) +@mock.patch("time.time") +@mock.patch("time.sleep", return_value=None) def test_wait_while_validating_timeout(sleep_mock, time_mock): time_mock.side_effect = [ time.mktime(datetime(2020, 10, 30).timetuple()), - time.mktime(datetime(2020, 10, 31).timetuple()) + time.mktime(datetime(2020, 10, 31).timetuple()), ] collection = mock.Mock() @@ -54,7 +59,8 @@ def test_wait_while_validating_timeout(sleep_mock, time_mock): with pytest.raises(ConditionTimeoutError): wait_while_validating(collection=collection, module=module, timeout=1.0) -@mock.patch('time.sleep', return_value=None) + +@mock.patch("time.sleep", return_value=None) def test_wait_while_executing(sleep_mock): captured_output = io.StringIO() sys.stdout = captured_output @@ -62,24 +68,28 @@ def test_wait_while_executing(sleep_mock): collection = mock.Mock() workflow_execution = mock.Mock(spec=DesignExecution) statuses = mock.PropertyMock(side_effect=["INPROGRESS", "SUCCEEDED", "SUCCEEDED"]) - status_detail = mock.PropertyMock(return_value=[StatusDetail(msg="Execution is complete.", level="Info")]) + status_detail = mock.PropertyMock( + return_value=[StatusDetail(msg="Execution is complete.", level="Info")] + ) in_progress = mock.PropertyMock(side_effect=[True, False, False]) type(workflow_execution).status = statuses type(workflow_execution).status_detail = status_detail workflow_execution.in_progress = in_progress collection.get.return_value = workflow_execution - wait_while_executing(collection=collection, - execution=workflow_execution, print_status_info=True) + wait_while_executing( + collection=collection, execution=workflow_execution, print_status_info=True + ) + + assert "SUCCEEDED" in captured_output.getvalue() - assert("SUCCEEDED" in captured_output.getvalue()) -@mock.patch('time.time') -@mock.patch('time.sleep', return_value=None) +@mock.patch("time.time") +@mock.patch("time.sleep", return_value=None) def test_wait_for_asynchronous_object(sleep_mock, time_mock): time_mock.side_effect = [ time.mktime(datetime(2021, 8, 1).timetuple()), - time.mktime(datetime(2021, 8, 2).timetuple()) + time.mktime(datetime(2021, 8, 2).timetuple()), ] resource = mock.Mock() @@ -89,5 +99,6 @@ def test_wait_for_asynchronous_object(sleep_mock, time_mock): with pytest.raises(ConditionTimeoutError) as exception: wait_for_asynchronous_object(collection=collection, resource=resource, timeout=1.0) - assert str(exception.value) == ("Timeout of 1.0 seconds reached, " - "but task 123456 is still in progress") + assert str(exception.value) == ( + "Timeout of 1.0 seconds reached, but task 123456 is still in progress" + ) diff --git a/tests/resources/test_analysis_workflow.py b/tests/resources/test_analysis_workflow.py index 8f234412b..507e6b5c8 100644 --- a/tests/resources/test_analysis_workflow.py +++ b/tests/resources/test_analysis_workflow.py @@ -6,7 +6,6 @@ from citrine.informatics.workflows.analysis_workflow import AnalysisWorkflow from citrine.resources.analysis_workflow import AnalysisWorkflowCollection - from tests.utils.factories import AnalysisWorkflowEntityDataFactory from tests.utils.session import FakeCall, FakeSession @@ -16,8 +15,8 @@ def paging_response(*items): def _assert_user_timestamp_equals_dict(user, time, ut_dict): - assert str(user) == ut_dict['user'] - assert time == datetime.fromtimestamp(ut_dict['time'] / 1000, tz=timezone.utc) + assert str(user) == ut_dict["user"] + assert time == datetime.fromtimestamp(ut_dict["time"] / 1000, tz=timezone.utc) def _assert_aw_plot_equals_dict(plot, plot_dict): @@ -26,34 +25,38 @@ def _assert_aw_plot_equals_dict(plot, plot_dict): def _assert_aw_equals_dict(aw, aw_dict): - assert str(aw.uid) == aw_dict['id'] - assert aw.name == aw_dict['data']['name'] - assert aw.description == aw_dict['data']['description'] - snapshot_id_dict = aw_dict['data'].get('snapshot_id') + assert str(aw.uid) == aw_dict["id"] + assert aw.name == aw_dict["data"]["name"] + assert aw.description == aw_dict["data"]["description"] + snapshot_id_dict = aw_dict["data"].get("snapshot_id") if snapshot_id_dict: assert str(aw.snapshot_id) == snapshot_id_dict else: assert aw.snapshot_id is None - _assert_user_timestamp_equals_dict(aw.created_by, aw.create_time, aw_dict['metadata']['created']) - _assert_user_timestamp_equals_dict(aw.updated_by, aw.update_time, aw_dict['metadata']['updated']) - - aw_dict_latest_build = aw_dict['metadata'].get('latest_build') or {} + _assert_user_timestamp_equals_dict( + aw.created_by, aw.create_time, aw_dict["metadata"]["created"] + ) + _assert_user_timestamp_equals_dict( + aw.updated_by, aw.update_time, aw_dict["metadata"]["updated"] + ) + + aw_dict_latest_build = aw_dict["metadata"].get("latest_build") or {} if aw_dict_latest_build: - assert aw.latest_build.status == aw_dict_latest_build['status'] - assert aw.latest_build.failures == aw_dict_latest_build['failure_reason'] - assert aw.status == aw_dict_latest_build['status'] + assert aw.latest_build.status == aw_dict_latest_build["status"] + assert aw.latest_build.failures == aw_dict_latest_build["failure_reason"] + assert aw.status == aw_dict_latest_build["status"] else: assert aw.latest_build is None - aw_dict_archived = aw_dict['metadata'].get('archived') or {} + aw_dict_archived = aw_dict["metadata"].get("archived") or {} if aw_dict_archived: _assert_user_timestamp_equals_dict(aw.archived_by, aw.archive_time, aw_dict_archived) else: assert aw.archived_by is None assert aw.archive_time is None - for plot, plot_dict in zip(aw._plots, aw_dict['data'].get('plots')): + for plot, plot_dict in zip(aw._plots, aw_dict["data"].get("plots")): _assert_aw_plot_equals_dict(plot, plot_dict) @@ -61,52 +64,60 @@ def _assert_aw_equals_dict(aw, aw_dict): def session(): return FakeSession() + @pytest.fixture def team_id(): return uuid.uuid4() + @pytest.fixture def collection(session, team_id): return AnalysisWorkflowCollection(session, team_id=team_id) + @pytest.fixture def base_path(team_id): - return f'/teams/{team_id}/analysis-workflows' + return f"/teams/{team_id}/analysis-workflows" def test_register(session, collection, base_path): aw_data = AnalysisWorkflowEntityDataFactory(data__plot_count=3) session.set_response(aw_data) - aw_module = AnalysisWorkflow(**aw_data['data']) + aw_module = AnalysisWorkflow(**aw_data["data"]) aw = collection.register(aw_module) expected_payload = { - **aw_data['data'], - "plots": [plot['data'] for plot in aw_data['data']['plots']] + **aw_data["data"], + "plots": [plot["data"] for plot in aw_data["data"]["plots"]], } - assert session.calls == [FakeCall(method='POST', path=base_path, json=expected_payload)] + assert session.calls == [FakeCall(method="POST", path=base_path, json=expected_payload)] _assert_aw_equals_dict(aw, aw_data) def test_get(session, collection, base_path): aw_data = AnalysisWorkflowEntityDataFactory() session.set_response(aw_data) - - aw = collection.get(aw_data['id']) - assert session.calls == [FakeCall(method='GET', path=f'{base_path}/{aw_data["id"]}')] + aw = collection.get(aw_data["id"]) + + assert session.calls == [FakeCall(method="GET", path=f"{base_path}/{aw_data['id']}")] _assert_aw_equals_dict(aw, aw_data) def test_list_all(session, collection, base_path): - aw_data = [AnalysisWorkflowEntityDataFactory(metadata__is_archived=random.choice((True, False))) for _ in range(5)] + aw_data = [ + AnalysisWorkflowEntityDataFactory(metadata__is_archived=random.choice((True, False))) + for _ in range(5) + ] session.set_response(paging_response(*aw_data)) - + aws = list(collection.list_all()) - expected_call = FakeCall(method='GET', path=base_path, params={'page': 1, 'per_page': 20, 'include_archived': True}) + expected_call = FakeCall( + method="GET", path=base_path, params={"page": 1, "per_page": 20, "include_archived": True} + ) assert session.calls == [expected_call] assert len(aws) == len(aw_data) @@ -114,10 +125,14 @@ def test_list_all(session, collection, base_path): def test_list_archived(session, collection, base_path): aw_data = [AnalysisWorkflowEntityDataFactory(metadata__is_archived=False) for _ in range(3)] session.set_response(paging_response(*aw_data)) - + aws = list(collection.list_archived(per_page=50)) - expected_call = FakeCall(method='GET', path=base_path, params={'page': 1, 'per_page': 50, 'filter': "archived eq 'true'"}) + expected_call = FakeCall( + method="GET", + path=base_path, + params={"page": 1, "per_page": 50, "filter": "archived eq 'true'"}, + ) assert session.calls == [expected_call] assert len(aws) == len(aw_data) @@ -125,12 +140,20 @@ def test_list_archived(session, collection, base_path): def test_list(session, collection, base_path): aw_data = [AnalysisWorkflowEntityDataFactory(metadata__is_archived=False) for _ in range(3)] session.set_responses(paging_response(*aw_data[0:2]), paging_response(*aw_data[2:4])) - + aws = list(collection.list(per_page=2)) expected_calls = [ - FakeCall(method='GET', path=base_path, params={'page': 1, 'per_page': 2, 'filter': "archived eq 'false'"}), - FakeCall(method='GET', path=base_path, params={'page': 2, 'per_page': 2, 'filter': "archived eq 'false'"}) + FakeCall( + method="GET", + path=base_path, + params={"page": 1, "per_page": 2, "filter": "archived eq 'false'"}, + ), + FakeCall( + method="GET", + path=base_path, + params={"page": 2, "per_page": 2, "filter": "archived eq 'false'"}, + ), ] assert session.calls == expected_calls assert len(aws) == len(aw_data) @@ -139,43 +162,51 @@ def test_list(session, collection, base_path): def test_archive(session, collection, base_path): aw_data = AnalysisWorkflowEntityDataFactory(metadata__is_archived=True) session.set_response(aw_data) - - aw = collection.archive(aw_data['id']) - assert session.calls == [FakeCall(method='PUT', path=f'{base_path}/{aw_data["id"]}/archive', json={})] + aw = collection.archive(aw_data["id"]) + + assert session.calls == [ + FakeCall(method="PUT", path=f"{base_path}/{aw_data['id']}/archive", json={}) + ] _assert_aw_equals_dict(aw, aw_data) def test_restore(session, collection, base_path): aw_data = AnalysisWorkflowEntityDataFactory(metadata__is_archived=False) session.set_response(aw_data) - - aw = collection.restore(aw_data['id']) - assert session.calls == [FakeCall(method='PUT', path=f'{base_path}/{aw_data["id"]}/restore', json={})] + aw = collection.restore(aw_data["id"]) + + assert session.calls == [ + FakeCall(method="PUT", path=f"{base_path}/{aw_data['id']}/restore", json={}) + ] _assert_aw_equals_dict(aw, aw_data) def test_update(session, collection, base_path): aw_data = AnalysisWorkflowEntityDataFactory(metadata__is_archived=False) session.set_response(aw_data) - - name, description = aw_data['data']['name'], aw_data['data']['description'] - - aw = collection.update(aw_data['id'], name=name, description=description) + + name, description = aw_data["data"]["name"], aw_data["data"]["description"] + + aw = collection.update(aw_data["id"], name=name, description=description) expected_payload = {"name": name, "description": description} - assert session.calls == [FakeCall(method='PUT', path=f'{base_path}/{aw_data["id"]}', json=expected_payload)] + assert session.calls == [ + FakeCall(method="PUT", path=f"{base_path}/{aw_data['id']}", json=expected_payload) + ] _assert_aw_equals_dict(aw, aw_data) def test_rebuild(session, collection, base_path): aw_data = AnalysisWorkflowEntityDataFactory(data__has_snapshot=True, metadata__has_build=True) session.set_response(aw_data) - - aw = collection.rebuild(aw_data['id']) - assert session.calls == [FakeCall(method='PUT', path=f'{base_path}/{aw_data["id"]}/query/rerun', json={})] + aw = collection.rebuild(aw_data["id"]) + + assert session.calls == [ + FakeCall(method="PUT", path=f"{base_path}/{aw_data['id']}/query/rerun", json={}) + ] _assert_aw_equals_dict(aw, aw_data) diff --git a/tests/resources/test_api_error.py b/tests/resources/test_api_error.py index 6be413fc5..85ba4fb9f 100644 --- a/tests/resources/test_api_error.py +++ b/tests/resources/test_api_error.py @@ -1,51 +1,44 @@ import pytest -from citrine.resources.api_error import ApiError, ValidationError +from citrine.resources.api_error import ApiError def test_has_failure(): - error = ApiError.build({ - "code": 400, - "message": "you messed up", - "validation_errors": [ - {"failure_message": 'failure 1', "failure_id": 'fail.one'}, - {"failure_message": 'failure 2', "failure_id": 'fail.two'}, - {"failure_message": 'vague failure'}, - ] - }) - assert error.has_failure('fail.one') - assert error.has_failure('fail.two') - assert not error.has_failure('not.present') + error = ApiError.build( + { + "code": 400, + "message": "you messed up", + "validation_errors": [ + {"failure_message": "failure 1", "failure_id": "fail.one"}, + {"failure_message": "failure 2", "failure_id": "fail.two"}, + {"failure_message": "vague failure"}, + ], + } + ) + assert error.has_failure("fail.one") + assert error.has_failure("fail.two") + assert not error.has_failure("not.present") with pytest.raises(ValueError): error.has_failure(None) with pytest.raises(ValueError): - error.has_failure('') + error.has_failure("") def test_deserialization(): - msg = 'ya failed' + msg = "ya failed" missing_id = { - 'code': 400, - 'message': 'an error', - 'validation_errors': [ - { - 'failure_message': msg, - } - ] + "code": 400, + "message": "an error", + "validation_errors": [{"failure_message": msg}], } error = ApiError.build(missing_id) assert error.validation_errors[0].failure_message == msg with_id = { - 'code': 400, - 'message': 'an error', - 'validation_errors': [ - { - 'failure_message': msg, - 'failure_id': 'foo.id' - } - ] + "code": 400, + "message": "an error", + "validation_errors": [{"failure_message": msg, "failure_id": "foo.id"}], } error_with_id = ApiError.build(with_id) - assert error_with_id.validation_errors[0].failure_id == 'foo.id' + assert error_with_id.validation_errors[0].failure_id == "foo.id" diff --git a/tests/resources/test_audit_info.py b/tests/resources/test_audit_info.py index 8db1bba9f..6a6680321 100644 --- a/tests/resources/test_audit_info.py +++ b/tests/resources/test_audit_info.py @@ -1,19 +1,17 @@ from uuid import uuid4 -from datetime import datetime from citrine.resources.audit_info import AuditInfo def test_audit_info_str(): - audit_info_full = AuditInfo.build({ - "created_by": str(uuid4()), - "created_at": 1559933807392, - "updated_by": str(uuid4()), - "updated_at": 1559933807392 - }) - audit_info_part = AuditInfo.build({ - "created_by": str(uuid4()), - "created_at": 1559933807392 - }) - assert 'Updated by' in str(audit_info_full) and 'Created by' in str(audit_info_full) - assert 'Updated by' not in str(audit_info_part) and 'Created by' in str(audit_info_part) + audit_info_full = AuditInfo.build( + { + "created_by": str(uuid4()), + "created_at": 1559933807392, + "updated_by": str(uuid4()), + "updated_at": 1559933807392, + } + ) + audit_info_part = AuditInfo.build({"created_by": str(uuid4()), "created_at": 1559933807392}) + assert "Updated by" in str(audit_info_full) and "Created by" in str(audit_info_full) + assert "Updated by" not in str(audit_info_part) and "Created by" in str(audit_info_part) diff --git a/tests/resources/test_branch.py b/tests/resources/test_branch.py index c063c748a..38711396d 100644 --- a/tests/resources/test_branch.py +++ b/tests/resources/test_branch.py @@ -6,12 +6,19 @@ from citrine._rest.resource import PredictorRef from citrine.exceptions import NotFound -from citrine.resources.data_version_update import NextBranchVersionRequest, DataVersionUpdate, BranchDataUpdate from citrine.resources.branch import Branch, BranchCollection -from tests.utils.factories import BranchDataFactory, BranchRootDataFactory, \ - BranchDataFieldFactory, BranchMetadataFieldFactory, BranchDataUpdateFactory -from tests.utils.session import FakeSession, FakeCall, FakePaginatedSession - +from citrine.resources.data_version_update import ( + BranchDataUpdate, + DataVersionUpdate, + NextBranchVersionRequest, +) +from tests.utils.factories import ( + BranchDataFactory, + BranchDataFieldFactory, + BranchDataUpdateFactory, + BranchMetadataFieldFactory, +) +from tests.utils.session import FakeCall, FakePaginatedSession, FakeSession LATEST_VER = "latest" @@ -28,10 +35,7 @@ def paginated_session() -> FakePaginatedSession: @pytest.fixture def collection(session) -> BranchCollection: - return BranchCollection( - project_id=uuid.uuid4(), - session=session - ) + return BranchCollection(project_id=uuid.uuid4(), session=session) @pytest.fixture @@ -42,7 +46,7 @@ def branch_path(collection) -> str: def test_str(): name = "Test Branch name" branch = Branch(name=name) - assert str(branch) == f'' + assert str(branch) == f"" def test_branch_build(collection): @@ -57,17 +61,13 @@ def test_branch_build(collection): def test_branch_register(session, collection, branch_path): # Given root_id = str(uuid.uuid4()) - name = 'branch-name' + name = "branch-name" now = datetime.now(tz.UTC).replace(microsecond=0) now_ms = int(now.timestamp() * 1000) # ms since epoch - branch_data = BranchDataFactory(data=BranchDataFieldFactory(name=name), - metadata=BranchMetadataFieldFactory( - created={ - 'time': now_ms - }, - updated={ - 'time': now_ms - })) + branch_data = BranchDataFactory( + data=BranchDataFieldFactory(name=name), + metadata=BranchMetadataFieldFactory(created={"time": now_ms}, updated={"time": now_ms}), + ) session.set_response(branch_data) # When @@ -75,14 +75,7 @@ def test_branch_register(session, collection, branch_path): # Then assert session.num_calls == 1 - expected_call = FakeCall( - method='POST', - path=branch_path, - json={ - 'name': name - }, - version="v2" - ) + expected_call = FakeCall(method="POST", path=branch_path, json={"name": name}, version="v2") assert expected_call == session.last_call assert new_branch.uid is not None @@ -94,8 +87,8 @@ def test_branch_register(session, collection, branch_path): def test_branch_get(session, collection, branch_path): # Given branch_data = BranchDataFactory() - root_id = branch_data['metadata']['root_id'] - version = branch_data['metadata']['version'] + root_id = branch_data["metadata"]["root_id"] + version = branch_data["metadata"]["version"] session.set_response({"response": [branch_data]}) # When @@ -103,7 +96,11 @@ def test_branch_get(session, collection, branch_path): # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=branch_path, params={'page': 1, 'per_page': 1, 'root': root_id, 'version': version}) + assert session.last_call == FakeCall( + method="GET", + path=branch_path, + params={"page": 1, "per_page": 1, "root": root_id, "version": version}, + ) def test_branch_get_not_found(session, collection, branch_path): @@ -118,7 +115,7 @@ def test_branch_get_not_found(session, collection, branch_path): def test_branch_get_by_version_id(session, collection, branch_path): # Given branch_data = BranchDataFactory() - version_id = branch_data['id'] + version_id = branch_data["id"] session.set_response(branch_data) # When @@ -126,21 +123,23 @@ def test_branch_get_by_version_id(session, collection, branch_path): # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=f"{branch_path}/{version_id}") + assert session.last_call == FakeCall(method="GET", path=f"{branch_path}/{version_id}") def test_branch_list(session, collection, branch_path): # Given branch_count = 5 branches_data = BranchDataFactory.create_batch(branch_count) - session.set_response({'response': branches_data}) + session.set_response({"response": branches_data}) # When branches = list(collection.list()) # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=branch_path, params={'archived': False, '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 @@ -148,14 +147,16 @@ def test_branch_list_all(session, collection, branch_path): # Given branch_count = 5 branches_data = BranchDataFactory.create_batch(branch_count) - session.set_response({'response': branches_data}) + session.set_response({"response": branches_data}) # When branches = list(collection.list_all()) # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=branch_path, params={'per_page': 20, 'page': 1}) + assert session.last_call == FakeCall( + method="GET", path=branch_path, params={"per_page": 20, "page": 1} + ) def test_branch_delete(session, collection, branch_path): @@ -167,7 +168,7 @@ def test_branch_delete(session, collection, branch_path): # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='DELETE', path=f'{branch_path}/{branch_id}') + assert session.last_call == FakeCall(method="DELETE", path=f"{branch_path}/{branch_id}") def test_branch_update(session, collection, branch_path): @@ -181,15 +182,13 @@ def test_branch_update(session, collection, branch_path): # Then assert session.num_calls == 1 expected_call = FakeCall( - method='PUT', - path=f'{branch_path}/{branch_data["id"]}', - json={ - 'name': branch_data['data']['name'] - }, - version='v2' + method="PUT", + path=f"{branch_path}/{branch_data['id']}", + json={"name": branch_data["data"]["name"]}, + version="v2", ) assert session.last_call == expected_call - assert updated_branch.name == branch_data['data']['name'] + assert updated_branch.name == branch_data["data"]["name"] def test_branch_get_design_workflows(collection): @@ -214,23 +213,21 @@ def test_branch_get_design_workflows_no_project_id(session): def test_branch_archive(session, collection, branch_path): # Given branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=True)) - branch_id = branch_data['id'] - root_id = branch_data['metadata']['root_id'] - version = branch_data['metadata']['version'] + branch_id = branch_data["id"] + root_id = branch_data["metadata"]["root_id"] + version = branch_data["metadata"]["version"] branch_data_get_resp = {"response": [branch_data]} - branch_data_get_params = { - 'page': 1, 'per_page': 1, 'root': str(root_id), 'version': version - } + branch_data_get_params = {"page": 1, "per_page": 1, "root": str(root_id), "version": version} session.set_responses(branch_data_get_resp, branch_data) # When archived_branch = collection.archive(root_id=root_id, version=version) # Then - expected_path = f'{branch_path}/{branch_id}/archive' + expected_path = f"{branch_path}/{branch_id}/archive" assert session.calls == [ - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='PUT', path=expected_path, json={}) + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + FakeCall(method="PUT", path=expected_path, json={}), ] assert archived_branch.archived is True @@ -238,11 +235,14 @@ def test_branch_archive(session, collection, branch_path): def test_archive_version_omitted(session, collection, branch_path): # Given branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=True)) - branch_id = branch_data['id'] - root_id = branch_data['metadata']['root_id'] + branch_id = branch_data["id"] + root_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_id), 'version': LATEST_VER + "page": 1, + "per_page": 1, + "root": str(root_id), + "version": LATEST_VER, } session.set_responses(branch_data_get_resp, branch_data) @@ -250,10 +250,10 @@ def test_archive_version_omitted(session, collection, branch_path): archived_branch = collection.archive(root_id=root_id) # Then - expected_path = f'{branch_path}/{branch_id}/archive' + expected_path = f"{branch_path}/{branch_id}/archive" assert session.calls == [ - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='PUT', path=expected_path, json={}) + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + FakeCall(method="PUT", path=expected_path, json={}), ] assert archived_branch.archived is True @@ -261,23 +261,21 @@ def test_archive_version_omitted(session, collection, branch_path): def test_branch_restore(session, collection, branch_path): # Given branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=False)) - branch_id = branch_data['id'] - root_id = branch_data['metadata']['root_id'] - version = branch_data['metadata']['version'] + branch_id = branch_data["id"] + root_id = branch_data["metadata"]["root_id"] + version = branch_data["metadata"]["version"] branch_data_get_resp = {"response": [branch_data]} - branch_data_get_params = { - 'page': 1, 'per_page': 1, 'root': str(root_id), 'version': version - } + branch_data_get_params = {"page": 1, "per_page": 1, "root": str(root_id), "version": version} session.set_responses(branch_data_get_resp, branch_data) # When restored_branch = collection.restore(root_id=root_id, version=version) # Then - expected_path = f'{branch_path}/{branch_id}/restore' + expected_path = f"{branch_path}/{branch_id}/restore" assert session.calls == [ - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='PUT', path=expected_path, json={}) + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + FakeCall(method="PUT", path=expected_path, json={}), ] assert restored_branch.archived is False @@ -285,11 +283,14 @@ def test_branch_restore(session, collection, branch_path): def test_restore_version_omitted(session, collection, branch_path): # Given branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(archived=False)) - branch_id = branch_data['id'] - root_id = branch_data['metadata']['root_id'] + branch_id = branch_data["id"] + root_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_id), 'version': LATEST_VER + "page": 1, + "per_page": 1, + "root": str(root_id), + "version": LATEST_VER, } session.set_responses(branch_data_get_resp, branch_data) @@ -297,10 +298,10 @@ def test_restore_version_omitted(session, collection, branch_path): restored_branch = collection.restore(root_id=root_id) # Then - expected_path = f'{branch_path}/{branch_id}/restore' + expected_path = f"{branch_path}/{branch_id}/restore" assert session.calls == [ - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='PUT', path=expected_path, json={}) + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + FakeCall(method="PUT", path=expected_path, json={}), ] assert restored_branch.archived is False @@ -309,20 +310,26 @@ def test_branch_list_archived(session, collection, branch_path): # Given branch_count = 5 branches_data = BranchDataFactory.create_batch(branch_count) - session.set_response({'response': branches_data}) + session.set_response({"response": branches_data}) # When branches = list(collection.list_archived()) # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', path=branch_path, params={'archived': True, 'per_page': 20, 'page': 1}) + assert session.last_call == FakeCall( + method="GET", path=branch_path, params={"archived": True, "per_page": 20, "page": 1} + ) # Needed for coverage checks def test_branch_data_update_inits(): - data_updates = [DataVersionUpdate(current="gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::1", - latest="gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::2")] + 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)] branch_update = BranchDataUpdate(data_updates=data_updates, predictors=predictors) assert branch_update.data_updates[0].current == "gemd::16f91e7e-0214-4866-8d7f-a4d5c2125d2b::1" @@ -331,92 +338,112 @@ def test_branch_data_update_inits(): def test_branch_data_updates(session, collection, branch_path): # Given branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] - branch_id = branch_data['id'] + root_branch_id = branch_data["metadata"]["root_id"] + branch_id = branch_data["id"] expected_data_updates = BranchDataUpdateFactory() 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'] + "page": 1, + "per_page": 1, + "root": str(root_branch_id), + "version": branch_data["metadata"]["version"], } session.set_responses(branch_data_get_resp, expected_data_updates) # When - actual_data_updates = collection.data_updates(root_id=root_branch_id, version=branch_data['metadata']['version']) + actual_data_updates = collection.data_updates( + root_id=root_branch_id, version=branch_data["metadata"]["version"] + ) # Then - expected_path = f'{branch_path}/{branch_id}/data-version-updates-predictor' + expected_path = f"{branch_path}/{branch_id}/data-version-updates-predictor" assert session.calls == [ - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='GET', path=expected_path, version='v2') + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + 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) + expected_update = expected_data_updates["data_updates"][0] + actual_update = actual_data_updates.data_updates[0] + assert expected_update["current"] == actual_update.current + assert expected_update["latest"] == actual_update.latest + expected_predictor = expected_data_updates["predictors"][0] + actual_predictor = actual_data_updates.predictors[0] + assert expected_predictor["predictor_id"] == str(actual_predictor.uid) def test_data_updates_version_omitted(session, collection, branch_path): # Given branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] - branch_id = branch_data['id'] + root_branch_id = branch_data["metadata"]["root_id"] + branch_id = branch_data["id"] expected_data_updates = BranchDataUpdateFactory() 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'] + "page": 1, + "per_page": 1, + "root": str(root_branch_id), + "version": branch_data["metadata"]["version"], } session.set_responses(branch_data_get_resp, expected_data_updates) # When - actual_data_updates = collection.data_updates(root_id=root_branch_id, version=branch_data['metadata']['version']) + actual_data_updates = collection.data_updates( + root_id=root_branch_id, version=branch_data["metadata"]["version"] + ) # Then - expected_path = f'{branch_path}/{branch_id}/data-version-updates-predictor' + expected_path = f"{branch_path}/{branch_id}/data-version-updates-predictor" assert session.calls == [ - FakeCall(method='GET', path=branch_path, params=branch_data_get_params), - FakeCall(method='GET', path=expected_path, version='v2') + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + 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) - - + expected_update = expected_data_updates["data_updates"][0] + actual_update = actual_data_updates.data_updates[0] + assert expected_update["current"] == actual_update.current + assert expected_update["latest"] == actual_update.latest + expected_predictor = expected_data_updates["predictors"][0] + actual_predictor = actual_data_updates.predictors[0] + assert expected_predictor["predictor_id"] == str(actual_predictor.uid) def test_branch_next_version(session, collection, branch_path): # Given branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] + 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")] + 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 - branchv2 = collection.next_version(root_id=root_branch_id, branch_instructions=req, retrain_models=False) + branchv2 = collection.next_version( + root_id=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') + 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 @@ -428,12 +455,15 @@ def test_branch_data_updates_normal(session, collection, branch_path): 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'] + "page": 1, + "per_page": 1, + "root": str(root_branch_id), + "version": branch_data["metadata"]["version"], } session.set_response(branch_data_get_resp) - branch = collection.get(root_id=root_branch_id, version=branch_data['metadata']['version']) + branch = collection.get(root_id=root_branch_id, version=branch_data["metadata"]["version"]) data_updates = BranchDataUpdateFactory() v2branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(root_id=root_branch_id)) @@ -441,30 +471,34 @@ def test_branch_data_updates_normal(session, collection, branch_path): v2branch = collection.update_data(root_id=branch.root_id, version=branch.version) # 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') + 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=branch_path, params=branch_data_get_params), - 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 + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + 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 @@ -472,41 +506,49 @@ def test_branch_data_updates_normal(session, collection, branch_path): def test_branch_data_updates_latest(session, collection, branch_path): # Given branch_data = BranchDataFactory() - root_branch_id = branch_data['metadata']['root_id'] + 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'] + "page": 1, + "per_page": 1, + "root": str(root_branch_id), + "version": branch_data["metadata"]["version"], } session.set_response(branch_data_get_resp) - branch = collection.get(root_id=root_branch_id, version=branch_data['metadata']['version']) + branch = collection.get(root_id=root_branch_id, version=branch_data["metadata"]["version"]) data_updates = BranchDataUpdateFactory() v2branch_data = BranchDataFactory(metadata=BranchMetadataFieldFactory(root_id=root_branch_id)) session.set_responses(branch_data_get_resp, data_updates, v2branch_data) - v2branch = collection.update_data(root_id=branch.root_id, version=branch.version, use_existing=False, retrain_models=True) + v2branch = collection.update_data( + root_id=branch.root_id, version=branch.version, 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') + 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=branch_path, params=branch_data_get_params), - 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 + FakeCall(method="GET", path=branch_path, params=branch_data_get_params), + 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 @@ -517,7 +559,9 @@ def test_branch_data_updates_nochange(session, collection, branch_path): branch_data_get_resp = {"response": [branch_data]} session.set_response(branch_data_get_resp) - branch = collection.get(root_id=branch_data['metadata']['root_id'], version=branch_data['metadata']['version']) + branch = collection.get( + root_id=branch_data["metadata"]["root_id"], version=branch_data["metadata"]["version"] + ) data_updates = BranchDataUpdateFactory(data_updates=[], predictors=[]) session.set_responses(branch_data_get_resp, data_updates) diff --git a/tests/resources/test_catalyst.py b/tests/resources/test_catalyst.py index 854d58405..21c2b54ba 100644 --- a/tests/resources/test_catalyst.py +++ b/tests/resources/test_catalyst.py @@ -2,146 +2,136 @@ import pytest -from citrine.resources.catalyst import CatalystResource -from citrine.resources.user import User -from citrine.informatics.catalyst.assistant import (AssistantResponse, - AssistantResponseMessage, - AssistantResponseConfig, - AssistantResponseUnsupported, - AssistantResponseInputErrors, - AssistantResponseExecError) +from citrine.informatics.catalyst.assistant import ( + AssistantResponseConfig, + AssistantResponseExecError, + AssistantResponseInputErrors, + AssistantResponseMessage, + AssistantResponseUnsupported, +) from citrine.informatics.catalyst.insights import InsightsResponse from citrine.informatics.predictors.graph_predictor import GraphPredictor +from citrine.resources.catalyst import CatalystResource from tests.utils.factories import UserDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture def assistant_message_data(): return { - "type": "message", - "data": { - "message": "We found the following available variables that may be relevant:\n * AtomicPolarizability for MolecularStructure" - } + "type": "message", + "data": { + "message": "We found the following available variables that may be relevant:\n * AtomicPolarizability for MolecularStructure" + }, } @pytest.fixture def assistant_config_data(): - return { - "type": "modified-config", - "data": { - "config": { - "type": "Graph", - "name": "Graph Model for 6 outputs", - "description": "Default Graph Model generated from data inspection.", - "predictors": [ - { - "type": "MeanProperty", - "name": "Mean properties for all ingredients", - "description": "Mean ingredient properties for all atomic ingredients. Missing property data is imputed from the training set.", - "input": { - "type": "Formulation", - "descriptor_key": "Flat Formulation" - }, - "properties": [ - { - "type": "Real", - "descriptor_key": "AtomicPolarizability for MolecularStructure", - "units": "", - "lower_bound": 0, - "upper_bound": 1000000000 - }, - { - "type": "Real", - "descriptor_key": "Density", - "units": "gram / centimeter ** 3", - "lower_bound": 0, - "upper_bound": 100 - } - ], - "p": 1, - "impute_properties": True, - "training_data": [], - "default_properties": {}, - "label": None - }, - { - "name": "", - "description": "", - "expression": "MixTime*Temperature", - "output": { - "descriptor_key": "MixTime_Temperature", - "lower_bound": -1.7976931348623157e+308, - "upper_bound": 1.7976931348623157e+308, - "units": "", - "type": "Real" - }, - "aliases": { - "MixTime": { - "descriptor_key": "Mix~Time", - "lower_bound": 0.0, - "upper_bound": 10000.0, - "units": "second", - "type": "Real" - }, - "Temperature": { - "descriptor_key": "Mix~Temperature", - "lower_bound": 0.0, - "upper_bound": 1000.0000000000001, - "units": "degree_Celsius", - "type": "Real" - } - }, - "type": "AnalyticExpression" - } - ] - } - } - } + return { + "type": "modified-config", + "data": { + "config": { + "type": "Graph", + "name": "Graph Model for 6 outputs", + "description": "Default Graph Model generated from data inspection.", + "predictors": [ + { + "type": "MeanProperty", + "name": "Mean properties for all ingredients", + "description": "Mean ingredient properties for all atomic ingredients. Missing property data is imputed from the training set.", + "input": {"type": "Formulation", "descriptor_key": "Flat Formulation"}, + "properties": [ + { + "type": "Real", + "descriptor_key": "AtomicPolarizability for MolecularStructure", + "units": "", + "lower_bound": 0, + "upper_bound": 1000000000, + }, + { + "type": "Real", + "descriptor_key": "Density", + "units": "gram / centimeter ** 3", + "lower_bound": 0, + "upper_bound": 100, + }, + ], + "p": 1, + "impute_properties": True, + "training_data": [], + "default_properties": {}, + "label": None, + }, + { + "name": "", + "description": "", + "expression": "MixTime*Temperature", + "output": { + "descriptor_key": "MixTime_Temperature", + "lower_bound": -1.7976931348623157e308, + "upper_bound": 1.7976931348623157e308, + "units": "", + "type": "Real", + }, + "aliases": { + "MixTime": { + "descriptor_key": "Mix~Time", + "lower_bound": 0.0, + "upper_bound": 10000.0, + "units": "second", + "type": "Real", + }, + "Temperature": { + "descriptor_key": "Mix~Temperature", + "lower_bound": 0.0, + "upper_bound": 1000.0000000000001, + "units": "degree_Celsius", + "type": "Real", + }, + }, + "type": "AnalyticExpression", + }, + ], + } + }, + } + @pytest.fixture def assistant_unsupported_data(): return { - "type": "unsupported", - "data": { - "message": "Sorry, adding a featurizer is not currently supported. Please try again." - } + "type": "unsupported", + "data": { + "message": "Sorry, adding a featurizer is not currently supported. Please try again." + }, } @pytest.fixture def assistant_input_error_data(): return { - "type": "input-error", - "data": { - "request_dict": { - "question": "Is polarizability being considered?", - "config": "hello", - "language_model": "gpt-4-16k" + "type": "input-error", + "data": { + "request_dict": { + "question": "Is polarizability being considered?", + "config": "hello", + "language_model": "gpt-4-16k", + }, + "errors": [ + {"field": "config", "error": "Input should be a valid dictionary"}, + { + "field": "language_model", + "error": "Input should be 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-4' or 'gpt-4-32k'", + }, + ], }, - "errors": [ - { - "field": "config", - "error": "Input should be a valid dictionary" - }, - { - "field": "language_model", - "error": "Input should be 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-4' or 'gpt-4-32k'" - } - ] - } } @pytest.fixture def assistant_exec_error_data(): - return { - "type": "exec-error", - "data": { - "error": "An internal error occurred." - } - } + return {"type": "exec-error", "data": {"error": "An internal error occurred."}} @pytest.fixture @@ -190,14 +180,23 @@ def test_assistant_external_user(session, catalyst, external_user_data): catalyst.assistant("Test query", predictor=assistant_predictor) -def test_assistant_invalid_response(session, catalyst, internal_user_data, assistant_message_data, assistant_predictor): +def test_assistant_invalid_response( + session, catalyst, internal_user_data, assistant_message_data, assistant_predictor +): session.set_responses(internal_user_data, {**assistant_message_data, "type": "foo"}) with pytest.raises(ValueError): catalyst.assistant("Test query", predictor=assistant_predictor) -def test_assistant_message(session, catalyst, internal_user_data, assistant_message_data, assistant_predictor, assistant_predictor_data): +def test_assistant_message( + session, + catalyst, + internal_user_data, + assistant_message_data, + assistant_predictor, + assistant_predictor_data, +): session.set_responses(internal_user_data, assistant_message_data) query = "Test query" @@ -207,11 +206,11 @@ def test_assistant_message(session, catalyst, internal_user_data, assistant_mess "question": query, "config": assistant_predictor_data["data"]["instance"], "temperature": 0.0, - "language_model": "gpt-4" + "language_model": "gpt-4", } expected_calls = [ FakeCall(method="GET", path="/users/me"), - FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request) + FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request), ] assert isinstance(resp, AssistantResponseMessage) @@ -219,7 +218,14 @@ def test_assistant_message(session, catalyst, internal_user_data, assistant_mess assert resp.message == assistant_message_data["data"]["message"] -def test_assistant_config(session, catalyst, internal_user_data, assistant_config_data, assistant_predictor, assistant_predictor_data): +def test_assistant_config( + session, + catalyst, + internal_user_data, + assistant_config_data, + assistant_predictor, + assistant_predictor_data, +): assistant_config_data_orig = deepcopy(assistant_config_data) session.set_responses(internal_user_data, assistant_config_data) @@ -231,19 +237,28 @@ def test_assistant_config(session, catalyst, internal_user_data, assistant_confi "question": query, "config": assistant_predictor_data["data"]["instance"], "temperature": 0.0, - "language_model": "gpt-4" + "language_model": "gpt-4", } expected_calls = [ FakeCall(method="GET", path="/users/me"), - FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request) + FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request), ] assert isinstance(resp, AssistantResponseConfig) assert session.calls == expected_calls - assert resp.predictor.dump() == GraphPredictor.build(GraphPredictor.wrap_instance(assistant_config_data_orig["data"]["config"])).dump() + graph_dict = GraphPredictor.wrap_instance(assistant_config_data_orig["data"]["config"]) + assert GraphPredictor.build(graph_dict).dump() == resp.predictor.dump() -def test_assistant_unsupported(session, catalyst, internal_user_data, assistant_unsupported_data, assistant_predictor, assistant_predictor_data): + +def test_assistant_unsupported( + session, + catalyst, + internal_user_data, + assistant_unsupported_data, + assistant_predictor, + assistant_predictor_data, +): session.set_responses(internal_user_data, assistant_unsupported_data) query = "Test query" @@ -253,11 +268,11 @@ def test_assistant_unsupported(session, catalyst, internal_user_data, assistant_ "question": query, "config": assistant_predictor_data["data"]["instance"], "temperature": 0.0, - "language_model": "gpt-4" + "language_model": "gpt-4", } expected_calls = [ FakeCall(method="GET", path="/users/me"), - FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request) + FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request), ] assert isinstance(resp, AssistantResponseUnsupported) @@ -265,7 +280,14 @@ def test_assistant_unsupported(session, catalyst, internal_user_data, assistant_ assert resp.message == assistant_unsupported_data["data"]["message"] -def test_assistant_input_error(session, catalyst, internal_user_data, assistant_input_error_data, assistant_predictor, assistant_predictor_data): +def test_assistant_input_error( + session, + catalyst, + internal_user_data, + assistant_input_error_data, + assistant_predictor, + assistant_predictor_data, +): session.set_responses(internal_user_data, assistant_input_error_data) query = "Test query" @@ -275,11 +297,11 @@ def test_assistant_input_error(session, catalyst, internal_user_data, assistant_ "question": query, "config": assistant_predictor_data["data"]["instance"], "temperature": 0.0, - "language_model": "gpt-4" + "language_model": "gpt-4", } expected_calls = [ FakeCall(method="GET", path="/users/me"), - FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request) + FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request), ] assert isinstance(resp, AssistantResponseInputErrors) @@ -287,7 +309,14 @@ def test_assistant_input_error(session, catalyst, internal_user_data, assistant_ assert resp.dump()["data"]["errors"] == assistant_input_error_data["data"]["errors"] -def test_assistant_exec_error(session, catalyst, internal_user_data, assistant_exec_error_data, assistant_predictor, assistant_predictor_data): +def test_assistant_exec_error( + session, + catalyst, + internal_user_data, + assistant_exec_error_data, + assistant_predictor, + assistant_predictor_data, +): session.set_responses(internal_user_data, assistant_exec_error_data) query = "Test query" @@ -297,11 +326,11 @@ def test_assistant_exec_error(session, catalyst, internal_user_data, assistant_e "question": query, "config": assistant_predictor_data["data"]["instance"], "temperature": 0.0, - "language_model": "gpt-4" + "language_model": "gpt-4", } expected_calls = [ FakeCall(method="GET", path="/users/me"), - FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request) + FakeCall(method="POST", path="/catalyst/assistant", json=expected_assistant_request), ] assert isinstance(resp, AssistantResponseExecError) @@ -309,9 +338,7 @@ def test_assistant_exec_error(session, catalyst, internal_user_data, assistant_e assert resp.error == assistant_exec_error_data["data"]["error"] -def test_insights_internal_user( - session, catalyst, internal_user_data, insights_response_data -): +def test_insights_internal_user(session, catalyst, internal_user_data, insights_response_data): session.set_responses(internal_user_data, insights_response_data) query = "What are the applications of ABS plastic?" @@ -326,11 +353,7 @@ def test_insights_internal_user( } expected_calls = [ FakeCall(method="GET", path="/users/me"), - FakeCall( - method="POST", - path="/catalyst/documents/search", - json=expected_insights_request, - ), + FakeCall(method="POST", path="/catalyst/documents/search", json=expected_insights_request), ] assert session.calls == expected_calls diff --git a/tests/resources/test_data_concepts.py b/tests/resources/test_data_concepts.py index 60a4af9a3..da617f43d 100644 --- a/tests/resources/test_data_concepts.py +++ b/tests/resources/test_data_concepts.py @@ -1,24 +1,23 @@ from collections.abc import Iterator -from uuid import uuid4, UUID +from uuid import uuid4 import pytest - -from gemd.entity.dict_serializable import DictSerializable -from gemd.entity.template import ProcessTemplate as GEMDTemplate from gemd.entity.link_by_uid import LinkByUID from citrine.resources.audit_info import AuditInfo -from citrine.resources.data_concepts import DataConcepts, _make_link_by_uid, CITRINE_SCOPE, DataConceptsCollection +from citrine.resources.data_concepts import CITRINE_SCOPE, DataConcepts, _make_link_by_uid from citrine.resources.process_run import ProcessRun -from citrine.resources.process_spec import ProcessSpec, ProcessSpecCollection -from tests.utils.session import FakeCall, FakeSession +from citrine.resources.process_spec import ProcessSpec +from tests.utils.session import FakeCall -def run_noop_gemd_relation_search_test(search_for, search_with, collection, search_fn, per_page=100): +def run_noop_gemd_relation_search_test( + search_for, search_with, collection, search_fn, per_page=100 +): """Test that relation searches hit the correct endpoint.""" - collection.session.set_response({'contents': []}) - test_id = 'foo-id' - test_scope = 'foo-scope' + collection.session.set_response({"contents": []}) + test_id = "foo-id" + test_scope = "foo-scope" result = search_fn(LinkByUID(id=test_id, scope=test_scope)) if isinstance(result, Iterator): # evaluate iterator to make calls happen @@ -26,40 +25,47 @@ def run_noop_gemd_relation_search_test(search_for, search_with, collection, sear assert collection.session.num_calls == 1 assert collection.session.last_call == FakeCall( method="GET", - path="teams/{}/{}/{}/{}/{}".format(collection.team_id, search_with, test_scope, test_id, search_for), - params={"dataset_id": str(collection.dataset_id), "forward": True, "ascending": True, "per_page": per_page} + path=f"teams/{collection.team_id}/{search_with}/{test_scope}/{test_id}/{search_for}", + params={ + "dataset_id": str(collection.dataset_id), + "forward": True, + "ascending": True, + "per_page": per_page, + }, ) + def test_assign_audit_info(): """Test that audit_info can be injected with build but not set""" - assert ProcessSpec("Spec with no audit info").audit_info is None, \ + assert ProcessSpec("Spec with no audit info").audit_info is None, ( "Audit info should be None by default" + ) - audit_info_dict = {'created_by': str(uuid4()), 'created_at': 1560033807392} + audit_info_dict = {"created_by": str(uuid4()), "created_at": 1560033807392} audit_info_obj = AuditInfo.build(audit_info_dict) - sample_object = ProcessSpec.build({ - 'type': 'process_spec', - 'name': "A process spec", - "audit_info": audit_info_dict - }) + sample_object = ProcessSpec.build( + {"type": "process_spec", "name": "A process spec", "audit_info": audit_info_dict} + ) assert sample_object.audit_info == audit_info_obj, "Audit info should be built from a dict" - another_object = ProcessSpec.build({ - 'type': 'process_spec', 'name': "A process spec", "audit_info": audit_info_obj - }) + another_object = ProcessSpec.build( + {"type": "process_spec", "name": "A process spec", "audit_info": audit_info_obj} + ) assert another_object.audit_info == audit_info_obj, "Audit info should be built from an obj" with pytest.raises(AttributeError, match=r"can't set attribute|has no setter"): sample_object.audit_info = None with pytest.raises(ValueError, match=r"is not one of valid types.*audit_info"): - ProcessSpec.build({ - 'type': 'process_spec', - 'name': "A process spec", - "audit_info": "Created by me, yesterday" - }) + ProcessSpec.build( + { + "type": "process_spec", + "name": "A process spec", + "audit_info": "Created by me, yesterday", + } + ) def test_make_link_by_uid(): diff --git a/tests/resources/test_dataset.py b/tests/resources/test_dataset.py index 2a3c6a73c..a9e70b181 100644 --- a/tests/resources/test_dataset.py +++ b/tests/resources/test_dataset.py @@ -1,31 +1,31 @@ from collections import defaultdict -from os.path import basename from uuid import UUID, uuid4 import pytest +from gemd.demo.cake import get_demo_scope, get_template_scope, make_cake from gemd.entity.bounds.integer_bounds import IntegerBounds -from gemd.demo.cake import make_cake, get_demo_scope, get_template_scope -from gemd.util import recursive_flatmap, flatten +from gemd.util import flatten, recursive_flatmap from citrine.exceptions import NotFound -from citrine.resources.condition_template import ConditionTemplateCollection, ConditionTemplate +from citrine.resources.condition_template import ConditionTemplate, ConditionTemplateCollection from citrine.resources.dataset import DatasetCollection -from citrine.resources.gemd_resource import GEMDResourceCollection -from citrine.resources.material_run import MaterialRunCollection, MaterialRun -from citrine.resources.material_spec import MaterialSpecCollection, MaterialSpec -from citrine.resources.material_template import MaterialTemplateCollection, MaterialTemplate -from citrine.resources.measurement_run import MeasurementRunCollection, MeasurementRun +from citrine.resources.delete import _async_gemd_batch_delete +from citrine.resources.material_run import MaterialRun, MaterialRunCollection +from citrine.resources.material_spec import MaterialSpec, MaterialSpecCollection +from citrine.resources.material_template import MaterialTemplate, MaterialTemplateCollection +from citrine.resources.measurement_run import MeasurementRun, MeasurementRunCollection from citrine.resources.measurement_spec import MeasurementSpec, MeasurementSpecCollection -from citrine.resources.measurement_template import MeasurementTemplate, \ - MeasurementTemplateCollection -from citrine.resources.parameter_template import ParameterTemplateCollection, ParameterTemplate -from citrine.resources.process_run import ProcessRunCollection, ProcessRun -from citrine.resources.process_spec import ProcessSpecCollection, ProcessSpec -from citrine.resources.process_template import ProcessTemplateCollection, ProcessTemplate -from citrine.resources.property_template import PropertyTemplateCollection, PropertyTemplate +from citrine.resources.measurement_template import ( + MeasurementTemplate, + MeasurementTemplateCollection, +) +from citrine.resources.parameter_template import ParameterTemplate, ParameterTemplateCollection +from citrine.resources.process_run import ProcessRun, ProcessRunCollection +from citrine.resources.process_spec import ProcessSpec, ProcessSpecCollection +from citrine.resources.process_template import ProcessTemplate, ProcessTemplateCollection +from citrine.resources.property_template import PropertyTemplate, PropertyTemplateCollection from tests.utils.factories import DatasetDataFactory, DatasetFactory -from citrine.resources.delete import _async_gemd_batch_delete -from tests.utils.session import FakeSession, FakePaginatedSession, FakeCall +from tests.utils.session import FakeCall, FakePaginatedSession, FakeSession @pytest.fixture @@ -40,43 +40,42 @@ def paginated_session() -> FakePaginatedSession: @pytest.fixture def collection(session) -> DatasetCollection: - return DatasetCollection( - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session - ) + return DatasetCollection(team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), session=session) @pytest.fixture def paginated_collection(paginated_session) -> DatasetCollection: return DatasetCollection( - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=paginated_session + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), session=paginated_session ) @pytest.fixture(scope="function") def dataset(): - dataset = DatasetFactory(name='Test Dataset') - dataset.team_id = UUID('6b608f78-e341-422c-8076-35adc8828545') + dataset = DatasetFactory(name="Test Dataset") + dataset.team_id = UUID("6b608f78-e341-422c-8076-35adc8828545") dataset.uid = UUID("503d7bf6-8e2d-4d29-88af-257af0d4fe4a") dataset.session = FakeSession() return dataset + def test_register_dataset(collection, session): # Given - name = 'Test Dataset' - summary = 'testing summary' - description = 'testing description' + name = "Test Dataset" + summary = "testing summary" + description = "testing description" session.set_response(DatasetDataFactory(name=name, summary=summary, description=description)) # When - dataset = collection.register(DatasetFactory(name=name, summary=summary, description=description)) + dataset = collection.register( + DatasetFactory(name=name, summary=summary, description=description) + ) expected_call = FakeCall( - method='POST', - path='teams/{}/datasets'.format(collection.team_id), - json={'name': name, 'summary': summary, 'description': description} + method="POST", + path=f"teams/{collection.team_id}/datasets", + json={"name": name, "summary": summary, "description": description}, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -85,20 +84,33 @@ def test_register_dataset(collection, session): def test_register_dataset_with_idempotent_put(collection, session): # Given - name = 'Test Dataset' - summary = 'testing summary' - description = 'testing description' - unique_name = 'foo' - session.set_response(DatasetDataFactory(name=name, summary=summary, description=description, unique_name=unique_name)) + name = "Test Dataset" + summary = "testing summary" + description = "testing description" + unique_name = "foo" + session.set_response( + DatasetDataFactory( + name=name, summary=summary, description=description, unique_name=unique_name + ) + ) # When session.use_idempotent_dataset_put = True - dataset = collection.register(DatasetFactory(name=name, summary=summary, description=description, unique_name=unique_name)) + dataset = collection.register( + DatasetFactory( + name=name, summary=summary, description=description, unique_name=unique_name + ) + ) expected_call = FakeCall( - method='PUT', - path='teams/{}/datasets'.format(collection.team_id), - json={'name': name, 'summary': summary, 'description': description, 'unique_name': unique_name} + method="PUT", + path=f"teams/{collection.team_id}/datasets", + json={ + "name": name, + "summary": summary, + "description": description, + "unique_name": unique_name, + }, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -107,24 +119,22 @@ def test_register_dataset_with_idempotent_put(collection, session): def test_register_dataset_with_existing_id(collection, session): # Given - name = 'Test Dataset' - summary = 'testing summary' - description = 'testing description' + name = "Test Dataset" + summary = "testing summary" + description = "testing description" session.set_response(DatasetDataFactory(name=name, summary=summary, description=description)) # When - dataset = DatasetFactory(name=name, summary=summary, - description=description) + dataset = DatasetFactory(name=name, summary=summary, description=description) - ds_uid = UUID('cafebeef-e341-422c-8076-35adc8828545') + ds_uid = UUID("cafebeef-e341-422c-8076-35adc8828545") dataset.uid = ds_uid dataset = collection.register(dataset) expected_call = FakeCall( - method='PUT', - path='teams/{}/datasets/{}'.format(collection.team_id, ds_uid), - json={'name': name, 'summary': summary, 'description': description, - 'id': str(ds_uid)} + method="PUT", + path=f"teams/{collection.team_id}/datasets/{ds_uid}", + json={"name": name, "summary": summary, "description": description, "id": str(ds_uid)}, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -133,7 +143,7 @@ def test_register_dataset_with_existing_id(collection, session): def test_get_by_unique_name_with_single_result(collection, session): # Given - name = 'Test Dataset' + name = "Test Dataset" unique_name = "foo" session.set_response([DatasetDataFactory(name=name, unique_name=unique_name)]) @@ -142,8 +152,7 @@ def test_get_by_unique_name_with_single_result(collection, session): # Then expected_call = FakeCall( - method='GET', - path='teams/{}/datasets?unique_name={}'.format(collection.team_id, unique_name) + method="GET", path=f"teams/{collection.team_id}/datasets?unique_name={unique_name}" ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -188,15 +197,21 @@ def test_list_datasets(paginated_collection, paginated_session): # Then assert 3 == paginated_session.num_calls - expected_first_call = FakeCall(method='GET', path='teams/{}/datasets'.format(paginated_collection.team_id), - params={'per_page': 20, 'page': 1}) - expected_last_call = FakeCall(method='GET', path='teams/{}/datasets'.format(paginated_collection.team_id), - params={'page': 3, 'per_page': 20}) + expected_first_call = FakeCall( + method="GET", + path=f"teams/{paginated_collection.team_id}/datasets", + params={"per_page": 20, "page": 1}, + ) + expected_last_call = FakeCall( + method="GET", + path=f"teams/{paginated_collection.team_id}/datasets", + params={"page": 3, "per_page": 20}, + ) assert expected_first_call == paginated_session.calls[0] assert expected_last_call == paginated_session.last_call assert 50 == len(datasets) - expected_uids = [d['id'] for d in datasets_data] + expected_uids = [d["id"] for d in datasets_data] dataset_ids = [str(d.uid) for d in datasets] assert dataset_ids == expected_uids @@ -214,15 +229,21 @@ def test_list_datasets_infinite_loop_detect(paginated_collection, paginated_sess # Then assert 2 == paginated_session.num_calls # duplicate UID detected on the second call - expected_first_call = FakeCall(method='GET', path='teams/{}/datasets'.format(paginated_collection.team_id), - params={'per_page': batch_size, 'page': 1}) - expected_last_call = FakeCall(method='GET', path='teams/{}/datasets'.format(paginated_collection.team_id), - params={'page': 2, 'per_page': batch_size}) + expected_first_call = FakeCall( + method="GET", + path=f"teams/{paginated_collection.team_id}/datasets", + params={"per_page": batch_size, "page": 1}, + ) + expected_last_call = FakeCall( + method="GET", + path=f"teams/{paginated_collection.team_id}/datasets", + params={"page": 2, "per_page": batch_size}, + ) assert expected_first_call == paginated_session.calls[0] assert expected_last_call == paginated_session.last_call assert len(datasets) == batch_size - expected_uids = [d['id'] for d in datasets_data[0:batch_size]] + expected_uids = [d["id"] for d in datasets_data[0:batch_size]] dataset_ids = [str(d.uid) for d in datasets] assert dataset_ids == expected_uids @@ -236,8 +257,7 @@ def test_delete_dataset(collection, session, dataset): # Then assert 1 == session.num_calls - expected_call = FakeCall(method='DELETE', path='teams/{}/datasets/{}'.format( - collection.team_id, uid)) + expected_call = FakeCall(method="DELETE", path=f"teams/{collection.team_id}/datasets/{uid}") assert expected_call == session.last_call @@ -327,11 +347,11 @@ def test_gemd_posts(dataset): MeasurementRunCollection: MeasurementRun("foo"), PropertyTemplateCollection: PropertyTemplate("bar", bounds=IntegerBounds(0, 1)), ParameterTemplateCollection: ParameterTemplate("bar", bounds=IntegerBounds(0, 1)), - ConditionTemplateCollection: ConditionTemplate("bar", bounds=IntegerBounds(0, 1)) + ConditionTemplateCollection: ConditionTemplate("bar", bounds=IntegerBounds(0, 1)), } for collection, obj in expected.items(): - obj.name = 'This is my name' + obj.name = "This is my name" # Register the objects assert len(obj.uids) == 0 @@ -342,7 +362,7 @@ def test_gemd_posts(dataset): assert pair[1] == registered.uids[pair[0]] # Update the objects - registered.name = 'Name change!' + registered.name = "Name change!" updated = dataset.update(registered) assert registered.name == updated.name assert len(updated.uids) == 1 @@ -413,10 +433,12 @@ def test_register_all_iterable(dataset): del wet_dict[c.to_link(scope)] assert len(wet_dict) == 0, f"{len(wet_dict)} unmatched objects" + def test_batch_delete_malformed(session): with pytest.raises(TypeError): _async_gemd_batch_delete(id_list=[uuid4()], session=session, team_id=None, dataset_id=None) + def test_gemd_batch_delete(dataset): """Pass through to GEMDResourceCollection working.""" with pytest.raises(TypeError): @@ -427,25 +449,25 @@ def test_gemd_batch_delete(dataset): @pytest.mark.parametrize("remove_templates", [False, True]) def test_delete_contents(dataset, prompt_to_confirm, remove_templates): - job_resp = { - 'job_id': '1234' - } + job_resp = {"job_id": "1234"} failed_job_resp = { - 'job_type': 'batch_delete', - 'status': 'Success', - 'tasks': [], - 'output': { + "job_type": "batch_delete", + "status": "Success", + "tasks": [], + "output": { # Keep in mind this is a stringified JSON value. Eww. - 'failures': '[]' - } + "failures": "[]" + }, } session = dataset.session session.set_responses(job_resp, failed_job_resp) # When - del_resp = dataset.delete_contents(prompt_to_confirm=prompt_to_confirm, remove_templates=remove_templates) + del_resp = dataset.delete_contents( + prompt_to_confirm=prompt_to_confirm, remove_templates=remove_templates + ) # Then assert len(del_resp) == 0 @@ -453,36 +475,30 @@ def test_delete_contents(dataset, prompt_to_confirm, remove_templates): # Ensure we made the expected delete call path = f"teams/{dataset.team_id}/datasets/{dataset.uid}/contents" params = {"remove_templates": remove_templates} - expected_call = FakeCall( - method='DELETE', - path=path, - params=params - ) + expected_call = FakeCall(method="DELETE", path=path, params=params) assert len(session.calls) == 2 assert session.calls[0] == expected_call def test_delete_contents_ok(dataset, monkeypatch): - job_resp = { - 'job_id': '1234' - } + job_resp = {"job_id": "1234"} failed_job_resp = { - 'job_type': 'batch_delete', - 'status': 'Success', - 'tasks': [], - 'output': { + "job_type": "batch_delete", + "status": "Success", + "tasks": [], + "output": { # Keep in mind this is a stringified JSON value. Eww. - 'failures': '[]' - } + "failures": "[]" + }, } session = dataset.session session.set_responses(job_resp, failed_job_resp) - user_responses = iter(['bad user response', 'Y']) - monkeypatch.setattr('builtins.input', lambda: next(user_responses)) + user_responses = iter(["bad user response", "Y"]) + monkeypatch.setattr("builtins.input", lambda: next(user_responses)) # When del_resp = dataset.delete_contents(prompt_to_confirm=True) @@ -492,16 +508,16 @@ def test_delete_contents_ok(dataset, monkeypatch): # Ensure we made the expected delete call expected_call = FakeCall( - method='DELETE', - path='teams/{}/datasets/{}/contents'.format(dataset.team_id, dataset.uid), - params={"remove_templates": True} + method="DELETE", + path=f"teams/{dataset.team_id}/datasets/{dataset.uid}/contents", + params={"remove_templates": True}, ) assert len(session.calls) == 2 assert session.calls[0] == expected_call def test_delete_contents_abort(dataset, monkeypatch): - user_responses = iter(['N']) - monkeypatch.setattr('builtins.input', lambda: next(user_responses)) + user_responses = iter(["N"]) + monkeypatch.setattr("builtins.input", lambda: next(user_responses)) with pytest.raises(RuntimeError): dataset.delete_contents(prompt_to_confirm=True) diff --git a/tests/resources/test_default_labels.py b/tests/resources/test_default_labels.py index b6d8267b0..357088da9 100644 --- a/tests/resources/test_default_labels.py +++ b/tests/resources/test_default_labels.py @@ -2,6 +2,7 @@ from citrine.resources._default_labels import _inject_default_label_tags + @pytest.mark.parametrize( "original_tags, default_labels, expected", [ diff --git a/tests/resources/test_descriptors.py b/tests/resources/test_descriptors.py index b289025d4..7c76e2db1 100644 --- a/tests/resources/test_descriptors.py +++ b/tests/resources/test_descriptors.py @@ -1,32 +1,30 @@ -import pytest - from uuid import uuid4 from citrine.informatics.data_sources import GemTableDataSource from citrine.informatics.descriptors import MolecularStructureDescriptor, RealDescriptor -from citrine.informatics.predictors import MolecularStructureFeaturizer, GraphPredictor +from citrine.informatics.predictors import GraphPredictor, MolecularStructureFeaturizer from citrine.resources.descriptors import DescriptorMethods from tests.utils.session import FakeSession def test_from_predictor_responses(): session = FakeSession() - col = 'smiles' + col = "smiles" response_json = { - 'responses': [ # shortened sample response + "responses": [ # shortened sample response { - 'type': 'Real', - 'descriptor_key': 'khs.sNH3 KierHallSmarts for {}'.format(col), - 'units': '', - 'lower_bound': 0, - 'upper_bound': 1000000000 + "type": "Real", + "descriptor_key": f"khs.sNH3 KierHallSmarts for {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 + "type": "Real", + "descriptor_key": f"khs.dsN KierHallSmarts for {col}", + "units": "", + "lower_bound": 0, + "upper_bound": 1000000000, }, ] } @@ -37,71 +35,78 @@ def test_from_predictor_responses(): description="description", input_descriptor=MolecularStructureDescriptor(col), features=["all"], - excludes=["standard"] + excludes=["standard"], + ) + results = descriptors.from_predictor_responses( + predictor=featurizer, inputs=[MolecularStructureDescriptor(col)] ) - results = descriptors.from_predictor_responses(predictor=featurizer, inputs=[MolecularStructureDescriptor(col)]) 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['responses'] + key=r["descriptor_key"], + lower_bound=r["lower_bound"], + upper_bound=r["upper_bound"], + units=r["units"], + ) + for r in response_json["responses"] ] - assert session.last_call.path == '/projects/{}/material-descriptors/predictor-responses'\ - .format(descriptors.project_id) - assert session.last_call.method == 'POST' + url = f"/projects/{descriptors.project_id}/material-descriptors/predictor-responses" + assert session.last_call.path == url + assert session.last_call.method == "POST" graph = GraphPredictor( - name="Graph", - description="Contains a featurizer", - predictors=[featurizer] + name="Graph", description="Contains a featurizer", predictors=[featurizer] + ) + graph_results = descriptors.from_predictor_responses( + predictor=graph, inputs=[MolecularStructureDescriptor(col)] ) - graph_results = descriptors.from_predictor_responses(predictor=graph, inputs=[MolecularStructureDescriptor(col)]) assert graph_results == [ RealDescriptor( - key=r['descriptor_key'], - lower_bound=r['lower_bound'], - upper_bound=r['upper_bound'], - units=r['units'] - ) for r in response_json['responses'] + key=r["descriptor_key"], + lower_bound=r["lower_bound"], + upper_bound=r["upper_bound"], + units=r["units"], + ) + for r in response_json["responses"] ] def test_from_data_source(): session = FakeSession() - col = 'smiles' + col = "smiles" response_json = { - 'descriptors': [ # shortened sample response + "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": f"khs.sNH3 KierHallSmarts for {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 + "type": "Real", + "descriptor_key": f"khs.dsN KierHallSmarts for {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) + data_source = GemTableDataSource( + table_id="43357a66-3644-4959-8115-77b2630aca45", table_version=123 + ) results = 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'] + 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' + url = f"/projects/{descriptors.project_id}/material-descriptors/from-data-source" + assert session.last_call.path == url + assert session.last_call.method == "POST" diff --git a/tests/resources/test_design_executions.py b/tests/resources/test_design_executions.py index 4dc786212..6fd98eddb 100644 --- a/tests/resources/test_design_executions.py +++ b/tests/resources/test_design_executions.py @@ -1,12 +1,13 @@ -import pytest import uuid from copy import deepcopy from datetime import datetime +import pytest + from citrine.informatics.executions.design_execution import DesignExecution from citrine.resources.design_execution import DesignExecutionCollection from tests.utils.factories import MLIScoreFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -17,14 +18,14 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> DesignExecutionCollection: return DesignExecutionCollection( - project_id=uuid.uuid4(), - workflow_id=uuid.uuid4(), - session=session, + project_id=uuid.uuid4(), workflow_id=uuid.uuid4(), session=session ) @pytest.fixture -def workflow_execution(collection: DesignExecutionCollection, design_execution_dict) -> DesignExecution: +def workflow_execution( + collection: DesignExecutionCollection, design_execution_dict +) -> DesignExecution: return collection.build(design_execution_dict) @@ -63,7 +64,9 @@ def test_build_new_execution(collection, design_execution_dict): assert execution.status_detail -def test_trigger_workflow_execution(collection: DesignExecutionCollection, design_execution_dict, session): +def test_trigger_workflow_execution( + collection: DesignExecutionCollection, design_execution_dict, session +): # Given session.set_response(design_execution_dict) score = MLIScoreFactory() @@ -74,18 +77,19 @@ def test_trigger_workflow_execution(collection: DesignExecutionCollection, desig # Then assert str(actual_execution.uid) == design_execution_dict["id"] - expected_path = '/projects/{}/design-workflows/{}/executions'.format( - collection.project_id, - collection.workflow_id, + expected_path = ( + f"/projects/{collection.project_id}/design-workflows/{collection.workflow_id}/executions" ) assert session.last_call == FakeCall( - method='POST', + method="POST", path=expected_path, - json={'score': score.dump(), 'max_candidates': max_candidates} + json={"score": score.dump(), "max_candidates": max_candidates}, ) -def test_workflow_execution_results(workflow_execution: DesignExecution, session, example_candidates): +def test_workflow_execution_results( + workflow_execution: DesignExecution, session, example_candidates +): # Given session.set_response(example_candidates) @@ -93,15 +97,15 @@ def test_workflow_execution_results(workflow_execution: DesignExecution, session list(workflow_execution.candidates(per_page=4)) # Then - expected_path = '/projects/{}/design-workflows/{}/executions/{}/candidates'.format( - workflow_execution.project_id, - workflow_execution.workflow_id, - workflow_execution.uid, + expected_path = f"/projects/{workflow_execution.project_id}/design-workflows/{workflow_execution.workflow_id}/executions/{workflow_execution.uid}/candidates" + assert session.last_call == FakeCall( + method="GET", path=expected_path, params={"per_page": 4, "page": 1} ) - assert session.last_call == FakeCall(method='GET', path=expected_path, params={"per_page": 4, 'page': 1}) -def test_workflow_execution_hierarchical_results(workflow_execution: DesignExecution, session, example_hierarchical_candidates): +def test_workflow_execution_hierarchical_results( + workflow_execution: DesignExecution, session, example_hierarchical_candidates +): # Given session.set_response(example_hierarchical_candidates) @@ -109,35 +113,30 @@ def test_workflow_execution_hierarchical_results(workflow_execution: DesignExecu list(workflow_execution.hierarchical_candidates(per_page=4)) # Then - expected_path = '/projects/{}/design-workflows/{}/executions/{}/candidate-histories'.format( - workflow_execution.project_id, - workflow_execution.workflow_id, - workflow_execution.uid, + expected_path = f"/projects/{workflow_execution.project_id}/design-workflows/{workflow_execution.workflow_id}/executions/{workflow_execution.uid}/candidate-histories" + assert session.last_call == FakeCall( + method="GET", path=expected_path, params={"per_page": 4, "page": 1} ) - assert session.last_call == FakeCall(method='GET', path=expected_path, params={"per_page": 4, 'page': 1}) -def test_workflow_execution_results_pinned(workflow_execution: DesignExecution, session, example_candidates): +def test_workflow_execution_results_pinned( + workflow_execution: DesignExecution, session, example_candidates +): # Given pinned_by = uuid.uuid4() pinned_time = datetime.now() example_candidates_pinned = deepcopy(example_candidates) - example_candidates_pinned["response"][0]["pinned"] = { - "user": pinned_by, - "time": pinned_time - } + example_candidates_pinned["response"][0]["pinned"] = {"user": pinned_by, "time": pinned_time} session.set_response(example_candidates_pinned) # When candidates = list(workflow_execution.candidates(per_page=4)) # Then - expected_path = '/projects/{}/design-workflows/{}/executions/{}/candidates'.format( - workflow_execution.project_id, - workflow_execution.workflow_id, - workflow_execution.uid, + expected_path = f"/projects/{workflow_execution.project_id}/design-workflows/{workflow_execution.workflow_id}/executions/{workflow_execution.uid}/candidates" + assert session.last_call == FakeCall( + method="GET", path=expected_path, params={"per_page": 4, "page": 1} ) - assert session.last_call == FakeCall(method='GET', path=expected_path, params={"per_page": 4, 'page': 1}) assert candidates[0].pinned_by == pinned_by assert candidates[0].pinned_time == pinned_time @@ -147,11 +146,11 @@ def test_list(collection: DesignExecutionCollection, session): lst = list(collection.list(per_page=4)) assert len(lst) == 0 - expected_path = '/projects/{}/design-workflows/{}/executions'.format(collection.project_id, collection.workflow_id) + expected_path = ( + f"/projects/{collection.project_id}/design-workflows/{collection.workflow_id}/executions" + ) assert session.last_call == FakeCall( - method='GET', - path=expected_path, - params={"per_page": 4, 'page': 1} + method="GET", path=expected_path, params={"per_page": 4, "page": 1} ) diff --git a/tests/resources/test_design_space.py b/tests/resources/test_design_space.py index 30bb83351..25b075893 100644 --- a/tests/resources/test_design_space.py +++ b/tests/resources/test_design_space.py @@ -3,42 +3,38 @@ from copy import deepcopy from datetime import datetime, timezone -import mock import pytest -from citrine.exceptions import ModuleRegistrationFailedException, NotFound -from citrine.informatics.descriptors import RealDescriptor, FormulationKey -from citrine.informatics.design_spaces import DefaultDesignSpaceMode, DesignSpaceSettings, \ - DesignSubspace, HierarchicalDesignSpace, ProductDesignSpace, TopLevelDesignSpace +from citrine.informatics.descriptors import FormulationKey +from citrine.informatics.design_spaces import ( + DefaultDesignSpaceMode, + DesignSpaceSettings, + DesignSubspace, + HierarchicalDesignSpace, + ProductDesignSpace, + TopLevelDesignSpace, +) from citrine.resources.design_space import DesignSpaceCollection -from citrine.resources.status_detail import StatusDetail, StatusLevelEnum from tests.utils.session import FakeCall, FakeSession + def _ds_dict_to_response(ds_dict, status="CREATED"): - time = '2020-04-23T15:46:26Z' + time = "2020-04-23T15:46:26Z" return { "id": str(uuid.uuid4()), "data": { "name": ds_dict["name"], "description": ds_dict["description"], - "instance": ds_dict + "instance": ds_dict, }, "metadata": { - "created": { - "user": str(uuid.uuid4()), - "time": time - }, - "updated": { - "user": str(uuid.uuid4()), - "time": time - }, - "status": { - "name": status, - "detail": [] - } - } + "created": {"user": str(uuid.uuid4()), "time": time}, + "updated": {"user": str(uuid.uuid4()), "time": time}, + "status": {"name": status, "detail": []}, + }, } + def _ds_to_response(ds, status="CREATED"): return _ds_dict_to_response(ds.dump()["instance"], status) @@ -59,8 +55,11 @@ def test_design_space_build(valid_product_design_space_data): # Then assert str(design_space.uid) == design_space_id - assert design_space.name == valid_product_design_space_data["data"]["instance"]["name"] - assert design_space.dimensions[0].descriptor.key == valid_product_design_space_data["data"]["instance"]["dimensions"][0]["descriptor"]["descriptor_key"] + instance = valid_product_design_space_data["data"]["instance"] + assert design_space.name == instance["name"] + expected_key = instance["dimensions"][0]["descriptor"]["descriptor_key"] + actual_key = design_space.dimensions[0].descriptor.key + assert expected_key == actual_key def test_design_space_build_with_status_detail(valid_product_design_space_data): @@ -69,7 +68,9 @@ def test_design_space_build_with_status_detail(valid_product_design_space_data): status_detail_data = {("Info", "info_msg"), ("Warning", "warning msg"), ("Error", "error msg")} data = deepcopy(valid_product_design_space_data) - data["metadata"]["status"]["detail"] = [{"level": level, "msg": msg} for level, msg in status_detail_data] + data["metadata"]["status"]["detail"] = [ + {"level": level, "msg": msg} for level, msg in status_detail_data + ] # When design_space = collection.build(data) @@ -81,12 +82,12 @@ def test_design_space_build_with_status_detail(valid_product_design_space_data): def test_formulation_build(valid_formulation_design_space_data): design_space = DesignSubspace.build(valid_formulation_design_space_data) - assert design_space.name == 'formulation design space' - assert design_space.description == 'formulates some things' + assert design_space.name == "formulation design space" + assert design_space.description == "formulates some things" assert design_space.formulation_descriptor.key == FormulationKey.HIERARCHICAL.value - assert design_space.ingredients == {'foo'} - assert design_space.labels == {'bar': {'foo'}} - assert design_space.untested_ingredients == {'qux'} + assert design_space.ingredients == {"foo"} + assert design_space.labels == {"bar": {"foo"}} + assert design_space.untested_ingredients == {"qux"} assert len(design_space.constraints) == 1 assert design_space.resolution == 0.1 @@ -98,8 +99,8 @@ def test_unsupported_subspace_type(): def test_hierarchical_build(valid_hierarchical_design_space_data): dc = DesignSpaceCollection(uuid.uuid4(), None) hds = dc.build(valid_hierarchical_design_space_data) - assert hds.name == 'hierarchical design space' - assert hds.description == 'does things but in levels' + assert hds.name == "hierarchical design space" + assert hds.description == "does things but in levels" assert hds.root.formulation_subspace is not None assert hds.root.template_link is not None assert hds.root.display_name is not None @@ -119,15 +120,12 @@ def test_convert_to_hierarchical(valid_hierarchical_design_space_data): predictor_id = uuid.uuid4() dc.convert_to_hierarchical(uid=ds_id, predictor_id=predictor_id, predictor_version=2) - expected_payload = { - "predictor_id": str(predictor_id), - "predictor_version": 2 - } + expected_payload = {"predictor_id": str(predictor_id), "predictor_version": 2} expected_call = FakeCall( - method='POST', + method="POST", path=f"projects/{dc.project_id}/design-spaces/{ds_id}/convert-hierarchical", json=expected_payload, - version="v3" + version="v3", ) assert session.num_calls == 1 @@ -138,12 +136,9 @@ def test_convert_to_hierarchical(valid_hierarchical_design_space_data): def test_create_default(predictor_version, valid_product_design_space): session = FakeSession() session.set_response(valid_product_design_space.dump()) - + predictor_id = uuid.uuid4() - collection = DesignSpaceCollection( - project_id=uuid.uuid4(), - session=session - ) + collection = DesignSpaceCollection(project_id=uuid.uuid4(), session=session) expected_payload = DesignSpaceSettings( predictor_id=predictor_id, @@ -152,37 +147,38 @@ def test_create_default(predictor_version, valid_product_design_space): include_label_fraction_constraints=False, include_label_count_constraints=False, include_parameter_constraints=False, - mode=DefaultDesignSpaceMode.ATTRIBUTE + mode=DefaultDesignSpaceMode.ATTRIBUTE, ).dump() expected_call = FakeCall( - method='POST', + method="POST", path=f"projects/{collection.project_id}/design-spaces/default", json=expected_payload, - version="v3" + version="v3", ) - default_design_space = collection.create_default(predictor_id=predictor_id, predictor_version=predictor_version) + default_design_space = collection.create_default( + predictor_id=predictor_id, predictor_version=predictor_version + ) assert session.num_calls == 1 assert session.last_call == expected_call - + expected_response = {**valid_product_design_space.dump(), "settings": expected_payload} assert default_design_space.dump() == expected_response @pytest.mark.parametrize("predictor_version", (2, "1", "latest", None)) def test_create_default_hierarchical(predictor_version, valid_hierarchical_design_space_data): - valid_hierarchical_design_space = HierarchicalDesignSpace.build(valid_hierarchical_design_space_data) + valid_hierarchical_design_space = HierarchicalDesignSpace.build( + valid_hierarchical_design_space_data + ) session = FakeSession() session.set_response(valid_hierarchical_design_space.dump()) - + predictor_id = uuid.uuid4() - collection = DesignSpaceCollection( - project_id=uuid.uuid4(), - session=session - ) + collection = DesignSpaceCollection(project_id=uuid.uuid4(), session=session) expected_payload = DesignSpaceSettings( predictor_id=predictor_id, @@ -191,25 +187,25 @@ def test_create_default_hierarchical(predictor_version, valid_hierarchical_desig include_label_fraction_constraints=False, include_label_count_constraints=False, include_parameter_constraints=False, - mode=DefaultDesignSpaceMode.HIERARCHICAL + mode=DefaultDesignSpaceMode.HIERARCHICAL, ).dump() expected_call = FakeCall( - method='POST', + method="POST", path=f"projects/{collection.project_id}/design-spaces/default", json=expected_payload, - version="v3" + version="v3", ) default_design_space = collection.create_default( predictor_id=predictor_id, predictor_version=predictor_version, - mode=DefaultDesignSpaceMode.HIERARCHICAL + mode=DefaultDesignSpaceMode.HIERARCHICAL, ) assert session.num_calls == 1 assert session.last_call == expected_call - + expected_response = {**valid_hierarchical_design_space.dump(), "settings": expected_payload} assert default_design_space.dump() == expected_response @@ -218,18 +214,16 @@ def test_create_default_hierarchical(predictor_version, valid_hierarchical_desig @pytest.mark.parametrize("label_fractions", (True, False)) @pytest.mark.parametrize("label_count", (True, False)) @pytest.mark.parametrize("parameters", (True, False)) -def test_create_default_with_config(valid_product_design_space, ingredient_fractions, - label_fractions, label_count, parameters): +def test_create_default_with_config( + valid_product_design_space, ingredient_fractions, label_fractions, label_count, parameters +): session = FakeSession() session.set_response(valid_product_design_space.dump()) - + predictor_id = uuid.uuid4() predictor_version = random.randint(1, 10) - collection = DesignSpaceCollection( - project_id=uuid.uuid4(), - session=session - ) - + collection = DesignSpaceCollection(project_id=uuid.uuid4(), session=session) + expected_payload = DesignSpaceSettings( predictor_id=predictor_id, predictor_version=predictor_version, @@ -237,14 +231,14 @@ def test_create_default_with_config(valid_product_design_space, ingredient_fract include_label_fraction_constraints=label_fractions, include_label_count_constraints=label_count, include_parameter_constraints=parameters, - mode=DefaultDesignSpaceMode.ATTRIBUTE + mode=DefaultDesignSpaceMode.ATTRIBUTE, ).dump() expected_call = FakeCall( - method='POST', + method="POST", path=f"projects/{collection.project_id}/design-spaces/default", json=expected_payload, - version="v3" + version="v3", ) default_design_space = collection.create_default( @@ -253,12 +247,12 @@ def test_create_default_with_config(valid_product_design_space, ingredient_fract include_ingredient_fraction_constraints=ingredient_fractions, include_label_fraction_constraints=label_fractions, include_label_count_constraints=label_count, - include_parameter_constraints=parameters + include_parameter_constraints=parameters, ) assert session.num_calls == 1 assert session.last_call == expected_call - + expected_response = {**valid_product_design_space.dump(), "settings": expected_payload} assert default_design_space.dump() == expected_response @@ -267,54 +261,70 @@ def test_list_design_spaces(valid_product_design_space_data, valid_hierarchical_ # Given session = FakeSession() collection = DesignSpaceCollection(uuid.uuid4(), session) - session.set_response({ - 'response': [valid_product_design_space_data, valid_hierarchical_design_space_data] - }) + session.set_response( + {"response": [valid_product_design_space_data, valid_hierarchical_design_space_data]} + ) # When design_spaces = list(collection.list(per_page=20)) # Then - expected_call = FakeCall(method='GET', path='/projects/{}/design-spaces'.format(collection.project_id), - params={'per_page': 20, 'page': 1, 'archived': False}, version="v4") + expected_call = FakeCall( + method="GET", + path=f"/projects/{collection.project_id}/design-spaces", + params={"per_page": 20, "page": 1, "archived": False}, + version="v4", + ) assert 1 == session.num_calls, session.calls assert expected_call == session.calls[0] assert len(design_spaces) == 2 -def test_list_all_design_spaces(valid_product_design_space_data, valid_hierarchical_design_space_data): +def test_list_all_design_spaces( + valid_product_design_space_data, valid_hierarchical_design_space_data +): # Given session = FakeSession() collection = DesignSpaceCollection(uuid.uuid4(), session) - session.set_response({ - 'response': [valid_product_design_space_data, valid_hierarchical_design_space_data] - }) + session.set_response( + {"response": [valid_product_design_space_data, valid_hierarchical_design_space_data]} + ) # When design_spaces = list(collection.list_all(per_page=25)) # Then - expected_call = FakeCall(method='GET', path='/projects/{}/design-spaces'.format(collection.project_id), - params={'per_page': 25, 'page': 1}, version="v4") + expected_call = FakeCall( + method="GET", + path=f"/projects/{collection.project_id}/design-spaces", + params={"per_page": 25, "page": 1}, + version="v4", + ) assert 1 == session.num_calls, session.calls assert expected_call == session.calls[0] assert len(design_spaces) == 2 -def test_list_archived_design_spaces(valid_product_design_space_data, valid_hierarchical_design_space_data): +def test_list_archived_design_spaces( + valid_product_design_space_data, valid_hierarchical_design_space_data +): # Given session = FakeSession() collection = DesignSpaceCollection(uuid.uuid4(), session) - session.set_response({ - 'response': [valid_product_design_space_data, valid_hierarchical_design_space_data] - }) + session.set_response( + {"response": [valid_product_design_space_data, valid_hierarchical_design_space_data]} + ) # When design_spaces = list(collection.list_archived(per_page=25)) # Then - expected_call = FakeCall(method='GET', path='/projects/{}/design-spaces'.format(collection.project_id), - params={'per_page': 25, 'page': 1, 'archived': True}, version="v4") + expected_call = FakeCall( + method="GET", + path=f"/projects/{collection.project_id}/design-spaces", + params={"per_page": 25, "page": 1, "archived": True}, + version="v4", + ) assert 1 == session.num_calls, session.calls assert expected_call == session.calls[0] assert len(design_spaces) == 2 @@ -333,9 +343,7 @@ def test_archive(valid_product_design_space_data): archived_design_space = dsc.archive(ds_id) assert archived_design_space.is_archived - assert session.calls == [ - FakeCall(method='PUT', path=f"{base_path}/{ds_id}/archive", json={}), - ] + assert session.calls == [FakeCall(method="PUT", path=f"{base_path}/{ds_id}/archive", json={})] def test_restore(valid_product_design_space_data): @@ -352,9 +360,7 @@ def test_restore(valid_product_design_space_data): restored_design_space = dsc.restore(ds_id) assert not restored_design_space.is_archived - assert session.calls == [ - FakeCall(method='PUT', path=f"{base_path}/{ds_id}/restore", json={}), - ] + assert session.calls == [FakeCall(method="PUT", path=f"{base_path}/{ds_id}/restore", json={})] def test_get_none(): @@ -369,19 +375,17 @@ def test_get_none(): def test_failed_register(valid_product_design_space_data): response_data = deepcopy(valid_product_design_space_data) - response_data['metadata']['status']['name'] = 'INVALID' + response_data["metadata"]["status"]["name"] = "INVALID" session = FakeSession() session.set_response(response_data) dsc = DesignSpaceCollection(uuid.uuid4(), session) ds = dsc.build(deepcopy(valid_product_design_space_data)) - + retval = dsc.register(ds) - + base_path = f"/projects/{dsc.project_id}/design-spaces" - assert session.calls == [ - FakeCall(method='POST', path=base_path, json=ds.dump()), - ] + assert session.calls == [FakeCall(method="POST", path=base_path, json=ds.dump())] assert retval.dump() == ds.dump() @@ -392,32 +396,30 @@ def test_update(valid_product_design_space_data): session.set_response(response_data) dsc = DesignSpaceCollection(uuid.uuid4(), session) ds = dsc.build(deepcopy(valid_product_design_space_data)) - + retval = dsc.update(ds) - + base_path = f"/projects/{dsc.project_id}/design-spaces" assert session.calls == [ - FakeCall(method='PUT', path=f'{base_path}/{ds.uid}', json=ds.dump()), - FakeCall(method='PUT', path=f'{base_path}/{ds.uid}/validate', json={}) + FakeCall(method="PUT", path=f"{base_path}/{ds.uid}", json=ds.dump()), + FakeCall(method="PUT", path=f"{base_path}/{ds.uid}/validate", json={}), ] assert retval.dump() == ds.dump() def test_failed_update(valid_product_design_space_data): response_data = deepcopy(valid_product_design_space_data) - response_data['metadata']['status']['name'] = 'INVALID' + response_data["metadata"]["status"]["name"] = "INVALID" session = FakeSession() session.set_response(response_data) dsc = DesignSpaceCollection(uuid.uuid4(), session) ds = dsc.build(deepcopy(valid_product_design_space_data)) - + retval = dsc.update(ds) - + base_path = f"/projects/{dsc.project_id}/design-spaces" - assert session.calls == [ - FakeCall(method='PUT', path=f'{base_path}/{ds.uid}', json=ds.dump()), - ] + assert session.calls == [FakeCall(method="PUT", path=f"{base_path}/{ds.uid}", json=ds.dump())] assert retval.dump() == ds.dump() @@ -441,7 +443,7 @@ def test_carrying_settings_from_create_default(valid_product_design_space): default_design_space = collection.create_default( predictor_id=predictor_id, predictor_version=predictor_version, - include_label_count_constraints=True + include_label_count_constraints=True, ) registered = collection.register(default_design_space) @@ -452,15 +454,15 @@ def test_carrying_settings_from_create_default(valid_product_design_space): include_label_fraction_constraints=False, include_label_count_constraints=True, include_parameter_constraints=False, - mode=DefaultDesignSpaceMode.ATTRIBUTE + mode=DefaultDesignSpaceMode.ATTRIBUTE, ) expected_payload = {**valid_product_design_space.dump(), "settings": expected_settings.dump()} expected_call = FakeCall( - method='POST', + method="POST", path=f"projects/{collection.project_id}/design-spaces", json=expected_payload, - version="v3" + version="v3", ) assert session.num_calls == 3 @@ -472,7 +474,7 @@ def test_carrying_settings_from_get(valid_product_design_space): predictor_version = 4 session = FakeSession() - + expected_settings = DesignSpaceSettings( predictor_id=predictor_id, predictor_version=predictor_version, @@ -481,7 +483,7 @@ def test_carrying_settings_from_get(valid_product_design_space): include_label_fraction_constraints=False, include_label_count_constraints=False, include_parameter_constraints=True, - mode=DefaultDesignSpaceMode.ATTRIBUTE + mode=DefaultDesignSpaceMode.ATTRIBUTE, ) ds_resp = _ds_to_response(valid_product_design_space) @@ -496,10 +498,10 @@ def test_carrying_settings_from_get(valid_product_design_space): expected_payload = {**valid_product_design_space.dump(), "settings": expected_settings.dump()} expected_call = FakeCall( - method='POST', + method="POST", path=f"projects/{collection.project_id}/design-spaces", json=expected_payload, - version="v3" + version="v3", ) assert session.num_calls == 3 @@ -523,7 +525,7 @@ def test_locked(valid_product_design_space_data): lock_timestamp = int(lock_time.timestamp()) * 1000 response_data = deepcopy(valid_product_design_space_data) - response_data['metadata']['locked'] = {'user': str(lock_user), 'time': lock_timestamp} + response_data["metadata"]["locked"] = {"user": str(lock_user), "time": lock_timestamp} session.set_response(response_data) diff --git a/tests/resources/test_design_workflows.py b/tests/resources/test_design_workflows.py index 077c7346c..0b4b267ce 100644 --- a/tests/resources/test_design_workflows.py +++ b/tests/resources/test_design_workflows.py @@ -7,14 +7,16 @@ from citrine.informatics.workflows import DesignWorkflow from citrine.resources.design_workflow import DesignWorkflowCollection from tests.utils.factories import ( - BranchDataFactory, DesignWorkflowDataFactory, TableDataSourceFactory + BranchDataFactory, + DesignWorkflowDataFactory, + TableDataSourceFactory, ) -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession PARTIAL_DW_ARGS = ( ("data_source_id", lambda: TableDataSourceFactory().to_data_source_id()), ("predictor_id", lambda: str(uuid.uuid4())), - ("design_space_id", lambda: str(uuid.uuid4())) + ("design_space_id", lambda: str(uuid.uuid4())), ) OPTIONAL_ARGS = PARTIAL_DW_ARGS + (("predictor_version", lambda: random.randint(1, 10)),) @@ -26,10 +28,7 @@ def session() -> FakeSession: @pytest.fixture def collection_without_branch(session) -> DesignWorkflowCollection: - return DesignWorkflowCollection( - project_id=uuid.uuid4(), - session=session, - ) + return DesignWorkflowCollection(project_id=uuid.uuid4(), session=session) @pytest.fixture @@ -42,8 +41,8 @@ def collection(branch_data, collection_without_branch) -> DesignWorkflowCollecti return DesignWorkflowCollection( project_id=collection_without_branch.project_id, session=collection_without_branch.session, - branch_root_id=uuid.UUID(branch_data['metadata']['root_id']), - branch_version=branch_data['metadata']['version'], + branch_root_id=uuid.UUID(branch_data["metadata"]["root_id"]), + branch_version=branch_data["metadata"]["version"], ) @@ -55,14 +54,16 @@ def workflow(collection, branch_data) -> DesignWorkflow: def all_combination_lengths(vals, maxlen=None): maxlen = maxlen or len(vals) - return [args for k in range(0, maxlen + 1) for args in itertools.combinations(vals, k)] + return [args for k in range(maxlen + 1) for args in itertools.combinations(vals, k)] + def workflow_path(collection, workflow=None): - path = f'/projects/{collection.project_id}/design-workflows' + path = f"/projects/{collection.project_id}/design-workflows" if workflow: - path = f'{path}/{workflow.uid}' + path = f"{path}/{workflow.uid}" return path + def assert_workflow(actual, expected, *, include_branch=False): assert actual.name == expected.name assert actual.description == expected.description @@ -79,7 +80,7 @@ def assert_workflow(actual, expected, *, include_branch=False): def test_basic_methods(workflow, collection): - assert 'DesignWorkflow' in str(workflow) + assert "DesignWorkflow" in str(workflow) assert workflow.design_executions.project_id == workflow.project_id @@ -90,7 +91,7 @@ def test_register(session, branch_data, collection, optional_args): workflow_data = DesignWorkflowDataFactory(**kw_args, branch=branch_data) # Given - post_dict = {k: v for k, v in workflow_data.items() if k != 'status_description'} + post_dict = {k: v for k, v in workflow_data.items() if k != "status_description"} session.set_responses(workflow_data) # When @@ -98,7 +99,9 @@ def test_register(session, branch_data, collection, optional_args): new_workflow = collection.register(old_workflow) # Then - assert session.calls == [FakeCall(method='POST', path=workflow_path(collection), json=post_dict)] + assert session.calls == [ + FakeCall(method="POST", path=workflow_path(collection), json=post_dict) + ] assert new_workflow.branch_root_id == collection.branch_root_id assert new_workflow.branch_version == collection.branch_version @@ -110,18 +113,24 @@ def test_register_conflicting_branches(session, branch_data, workflow, collectio old_branch_root_id = uuid.uuid4() workflow.branch_root_id = old_branch_root_id assert workflow.branch_root_id != collection.branch_root_id - + new_branch_root_id = str(branch_data["metadata"]["root_id"]) new_branch_version = branch_data["metadata"]["version"] - post_dict = {**workflow.dump(), "branch_root_id": new_branch_root_id, "branch_version": new_branch_version} - session.set_responses({**post_dict, 'status_description': 'status'}) + post_dict = { + **workflow.dump(), + "branch_root_id": new_branch_root_id, + "branch_version": new_branch_version, + } + session.set_responses({**post_dict, "status_description": "status"}) # When new_workflow = collection.register(workflow) # Then - assert session.calls == [FakeCall(method='POST', path=workflow_path(collection), json=post_dict)] + assert session.calls == [ + FakeCall(method="POST", path=workflow_path(collection), json=post_dict) + ] assert workflow.branch_root_id == old_branch_root_id assert new_workflow.branch_root_id == collection.branch_root_id @@ -136,14 +145,14 @@ def test_register_partial_workflow_without_branch(session, collection_without_br def test_archive(workflow, collection): collection.archive(workflow.uid) - expected_path = '/projects/{}/design-workflows/{}/archive'.format(collection.project_id, workflow.uid) - assert collection.session.last_call == FakeCall(method='PUT', path=expected_path, json={}) + expected_path = f"/projects/{collection.project_id}/design-workflows/{workflow.uid}/archive" + assert collection.session.last_call == FakeCall(method="PUT", path=expected_path, json={}) def test_restore(workflow, collection): collection.restore(workflow.uid) - expected_path = '/projects/{}/design-workflows/{}/restore'.format(collection.project_id, workflow.uid) - assert collection.session.last_call == FakeCall(method='PUT', path=expected_path, json={}) + expected_path = f"/projects/{collection.project_id}/design-workflows/{workflow.uid}/restore" + assert collection.session.last_call == FakeCall(method="PUT", path=expected_path, json={}) def test_delete(collection): @@ -152,20 +161,26 @@ def test_delete(collection): def test_list_archived(branch_data, workflow, collection: DesignWorkflowCollection): - branch_root_id = uuid.UUID(branch_data['metadata']['root_id']) - branch_version = branch_data['metadata']['version'] + branch_root_id = uuid.UUID(branch_data["metadata"]["root_id"]) + branch_version = branch_data["metadata"]["version"] collection.session.set_responses({"response": []}) lst = list(collection.list_archived(per_page=10)) assert len(lst) == 0 - expected_path = '/projects/{}/design-workflows'.format(collection.project_id) + expected_path = f"/projects/{collection.project_id}/design-workflows" assert collection.session.last_call == FakeCall( - method='GET', + method="GET", path=expected_path, - params={'page': 1, 'per_page': 10, 'filter': "archived eq 'true'", 'branch_root_id': branch_root_id, 'branch_version': branch_version}, - json=None + params={ + "page": 1, + "per_page": 10, + "filter": "archived eq 'true'", + "branch_root_id": branch_root_id, + "branch_version": branch_version, + }, + json=None, ) @@ -183,28 +198,32 @@ def test_update(session, branch_data, workflow, collection_without_branch): # Given post_dict = workflow.dump() session.set_responses( - {"per_page": 1, "next": "", "response": []}, - {**post_dict, 'status_description': 'status'}, + {"per_page": 1, "next": "", "response": []}, {**post_dict, "status_description": "status"} ) # When new_workflow = collection_without_branch.update(workflow) # Then - executions_path = f'/projects/{collection_without_branch.project_id}/design-workflows/{workflow.uid}/executions' + executions_path = f"/projects/{collection_without_branch.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_without_branch, workflow), json=post_dict), + FakeCall(method="GET", path=executions_path, params={"page": 1, "per_page": 100}), + FakeCall( + method="PUT", path=workflow_path(collection_without_branch, workflow), json=post_dict + ), ] assert_workflow(new_workflow, workflow) -def test_update_failure_with_existing_execution(session, branch_data, workflow, collection_without_branch, design_execution_dict): +def test_update_failure_with_existing_execution( + session, branch_data, workflow, collection_without_branch, design_execution_dict +): workflow.branch_root_id = uuid.uuid4() post_dict = workflow.dump() session.set_responses( {"per_page": 1, "next": "", "response": [design_execution_dict]}, - {**post_dict, 'status_description': 'status'}) + {**post_dict, "status_description": "status"}, + ) with pytest.raises(RuntimeError): collection_without_branch.update(workflow) @@ -244,6 +263,7 @@ def test_update_branch_not_found(collection, workflow): with pytest.raises(ValueError): collection.update(workflow) + def test_data_source_id(workflow): original_id = workflow.data_source_id assert workflow.data_source.to_data_source_id() == original_id diff --git a/tests/resources/test_file_link.py b/tests/resources/test_file_link.py index 50dbf64b0..00fb3169c 100644 --- a/tests/resources/test_file_link.py +++ b/tests/resources/test_file_link.py @@ -1,25 +1,32 @@ -from pathlib import Path import platform -from typing import Collection -from uuid import uuid4, UUID +from collections.abc import Collection +from pathlib import Path +from uuid import UUID, uuid4 import pytest import requests_mock from boto3 import Session from botocore.exceptions import ClientError +from citrine.exceptions import NotFound from citrine.resources.api_error import ValidationError -from citrine.resources.file_link import FileCollection, FileLink, GEMDFileLink, _Uploader, \ - _get_ids_from_url +from citrine.resources.file_link import ( + FileCollection, + FileLink, + GEMDFileLink, + _get_ids_from_url, + _Uploader, +) from citrine.resources.ingestion import Ingestion, IngestionCollection -from citrine.exceptions import NotFound - from tests.utils.factories import ( - FileLinkDataFactory, _UploaderFactory, JobStatusResponseDataFactory, - IngestionStatusResponseDataFactory, IngestFilesResponseDataFactory, JobSubmissionResponseDataFactory + FileLinkDataFactory, + IngestFilesResponseDataFactory, + IngestionStatusResponseDataFactory, + JobStatusResponseDataFactory, + JobSubmissionResponseDataFactory, + _UploaderFactory, ) -from tests.utils.session import FakeSession, FakeS3Client, FakeCall, FakeRequestResponseApiError - +from tests.utils.session import FakeCall, FakeRequestResponseApiError, FakeS3Client, FakeSession PYTHON_VERSION_TUPLE = tuple(int(n) for n in platform.python_version_tuple()[:2]) @@ -31,29 +38,25 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> FileCollection: - return FileCollection( - team_id=uuid4(), - dataset_id=uuid4(), - session=session - ) + return FileCollection(team_id=uuid4(), dataset_id=uuid4(), session=session) @pytest.fixture def valid_data() -> dict: - return FileLinkDataFactory(url='www.citrine.io', filename='materials.txt') + return FileLinkDataFactory(url="www.citrine.io", filename="materials.txt") @pytest.mark.parametrize( ("filename", "mimetype"), [ pytest.param( - "asdf.xlsx", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "asdf.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", marks=pytest.mark.xfail( platform.system() == "Windows" and PYTHON_VERSION_TUPLE <= (3, 13), - reason="windows-latest test servers omit xlsx from their registry", - strict=True - ) + reason="windows-latest test servers omit xlsx from their registry", + strict=True, + ), ), ("asdf.xls", "application/vnd.ms-excel"), ("asdf.XLS", "application/vnd.ms-excel"), @@ -84,13 +87,13 @@ def test_name_alias(valid_data): def test_string_representation(valid_data): """Test the string representation.""" - assert str(FileLink.build(valid_data)) == '' + assert str(FileLink.build(valid_data)) == "" def test_from_path(): """Test the string representation.""" - path = Path.cwd() / 'some' / 'path' / 'with' / 'file.txt' - assert FileLink.from_path(path).filename == 'file.txt' + path = Path.cwd() / "some" / "path" / "with" / "file.txt" + assert FileLink.from_path(path).filename == "file.txt" assert FileLink.from_path(str(path)).url == path.as_uri() assert FileCollection._is_local_url(FileLink.from_path(path).url) @@ -100,6 +103,7 @@ def uploader() -> _Uploader: """An _Uploader object with all of its fields filled in.""" return _UploaderFactory() + def test_delete(collection: FileCollection, session): """Test that deletion calls the expected endpoint and checks the url structure.""" # Given @@ -112,21 +116,18 @@ def test_delete(collection: FileCollection, session): # Then assert 1 == session.num_calls - expected_call = FakeCall( - method='DELETE', - path=collection._get_path(file_id) - ) + expected_call = FakeCall(method="DELETE", path=collection._get_path(file_id)) assert expected_call == session.last_call # A URL that does not follow the files/{id}/versions/{id} format is invalid - for chunk in (f'{file_id}', f'{file_id}/{version_id}'): - invalid_url = f'{collection._get_path}/{chunk}' + for chunk in (f"{file_id}", f"{file_id}/{version_id}"): + invalid_url = f"{collection._get_path}/{chunk}" invalid_file_link = collection.build(FileLinkDataFactory(url=invalid_url)) with pytest.raises(ValueError): collection.delete(invalid_file_link) # A remote URL is invalid - ext_invalid_url = f'http://www.citrine.io/develop/files/{file_id}/versions/{version_id}' + ext_invalid_url = f"http://www.citrine.io/develop/files/{file_id}/versions/{version_id}" ext_invalid_file_link = collection.build(FileLinkDataFactory(url=ext_invalid_url)) with pytest.raises(ValueError): collection.delete(ext_invalid_file_link) @@ -134,37 +135,29 @@ def test_delete(collection: FileCollection, session): def test_upload(collection: FileCollection, session, tmpdir, monkeypatch): """Test signaling that an upload has completed and the creation of a FileLink object.""" - monkeypatch.setattr(Session, 'client', lambda *args, **kwargs: FakeS3Client({'VersionId': '42'})) + monkeypatch.setattr( + Session, "client", lambda *args, **kwargs: FakeS3Client({"VersionId": "42"}) + ) # It would be good to test these, but the values assigned are not accessible dest_names = { - 'foo.txt': 'text/plain', - 'foo.TXT': 'text/plain', # Capitalization in extension is fine - 'foo.bar': 'application/octet-stream' # No match == generic binary + "foo.txt": "text/plain", + "foo.TXT": "text/plain", # Capitalization in extension is fine + "foo.bar": "application/octet-stream", # No match == generic binary } file_id = str(uuid4()) version = str(uuid4()) # This is the dictionary structure we expect from the upload completion request - file_info_response = { - 'file_info': { - 'file_id': file_id, - 'version': version - } - } + file_info_response = {"file_info": {"file_id": file_id, "version": version}} uploads_response = { - 's3_region': 'us-east-1', - 's3_bucket': 'temp-bucket', - 'temporary_credentials': { - 'access_key_id': '1234', - 'secret_access_key': 'abbb8777', - 'session_token': 'hefheuhuhhu83772333', + "s3_region": "us-east-1", + "s3_bucket": "temp-bucket", + "temporary_credentials": { + "access_key_id": "1234", + "secret_access_key": "abbb8777", + "session_token": "hefheuhuhhu83772333", }, - 'uploads': [ - { - 's3_key': '66377378', - 'upload_id': '111', - } - ] + "uploads": [{"s3_key": "66377378", "upload_id": "111"}], } for dest_name in dest_names: @@ -174,8 +167,7 @@ def test_upload(collection: FileCollection, session, tmpdir, monkeypatch): session.set_responses(uploads_response, file_info_response) file_link = collection.upload(file_path=tmp_path) - url = 'teams/{}/datasets/{}/files/{}/versions/{}'\ - .format(collection.team_id, collection.dataset_id, file_id, version) + url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/files/{file_id}/versions/{version}" assert file_link.dump() == FileLink(dest_name, url=url).dump() assert session.num_calls == 2 * len(dest_names) @@ -183,30 +175,25 @@ def test_upload(collection: FileCollection, session, tmpdir, monkeypatch): def test_upload_missing_file(collection: FileCollection): with pytest.raises(ValueError): - collection.upload(file_path='this-file-does-not-exist.xls') + collection.upload(file_path="this-file-does-not-exist.xls") def test_upload_request(collection: FileCollection, session, uploader, tmpdir): """Test that an upload request response contains all required fields.""" - filename = 'foo.txt' + filename = "foo.txt" tmppath = Path(tmpdir) / filename tmppath.write_text("Arbitrary text") # This is the dictionary structure we expect from the upload request upload_request_response = { - 's3_region': uploader.region_name, - 's3_bucket': uploader.bucket, - 'temporary_credentials': { - 'access_key_id': uploader.aws_access_key_id, - 'secret_access_key': uploader.aws_secret_access_key, - 'session_token': uploader.aws_session_token, + "s3_region": uploader.region_name, + "s3_bucket": uploader.bucket, + "temporary_credentials": { + "access_key_id": uploader.aws_access_key_id, + "secret_access_key": uploader.aws_secret_access_key, + "session_token": uploader.aws_session_token, }, - 'uploads': [ - { - 's3_key': uploader.object_key, - 'upload_id': uploader.upload_id - } - ] + "uploads": [{"s3_key": uploader.object_key, "upload_id": uploader.upload_id}], } session.set_response(upload_request_response) new_uploader = collection._make_upload_request(tmppath, filename) @@ -224,38 +211,33 @@ def test_upload_request(collection: FileCollection, session, uploader, tmpdir): assert new_uploader.s3_addressing_style == uploader.s3_addressing_style # Using a request response that is missing a field throws a RuntimeError - del upload_request_response['s3_bucket'] + del upload_request_response["s3_bucket"] with pytest.raises(RuntimeError): collection._make_upload_request(tmppath, filename) def test_upload_request_s3_overrides(collection: FileCollection, session, uploader, tmpdir): """Test that an upload request response contains all required fields.""" - filename = 'foo.txt' + filename = "foo.txt" tmppath = Path(tmpdir) / filename tmppath.write_text("Arbitrary text") # This is the dictionary structure we expect from the upload request upload_request_response = { - 's3_region': uploader.region_name, - 's3_bucket': uploader.bucket, - 'temporary_credentials': { - 'access_key_id': uploader.aws_access_key_id, - 'secret_access_key': uploader.aws_secret_access_key, - 'session_token': uploader.aws_session_token, + "s3_region": uploader.region_name, + "s3_bucket": uploader.bucket, + "temporary_credentials": { + "access_key_id": uploader.aws_access_key_id, + "secret_access_key": uploader.aws_secret_access_key, + "session_token": uploader.aws_session_token, }, - 'uploads': [ - { - 's3_key': uploader.object_key, - 'upload_id': uploader.upload_id - } - ] + "uploads": [{"s3_key": uploader.object_key, "upload_id": uploader.upload_id}], } session.set_response(upload_request_response) # Override the s3 endpoint settings in the session, ensure they make it to the upload - endpoint = 'http://foo.bar' - addressing_style = 'path' + endpoint = "http://foo.bar" + addressing_style = "path" use_ssl = False session.s3_endpoint_url = endpoint session.s3_addressing_style = addressing_style @@ -269,28 +251,28 @@ def test_upload_request_s3_overrides(collection: FileCollection, session, upload def test_upload_file(collection: FileCollection, session, uploader, tmpdir, monkeypatch): """Test that uploading a file returns the version ID.""" - filename = 'foo.txt' + filename = "foo.txt" tmppath = Path(tmpdir) / filename tmppath.write_text("Arbitrary text") # A successful file upload sets uploader.s3_version - new_version = '3' + new_version = "3" with monkeypatch.context() as m: - client = FakeS3Client({'VersionId': new_version}) - m.setattr(Session, 'client', lambda *args, **kwargs: client) + client = FakeS3Client({"VersionId": new_version}) + m.setattr(Session, "client", lambda *args, **kwargs: client) new_uploader = collection._upload_file(tmppath, uploader) assert new_uploader.s3_version == new_version # If the client throws a ClientError when attempting to upload, throw a RuntimeError with monkeypatch.context() as m: - client = FakeS3Client(ClientError(error_response={}, operation_name='put'), raises=True) - m.setattr(Session, 'client', lambda *args, **kwargs: client) + client = FakeS3Client(ClientError(error_response={}, operation_name="put"), raises=True) + m.setattr(Session, "client", lambda *args, **kwargs: client) with pytest.raises(RuntimeError): collection._upload_file(tmppath, uploader) - s3_addressing_style = 'path' - s3_endpoint_url = 'http://foo.bar' + s3_addressing_style = "path" + s3_endpoint_url = "http://foo.bar" s3_use_ssl = False uploader.s3_addressing_style = s3_addressing_style @@ -302,26 +284,24 @@ def test_upload_file(collection: FileCollection, session, uploader, tmpdir, monk def _stash_kwargs(*_, **kwargs): stashed_kwargs.update(kwargs) - return FakeS3Client({'VersionId': '71'}) + return FakeS3Client({"VersionId": "71"}) - m.setattr(Session, 'client', _stash_kwargs) + m.setattr(Session, "client", _stash_kwargs) collection._upload_file(tmppath, uploader) - assert stashed_kwargs['config'].s3['addressing_style'] is s3_addressing_style - assert stashed_kwargs['endpoint_url'] is s3_endpoint_url - assert stashed_kwargs['use_ssl'] is s3_use_ssl + assert stashed_kwargs["config"].s3["addressing_style"] is s3_addressing_style + assert stashed_kwargs["endpoint_url"] is s3_endpoint_url + assert stashed_kwargs["use_ssl"] is s3_use_ssl def test_upload_missing_version(collection: FileCollection, session, uploader): - dest_name = 'foo.txt' - file_id = '12345' - version = '14' + dest_name = "foo.txt" + file_id = "12345" + version = "14" bad_complete_response = { - 'file_info': { - 'file_id': file_id - }, - 'version': version # 'version' is supposed to go inside 'file_info' + "file_info": {"file_id": file_id}, + "version": version, # 'version' is supposed to go inside 'file_info' } with pytest.raises(RuntimeError): session.set_response(bad_complete_response) @@ -332,37 +312,28 @@ def test_list_file_links(collection: FileCollection, session, valid_data): """Test that all files in a dataset can be turned into FileLink and listed.""" file_id = str(uuid4()) version = str(uuid4()) - filename = 'materials.txt' + filename = "materials.txt" # The actual response contains more fields, but these are the only ones we use. - returned_data = { - 'id': file_id, - 'version': version, - 'filename': filename, - } - returned_data["unversioned_url"] = f"http://test.domain.net:8002/api/v1/files/{returned_data['id']}" - returned_data["versioned_url"] = f"http://test.domain.net:8002/api/v1/files/{returned_data['id']}" \ - f"/versions/{returned_data['version']}" - session.set_response({ - 'files': [returned_data] - }) + returned_data = {"id": file_id, "version": version, "filename": filename} + returned_data["unversioned_url"] = ( + f"http://test.domain.net:8002/api/v1/files/{returned_data['id']}" + ) + returned_data["versioned_url"] = ( + f"http://test.domain.net:8002/api/v1/files/{returned_data['id']}" + f"/versions/{returned_data['version']}" + ) + session.set_response({"files": [returned_data]}) files_iterator = collection.list(per_page=15) files = [file for file in files_iterator] assert session.num_calls == 1 expected_call = FakeCall( - method='GET', - path=collection._get_path(), - params={ - 'per_page': 15, - 'page': 1 - } + method="GET", path=collection._get_path(), params={"per_page": 15, "page": 1} ) assert expected_call == session.last_call assert len(files) == 1 - expected_url = "teams/{}/datasets/{}/files/{}/versions/{}".format( - collection.team_id, collection.dataset_id, file_id, version - ) + expected_url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/files/{file_id}/versions/{version}" expected_file = FileLinkDataFactory(url=expected_url, filename=filename) assert files[0].dump() == FileLink.build(expected_file).dump() @@ -375,16 +346,16 @@ def test_file_download(collection: FileCollection, session, tmpdir): it does not exist, make a call to get the pre-signed URL, and another to download. """ # Given - filename = 'diagram.pdf' + filename = "diagram.pdf" file_uid = str(uuid4()) version_uid = str(uuid4()) url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/files/{file_uid}/versions/{version_uid}" - file = FileLink.build(FileLinkDataFactory(url=url, filename=filename, id=file_uid, version=version_uid)) + file = FileLink.build( + FileLinkDataFactory(url=url, filename=filename, id=file_uid, version=version_uid) + ) pre_signed_url = "http://files.citrine.io/secret-codes/jiifema987pjfsda" # arbitrary - session.set_response({ - 'pre_signed_read_link': pre_signed_url, - }) - target_dir = str(tmpdir) + 'some/new/directory/' + session.set_response({"pre_signed_read_link": pre_signed_url}) + target_dir = str(tmpdir) + "some/new/directory/" target_file = target_dir + filename def _checked_write(path, content): @@ -395,28 +366,25 @@ def _checked_write(path, content): # When assert mock_get.call_count == 1 - expected_call = FakeCall( - method='GET', - path=url + '/content-link' - ) + expected_call = FakeCall(method="GET", path=url + "/content-link") assert expected_call == session.last_call - _checked_write(target_dir, 'content') - assert Path(target_file).read_text() == 'content' + _checked_write(target_dir, "content") + assert Path(target_file).read_text() == "content" # Now the directory exists - _checked_write(Path(target_dir), 'other content') - assert Path(target_file).read_text() == 'other content' + _checked_write(Path(target_dir), "other content") + assert Path(target_file).read_text() == "other content" # Give it the filename instead - _checked_write(target_file, 'more content') - assert Path(target_file).read_text() == 'more content' + _checked_write(target_file, "more content") + assert Path(target_file).read_text() == "more content" # And as a Path - _checked_write(target_file, 'love that content') - assert Path(target_file).read_text() == 'love that content' + _checked_write(target_file, "love that content") + assert Path(target_file).read_text() == "love that content" - bad_url = f"bin/uuid3/versions/uuid4" + bad_url = "bin/uuid3/versions/uuid4" bad_file = FileLink.build(FileLinkDataFactory(url=bad_url, filename=filename)) with pytest.raises(ValueError, match="Citrine"): collection.download(file_link=bad_file, local_path=target_dir) @@ -428,62 +396,55 @@ def test_read(collection: FileCollection, session, tmp_path): """ # Given - filename = 'diagram.pdf' + filename = "diagram.pdf" file_uid = str(uuid4()) version_uid = str(uuid4()) url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/files/{file_uid}/versions/{version_uid}" - file = FileLink.build(FileLinkDataFactory(url=url, filename=filename, id=file_uid, version=version_uid)) + file = FileLink.build( + FileLinkDataFactory(url=url, filename=filename, id=file_uid, version=version_uid) + ) pre_signed_url = "http://files.citrine.io/secret-codes/jiifema987pjfsda" # arbitrary - session.set_response({ - 'pre_signed_read_link': pre_signed_url, - }) + session.set_response({"pre_signed_read_link": pre_signed_url}) with requests_mock.mock() as mock_get: mock_get.get(pre_signed_url, text="lorem ipsum") # When io = collection.read(file_link=file) - assert io.decode('UTF-8') == 'lorem ipsum' + assert io.decode("UTF-8") == "lorem ipsum" # When assert mock_get.call_count == 1 - expected_call = FakeCall( - method='GET', - path=url + '/content-link' - ) + expected_call = FakeCall(method="GET", path=url + "/content-link") assert expected_call == session.last_call - bad_url = f"bin/uuid3/versions/uuid4" + bad_url = "bin/uuid3/versions/uuid4" bad_file = FileLink.build(FileLinkDataFactory(url=bad_url, filename=filename)) with pytest.raises(ValueError, match="Citrine"): collection.read(file_link=bad_file) # Test with files.list endpoint-like object - filelink = collection.build({"id": str(uuid4()), - "version": str(uuid4()), - "filename": filename, - "type": FileLink.typ}) + filelink = collection.build( + {"id": str(uuid4()), "version": str(uuid4()), "filename": filename, "type": FileLink.typ} + ) pre_signed_url_2 = "http://files.citrine.io/secret-codes/2222222222222" # arbitrary - session.set_response({'pre_signed_read_link': pre_signed_url_2}) + session.set_response({"pre_signed_read_link": pre_signed_url_2}) with requests_mock.mock() as mock_get: mock_get.get(pre_signed_url_2, text="quite lovely") # When io = collection.read(file_link=filelink) - assert io.decode('UTF-8') == 'quite lovely' + assert io.decode("UTF-8") == "quite lovely" # When assert mock_get.call_count == 1 - expected_call_2 = FakeCall( - method='GET', - path=filelink.url + '/content-link' - ) + expected_call_2 = FakeCall(method="GET", path=filelink.url + "/content-link") assert expected_call_2 == session.last_call # Test the local read behaves with requests_mock.mock() as mock_get: - local = tmp_path / 'test.txt' + local = tmp_path / "test.txt" content = "This is content" local.write_text(content) # When io = collection.read(file_link=FileLink.from_path(local)) - assert io.decode('UTF-8') == content + assert io.decode("UTF-8") == content # When assert mock_get.call_count == 0 @@ -494,16 +455,16 @@ def test_external_file_read(collection: FileCollection, session): """ # Given - filename = 'spreadsheet.xlsx' + filename = "spreadsheet.xlsx" url = "http://customer.com/data-lake/files/123/versions/456" file = FileLink.build(FileLinkDataFactory(url=url, filename=filename)) with requests_mock.mock() as mock_get: - mock_get.get(url, text='010111011') + mock_get.get(url, text="010111011") # When io = collection.read(file_link=file) - assert io.decode('UTF-8') == '010111011' + assert io.decode("UTF-8") == "010111011" # When assert mock_get.call_count == 1 @@ -517,13 +478,13 @@ def test_external_file_download(collection: FileCollection, session, tmpdir): it does not exist, and make a single call to download. """ # Given - filename = 'spreadsheet.xlsx' + filename = "spreadsheet.xlsx" url = "http://customer.com/data-lake/files/123/versions/456" file = FileLink.build(FileLinkDataFactory(url=url, filename=filename)) - local_path = Path(tmpdir) / 'test_external_file_download/new_name.xlsx' + local_path = Path(tmpdir) / "test_external_file_download/new_name.xlsx" with requests_mock.mock() as mock_get: - mock_get.get(url, text='010111011') + mock_get.get(url, text="010111011") # When collection.download(file_link=file, local_path=local_path) @@ -531,20 +492,23 @@ def test_external_file_download(collection: FileCollection, session, tmpdir): # When assert mock_get.call_count == 1 - assert local_path.read_text() == '010111011' + assert local_path.read_text() == "010111011" 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())}) - good_file2 = collection.build({"filename": "also.csv", "id": str(uuid4()), "version": str(uuid4())}) + good_file1 = collection.build( + {"filename": "good.csv", "id": str(uuid4()), "version": str(uuid4())} + ) + good_file2 = collection.build( + {"filename": "also.csv", "id": str(uuid4()), "version": str(uuid4())} + ) bad_file = FileLink(filename="bad.csv", url="http://files.com/input.csv") ingest_files_resp = IngestFilesResponseDataFactory() job_id_resp = JobSubmissionResponseDataFactory() job_status_resp = JobStatusResponseDataFactory( - job_id=job_id_resp['job_id'], - job_type='create-gemd-objects', + job_id=job_id_resp["job_id"], job_type="create-gemd-objects" ) ingest_status_resp = IngestionStatusResponseDataFactory() @@ -564,13 +528,13 @@ def test_ingest(collection: FileCollection, session): def test_ingest_with_upload(collection, monkeypatch, tmp_path, session): """Test more advanced workflows, patching to avoid unnecessary complexity.""" - platform_file = FileLink(url='relative/path', filename='file.txt') + platform_file = FileLink(url="relative/path", filename="file.txt") platform_file.uid = uuid4() - external_file = FileLink(url='http://citrine.io/other.txt', filename='other.txt') - local_file = tmp_path / 'file.csv' + external_file = FileLink(url="http://citrine.io/other.txt", filename="other.txt") + local_file = tmp_path / "file.csv" local_file.write_text("a,b,c\n1,2,3") local_file_link = FileLink(filename=local_file.name, url=local_file.as_uri()) - local_none = tmp_path / 'not_here.csv' + local_none = tmp_path / "not_here.csv" def _mock_download(self, *, file_link, local_path): assert file_link == external_file or file_link == local_file_link @@ -579,24 +543,24 @@ def _mock_download(self, *, file_link, local_path): def _mock_upload(self, *, file_path, dest_name=None): uploads.add(dest_name) - return FileLink(url='relative/path', filename=file_path.name) + return FileLink(url="relative/path", filename=file_path.name) - def _mock_build_from_file_links(self: IngestionCollection, - file_links: Collection[FileLink], - *, - raise_errors: bool = True - ): + def _mock_build_from_file_links( + self: IngestionCollection, file_links: Collection[FileLink], *, raise_errors: bool = True + ): assert len(file_links) == 3 assert platform_file in file_links assert external_file not in file_links assert local_file not in file_links - return Ingestion.build({ - "ingestion_id": uuid4(), - "team_id": self.team_id, - "dataset_id": self.dataset_id, - "session": self.session, - "raise_errors": raise_errors, - }) + return Ingestion.build( + { + "ingestion_id": uuid4(), + "team_id": self.team_id, + "dataset_id": self.dataset_id, + "session": self.session, + "raise_errors": raise_errors, + } + ) def _mock_build_objects(self, **_): pass @@ -614,9 +578,7 @@ def _mock_build_objects(self, **_): # Paths must be resolvable locally collection.ingest([local_none], upload=True) - session.set_response( - NotFound("path", FakeRequestResponseApiError(400, "Not found", [])) - ) + session.set_response(NotFound("path", FakeRequestResponseApiError(400, "Not found", []))) with pytest.raises(NotFound): # strings will fail when they can't resolve collection.ingest([str(local_none)], upload=True) @@ -626,48 +588,48 @@ def test_resolve_file_link(collection: FileCollection, session): # The actual response contains more fields, but these are the only ones we use. raw_files = [ { - 'id': str(uuid4()), - 'version': str(uuid4()), - 'filename': 'file0.txt', - 'version_number': 1 + "id": str(uuid4()), + "version": str(uuid4()), + "filename": "file0.txt", + "version_number": 1, }, { - 'id': str(uuid4()), - 'version': str(uuid4()), - 'filename': 'file1.txt', - 'version_number': 3 + "id": str(uuid4()), + "version": str(uuid4()), + "filename": "file1.txt", + "version_number": 3, }, { - 'id': str(uuid4()), - 'version': str(uuid4()), - 'filename': 'file2.txt', - 'version_number': 1 + "id": str(uuid4()), + "version": str(uuid4()), + "filename": "file2.txt", + "version_number": 1, }, ] file1_versions = [raw_files[1].copy() for _ in range(3)] - file1_versions[0]['version'] = str(uuid4()) - file1_versions[0]['version_number'] = 1 - file1_versions[2]['version'] = str(uuid4()) - file1_versions[2]['version_number'] = 2 + file1_versions[0]["version"] = str(uuid4()) + file1_versions[0]["version_number"] = 1 + file1_versions[2]["version"] = str(uuid4()) + file1_versions[2]["version_number"] = 2 for raw in raw_files: - raw['unversioned_url'] = f"http://test.domain.net:8002/api/v1/files/{raw['id']}" - raw['versioned_url'] = f"http://test.domain.net:8002/api/v1/files/{raw['id']}/versions/{raw['version']}" + raw["unversioned_url"] = f"http://test.domain.net:8002/api/v1/files/{raw['id']}" + raw["versioned_url"] = ( + f"http://test.domain.net:8002/api/v1/files/{raw['id']}/versions/{raw['version']}" + ) for f1 in file1_versions: - f1['unversioned_url'] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}" - f1['versioned_url'] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}/versions/{f1['version']}" + f1["unversioned_url"] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}" + f1["versioned_url"] = ( + f"http://test.domain.net:8002/api/v1/files/{f1['id']}/versions/{f1['version']}" + ) - session.set_response({ - 'files': raw_files - }) + session.set_response({"files": raw_files}) file1 = collection.build(raw_files[1]) assert collection._resolve_file_link(file1) == file1, "Resolving a FileLink is a no-op" assert session.num_calls == 0, "No-op still hit server" - session.set_response({ - 'files': [raw_files[1]] - }) + session.set_response({"files": [raw_files[1]]}) unresolved = GEMDFileLink(filename=file1.filename, url=file1.url) assert collection._resolve_file_link(unresolved) == file1, "FileLink didn't resolve" @@ -684,31 +646,27 @@ def test_resolve_file_link(collection: FileCollection, session): collection._resolve_file_link(unresolved) assert session.num_calls == 2 - assert collection._resolve_file_link(UUID(raw_files[1]['id'])) == file1, "UUID didn't resolve" + assert collection._resolve_file_link(UUID(raw_files[1]["id"])) == file1, "UUID didn't resolve" assert session.num_calls == 3 - session.set_response({ - 'files': [raw_files[1]] - }) - assert collection._resolve_file_link(raw_files[1]['id']) == file1, "String UUID didn't resolve" + session.set_response({"files": [raw_files[1]]}) + assert collection._resolve_file_link(raw_files[1]["id"]) == file1, "String UUID didn't resolve" assert session.num_calls == 4 - assert collection._resolve_file_link(raw_files[1]['version']) == file1, "Version UUID didn't resolve" + assert collection._resolve_file_link(raw_files[1]["version"]) == file1, ( + "Version UUID didn't resolve" + ) assert session.num_calls == 5 abs_link = "https://wwww.website.web/web.pdf" assert collection._resolve_file_link(abs_link).filename == "web.pdf" assert collection._resolve_file_link(abs_link).url == abs_link - session.set_response({ - 'files': [raw_files[1]] - }) + session.set_response({"files": [raw_files[1]]}) assert collection._resolve_file_link(file1.url) == file1, "Relative path didn't resolve" assert session.num_calls == 6 - session.set_response({ - 'files': [raw_files[1]] - }) + session.set_response({"files": [raw_files[1]]}) assert collection._resolve_file_link(file1.filename) == file1, "Filename didn't resolve" assert session.num_calls == 7 @@ -722,10 +680,7 @@ def test_get_ids_from_url(collection: FileCollection): f"teams/{uuid4()}/datasets/{uuid4()}/files/{uuid4()}/versions/{uuid4()}", f"/files/{uuid4()}/versions/{uuid4()}", ] - file = [ - f"teams/{uuid4()}/datasets/{uuid4()}/files/{uuid4()}", - f"/files/{uuid4()}", - ] + file = [f"teams/{uuid4()}/datasets/{uuid4()}/files/{uuid4()}", f"/files/{uuid4()}"] bad = [ f"/teams/{uuid4()}/datasets/{uuid4()}/files/{uuid4()}/versions/{uuid4()}/action", f"/teams/{uuid4()}/datasets/{uuid4()}/{uuid4()}/versions/{uuid4()}", @@ -748,68 +703,68 @@ def test_get_ids_from_url(collection: FileCollection): def test_get(collection: FileCollection, session): raw_files = [ { - 'id': str(uuid4()), - 'version': str(uuid4()), - 'filename': 'file0.txt', - 'version_number': 1 + "id": str(uuid4()), + "version": str(uuid4()), + "filename": "file0.txt", + "version_number": 1, }, { - 'id': str(uuid4()), - 'version': str(uuid4()), - 'filename': 'file1.txt', - 'version_number': 3 + "id": str(uuid4()), + "version": str(uuid4()), + "filename": "file1.txt", + "version_number": 3, }, { - 'id': str(uuid4()), - 'version': str(uuid4()), - 'filename': 'file2.txt', - 'version_number': 1 + "id": str(uuid4()), + "version": str(uuid4()), + "filename": "file2.txt", + "version_number": 1, }, ] file1_versions = [raw_files[1].copy() for _ in range(3)] - file1_versions[0]['version'] = str(uuid4()) - file1_versions[0]['version_number'] = 1 - file1_versions[2]['version'] = str(uuid4()) - file1_versions[2]['version_number'] = 2 + file1_versions[0]["version"] = str(uuid4()) + file1_versions[0]["version_number"] = 1 + file1_versions[2]["version"] = str(uuid4()) + file1_versions[2]["version_number"] = 2 for raw in raw_files: - raw['unversioned_url'] = f"http://test.domain.net:8002/api/v1/files/{raw['id']}" - raw['versioned_url'] = f"http://test.domain.net:8002/api/v1/files/{raw['id']}/versions/{raw['version']}" + raw["unversioned_url"] = f"http://test.domain.net:8002/api/v1/files/{raw['id']}" + raw["versioned_url"] = ( + f"http://test.domain.net:8002/api/v1/files/{raw['id']}/versions/{raw['version']}" + ) for f1 in file1_versions: - f1['unversioned_url'] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}" - f1['versioned_url'] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}/versions/{f1['version']}" + f1["unversioned_url"] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}" + f1["versioned_url"] = ( + f"http://test.domain.net:8002/api/v1/files/{f1['id']}/versions/{f1['version']}" + ) file0 = collection.build(raw_files[0]) file1 = collection.build(raw_files[1]) - session.set_response({ - 'files': [raw_files[1]] - }) - assert collection.get(uid=raw_files[1]['id'], version=raw_files[1]['version']) == file1 + session.set_response({"files": [raw_files[1]]}) + assert collection.get(uid=raw_files[1]["id"], version=raw_files[1]["version"]) == file1 - session.set_response({ - 'files': [raw_files[0]] - }) - assert collection.get(uid=raw_files[0]['id'], version=raw_files[0]['version_number']) == file0 + session.set_response({"files": [raw_files[0]]}) + assert collection.get(uid=raw_files[0]["id"], version=raw_files[0]["version_number"]) == file0 - session.set_response({ - 'files': [raw_files[1]] - }) - assert collection.get(uid=raw_files[1]['filename'], version=raw_files[1]['version_number']) == file1 + session.set_response({"files": [raw_files[1]]}) + assert collection.get(uid=raw_files[1]["filename"], version=raw_files[1]["version_number"]) == file1 # fmt: skip - session.set_response({ - 'files': [raw_files[1]] - }) - assert collection.get(uid=raw_files[1]['filename'], version=raw_files[1]['version']) == file1 + session.set_response({"files": [raw_files[1]]}) + assert collection.get(uid=raw_files[1]["filename"], version=raw_files[1]["version"]) == file1 - validation_error = ValidationError.build({"failure_message": "file not found", "failure_id": "failure_id"}) + validation_error = ValidationError.build( + {"failure_message": "file not found", "failure_id": "failure_id"} + ) session.set_response( NotFound("path", FakeRequestResponseApiError(400, "Not found", [validation_error])) ) with pytest.raises(NotFound): - collection.get(uid=raw_files[1]['filename'], version=4) + collection.get(uid=raw_files[1]["filename"], version=4) def test_exceptions(collection: FileCollection, session): - file_link = FileLink(url="http://customer.com/data-lake/files/123/versions/456", filename="456") + file_link = FileLink( + url="http://customer.com/data-lake/files/123/versions/456", filename="456" + ) with pytest.raises(ValueError): collection._get_path_from_file_link(file_link) @@ -822,7 +777,9 @@ def test_exceptions(collection: FileCollection, session): with pytest.raises(ValueError): collection.get(uid=uuid4(), version="Words!") - validation_error = ValidationError.build({"failure_message": "file not found", "failure_id": "failure_id"}) + validation_error = ValidationError.build( + {"failure_message": "file not found", "failure_id": "failure_id"} + ) session.set_response( NotFound("path", FakeRequestResponseApiError(400, "Not found", [validation_error])) ) @@ -830,4 +787,4 @@ def test_exceptions(collection: FileCollection, session): collection.get(uid="name") with pytest.raises(ValueError, match="Windows"): - collection.read(file_link=FileLink('File', 'file://remote/network/file.txt')) + collection.read(file_link=FileLink("File", "file://remote/network/file.txt")) diff --git a/tests/resources/test_gem_table.py b/tests/resources/test_gem_table.py index d71656fb7..6f5d95516 100644 --- a/tests/resources/test_gem_table.py +++ b/tests/resources/test_gem_table.py @@ -1,15 +1,15 @@ import json +from unittest.mock import call, patch from uuid import UUID, uuid4 import pytest import requests_mock + from citrine.exceptions import JobFailureError, PollingTimeoutError -from citrine.resources.gemtables import GemTableCollection, GemTable +from citrine.resources.gemtables import GemTable, GemTableCollection from citrine.resources.table_config import TableConfig -from mock import patch, call - from tests.utils.factories import GemTableDataFactory, ListGemTableVersionsDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -20,29 +20,27 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> GemTableCollection: return GemTableCollection( - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - project_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + project_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + session=session, ) def test_deprecated_create_collection(session): with pytest.raises(TypeError): return GemTableCollection( - project_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session + project_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), session=session ) with pytest.raises(TypeError): return GemTableCollection( - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), session=session ) with pytest.raises(TypeError): return GemTableCollection( - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - project_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + project_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), ) @@ -66,7 +64,7 @@ def test_get_table_metadata(collection, session): assert 1 == session.num_calls expect_call = FakeCall( method="GET", - path=f"projects/{collection.project_id}/display-tables/{gem_table['id']}/versions/{gem_table['version']}" + path=f"projects/{collection.project_id}/display-tables/{gem_table['id']}/versions/{gem_table['version']}", ) assert session.last_call == expect_call assert str(retrieved_table.uid) == gem_table["id"] @@ -85,15 +83,19 @@ def test_get_table_metadata(collection, session): assert retrieved_table.version == version_number # Given - config = TableConfig(name="foo", description="bar", datasets=[], variables=[], rows=[], columns=[]) - session.set_response({ - "version": { - "ara_definition": config.dump(), - "version_number": config.version_number, - "id": config.config_uid, - }, - "definition": {"id": uuid4()} - }) + config = TableConfig( + name="foo", description="bar", datasets=[], variables=[], rows=[], columns=[] + ) + session.set_response( + { + "version": { + "ara_definition": config.dump(), + "version_number": config.version_number, + "id": config.config_uid, + }, + "definition": {"id": uuid4()}, + } + ) # Then assert retrieved_table.config.name == config.name @@ -101,7 +103,7 @@ def test_get_table_metadata(collection, session): assert retrieved_table.description == config.description expect_call = FakeCall( method="GET", - path=f"projects/{collection.project_id}/display-tables/{retrieved_table.uid}/versions/{retrieved_table.version}/definition" + path=f"projects/{collection.project_id}/display-tables/{retrieved_table.uid}/versions/{retrieved_table.version}/definition", ) assert session.last_call == expect_call @@ -125,7 +127,7 @@ def test_list_table_versions(collection, session): session.set_response(table_versions) # When - results = list(collection.list_versions(table_versions['tables'][0]['id'])) + results = list(collection.list_versions(table_versions["tables"][0]["id"])) # Then assert len(results) == 3 @@ -140,7 +142,7 @@ def test_list_by_config(collection, session): # When # NOTE: list_by_config returns slightly more info in this call, but it's a superset of # a typical Table, and parsed identically in citrine-python. - results = list(collection.list_by_config(table_versions['tables'][0]['id'])) + results = list(collection.list_by_config(table_versions["tables"][0]["id"])) # Then assert len(results) == 3 @@ -156,7 +158,7 @@ def test_init_table(): def test_str_serialization(table): t = table("http://somewhere.cool") - assert str(t) == "".format(t.uid, 2) + assert str(t) == f"" def test_register_table(collection): @@ -178,29 +180,29 @@ def test_build_from_config(collection: GemTableCollection, session): config_uid = uuid4() config_version = 2 config = TableConfig( - name='foo', - description='bar', - columns=[], - rows=[], - variables=[], - datasets=[] + name="foo", description="bar", columns=[], rows=[], variables=[], datasets=[] ) config.config_uid = config_uid config.version_number = config_version expected_table_data = GemTableDataFactory() session.set_responses( - {'job_id': '12345678-1234-1234-1234-123456789ccc'}, - {'job_type': 'foo', 'status': 'In Progress', 'tasks': []}, - {'job_type': 'foo', 'status': 'Success', 'tasks': [], 'output': { - 'display_table_id': expected_table_data['id'], - 'display_table_version': str(expected_table_data['version']), - 'table_warnings': json.dumps([ - {'limited_results': ['foo', 'bar'], 'total_count': 3}, - ]) - }}, + {"job_id": "12345678-1234-1234-1234-123456789ccc"}, + {"job_type": "foo", "status": "In Progress", "tasks": []}, + { + "job_type": "foo", + "status": "Success", + "tasks": [], + "output": { + "display_table_id": expected_table_data["id"], + "display_table_version": str(expected_table_data["version"]), + "table_warnings": json.dumps( + [{"limited_results": ["foo", "bar"], "total_count": 3}] + ), + }, + }, expected_table_data, ) - gem_table = collection.build_from_config(config, version='ignored') + gem_table = collection.build_from_config(config, version="ignored") assert isinstance(gem_table, GemTable) assert session.num_calls == 4 @@ -209,12 +211,7 @@ def test_build_from_config_failures(collection: GemTableCollection, session): with pytest.raises(ValueError): collection.build_from_config(uuid4()) config = TableConfig( - name='foo', - description='bar', - columns=[], - rows=[], - variables=[], - datasets=[] + name="foo", description="bar", columns=[], rows=[], variables=[], datasets=[] ) config.definition_uid = uuid4() with pytest.raises(ValueError): @@ -225,16 +222,26 @@ def test_build_from_config_failures(collection: GemTableCollection, session): collection.build_from_config(config) config.config_uid = uuid4() session.set_responses( - {'job_id': '12345678-1234-1234-1234-123456789ccc'}, - {'job_type': 'foo', 'status': 'Failure', 'tasks': [ - {'task_type': 'foo', 'id': 'foo', 'status': 'Failure', 'failure_reason': 'because', 'dependencies': []} - ]}, + {"job_id": "12345678-1234-1234-1234-123456789ccc"}, + { + "job_type": "foo", + "status": "Failure", + "tasks": [ + { + "task_type": "foo", + "id": "foo", + "status": "Failure", + "failure_reason": "because", + "dependencies": [], + } + ], + }, ) with pytest.raises(JobFailureError): collection.build_from_config(uuid4(), version=1) session.set_responses( - {'job_id': '12345678-1234-1234-1234-123456789ccc'}, - {'job_type': 'foo', 'status': 'In Progress', 'tasks': []}, + {"job_id": "12345678-1234-1234-1234-123456789ccc"}, + {"job_type": "foo", "status": "In Progress", "tasks": []}, ) with pytest.raises(PollingTimeoutError): collection.build_from_config(config, timeout=0) @@ -245,44 +252,44 @@ def test_read_table_from_collection(mock_write_files_locally, collection, table) # When with requests_mock.mock() as mock_get: remote_url = "http://otherhost:4566/anywhere" - mock_get.get(remote_url, text='stuff') + mock_get.get(remote_url, text="stuff") collection.read(table=table(remote_url), local_path="table.pdf") assert mock_get.call_count == 1 assert mock_write_files_locally.call_count == 1 - assert mock_write_files_locally.call_args == call(b'stuff', "table.pdf") + assert mock_write_files_locally.call_args == call(b"stuff", "table.pdf") with requests_mock.mock() as mock_get: # When localstack_url = "http://localstack:4566/anywhere" - mock_get.get(localstack_url, text='stuff') + mock_get.get(localstack_url, text="stuff") collection.read(table=table(localstack_url), local_path="table2.pdf") assert mock_get.call_count == 1 assert mock_write_files_locally.call_count == 2 - assert mock_write_files_locally.call_args == call(b'stuff', "table2.pdf") + assert mock_write_files_locally.call_args == call(b"stuff", "table2.pdf") with requests_mock.mock() as mock_get: # When localstack_url = "http://localstack:4566/anywhere" override_url = "https://fakestack:1337" collection.session.s3_endpoint_url = override_url - mock_get.get(override_url + "/anywhere", text='stuff') + mock_get.get(override_url + "/anywhere", text="stuff") collection.read(table=table(localstack_url), local_path="table3.pdf") assert mock_get.call_count == 1 assert mock_write_files_locally.call_count == 3 - assert mock_write_files_locally.call_args == call(b'stuff', "table3.pdf") + assert mock_write_files_locally.call_args == call(b"stuff", "table3.pdf") with requests_mock.mock() as mock_get: # When localstack_url = "http://localstack:4566/anywhere" override_url = "https://fakestack:1337" collection.session.s3_endpoint_url = override_url - mock_get.get(override_url + "/anywhere", text='stuff') + mock_get.get(override_url + "/anywhere", text="stuff") this_table = table(localstack_url) collection.session.set_response({"tables": [this_table.dump()]}) collection.read(table=this_table.uid, local_path="table4.pdf") assert mock_get.call_count == 1 assert mock_write_files_locally.call_count == 4 - assert mock_write_files_locally.call_args == call(b'stuff', "table4.pdf") + assert mock_write_files_locally.call_args == call(b"stuff", "table4.pdf") def test_read_table_into_memory_from_collection(table, session, collection): @@ -302,7 +309,4 @@ def test_gem_table_entity_dict(): table = GemTable.build(GemTableDataFactory()) entity = table.access_control_dict() - assert entity == { - 'id': str(table.uid), - 'type': 'TABLE' - } + assert entity == {"id": str(table.uid), "type": "TABLE"} diff --git a/tests/resources/test_gemd_resource.py b/tests/resources/test_gemd_resource.py index 8b68ce092..1819e86b8 100644 --- a/tests/resources/test_gemd_resource.py +++ b/tests/resources/test_gemd_resource.py @@ -1,39 +1,41 @@ import random -from uuid import uuid4, UUID from os.path import basename +from uuid import UUID, uuid4 import pytest - +from gemd.entity.attribute import Condition, Parameter, Property, PropertyAndConditions from gemd.entity.bounds.integer_bounds import IntegerBounds -from gemd.entity.attribute import Property, Condition, Parameter, PropertyAndConditions -from gemd.entity.value import NominalInteger from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.object.material_spec import MaterialSpec as GemdMaterialSpec +from gemd.entity.object.ingredient_run import IngredientRun as GemdIngredientRun +from gemd.entity.object.ingredient_spec import IngredientSpec as GemdIngredientSpec from gemd.entity.object.material_run import MaterialRun as GemdMaterialRun -from gemd.entity.object.process_spec import ProcessSpec as GemdProcessSpec -from gemd.entity.object.process_run import ProcessRun as GemdProcessRun -from gemd.entity.object.measurement_spec import MeasurementSpec as GemdMeasurementSpec +from gemd.entity.object.material_spec import MaterialSpec as GemdMaterialSpec from gemd.entity.object.measurement_run import MeasurementRun as GemdMeasurementRun -from gemd.entity.object.ingredient_spec import IngredientSpec as GemdIngredientSpec -from gemd.entity.object.ingredient_run import IngredientRun as GemdIngredientRun -from gemd.entity.template.material_template import MaterialTemplate as GemdMaterialTemplate -from gemd.entity.template.process_template import ProcessTemplate as GemdProcessTemplate -from gemd.entity.template.measurement_template import MeasurementTemplate as GemdMeasurementTemplate +from gemd.entity.object.measurement_spec import MeasurementSpec as GemdMeasurementSpec +from gemd.entity.object.process_run import ProcessRun as GemdProcessRun +from gemd.entity.object.process_spec import ProcessSpec as GemdProcessSpec from gemd.entity.template.condition_template import ConditionTemplate as GemdConditionTemplate +from gemd.entity.template.material_template import MaterialTemplate as GemdMaterialTemplate +from gemd.entity.template.measurement_template import ( + MeasurementTemplate as GemdMeasurementTemplate, +) from gemd.entity.template.parameter_template import ParameterTemplate as GemdParameterTemplate +from gemd.entity.template.process_template import ProcessTemplate as GemdProcessTemplate from gemd.entity.template.property_template import PropertyTemplate as GemdPropertyTemplate +from gemd.entity.value import NominalInteger -from citrine.exceptions import PollingTimeoutError, JobFailureError -from citrine.resources.api_error import ApiError, ValidationError +from citrine._utils.functions import format_escaped_url +from citrine.exceptions import JobFailureError, PollingTimeoutError +from citrine.resources.api_error import ApiError from citrine.resources.audit_info import AuditInfo from citrine.resources.condition_template import ConditionTemplate -from citrine.resources.data_concepts import DataConcepts, CITRINE_SCOPE, CITRINE_TAG_PREFIX +from citrine.resources.data_concepts import CITRINE_SCOPE, CITRINE_TAG_PREFIX, DataConcepts from citrine.resources.gemd_resource import GEMDResourceCollection from citrine.resources.ingredient_run import IngredientRun from citrine.resources.ingredient_spec import IngredientSpec from citrine.resources.material_run import MaterialRun -from citrine.resources.material_spec import MaterialSpecCollection, MaterialSpec -from citrine.resources.material_template import MaterialTemplateCollection, MaterialTemplate +from citrine.resources.material_spec import MaterialSpec, MaterialSpecCollection +from citrine.resources.material_template import MaterialTemplate, MaterialTemplateCollection from citrine.resources.measurement_run import MeasurementRun from citrine.resources.measurement_spec import MeasurementSpec from citrine.resources.measurement_template import MeasurementTemplate @@ -42,11 +44,12 @@ from citrine.resources.process_spec import ProcessSpec from citrine.resources.process_template import ProcessTemplate from citrine.resources.property_template import PropertyTemplate -from citrine._utils.functions import format_escaped_url - -from tests.utils.factories import MaterialRunDataFactory, MaterialSpecDataFactory -from tests.utils.factories import JobSubmissionResponseDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.factories import ( + JobSubmissionResponseDataFactory, + MaterialRunDataFactory, + MaterialSpecDataFactory, +) +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -56,17 +59,15 @@ def session() -> FakeSession: @pytest.fixture def gemd_collection(session) -> GEMDResourceCollection: - return GEMDResourceCollection( - team_id=uuid4(), - dataset_id=uuid4(), - session=session - ) + return GEMDResourceCollection(team_id=uuid4(), dataset_id=uuid4(), session=session) + def test_invalid_collection_construction(): with pytest.raises(TypeError): return GEMDResourceCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), session=session + ) + def sample_gems(nsamples, **kwargs): factories = [MaterialRunDataFactory, MaterialSpecDataFactory] @@ -80,9 +81,7 @@ def test_get_type(gemd_collection): def test_list(gemd_collection, session): # Given samples = sample_gems(20) - session.set_response({ - 'contents': samples - }) + session.set_response({"contents": samples}) # When gems = list(gemd_collection.list()) @@ -90,52 +89,65 @@ def test_list(gemd_collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='GET', - path=format_escaped_url('teams/{}/storables', gemd_collection.team_id, gemd_collection.dataset_id), + method="GET", + path=format_escaped_url( + "teams/{}/storables", gemd_collection.team_id, gemd_collection.dataset_id + ), params={ - 'dataset_id': str(gemd_collection.dataset_id), - 'forward': True, - 'ascending': True, - 'per_page': 100 - } + "dataset_id": str(gemd_collection.dataset_id), + "forward": True, + "ascending": True, + "per_page": 100, + }, ) assert expected_call == session.last_call assert len(samples) == len(gems) for i in range(len(gems)): - assert samples[i]['uids']['id'] == gems[i].uids['id'] + assert samples[i]["uids"]["id"] == gems[i].uids["id"] def test_register(gemd_collection): """Check that register routes to the correct collections""" targets = [ MaterialTemplate("foo"), - MaterialSpec("foo", - properties=[PropertyAndConditions( - property=Property("prop", value=NominalInteger(1)), - conditions=[Condition("cond", value=NominalInteger(1))], - )] - ), + MaterialSpec( + "foo", + properties=[ + PropertyAndConditions( + property=Property("prop", value=NominalInteger(1)), + conditions=[Condition("cond", value=NominalInteger(1))], + ) + ], + ), MaterialRun("foo"), ProcessTemplate("foo"), - ProcessSpec("foo", - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), - ProcessRun("foo", - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), + ProcessSpec( + "foo", + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), + ProcessRun( + "foo", + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), MeasurementTemplate("foo"), - MeasurementSpec("foo", - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), - MeasurementRun("foo", - properties=[Property("prop", value=NominalInteger(1))], - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), + MeasurementSpec( + "foo", + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), + MeasurementRun( + "foo", + properties=[Property("prop", value=NominalInteger(1))], + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), IngredientSpec("foo"), IngredientRun(), PropertyTemplate("bar", bounds=IntegerBounds(0, 1)), ParameterTemplate("bar", bounds=IntegerBounds(0, 1)), - ConditionTemplate("bar", bounds=IntegerBounds(0, 1)) + ConditionTemplate("bar", bounds=IntegerBounds(0, 1)), ] for obj in targets: @@ -145,7 +157,9 @@ def test_register(gemd_collection): registered = gemd_collection.register(obj, dry_run=False) assert len(obj.uids) == 1 assert len(registered.uids) == 1 - assert basename(gemd_collection.session.calls[-1].path) == basename(gemd_collection._path_template) + assert basename(gemd_collection.session.calls[-1].path) == basename( + gemd_collection._path_template + ) for pair in obj.uids.items(): assert pair[1] == registered.uids[pair[0]] @@ -154,33 +168,44 @@ def test_gemd_register(gemd_collection): """Check that register routes to the correct collections""" targets = [ GemdMaterialTemplate("foo"), - GemdMaterialSpec("foo", - properties=[PropertyAndConditions( - property=Property("prop", value=NominalInteger(1)), - conditions=[Condition("cond", value=NominalInteger(1))], - )] - ), + GemdMaterialSpec( + "foo", + properties=[ + PropertyAndConditions( + property=Property("prop", value=NominalInteger(1)), + conditions=[Condition("cond", value=NominalInteger(1))], + ) + ], + ), GemdMaterialRun("foo"), GemdProcessTemplate("foo"), - GemdProcessSpec("foo", - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), - GemdProcessRun("foo", - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), + GemdProcessSpec( + "foo", + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), + GemdProcessRun( + "foo", + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), GemdMeasurementTemplate("foo"), - GemdMeasurementSpec("foo", - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), - GemdMeasurementRun("foo", - properties=[Property("prop", value=NominalInteger(1))], - conditions=[Condition("cond", value=NominalInteger(1))], - parameters=[Parameter("para", value=NominalInteger(1))]), + GemdMeasurementSpec( + "foo", + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), + GemdMeasurementRun( + "foo", + properties=[Property("prop", value=NominalInteger(1))], + conditions=[Condition("cond", value=NominalInteger(1))], + parameters=[Parameter("para", value=NominalInteger(1))], + ), GemdIngredientSpec("foo"), GemdIngredientRun(), GemdPropertyTemplate("bar", bounds=IntegerBounds(0, 1)), GemdParameterTemplate("bar", bounds=IntegerBounds(0, 1)), - GemdConditionTemplate("bar", bounds=IntegerBounds(0, 1)) + GemdConditionTemplate("bar", bounds=IntegerBounds(0, 1)), ] for obj in targets: @@ -190,7 +215,9 @@ def test_gemd_register(gemd_collection): registered = gemd_collection.register(obj, dry_run=False) assert len(obj.uids) == 1 assert len(registered.uids) == 1 - assert basename(gemd_collection.session.calls[-1].path) == basename(gemd_collection._path_template) + assert basename(gemd_collection.session.calls[-1].path) == basename( + gemd_collection._path_template + ) for pair in obj.uids.items(): assert pair[1] == registered.uids[pair[0]] @@ -198,21 +225,15 @@ def test_gemd_register(gemd_collection): def test_register_no_mutate(gemd_collection): """Check that register routes to the correct collections""" expected = { - MaterialTemplateCollection: MaterialTemplate("foo", - uids={'scope1': 'A', - 'scope2': 'B' - } - ), - MaterialSpecCollection: MaterialSpec("foo", - uids={'id': str(uuid4())} - ), + MaterialTemplateCollection: MaterialTemplate("foo", uids={"scope1": "A", "scope2": "B"}), + MaterialSpecCollection: MaterialSpec("foo", uids={"id": str(uuid4())}), } for specific_collection, obj in expected.items(): len_before = len(obj.uids) registered = gemd_collection.register(obj) assert len(obj.uids) == len_before for pair in registered.uids.items(): - assert pair[1] == obj.uids.get(pair[0], 'No such key') + assert pair[1] == obj.uids.get(pair[0], "No such key") def test_register_all(gemd_collection): @@ -221,39 +242,55 @@ def test_register_all(gemd_collection): property_template = PropertyTemplate("bar", bounds=bounds) parameter_template = ParameterTemplate("bar", bounds=bounds) condition_template = ConditionTemplate("bar", bounds=bounds) - foo_process_template = ProcessTemplate("foo", - conditions=[[condition_template, bounds]], - parameters=[[parameter_template, bounds]]) + foo_process_template = ProcessTemplate( + "foo", conditions=[[condition_template, bounds]], parameters=[[parameter_template, bounds]] + ) foo_process_spec = ProcessSpec("foo", template=foo_process_template) foo_process_run = ProcessRun("foo", spec=foo_process_spec) foo_material_template = MaterialTemplate("foo", properties=[[property_template, bounds]]) - foo_material_spec = MaterialSpec("foo", template=foo_material_template, process=foo_process_spec) + foo_material_spec = MaterialSpec( + "foo", template=foo_material_template, process=foo_process_spec + ) foo_material_run = MaterialRun("foo", spec=foo_material_spec, process=foo_process_run) - foo_measurement_template = MeasurementTemplate("foo", - conditions=[[condition_template, bounds]], - parameters=[[parameter_template, bounds]], - properties=[[property_template, bounds]]) + foo_measurement_template = MeasurementTemplate( + "foo", + conditions=[[condition_template, bounds]], + parameters=[[parameter_template, bounds]], + properties=[[property_template, bounds]], + ) foo_measurement_spec = MeasurementSpec("foo", template=foo_measurement_template) - foo_measurement_run = MeasurementRun("foo", spec=foo_measurement_spec, material=foo_material_run) + foo_measurement_run = MeasurementRun( + "foo", spec=foo_measurement_spec, material=foo_material_run + ) - baz_process_template = ProcessTemplate("baz", - conditions=[[condition_template, bounds]], - parameters=[[parameter_template, bounds]]) + baz_process_template = ProcessTemplate( + "baz", conditions=[[condition_template, bounds]], parameters=[[parameter_template, bounds]] + ) baz_process_spec = ProcessSpec("baz", template=baz_process_template) baz_process_run = ProcessRun("baz", spec=baz_process_spec) baz_material_template = MaterialTemplate("baz", properties=[[property_template, bounds]]) - baz_material_spec = MaterialSpec("baz", template=baz_material_template, process=baz_process_spec) + baz_material_spec = MaterialSpec( + "baz", template=baz_material_template, process=baz_process_spec + ) baz_material_run = MaterialRun("baz", spec=baz_material_spec, process=baz_process_run) - baz_measurement_template = MeasurementTemplate("baz", - conditions=[[condition_template, bounds]], - parameters=[[parameter_template, bounds]], - properties=[[property_template, bounds]]) + baz_measurement_template = MeasurementTemplate( + "baz", + conditions=[[condition_template, bounds]], + parameters=[[parameter_template, bounds]], + properties=[[property_template, bounds]], + ) baz_measurement_spec = MeasurementSpec("baz", template=baz_measurement_template) - baz_measurement_run = MeasurementRun("baz", spec=baz_measurement_spec, material=baz_material_run) + baz_measurement_run = MeasurementRun( + "baz", spec=baz_measurement_spec, material=baz_material_run + ) - foo_baz_ingredient_spec = IngredientSpec("foo", material=foo_material_spec, process=baz_process_spec) - foo_baz_ingredient_run = IngredientRun(spec=foo_baz_ingredient_spec, material=foo_material_run, process=baz_process_run) + foo_baz_ingredient_spec = IngredientSpec( + "foo", material=foo_material_spec, process=baz_process_spec + ) + foo_baz_ingredient_run = IngredientRun( + spec=foo_baz_ingredient_spec, material=foo_material_run, process=baz_process_run + ) expected = [ foo_baz_ingredient_run, @@ -267,7 +304,6 @@ def test_register_all(gemd_collection): foo_process_run, foo_process_spec, foo_process_template, - baz_measurement_run, baz_measurement_spec, baz_measurement_template, @@ -277,10 +313,9 @@ def test_register_all(gemd_collection): baz_process_run, baz_process_spec, baz_process_template, - property_template, parameter_template, - condition_template + condition_template, ] for obj in expected: @@ -313,7 +348,7 @@ def test_register_all(gemd_collection): def test_register_all_dry_run(gemd_collection): """Verify expected behavior around batching. Note we cannot actually test dependencies.""" - from gemd.demo.cake import make_cake_templates, make_cake_spec, make_cake, change_scope + from gemd.demo.cake import change_scope, make_cake, make_cake_spec, make_cake_templates from gemd.util import flatten change_scope("pr-688") @@ -369,14 +404,19 @@ def test_delete(gemd_collection, session): for obj in targets: for dry_run in True, False: - session.set_response(obj.dump()) # Delete calls get, must return object data internally + # Delete calls get, must return object data internally + session.set_response(obj.dump()) gemd_collection.delete(obj, dry_run=dry_run) - assert gemd_collection.session.calls[-1].path.split("/")[-3] == basename(gemd_collection._path_template) + assert gemd_collection.session.calls[-1].path.split("/")[-3] == basename( + gemd_collection._path_template + ) - # And again, with uids - session.set_response(obj.dump()) # Delete calls get, must return object data internally + # And again, with uids (repeating delete bypass) + session.set_response(obj.dump()) gemd_collection.delete(obj.uid, dry_run=dry_run) - assert gemd_collection.session.calls[-1].path.split("/")[-3] == basename(gemd_collection._path_template) + assert gemd_collection.session.calls[-1].path.split("/")[-3] == basename( + gemd_collection._path_template + ) def test_update(gemd_collection): @@ -391,16 +431,8 @@ def test_update(gemd_collection): def test_async_update(gemd_collection, session): """Check that async update returns appropriately returns None on success.""" - obj = ProcessTemplate( - "foo", - uids={'id': str(uuid4())} - ) - fake_job_status_resp = { - 'job_type': 'some_typ', - 'status': 'Success', - 'tasks': [], - 'output': {} - } + obj = ProcessTemplate("foo", uids={"id": str(uuid4())}) + fake_job_status_resp = {"job_type": "some_typ", "status": "Success", "tasks": [], "output": {}} session.set_responses(JobSubmissionResponseDataFactory(), fake_job_status_resp) @@ -411,10 +443,7 @@ def test_async_update(gemd_collection, session): def test_async_update_and_no_dataset_id(gemd_collection, session): """Ensure async_update requires a dataset id""" - obj = ProcessTemplate( - "foo", - uids={'id': str(uuid4())} - ) + obj = ProcessTemplate("foo", uids={"id": str(uuid4())}) session.set_response(JobSubmissionResponseDataFactory()) gemd_collection.dataset_id = None @@ -426,37 +455,20 @@ def test_async_update_and_no_dataset_id(gemd_collection, session): def test_async_update_timeout(gemd_collection, session): """Ensure the proper exception is thrown on a timeout error""" - obj = ProcessTemplate( - "foo", - uids={'id': str(uuid4())} - ) - fake_job_status_resp = { - 'job_type': 'some_typ', - 'status': 'Pending', - 'tasks': [], - 'output': {} - } + obj = ProcessTemplate("foo", uids={"id": str(uuid4())}) + fake_job_status_resp = {"job_type": "some_typ", "status": "Pending", "tasks": [], "output": {}} session.set_responses(JobSubmissionResponseDataFactory(), fake_job_status_resp) with pytest.raises(PollingTimeoutError): - gemd_collection.async_update(obj, wait_for_response=True, - timeout=-1.0) + gemd_collection.async_update(obj, wait_for_response=True, timeout=-1.0) def test_async_update_and_wait(gemd_collection, session): """Check that async_update parses the response when waiting""" - obj = ProcessTemplate( - "foo", - uids={'id': str(uuid4())} - ) - fake_job_status_resp = { - 'job_type': 'some_typ', - 'status': 'Success', - 'tasks': [], - 'output': {} - } + obj = ProcessTemplate("foo", uids={"id": str(uuid4())}) + fake_job_status_resp = {"job_type": "some_typ", "status": "Success", "tasks": [], "output": {}} session.set_responses(JobSubmissionResponseDataFactory(), fake_job_status_resp) @@ -467,16 +479,8 @@ def test_async_update_and_wait(gemd_collection, session): def test_async_update_and_wait_failure(gemd_collection, session): """Check that async_update parses the failure correctly""" - obj = ProcessTemplate( - "foo", - uids={'id': str(uuid4())} - ) - fake_job_status_resp = { - 'job_type': 'some_typ', - 'status': 'Failure', - 'tasks': [], - 'output': {} - } + obj = ProcessTemplate("foo", uids={"id": str(uuid4())}) + fake_job_status_resp = {"job_type": "some_typ", "status": "Failure", "tasks": [], "output": {}} session.set_responses(JobSubmissionResponseDataFactory(), fake_job_status_resp) @@ -487,10 +491,7 @@ def test_async_update_and_wait_failure(gemd_collection, session): def test_async_update_with_no_wait(gemd_collection, session): """Check that async_update parses the response when not waiting""" - obj = ProcessTemplate( - "foo", - uids={'id': str(uuid4())} - ) + obj = ProcessTemplate("foo", uids={"id": str(uuid4())}) session.set_response(JobSubmissionResponseDataFactory()) job_id = gemd_collection.async_update(obj, wait_for_response=False) @@ -498,44 +499,36 @@ def test_async_update_with_no_wait(gemd_collection, session): def test_batch_delete(gemd_collection, session): - job_resp = { - 'job_id': '1234' - } + job_resp = {"job_id": "1234"} import json - failures_escaped_json = json.dumps([ - { - "id": { - 'scope': 'somescope', - 'id': 'abcd-1234' - }, - 'cause': { - "code": 400, - "message": "", - "validation_errors": [ - { - "failure_message": "fail msg", - "failure_id": "identifier.coreid.missing" - } - ] + + failures_escaped_json = json.dumps( + [ + { + "id": {"scope": "somescope", "id": "abcd-1234"}, + "cause": { + "code": 400, + "message": "", + "validation_errors": [ + {"failure_message": "fail msg", "failure_id": "identifier.coreid.missing"} + ], + }, } - } - ]) + ] + ) failed_job_resp = { - 'job_type': 'batch_delete', - 'status': 'Success', - 'tasks': [], - 'output': { - 'failures': failures_escaped_json - } + "job_type": "batch_delete", + "status": "Success", + "tasks": [], + "output": {"failures": failures_escaped_json}, } session.set_responses(job_resp, failed_job_resp) # When - del_resp = gemd_collection.batch_delete([UUID( - '16fd2706-8baf-433b-82eb-8c7fada847da')]) + del_resp = gemd_collection.batch_delete([UUID("16fd2706-8baf-433b-82eb-8c7fada847da")]) # Then assert 2 == session.num_calls @@ -543,13 +536,17 @@ def test_batch_delete(gemd_collection, session): assert len(del_resp) == 1 first_failure = del_resp[0] - expected_api_error = ApiError.build({ - "code": "400", - "message": "", - "validation_errors": [{"failure_message": "fail msg", "failure_id": "identifier.coreid.missing"}] - }) + expected_api_error = ApiError.build( + { + "code": "400", + "message": "", + "validation_errors": [ + {"failure_message": "fail msg", "failure_id": "identifier.coreid.missing"} + ], + } + ) - assert first_failure[0] == LinkByUID('somescope', 'abcd-1234') + assert first_failure[0] == LinkByUID("somescope", "abcd-1234") assert first_failure[1].dump() == expected_api_error.dump() @@ -562,20 +559,22 @@ def test_type_passthrough(gemd_collection, session): """Verify objects that are not directly referenced by objects (e.g., a tuple of Templates) don't get type information stripped.""" # Generate some metadata metadata = { - 'dataset': str(uuid4()), - 'audit_info': AuditInfo.build({"created_by": str(uuid4()), - "created_at": 1559933807392 - }), - "tags": [f"{CITRINE_TAG_PREFIX}::added"] + "dataset": str(uuid4()), + "audit_info": AuditInfo.build({"created_by": str(uuid4()), "created_at": 1559933807392}), + "tags": [f"{CITRINE_TAG_PREFIX}::added"], } # Set up the Condition Templates low_tmpl, high_tmpl = [ - ConditionTemplate('condition low', uids={CITRINE_SCOPE: str(uuid4())}, bounds=IntegerBounds(1, 10)), - ConditionTemplate('condition high', uids={CITRINE_SCOPE: str(uuid4())}, bounds=IntegerBounds(11, 20)), + ConditionTemplate( + "condition low", uids={CITRINE_SCOPE: str(uuid4())}, bounds=IntegerBounds(1, 10) + ), + ConditionTemplate( + "condition high", uids={CITRINE_SCOPE: str(uuid4())}, bounds=IntegerBounds(11, 20) + ), ] - session.set_response({"objects": [dict(low_tmpl.dump(), **metadata), - dict(high_tmpl.dump(), **metadata), - ]}) + session.set_response( + {"objects": [dict(low_tmpl.dump(), **metadata), dict(high_tmpl.dump(), **metadata)]} + ) low_tmpl, high_tmpl = gemd_collection.register_all([low_tmpl, high_tmpl]) assert low_tmpl.dataset is not None assert low_tmpl.audit_info is not None @@ -583,10 +582,9 @@ def test_type_passthrough(gemd_collection, session): assert high_tmpl.audit_info is not None ptempl = ProcessTemplate( - 'my template', + "my template", uids={CITRINE_SCOPE: str(uuid4())}, conditions=[(low_tmpl, IntegerBounds(2, 4)), (high_tmpl, IntegerBounds(12, 15))], - ) session.set_response(dict(ptempl.dump(), **metadata)) ptempl = gemd_collection.register(ptempl) @@ -595,37 +593,34 @@ def test_type_passthrough(gemd_collection, session): arr = [ ProcessSpec( - 'foo', + "foo", uids={CITRINE_SCOPE: str(uuid4())}, template=ptempl, conditions=[ - Condition(name='low', value=NominalInteger(3), template=low_tmpl), - Condition(name='high', value=NominalInteger(13), template=high_tmpl), - ] + Condition(name="low", value=NominalInteger(3), template=low_tmpl), + Condition(name="high", value=NominalInteger(13), template=high_tmpl), + ], ), ProcessSpec( - 'bar', + "bar", uids={CITRINE_SCOPE: str(uuid4())}, template=ptempl, - conditions=[ - Condition(name='high', value=NominalInteger(14), template=high_tmpl), - ] + conditions=[Condition(name="high", value=NominalInteger(14), template=high_tmpl)], ), - ProcessSpec('baz', uids={CITRINE_SCOPE: str(uuid4())}), + ProcessSpec("baz", uids={CITRINE_SCOPE: str(uuid4())}), ] session.set_response({"objects": [dict(x.dump(), **metadata) for x in arr]}) pspecs = gemd_collection.register_all(arr) - assert [s.name for s in pspecs] == ['foo', 'bar', 'baz'] + assert [s.name for s in pspecs] == ["foo", "bar", "baz"] assert pspecs == arr def test_tag_magic(gemd_collection, session): auto_tag = f"{CITRINE_TAG_PREFIX}::added" - additions = {"tags": ["tag", auto_tag], - "uids": {CITRINE_SCOPE: str(uuid4()), - "original": "id" - } - } + additions = { + "tags": ["tag", auto_tag], + "uids": {CITRINE_SCOPE: str(uuid4()), "original": "id"}, + } obj1 = ProcessSpec("one", tags=["tag"], uids={"original": "id"}) session.set_response(dict(obj1.dump(), **additions)) diff --git a/tests/resources/test_generative_design_execution.py b/tests/resources/test_generative_design_execution.py index 0a5327d83..fb321652a 100644 --- a/tests/resources/test_generative_design_execution.py +++ b/tests/resources/test_generative_design_execution.py @@ -1,11 +1,15 @@ -import pytest import uuid -from citrine.informatics.generative_design import GenerativeDesignInput +import pytest + from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution +from citrine.informatics.generative_design import ( + FingerprintType, + GenerativeDesignInput, + StructureExclusion, +) from citrine.resources.generative_design_execution import GenerativeDesignExecutionCollection -from citrine.informatics.generative_design import FingerprintType, StructureExclusion -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -15,14 +19,13 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> GenerativeDesignExecutionCollection: - return GenerativeDesignExecutionCollection( - project_id=uuid.uuid4(), - session=session, - ) + return GenerativeDesignExecutionCollection(project_id=uuid.uuid4(), session=session) @pytest.fixture -def generative_design_execution(collection: GenerativeDesignExecutionCollection, generative_design_execution_dict) -> GenerativeDesignExecution: +def generative_design_execution( + collection: GenerativeDesignExecutionCollection, generative_design_execution_dict +) -> GenerativeDesignExecution: return collection.build(generative_design_execution_dict) @@ -46,7 +49,9 @@ def test_build_new_execution(collection, generative_design_execution_dict): assert execution.status_detail -def test_trigger_execution(collection: GenerativeDesignExecutionCollection, generative_design_execution_dict, session): +def test_trigger_execution( + collection: GenerativeDesignExecutionCollection, generative_design_execution_dict, session +): # Given session.set_response(generative_design_execution_dict) design_execution_input = GenerativeDesignInput( @@ -63,26 +68,26 @@ def test_trigger_execution(collection: GenerativeDesignExecutionCollection, gene # Then assert str(actual_execution.uid) == generative_design_execution_dict["id"] - expected_path = '/projects/{}/generative-design/executions'.format( - collection.project_id, - ) + expected_path = f"/projects/{collection.project_id}/generative-design/executions" assert session.last_call == FakeCall( - method='POST', + method="POST", path=expected_path, json={ - 'seeds': design_execution_input.seeds, - 'fingerprint_type': design_execution_input.fingerprint_type.value, - 'min_fingerprint_similarity': design_execution_input.min_fingerprint_similarity, - 'mutation_per_seed': design_execution_input.mutation_per_seed, - 'structure_exclusions': [ + "seeds": design_execution_input.seeds, + "fingerprint_type": design_execution_input.fingerprint_type.value, + "min_fingerprint_similarity": design_execution_input.min_fingerprint_similarity, + "mutation_per_seed": design_execution_input.mutation_per_seed, + "structure_exclusions": [ exclusion.value for exclusion in design_execution_input.structure_exclusions ], - 'min_substructure_counts': design_execution_input.min_substructure_counts, - } + "min_substructure_counts": design_execution_input.min_substructure_counts, + }, ) -def test_generative_design_execution_results(generative_design_execution: GenerativeDesignExecution, session, example_generation_results): +def test_generative_design_execution_results( + generative_design_execution: GenerativeDesignExecution, session, example_generation_results +): # Given session.set_response(example_generation_results) @@ -90,28 +95,25 @@ def test_generative_design_execution_results(generative_design_execution: Genera list(generative_design_execution.results(per_page=4)) # Then - expected_path = '/projects/{}/generative-design/executions/{}/results'.format( - generative_design_execution.project_id, - generative_design_execution.uid, + expected_path = f"/projects/{generative_design_execution.project_id}/generative-design/executions/{generative_design_execution.uid}/results" + assert session.last_call == FakeCall( + method="GET", path=expected_path, params={"per_page": 4, "page": 1} ) - assert session.last_call == FakeCall(method='GET', path=expected_path, params={"per_page": 4, "page": 1}) -def test_generative_design_execution_result(generative_design_execution: GenerativeDesignExecution, session, example_generation_results): +def test_generative_design_execution_result( + generative_design_execution: GenerativeDesignExecution, session, example_generation_results +): # Given session.set_response(example_generation_results["response"][0]) # When - result_id=example_generation_results["response"][0]["id"] + result_id = example_generation_results["response"][0]["id"] generative_design_execution.result(result_id=result_id) # Then - expected_path = '/projects/{}/generative-design/executions/{}/results/{}'.format( - generative_design_execution.project_id, - generative_design_execution.uid, - result_id, - ) - assert session.last_call == FakeCall(method='GET', path=expected_path) + expected_path = f"/projects/{generative_design_execution.project_id}/generative-design/executions/{generative_design_execution.uid}/results/{result_id}" + assert session.last_call == FakeCall(method="GET", path=expected_path) def test_list(collection: GenerativeDesignExecutionCollection, session): @@ -119,11 +121,9 @@ def test_list(collection: GenerativeDesignExecutionCollection, session): lst = list(collection.list(per_page=4)) assert len(lst) == 0 - expected_path = '/projects/{}/generative-design/executions'.format(collection.project_id) + expected_path = f"/projects/{collection.project_id}/generative-design/executions" assert session.last_call == FakeCall( - method='GET', - path=expected_path, - params={"page": 1, "per_page": 4} + method="GET", path=expected_path, params={"page": 1, "per_page": 4} ) diff --git a/tests/resources/test_ingestion.py b/tests/resources/test_ingestion.py index 2205fcd72..f214e5361 100644 --- a/tests/resources/test_ingestion.py +++ b/tests/resources/test_ingestion.py @@ -1,23 +1,32 @@ +from uuid import uuid4 + import pytest -from uuid import uuid4, UUID from citrine._session import Session from citrine.exceptions import BadRequest +from citrine.jobs.job import JobFailureError, JobStatusResponse, JobSubmissionResponse from citrine.resources.api_error import ValidationError from citrine.resources.dataset import Dataset from citrine.resources.file_link import FileLink from citrine.resources.ingestion import ( - Ingestion, IngestionCollection, IngestionStatus, IngestionStatusType, IngestionException, - IngestionErrorTrace, IngestionErrorType, IngestionErrorFamily, IngestionErrorLevel + Ingestion, + IngestionCollection, + IngestionErrorFamily, + IngestionErrorLevel, + IngestionErrorTrace, + IngestionErrorType, + IngestionException, + IngestionStatus, + IngestionStatusType, ) -from citrine.jobs.job import JobSubmissionResponse, JobStatusResponse, JobFailureError from citrine.resources.project import Project - from tests.utils.factories import ( - DatasetFactory, IngestionStatusResponseDataFactory, JobSubmissionResponseDataFactory, - JobStatusResponseDataFactory + DatasetFactory, + IngestionStatusResponseDataFactory, + JobStatusResponseDataFactory, + JobSubmissionResponseDataFactory, ) -from tests.utils.session import FakeCall, FakeSession, FakeRequestResponseApiError +from tests.utils.session import FakeRequestResponseApiError, FakeSession @pytest.fixture @@ -27,7 +36,7 @@ def session() -> FakeSession: @pytest.fixture def dataset(session: Session): - dataset = DatasetFactory(name='Test Dataset') + dataset = DatasetFactory(name="Test Dataset") dataset.team_id = uuid4() dataset.uid = uuid4() dataset.session = session @@ -49,26 +58,23 @@ def file_link(dataset: Dataset) -> FileLink: @pytest.fixture def ingest(collection) -> Ingestion: - return collection.build({ - "ingestion_id": uuid4(), - "team_id": collection.team_id, - "dataset_id": collection.dataset_id - }) + return collection.build( + { + "ingestion_id": uuid4(), + "team_id": collection.team_id, + "dataset_id": collection.dataset_id, + } + ) @pytest.fixture def operation() -> JobSubmissionResponse: - return JobSubmissionResponse.build({ - "job_id": uuid4() - }) + return JobSubmissionResponse.build({"job_id": uuid4()}) @pytest.fixture def status() -> IngestionStatus: - return IngestionStatus.build({ - "status": IngestionStatusType.INGESTION_CREATED, - "errors": [] - }) + return IngestionStatus.build({"status": IngestionStatusType.INGESTION_CREATED, "errors": []}) def test_not_implementeds(collection): @@ -94,14 +100,15 @@ def test_poll_for_job_completion_signature(ingest, operation, status, monkeypatc outer_raise_errors = None def _mock_poll_for_job_completion( - session, - team_id, - job, - *, - project_id=None, - timeout=-1.0, - polling_delay=-2.0, - raise_errors=True): + session, + team_id, + job, + *, + project_id=None, + timeout=-1.0, + polling_delay=-2.0, + raise_errors=True, + ): nonlocal outer_timeout nonlocal outer_polling_delay nonlocal outer_raise_errors @@ -114,7 +121,9 @@ def _mock_poll_for_job_completion( def _mock_status(self) -> IngestionStatus: return status - monkeypatch.setattr("citrine.resources.ingestion._poll_for_job_completion", _mock_poll_for_job_completion) + monkeypatch.setattr( + "citrine.resources.ingestion._poll_for_job_completion", _mock_poll_for_job_completion + ) monkeypatch.setattr(Ingestion, "status", _mock_status) ingest.poll_for_job_completion(operation) @@ -134,14 +143,17 @@ def _mock_poll_for_job_completion(**_): return JobStatusResponse.build(JobStatusResponseDataFactory()) # This is mocked equivalently for all tests - monkeypatch.setattr("citrine.resources.ingestion._poll_for_job_completion", _mock_poll_for_job_completion) - validation_error = ValidationError.build({"failure_message": "you failed", "failure_id": "failure_id"}) + monkeypatch.setattr( + "citrine.resources.ingestion._poll_for_job_completion", _mock_poll_for_job_completion + ) + validation_error = ValidationError.build( + {"failure_message": "you failed", "failure_id": "failure_id"} + ) # Raise exceptions, but it worked ingest.raise_errors = True session.set_responses( - {"job_id": str(uuid4())}, - {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} + {"job_id": str(uuid4())}, {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} ) result = ingest.build_objects() assert result.success @@ -151,7 +163,7 @@ def _mock_poll_for_job_completion(**_): ingest.raise_errors = True session.set_responses( BadRequest("path", FakeRequestResponseApiError(400, "Bad Request", [validation_error])), - {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} + {"status": IngestionStatusType.INGESTION_CREATED, "errors": []}, ) with pytest.raises(IngestionException, match="you failed"): ingest.build_objects() @@ -160,7 +172,7 @@ def _mock_poll_for_job_completion(**_): ingest.raise_errors = True session.set_responses( BadRequest("path", FakeRequestResponseApiError(400, "This has no details", [])), - {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} + {"status": IngestionStatusType.INGESTION_CREATED, "errors": []}, ) with pytest.raises(IngestionException, match="no details"): ingest.build_objects() @@ -169,7 +181,7 @@ def _mock_poll_for_job_completion(**_): ingest.raise_errors = True session.set_responses( BadRequest("path", FakeRequestResponseApiError(500, "This was internal", [])), - {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} + {"status": IngestionStatusType.INGESTION_CREATED, "errors": []}, ) with pytest.raises(IngestionException, match="internal"): ingest.build_objects() @@ -178,11 +190,17 @@ def _mock_poll_for_job_completion(**_): ingest.raise_errors = True session.set_responses( {"job_id": str(uuid4())}, - {"status": IngestionStatusType.INGESTION_CREATED, - "errors": [{"msg": "Bad things!", - "level": IngestionErrorLevel.ERROR, - "family": IngestionErrorFamily.STRUCTURE, - "error_type": IngestionErrorType.INVALID_DUPLICATE_NAME}]} + { + "status": IngestionStatusType.INGESTION_CREATED, + "errors": [ + { + "msg": "Bad things!", + "level": IngestionErrorLevel.ERROR, + "family": IngestionErrorFamily.STRUCTURE, + "error_type": IngestionErrorType.INVALID_DUPLICATE_NAME, + } + ], + }, ) with pytest.raises(IngestionException, match="Bad things"): ingest.build_objects() @@ -190,8 +208,7 @@ def _mock_poll_for_job_completion(**_): # Suppress exceptions, but it worked ingest.raise_errors = False session.set_responses( - {"job_id": str(uuid4())}, - {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} + {"job_id": str(uuid4())}, {"status": IngestionStatusType.INGESTION_CREATED, "errors": []} ) result = ingest.build_objects() assert result.success @@ -200,22 +217,30 @@ def _mock_poll_for_job_completion(**_): ingest.raise_errors = False session.set_responses( BadRequest("path", FakeRequestResponseApiError(400, "Bad Request", [validation_error])), - {"status": IngestionStatusType.INGESTION_CREATED, - "errors": [{"msg": validation_error.failure_message, - "level": IngestionErrorLevel.ERROR, - "family": IngestionErrorFamily.DATA, - "error_type": IngestionErrorType.INVALID_DUPLICATE_NAME}]} + { + "status": IngestionStatusType.INGESTION_CREATED, + "errors": [ + { + "msg": validation_error.failure_message, + "level": IngestionErrorLevel.ERROR, + "family": IngestionErrorFamily.DATA, + "error_type": IngestionErrorType.INVALID_DUPLICATE_NAME, + } + ], + }, ) result = ingest.build_objects() assert not result.success - assert any('you failed' in str(e) for e in result.errors) + assert any("you failed" in str(e) for e in result.errors) # Suppress exceptions, and build_objects_async returned errors ingest.raise_errors = False session.set_responses( BadRequest("No API error, so it's thrown", None), - {"status": IngestionStatusType.INGESTION_CREATED, - "errors": [IngestionErrorTrace(validation_error.failure_message).dump()]} + { + "status": IngestionStatusType.INGESTION_CREATED, + "errors": [IngestionErrorTrace(validation_error.failure_message).dump()], + }, ) with pytest.raises(BadRequest): ingest.build_objects() @@ -224,18 +249,19 @@ def _mock_poll_for_job_completion(**_): ingest.raise_errors = False session.set_responses( {"job_id": str(uuid4())}, - {"status": IngestionStatusType.INGESTION_CREATED, - "errors": [IngestionErrorTrace("Sad").dump()] * 3} + { + "status": IngestionStatusType.INGESTION_CREATED, + "errors": [IngestionErrorTrace("Sad").dump()] * 3, + }, ) result = ingest.build_objects() assert not result.success - assert any('Sad' in e.msg for e in result.errors) + assert any("Sad" in e.msg for e in result.errors) -def test_ingestion_with_table_build(session: FakeSession, - ingest: Ingestion, - dataset: Dataset, - file_link: FileLink): +def test_ingestion_with_table_build( + session: FakeSession, ingest: Ingestion, dataset: Dataset, file_link: FileLink +): # build_objects_async will always approve, if we get that far session.set_responses(JobSubmissionResponseDataFactory()) @@ -257,29 +283,28 @@ def test_ingestion_with_table_build(session: FakeSession, # full build_objects full_build_job = JobSubmissionResponseDataFactory() output = { - 'ingestion_id': str(ingest.uid), - 'gemd_table_config_version': '1', - 'table_build_job_id': str(uuid4()), - 'gemd_table_config_id': str(uuid4()) + "ingestion_id": str(ingest.uid), + "gemd_table_config_version": "1", + "table_build_job_id": str(uuid4()), + "gemd_table_config_id": str(uuid4()), } session.set_responses( full_build_job, - JobStatusResponseDataFactory( - job_id=full_build_job["job_id"], - output=output, - ), + JobStatusResponseDataFactory(job_id=full_build_job["job_id"], output=output), JobStatusResponseDataFactory(), - IngestionStatusResponseDataFactory() + IngestionStatusResponseDataFactory(), ) status = ingest.build_objects(build_table=True, project=str(project_uuid)) assert status.success -def test_ingestion_flow(session: FakeSession, - ingest: Ingestion, - collection: IngestionCollection, - file_link: FileLink, - monkeypatch): +def test_ingestion_flow( + session: FakeSession, + ingest: Ingestion, + collection: IngestionCollection, + file_link: FileLink, + monkeypatch, +): validation_error = ValidationError.build({"failure_message": "I've failed"}) with pytest.raises(ValueError, match="No files"): @@ -298,7 +323,9 @@ def test_ingestion_flow(session: FakeSession, session.set_response(BadRequest("Generic Failure", None)) with pytest.raises(BadRequest): assert collection.build_from_file_links([file_link], raise_errors=False) - session.set_response(BadRequest("path", FakeRequestResponseApiError(400, "Bad Request", [validation_error]))) + session.set_response( + BadRequest("path", FakeRequestResponseApiError(400, "Bad Request", [validation_error])) + ) failed = collection.build_from_file_links([file_link], raise_errors=False) def _raise_exception(): @@ -306,7 +333,7 @@ def _raise_exception(): with monkeypatch.context() as m: # There should be no calls given a failed ingest object - m.setattr(Session, 'request', _raise_exception) + m.setattr(Session, "request", _raise_exception) assert not failed.status().success assert not failed.build_objects().success with pytest.raises(JobFailureError): @@ -325,12 +352,14 @@ def _raise_exception(): JobSubmissionResponseDataFactory(), JobStatusResponseDataFactory(), IngestionStatusResponseDataFactory( - errors=[{ - "family": IngestionErrorFamily.DATA, - "error_type": IngestionErrorType.MISSING_RAW_FOR_INGREDIENT, - "level": IngestionErrorLevel.ERROR, - "msg": "Missing ingredient: \"myristic (14:0)\" (Note ingredient IDs are case sensitive)" - }] + errors=[ + { + "family": IngestionErrorFamily.DATA, + "error_type": IngestionErrorType.MISSING_RAW_FOR_INGREDIENT, + "level": IngestionErrorLevel.ERROR, + "msg": 'Missing ingredient: "myristic (14:0)" (Note ingredient IDs are case sensitive)', + } + ] ), ) with pytest.raises(IngestionException, match="Missing ingredient"): diff --git a/tests/resources/test_ingredient_run.py b/tests/resources/test_ingredient_run.py index 4217cc515..ca71f2ed8 100644 --- a/tests/resources/test_ingredient_run.py +++ b/tests/resources/test_ingredient_run.py @@ -4,7 +4,7 @@ from citrine.resources.ingredient_run import IngredientRunCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.session import FakeCall, FakeSession +from tests.utils.session import FakeSession @pytest.fixture @@ -15,16 +15,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> IngredientRunCollection: return IngredientRunCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), session=session, - team_id=UUID('6b608f78-e341-422c-8076-35adc8828000') + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), ) def test_list_by_spec(collection: IngredientRunCollection): run_noop_gemd_relation_search_test( - search_for='ingredient-runs', - search_with='ingredient-specs', + search_for="ingredient-runs", + search_with="ingredient-specs", collection=collection, search_fn=collection.list_by_spec, ) @@ -32,8 +32,8 @@ def test_list_by_spec(collection: IngredientRunCollection): def test_list_by_material(collection: IngredientRunCollection): run_noop_gemd_relation_search_test( - search_for='ingredient-runs', - search_with='material-runs', + search_for="ingredient-runs", + search_with="material-runs", collection=collection, search_fn=collection.list_by_material, ) @@ -41,8 +41,8 @@ def test_list_by_material(collection: IngredientRunCollection): def test_list_by_process(collection: IngredientRunCollection): run_noop_gemd_relation_search_test( - search_for='ingredient-runs', - search_with='process-runs', + search_for="ingredient-runs", + search_with="process-runs", collection=collection, search_fn=collection.list_by_process, ) @@ -50,19 +50,16 @@ def test_list_by_process(collection: IngredientRunCollection): def test_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.ingredient_run import IngredientRun as CitrineIngredientRun from gemd.entity.object import IngredientRun as GEMDIngredientRun from gemd.entity.value import NominalReal + from citrine.resources.ingredient_run import IngredientRun as CitrineIngredientRun + gemd_obj = GEMDIngredientRun( - mass_fraction=NominalReal(1.0, ""), - notes="I have notes", - tags=["tag!"] + mass_fraction=NominalReal(1.0, ""), notes="I have notes", tags=["tag!"] ) citrine_obj = CitrineIngredientRun( - mass_fraction=NominalReal(1.0, ""), - notes="I have notes", - tags=["tag!"] + mass_fraction=NominalReal(1.0, ""), notes="I have notes", tags=["tag!"] ) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" diff --git a/tests/resources/test_ingredient_spec.py b/tests/resources/test_ingredient_spec.py index 8301c7471..70ace2b50 100644 --- a/tests/resources/test_ingredient_spec.py +++ b/tests/resources/test_ingredient_spec.py @@ -1,14 +1,13 @@ from uuid import UUID import pytest - from gemd.entity.object import IngredientSpec as GEMDIngredientSpec from gemd.entity.value import NominalReal -from citrine.resources.ingredient_spec import IngredientSpecCollection from citrine.resources.ingredient_spec import IngredientSpec as CitrineIngredientSpec +from citrine.resources.ingredient_spec import IngredientSpecCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.session import FakeCall, FakeSession +from tests.utils.session import FakeSession @pytest.fixture @@ -19,15 +18,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> IngredientSpecCollection: return IngredientSpecCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + session=session, + ) def test_list_by_material(collection: IngredientSpecCollection): run_noop_gemd_relation_search_test( - search_for='ingredient-specs', - search_with='material-specs', + search_for="ingredient-specs", + search_with="material-specs", collection=collection, search_fn=collection.list_by_material, ) @@ -35,8 +35,8 @@ def test_list_by_material(collection: IngredientSpecCollection): def test_list_by_process(collection: IngredientSpecCollection): run_noop_gemd_relation_search_test( - search_for='ingredient-specs', - search_with='process-specs', + search_for="ingredient-specs", + search_with="process-specs", collection=collection, search_fn=collection.list_by_process, ) @@ -49,14 +49,14 @@ def test_equals(): labels=["nice", "words"], mass_fraction=NominalReal(1.0, ""), notes="I have notes", - tags=["tag!"] + tags=["tag!"], ) citrine_obj = CitrineIngredientSpec( name="My Name", labels=["nice", "words"], mass_fraction=NominalReal(1.0, ""), notes="I have notes", - tags=["tag!"] + tags=["tag!"], ) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" diff --git a/tests/resources/test_material_run.py b/tests/resources/test_material_run.py index 96d10ca11..9809521c1 100644 --- a/tests/resources/test_material_run.py +++ b/tests/resources/test_material_run.py @@ -1,18 +1,8 @@ -from uuid import UUID import json +from uuid import UUID import pytest -from citrine._session import Session -from citrine._utils.functions import scrub_none -from citrine.exceptions import BadRequest -from citrine.resources.api_error import ValidationError -from citrine.resources.data_concepts import CITRINE_SCOPE -from citrine.resources.material_run import MaterialRunCollection -from citrine.resources.material_run import MaterialRun as CitrineRun -from citrine.resources.material_run import _inject_default_label_tags -from citrine.resources.gemd_resource import GEMDResourceCollection - -from gemd.demo.cake import make_cake, change_scope +from gemd.demo.cake import change_scope, make_cake from gemd.entity.bounds.integer_bounds import IntegerBounds from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object.material_run import MaterialRun as GEMDRun @@ -20,11 +10,30 @@ from gemd.json import GEMDJson from gemd.util import flatten +from citrine._session import Session +from citrine._utils.functions import scrub_none +from citrine.exceptions import BadRequest +from citrine.resources.api_error import ValidationError +from citrine.resources.data_concepts import CITRINE_SCOPE +from citrine.resources.gemd_resource import GEMDResourceCollection +from citrine.resources.material_run import MaterialRun as CitrineRun +from citrine.resources.material_run import MaterialRunCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.factories import MaterialRunFactory, MaterialRunDataFactory, LinkByUIDFactory, \ - MaterialTemplateFactory, MaterialSpecDataFactory, ProcessTemplateFactory -from tests.utils.session import FakeSession, FakeCall, make_fake_cursor_request_function, FakeRequestResponseApiError, \ - FakeRequestResponse +from tests.utils.factories import ( + LinkByUIDFactory, + MaterialRunDataFactory, + MaterialRunFactory, + MaterialSpecDataFactory, + MaterialTemplateFactory, + ProcessTemplateFactory, +) +from tests.utils.session import ( + FakeCall, + FakeRequestResponse, + FakeRequestResponseApiError, + FakeSession, + make_fake_cursor_request_function, +) @pytest.fixture @@ -35,19 +44,22 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> MaterialRunCollection: return MaterialRunCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), session=session, - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000')) + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + ) + def test_invalid_collection_construction(): with pytest.raises(TypeError): - mr = MaterialRunCollection(dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - session=session) + mr = MaterialRunCollection( + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), session=session + ) def test_register_material_run(collection, session): # Given - session.set_response(MaterialRunDataFactory(name='Test MR 123')) + session.set_response(MaterialRunDataFactory(name="Test MR 123")) material_run = MaterialRunFactory() # When @@ -58,21 +70,29 @@ def test_register_material_run(collection, session): def test_register_all(collection, session): - runs = [MaterialRunFactory(name='1'), MaterialRunFactory(name='2'), MaterialRunFactory(name='3')] - session.set_response({'objects': [r.dump() for r in runs]}) + runs = [ + MaterialRunFactory(name="1"), + MaterialRunFactory(name="2"), + MaterialRunFactory(name="3"), + ] + session.set_response({"objects": [r.dump() for r in runs]}) registered = collection.register_all(runs) assert [r.name for r in runs] == [r.name for r in registered] assert len(session.calls) == 1 - assert session.calls[0].method == 'PUT' - assert GEMDResourceCollection(team_id = collection.team_id, dataset_id = collection.dataset_id, session = collection.session)._get_path() \ - in session.calls[0].path + assert session.calls[0].method == "PUT" + path = GEMDResourceCollection( + team_id=collection.team_id, dataset_id=collection.dataset_id, session=collection.session + )._get_path() + assert path in session.calls[0].path with pytest.raises(RuntimeError): - MaterialRunCollection(team_id=collection.team_id, dataset_id=None, session=session).register_all([]) + MaterialRunCollection( + team_id=collection.team_id, dataset_id=None, session=session + ).register_all([]) def test_dry_run_register_material_run(collection, session): # Given - session.set_response(MaterialRunDataFactory(name='Test MR 123')) + session.set_response(MaterialRunDataFactory(name="Test MR 123")) material_run = MaterialRunFactory() # When @@ -80,14 +100,14 @@ def test_dry_run_register_material_run(collection, session): # Then assert "" == str(registered) - assert session.last_call.params == {'dry_run': True} + assert session.last_call.params == {"dry_run": True} def test_nomutate_gemd(collection, session): """When registering a GEMD object, the object should not change (aside from auto ids)""" # Given - session.set_response(MaterialRunDataFactory(name='Test MR mutation')) - before, after = (GEMDRun(name='Main', uids={'nomutate': 'please'}) for _ in range(2)) + session.set_response(MaterialRunDataFactory(name="Test MR mutation")) + before, after = (GEMDRun(name="Main", uids={"nomutate": "please"}) for _ in range(2)) # When registered = collection.register(after) @@ -103,10 +123,12 @@ def test_get_history(collection, session): # Given cake = make_cake() cake_json = json.loads(GEMDJson(scope=CITRINE_SCOPE).dumps(cake)) - root_link = LinkByUID.build(cake_json.pop('object')) - root_obj = next(o for o in cake_json['context'] if root_link.id == o['uids'].get(root_link.scope)) - cake_json['roots'] = [root_obj] - cake_json['context'].remove(root_obj) + root_link = LinkByUID.build(cake_json.pop("object")) + root_obj = next( + o for o in cake_json["context"] if root_link.id == o["uids"].get(root_link.scope) + ) + cake_json["roots"] = [root_obj] + cake_json["context"].remove(root_obj) session.set_response([cake_json]) @@ -116,17 +138,17 @@ def test_get_history(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'teams/{collection.team_id}/gemd/query/material-histories?filter_nonroot_materials=true', + method="POST", + path=f"teams/{collection.team_id}/gemd/query/material-histories?filter_nonroot_materials=true", json={ - 'criteria': [ + "criteria": [ { - 'datasets': [str(collection.dataset_id)], - 'type': 'terminal_material_run_identifiers_criteria', - 'terminal_material_ids': [{'scope': root_link.scope, 'id': root_link.id}] + "datasets": [str(collection.dataset_id)], + "type": "terminal_material_run_identifiers_criteria", + "terminal_material_ids": [{"scope": root_link.scope, "id": root_link.id}], } ] - } + }, ) assert expected_call == session.last_call assert run == cake @@ -144,17 +166,17 @@ def test_get_history_no_histories(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'teams/{collection.team_id}/gemd/query/material-histories?filter_nonroot_materials=true', + method="POST", + path=f"teams/{collection.team_id}/gemd/query/material-histories?filter_nonroot_materials=true", json={ - 'criteria': [ + "criteria": [ { - 'datasets': [str(collection.dataset_id)], - 'type': 'terminal_material_run_identifiers_criteria', - 'terminal_material_ids': [{'scope': CITRINE_SCOPE, 'id': str(root_id)}] + "datasets": [str(collection.dataset_id)], + "type": "terminal_material_run_identifiers_criteria", + "terminal_material_ids": [{"scope": CITRINE_SCOPE, "id": str(root_id)}], } ] - } + }, ) assert expected_call == session.last_call assert run is None @@ -164,10 +186,12 @@ def test_get_history_no_roots(collection, session): # Given cake = make_cake() cake_json = json.loads(GEMDJson(scope=CITRINE_SCOPE).dumps(cake)) - root_link = LinkByUID.build(cake_json.pop('object')) - root_obj = next(o for o in cake_json['context'] if root_link.id == o['uids'].get(root_link.scope)) - cake_json['roots'] = [] - cake_json['context'].remove(root_obj) + root_link = LinkByUID.build(cake_json.pop("object")) + root_obj = next( + o for o in cake_json["context"] if root_link.id == o["uids"].get(root_link.scope) + ) + cake_json["roots"] = [] + cake_json["context"].remove(root_obj) session.set_response([cake_json]) @@ -177,17 +201,17 @@ def test_get_history_no_roots(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'teams/{collection.team_id}/gemd/query/material-histories?filter_nonroot_materials=true', + method="POST", + path=f"teams/{collection.team_id}/gemd/query/material-histories?filter_nonroot_materials=true", json={ - 'criteria': [ + "criteria": [ { - 'datasets': [str(collection.dataset_id)], - 'type': 'terminal_material_run_identifiers_criteria', - 'terminal_material_ids': [{'scope': root_link.scope, 'id': root_link.id}] + "datasets": [str(collection.dataset_id)], + "type": "terminal_material_run_identifiers_criteria", + "terminal_material_ids": [{"scope": root_link.scope, "id": root_link.id}], } ] - } + }, ) assert expected_call == session.last_call assert run is None @@ -195,8 +219,8 @@ def test_get_history_no_roots(collection, session): def test_get_material_run(collection, session): # Given - run_data = MaterialRunDataFactory(name='Cake 2') - mr_id = run_data['uids']['id'] + run_data = MaterialRunDataFactory(name="Cake 2") + mr_id = run_data["uids"]["id"] session.set_response(run_data) # When @@ -205,18 +229,17 @@ def test_get_material_run(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='GET', - path='teams/{}/datasets/{}/material-runs/id/{}'.format(collection.team_id, collection.dataset_id, mr_id) + method="GET", + path=f"teams/{collection.team_id}/datasets/{collection.dataset_id}/material-runs/id/{mr_id}", ) assert expected_call == session.last_call - assert 'Cake 2' == run.name + assert "Cake 2" == run.name + def test_list_material_runs(collection, session): # Given sample_run = MaterialRunDataFactory() - session.set_response({ - 'contents': [sample_run] - }) + session.set_response({"contents": [sample_run]}) # When runs = list(collection.list()) @@ -225,18 +248,18 @@ def test_list_material_runs(collection, session): assert 1 == session.num_calls expected_call = FakeCall( - method='GET', - path='teams/{}/material-runs'.format(collection.team_id, collection.dataset_id), + method="GET", + path=f"teams/{collection.team_id}/material-runs", params={ - 'dataset_id': str(collection.dataset_id), - 'forward': True, - 'ascending': True, - 'per_page': 100 - } + "dataset_id": str(collection.dataset_id), + "forward": True, + "ascending": True, + "per_page": 100, + }, ) assert expected_call == session.last_call assert 1 == len(runs) - assert sample_run['uids'] == runs[0].uids + assert sample_run["uids"] == runs[0].uids def test_cursor_paginated_searches(collection, session): @@ -244,39 +267,42 @@ def test_cursor_paginated_searches(collection, session): Tests that search methods using cursor-pagination are hooked up correctly. There is no real search logic tested here. """ - all_runs = [ - MaterialRunDataFactory(name="foo_{}".format(i)) for i in range(20) - ] + all_runs = [MaterialRunDataFactory(name=f"foo_{i}") for i in range(20)] fake_request = make_fake_cursor_request_function(all_runs) # pretty shady, need to add these methods to the fake session to test their # interactions with the actual search methods - setattr(session, 'get_resource', fake_request) - setattr(session, 'post_resource', fake_request) - setattr(session, 'cursor_paged_resource', Session.cursor_paged_resource) + session.get_resource = fake_request + session.post_resource = fake_request + session.cursor_paged_resource = Session.cursor_paged_resource - assert len(list(collection.list_by_name('unused', per_page=2))) == len(all_runs) + assert len(list(collection.list_by_name("unused", per_page=2))) == len(all_runs) assert len(list(collection.list(per_page=2))) == len(all_runs) - assert len(list(collection.list_by_tag('unused', per_page=2))) == len(all_runs) - assert len(list(collection.list_by_attribute_bounds( - {LinkByUIDFactory(): IntegerBounds(1, 5)}, per_page=2))) == len(all_runs) + assert len(list(collection.list_by_tag("unused", per_page=2))) == len(all_runs) + assert len( + list( + collection.list_by_attribute_bounds( + {LinkByUIDFactory(): IntegerBounds(1, 5)}, per_page=2 + ) + ) + ) == len(all_runs) # invalid inputs with pytest.raises(TypeError): collection.list_by_attribute_bounds([1, 5], per_page=2) with pytest.raises(NotImplementedError): - collection.list_by_attribute_bounds({ - LinkByUIDFactory(): IntegerBounds(1, 5), - LinkByUIDFactory(): IntegerBounds(1, 5), - }, per_page=2) + collection.list_by_attribute_bounds( + {LinkByUIDFactory(): IntegerBounds(1, 5), LinkByUIDFactory(): IntegerBounds(1, 5)}, + per_page=2, + ) with pytest.raises(RuntimeError): collection.dataset_id = None - collection.list_by_name('unused', per_page=2) + collection.list_by_name("unused", per_page=2) def test_delete_material_run(collection, session): # Given - material_run_uid = '2d3a782f-aee7-41db-853c-36bf4bff0626' - material_run_scope = 'id' + material_run_uid = "2d3a782f-aee7-41db-853c-36bf4bff0626" + material_run_scope = "id" # When collection.delete(material_run_uid) @@ -284,22 +310,17 @@ def test_delete_material_run(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='DELETE', - path='teams/{}/datasets/{}/material-runs/{}/{}'.format( - collection.team_id, - collection.dataset_id, - material_run_scope, - material_run_uid - ), - params={'dry_run': False} + method="DELETE", + path=f"teams/{collection.team_id}/datasets/{collection.dataset_id}/material-runs/{material_run_scope}/{material_run_uid}", + params={"dry_run": False}, ) assert expected_call == session.last_call def test_dry_run_delete_material_run(collection, session): # Given - material_run_uid = '2d3a782f-aee7-41db-853c-36bf4bff0626' - material_run_scope = 'id' + material_run_uid = "2d3a782f-aee7-41db-853c-36bf4bff0626" + material_run_scope = "id" # When collection.delete(material_run_uid, dry_run=True) @@ -307,14 +328,9 @@ def test_dry_run_delete_material_run(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='DELETE', - path='teams/{}/datasets/{}/material-runs/{}/{}'.format( - collection.team_id, - collection.dataset_id, - material_run_scope, - material_run_uid - ), - params={'dry_run': True} + method="DELETE", + path=f"teams/{collection.team_id}/datasets/{collection.dataset_id}/material-runs/{material_run_scope}/{material_run_uid}", + params={"dry_run": True}, ) assert expected_call == session.last_call @@ -332,8 +348,8 @@ def test_material_run_can_get_with_no_id(collection, session): # Given collection.dataset_id = None - run_data = MaterialRunDataFactory(name='Cake 2') - mr_id = run_data['uids']['id'] + run_data = MaterialRunDataFactory(name="Cake 2") + mr_id = run_data["uids"]["id"] session.set_response(run_data) # When @@ -342,17 +358,16 @@ def test_material_run_can_get_with_no_id(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='GET', - path='teams/{}/material-runs/id/{}'.format(collection.team_id, mr_id) + method="GET", path=f"teams/{collection.team_id}/material-runs/id/{mr_id}" ) assert expected_call == session.last_call - assert 'Cake 2' == run.name + assert "Cake 2" == run.name def test_get_by_process(collection): run_noop_gemd_relation_search_test( - search_for='material-runs', - search_with='process-runs', + search_for="material-runs", + search_with="process-runs", collection=collection, search_fn=collection.get_by_process, per_page=1, @@ -361,8 +376,8 @@ def test_get_by_process(collection): def test_list_by_spec(collection): run_noop_gemd_relation_search_test( - search_for='material-runs', - search_with='material-specs', + search_for="material-runs", + search_with="material-specs", collection=collection, search_fn=collection.list_by_spec, ) @@ -386,8 +401,9 @@ def test_validate_templates_successful_minimal_params(collection, session): assert 1 == session.num_calls expected_call = FakeCall( method="PUT", - path="teams/{}/material-runs/validate-templates".format(team_id), - json={"dataObject": scrub_none(run.dump())}) + path=f"teams/{team_id}/material-runs/validate-templates", + json={"dataObject": scrub_none(run.dump())}, + ) assert session.last_call == expected_call assert errors == [] @@ -406,16 +422,21 @@ def test_validate_templates_successful_all_params(collection, session): # When session.set_response("") - errors = collection.validate_templates(model=run, object_template=template, ingredient_process_template=unused_process_template) + errors = collection.validate_templates( + model=run, object_template=template, ingredient_process_template=unused_process_template + ) # Then assert 1 == session.num_calls expected_call = FakeCall( method="PUT", - path="teams/{}/material-runs/validate-templates".format(team_id), - json={"dataObject": scrub_none(run.dump()), - "objectTemplate": scrub_none(template.dump()), - "ingredientProcessTemplate": scrub_none(unused_process_template.dump())}) + path=f"teams/{team_id}/material-runs/validate-templates", + json={ + "dataObject": scrub_none(run.dump()), + "objectTemplate": scrub_none(template.dump()), + "ingredientProcessTemplate": scrub_none(unused_process_template.dump()), + }, + ) assert session.last_call == expected_call assert errors == [] @@ -429,16 +450,21 @@ def test_validate_templates_errors(collection, session): run = MaterialRunFactory(name="") # When - validation_error = ValidationError.build({"failure_message": "you failed", "failure_id": "failure_id"}) - session.set_response(BadRequest("path", FakeRequestResponseApiError(400, "Bad Request", [validation_error]))) + validation_error = ValidationError.build( + {"failure_message": "you failed", "failure_id": "failure_id"} + ) + session.set_response( + BadRequest("path", FakeRequestResponseApiError(400, "Bad Request", [validation_error])) + ) errors = collection.validate_templates(model=run) # Then assert 1 == session.num_calls expected_call = FakeCall( method="PUT", - path="teams/{}/material-runs/validate-templates".format(team_id), - json={"dataObject": scrub_none(run.dump())}) + path=f"teams/{team_id}/material-runs/validate-templates", + json={"dataObject": scrub_none(run.dump())}, + ) assert session.last_call == expected_call assert len(errors) == 1 assert errors[0].dump() == validation_error.dump() @@ -465,7 +491,9 @@ def test_validate_templates_unrelated_400_with_api_error(collection, session): run = MaterialRunFactory() # When - session.set_response(BadRequest("path", FakeRequestResponseApiError(400, "I am not a validation error", []))) + session.set_response( + BadRequest("path", FakeRequestResponseApiError(400, "I am not a validation error", [])) + ) with pytest.raises(BadRequest): collection.validate_templates(model=run) @@ -476,48 +504,47 @@ def test_list_by_template(collection, session): """ # Given material_template = MaterialTemplateFactory() - test_scope = 'id' + test_scope = "id" template_id = material_template.uids[test_scope] sample_spec1 = MaterialSpecDataFactory(template=material_template) sample_spec2 = MaterialSpecDataFactory(template=material_template) - key = 'contents' + key = "contents" sample_run1_1 = MaterialRunDataFactory(spec=sample_spec1) sample_run2_1 = MaterialRunDataFactory(spec=sample_spec2) sample_run1_2 = MaterialRunDataFactory(spec=sample_spec1) sample_run2_2 = MaterialRunDataFactory(spec=sample_spec2) - session.set_responses({key: [sample_spec1, sample_spec2]}, {key: [sample_run1_1, sample_run1_2]}, - {key: [sample_run2_1, sample_run2_2]}) + session.set_responses( + {key: [sample_spec1, sample_spec2]}, + {key: [sample_run1_1, sample_run1_2]}, + {key: [sample_run2_1, sample_run2_2]}, + ) # When runs = [run for run in collection.list_by_template(template_id)] # Then assert 3 == session.num_calls - assert runs == [collection.build(run) for run in [sample_run1_1, sample_run1_2, sample_run2_1, sample_run2_2]] + assert runs == [ + collection.build(run) + for run in [sample_run1_1, sample_run1_2, sample_run2_1, sample_run2_2] + ] def test_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.material_run import MaterialRun as CitrineMaterialRun from gemd.entity.object import MaterialRun as GEMDMaterialRun - gemd_obj = GEMDMaterialRun( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) - citrine_obj = CitrineMaterialRun( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) + from citrine.resources.material_run import MaterialRun as CitrineMaterialRun + + gemd_obj = GEMDMaterialRun(name="My Name", notes="I have notes", tags=["tag!"]) + citrine_obj = CitrineMaterialRun(name="My Name", notes="I have notes", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" def test_deep_equals(collection): - change_scope('test_deep_equals_scope') + change_scope("test_deep_equals_scope") cake = make_cake() flat_list = flatten(cake) # Note that registered turns them into a flat list of Citrine resources @@ -535,7 +562,7 @@ def test_deep_equals(collection): def test_nonmutating_dry_run(collection): - change_scope('test_deep_equals_scope') + change_scope("test_deep_equals_scope") cake = make_cake() uid_stash = cake.uids.copy() @@ -555,9 +582,9 @@ def test_nonmutating_dry_run(collection): def test_args_only(collection): - """"Test that only arguments to register_all get registered/tested/returned.""" + """ "Test that only arguments to register_all get registered/tested/returned.""" obj = GEMDRun("name", spec=GEMDSpec("name")) - GEMDJson(scope='test_args_only').dumps(obj) # no-op to populate ids + GEMDJson(scope="test_args_only").dumps(obj) # no-op to populate ids dry = collection.register_all([obj], dry_run=True) assert obj in dry assert obj.spec not in dry diff --git a/tests/resources/test_material_spec.py b/tests/resources/test_material_spec.py index d6002b70d..501e12736 100644 --- a/tests/resources/test_material_spec.py +++ b/tests/resources/test_material_spec.py @@ -1,13 +1,13 @@ from uuid import UUID import pytest +from gemd.entity.object import MaterialSpec as GEMDMaterialSpec -from citrine.resources.material_spec import MaterialSpec as CitrineMaterialSpec, MaterialSpecCollection +from citrine.resources.material_spec import MaterialSpec as CitrineMaterialSpec +from citrine.resources.material_spec import MaterialSpecCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test from tests.utils.factories import MaterialSpecDataFactory -from tests.utils.session import FakeCall, FakeSession - -from gemd.entity.object import MaterialSpec as GEMDMaterialSpec +from tests.utils.session import FakeSession @pytest.fixture @@ -18,15 +18,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> MaterialSpecCollection: return MaterialSpecCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + session=session, + ) def test_list_by_template(collection): run_noop_gemd_relation_search_test( - search_for='material-specs', - search_with='material-templates', + search_for="material-specs", + search_with="material-templates", collection=collection, search_fn=collection.list_by_template, ) @@ -34,8 +35,8 @@ def test_list_by_template(collection): def test_get_by_process(collection): run_noop_gemd_relation_search_test( - search_for='material-specs', - search_with='process-specs', + search_for="material-specs", + search_with="process-specs", collection=collection, search_fn=collection.get_by_process, per_page=1, @@ -49,31 +50,25 @@ def test_repeat_serialization_gemd(collection, session): """ from gemd.entity.object.material_spec import MaterialSpec as GEMDMaterial from gemd.entity.object.process_spec import ProcessSpec as GEMDProcess + # Given - session.set_response(MaterialSpecDataFactory(name='Test gemd mutation')) - proc = GEMDProcess(name='Test gemd mutation (process)', uids={'nomutate': 'process'}) - mat = GEMDMaterial(name='Test gemd mutation', uids={'nomutate': 'material'}, process=proc) + session.set_response(MaterialSpecDataFactory(name="Test gemd mutation")) + proc = GEMDProcess(name="Test gemd mutation (process)", uids={"nomutate": "process"}) + mat = GEMDMaterial(name="Test gemd mutation", uids={"nomutate": "material"}, process=proc) # When collection.register(proc) - session.set_response(MaterialSpecDataFactory(name='Test gemd mutation')) - registered = collection.register(mat) # This will serialize the linked process as a side effect + session.set_response(MaterialSpecDataFactory(name="Test gemd mutation")) + # This will serialize the linked process as a side effect + registered = collection.register(mat) # Then assert "" == str(registered) def test_equals(): - gemd_obj = GEMDMaterialSpec( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) - citrine_obj = CitrineMaterialSpec( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) + gemd_obj = GEMDMaterialSpec(name="My Name", notes="I have notes", tags=["tag!"]) + citrine_obj = CitrineMaterialSpec(name="My Name", notes="I have notes", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" diff --git a/tests/resources/test_measurement_run.py b/tests/resources/test_measurement_run.py index eb94f6aaa..cf7768a00 100644 --- a/tests/resources/test_measurement_run.py +++ b/tests/resources/test_measurement_run.py @@ -4,7 +4,7 @@ from citrine.resources.measurement_run import MeasurementRunCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.session import FakeCall, FakeSession +from tests.utils.session import FakeSession @pytest.fixture @@ -15,15 +15,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> MeasurementRunCollection: return MeasurementRunCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + session=session, + ) def test_list_by_template(collection: MeasurementRunCollection): run_noop_gemd_relation_search_test( - search_for='measurement-runs', - search_with='measurement-specs', + search_for="measurement-runs", + search_with="measurement-specs", collection=collection, search_fn=collection.list_by_spec, ) @@ -31,8 +32,8 @@ def test_list_by_template(collection: MeasurementRunCollection): def test_list_by_material(collection: MeasurementRunCollection): run_noop_gemd_relation_search_test( - search_for='measurement-runs', - search_with='material-runs', + search_for="measurement-runs", + search_with="material-runs", collection=collection, search_fn=collection.list_by_material, ) @@ -40,19 +41,12 @@ def test_list_by_material(collection: MeasurementRunCollection): def test_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.measurement_run import MeasurementRun as CitrineMeasurementRun from gemd.entity.object import MeasurementRun as GEMDMeasurementRun - gemd_obj = GEMDMeasurementRun( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) - citrine_obj = CitrineMeasurementRun( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) + from citrine.resources.measurement_run import MeasurementRun as CitrineMeasurementRun + + gemd_obj = GEMDMeasurementRun(name="My Name", notes="I have notes", tags=["tag!"]) + citrine_obj = CitrineMeasurementRun(name="My Name", notes="I have notes", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" diff --git a/tests/resources/test_measurement_spec.py b/tests/resources/test_measurement_spec.py index abe8b1b56..baf3ac80a 100644 --- a/tests/resources/test_measurement_spec.py +++ b/tests/resources/test_measurement_spec.py @@ -1,12 +1,12 @@ from uuid import UUID import pytest - from gemd.entity.object import MeasurementSpec as GEMDMeasurementSpec -from citrine.resources.measurement_spec import MeasurementSpec as CitrineMeasurementSpec, MeasurementSpecCollection +from citrine.resources.measurement_spec import MeasurementSpec as CitrineMeasurementSpec +from citrine.resources.measurement_spec import MeasurementSpecCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.session import FakeCall, FakeSession +from tests.utils.session import FakeSession @pytest.fixture @@ -17,15 +17,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> MeasurementSpecCollection: return MeasurementSpecCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + session=session, + ) def test_list_by_template(collection: MeasurementSpecCollection): run_noop_gemd_relation_search_test( - search_for='measurement-specs', - search_with='measurement-templates', + search_for="measurement-specs", + search_with="measurement-templates", collection=collection, search_fn=collection.list_by_template, ) @@ -33,16 +34,8 @@ def test_list_by_template(collection: MeasurementSpecCollection): def test_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - gemd_obj = GEMDMeasurementSpec( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) - citrine_obj = CitrineMeasurementSpec( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) + gemd_obj = GEMDMeasurementSpec(name="My Name", notes="I have notes", tags=["tag!"]) + citrine_obj = CitrineMeasurementSpec(name="My Name", notes="I have notes", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" diff --git a/tests/resources/test_object_setters.py b/tests/resources/test_object_setters.py index 52e10c3cb..f7d46f99b 100644 --- a/tests/resources/test_object_setters.py +++ b/tests/resources/test_object_setters.py @@ -1,16 +1,15 @@ """Test that setting objects in citrine-python activates the setter logic in gemd.""" import pytest - -from gemd.entity.value.discrete_categorical import DiscreteCategorical from gemd.entity.attribute.property import Property -from citrine.resources.process_run import ProcessRun -from citrine.resources.process_spec import ProcessSpec +from gemd.entity.value.discrete_categorical import DiscreteCategorical + +from citrine.resources.ingredient_spec import IngredientSpec from citrine.resources.material_run import MaterialRun from citrine.resources.material_spec import MaterialSpec from citrine.resources.measurement_run import MeasurementRun -from citrine.resources.ingredient_run import IngredientRun -from citrine.resources.ingredient_spec import IngredientSpec +from citrine.resources.process_run import ProcessRun +from citrine.resources.process_spec import ProcessSpec def test_soft_process_material_attachment(): @@ -23,8 +22,11 @@ def test_soft_process_material_attachment(): def test_soft_measurement_material_attachment(): """Test that soft attachments are formed from materials to measurements.""" cake = MaterialRun("A cake") - smell_test = MeasurementRun("use your nose", material=cake, properties=[ - Property(name="Smell", value=DiscreteCategorical("yummy"))]) + smell_test = MeasurementRun( + "use your nose", + material=cake, + properties=[Property(name="Smell", value=DiscreteCategorical("yummy"))], + ) taste_test = MeasurementRun("taste", material=cake) assert cake.measurements == [smell_test, taste_test] @@ -37,8 +39,9 @@ def test_soft_process_ingredient_attachment(): vinegar_sample = IngredientSpec("a bit of vinegar", material=vinegar, process=eruption) baking_soda_sample = IngredientSpec("a bit of NaOh", material=baking_soda) baking_soda_sample.process = eruption - assert set(eruption.ingredients) == {vinegar_sample, baking_soda_sample}, \ + assert set(eruption.ingredients) == {vinegar_sample, baking_soda_sample}, ( "Creating an ingredient for a process did not auto-populate that process's ingredient list" + ) def test_object_pointer_serde(): diff --git a/tests/resources/test_predictor.py b/tests/resources/test_predictor.py index efce269ec..7d9ff0a4f 100644 --- a/tests/resources/test_predictor.py +++ b/tests/resources/test_predictor.py @@ -1,41 +1,44 @@ """Tests predictor collection""" -import mock -import pytest + import uuid from copy import deepcopy +from unittest import mock + +import pytest -from citrine.exceptions import BadRequest, Conflict, ModuleRegistrationFailedException, NotFound +from citrine.exceptions import ModuleRegistrationFailedException, NotFound from citrine.informatics.data_sources import GemTableDataSource from citrine.informatics.descriptors import RealDescriptor from citrine.informatics.predictors import ( AutoMLPredictor, ExpressionPredictor, GraphPredictor, - SimpleMixturePredictor + SimpleMixturePredictor, ) -from citrine.resources.predictor import PredictorCollection, _PredictorVersionCollection, AutoConfigureMode -from tests.conftest import build_predictor_entity -from tests.utils.session import ( - FakeCall, - FakeRequestResponse, - FakeSession +from citrine.resources.predictor import ( + AutoConfigureMode, + PredictorCollection, + _PredictorVersionCollection, ) +from tests.conftest import build_predictor_entity from tests.utils.factories import ( - AsyncDefaultPredictorResponseFactory, AsyncDefaultPredictorResponseMetadataFactory, - TableDataSourceDataFactory + AsyncDefaultPredictorResponseFactory, + AsyncDefaultPredictorResponseMetadataFactory, + TableDataSourceDataFactory, ) +from tests.utils.session import FakeCall, FakeRequestResponse, FakeSession def paging_response(*items): return {"response": items} -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def basic_predictor_report_data(): return { - 'id': str(uuid.uuid4()), - 'status': 'VALID', - 'report': {'descriptors': [], 'models': []} + "id": str(uuid.uuid4()), + "status": "VALID", + "report": {"descriptors": [], "models": []}, } @@ -44,8 +47,8 @@ def test_build(valid_graph_predictor_data, basic_predictor_report_data): session.set_response(basic_predictor_report_data) pc = PredictorCollection(uuid.uuid4(), session) predictor = pc.build(valid_graph_predictor_data) - assert predictor.name == 'Graph predictor' - assert predictor.description == 'description' + assert predictor.name == "Graph predictor" + assert predictor.description == "description" def test_build_with_status(valid_graph_predictor_data, basic_predictor_report_data): @@ -54,7 +57,9 @@ def test_build_with_status(valid_graph_predictor_data, basic_predictor_report_da status_detail_data = {("Info", "info_msg"), ("Warning", "warning msg"), ("Error", "error msg")} data = deepcopy(valid_graph_predictor_data) - data["metadata"]["status"]["detail"] = [{"level": level, "msg": msg} for level, msg in status_detail_data] + data["metadata"]["status"]["detail"] = [ + {"level": level, "msg": msg} for level, msg in status_detail_data + ] pc = PredictorCollection(uuid.uuid4(), session) predictor = pc.build(data) @@ -85,7 +90,9 @@ def test_archive_root(valid_graph_predictor_data): pc.archive_root(pred_id) - assert session.calls == [FakeCall(method='PUT', path=f"{predictors_path}/{pred_id}/archive", json={})] + assert session.calls == [ + FakeCall(method="PUT", path=f"{predictors_path}/{pred_id}/archive", json={}) + ] def test_restore_root(valid_graph_predictor_data): @@ -98,7 +105,9 @@ def test_restore_root(valid_graph_predictor_data): pc.restore_root(pred_id) - assert session.calls == [FakeCall(method='PUT', path=f"{predictors_path}/{pred_id}/restore", json={})] + assert session.calls == [ + FakeCall(method="PUT", path=f"{predictors_path}/{pred_id}/restore", json={}) + ] def test_root_is_archived(valid_graph_predictor_data): @@ -123,8 +132,8 @@ def test_graph_build(valid_graph_predictor_data, basic_predictor_report_data): session.get_resource.return_value = basic_predictor_report_data pc = PredictorCollection(uuid.uuid4(), session) predictor = pc.build(valid_graph_predictor_data) - assert predictor.name == 'Graph predictor' - assert predictor.description == 'description' + assert predictor.name == "Graph predictor" + assert predictor.description == "description" assert len(predictor.predictors) == 5 assert len(predictor.training_data) == 1 @@ -140,7 +149,12 @@ def test_register(valid_graph_predictor_data): predictors_path = f"/projects/{pc.project_id}/predictors" expected_calls = [ FakeCall(method="POST", path=predictors_path, json=predictor.dump()), - FakeCall(method="PUT", path=f"{predictors_path}/{entity['id']}/train", params={"create_version": True}, json={}), + FakeCall( + method="PUT", + path=f"{predictors_path}/{entity['id']}/train", + params={"create_version": True}, + json={}, + ), ] pc.register(predictor) @@ -157,9 +171,7 @@ def test_register_no_train(valid_graph_predictor_data): predictor = pc.build(entity) predictors_path = f"/projects/{pc.project_id}/predictors" - expected_calls = [ - FakeCall(method="POST", path=predictors_path, json=predictor.dump()), - ] + expected_calls = [FakeCall(method="POST", path=predictors_path, json=predictor.dump())] pc.register(predictor, train=False) @@ -174,20 +186,21 @@ def test_graph_register(valid_graph_predictor_data): pc = PredictorCollection(uuid.uuid4(), session) predictor = GraphPredictor.build(valid_graph_predictor_data) registered = pc.register(predictor) - - assert registered.name == 'Graph predictor' + + assert registered.name == "Graph predictor" def test_failed_register(valid_graph_predictor_data): session = mock.Mock() - session.post_resource.side_effect = NotFound("/projects/uuid/not_found", - FakeRequestResponse(400)) + session.post_resource.side_effect = NotFound( + "/projects/uuid/not_found", FakeRequestResponse(400) + ) pc = PredictorCollection(uuid.uuid4(), session) predictor = GraphPredictor.build(valid_graph_predictor_data) with pytest.raises(ModuleRegistrationFailedException) as e: pc.register(predictor) assert 'The "GraphPredictor" failed to register.' in str(e.value) - assert '/projects/uuid/not_found' in str(e.value) + assert "/projects/uuid/not_found" in str(e.value) def test_update(valid_graph_predictor_data): @@ -202,7 +215,9 @@ def test_update(valid_graph_predictor_data): entity_path = f"{predictors_path}/{entity['id']}" expected_calls = [ FakeCall(method="PUT", path=entity_path, json=predictor.dump()), - FakeCall(method="PUT", path=f"{entity_path}/train", params={"create_version": True}, json={}), + FakeCall( + method="PUT", path=f"{entity_path}/train", params={"create_version": True}, json={} + ), ] pc.update(predictor) @@ -220,9 +235,7 @@ def test_update_no_train(valid_graph_predictor_data): predictors_path = PredictorCollection._path_template.format(project_id=pc.project_id) entity_path = f"{predictors_path}/{entity['id']}" - expected_calls = [ - FakeCall(method="PUT", path=entity_path, json=predictor.dump()), - ] + expected_calls = [FakeCall(method="PUT", path=entity_path, json=predictor.dump())] pc.update(predictor, train=False) @@ -241,7 +254,7 @@ def test_register_update_checks_status(valid_graph_predictor_data): invalid_entity = build_predictor_entity( instance, status_name="INVALID", - status_detail=[{"level": "Error", "msg": "AHH IT BURNSSSSS!!!!"}] + status_detail=[{"level": "Error", "msg": "AHH IT BURNSSSSS!!!!"}], ) # Register returns first (invalid) response if failed @@ -270,7 +283,9 @@ def test_train(valid_graph_predictor_data): predictors_path = PredictorCollection._path_template.format(project_id=pc.project_id) entity_path = f"{predictors_path}/{entity['id']}" expected_calls = [ - FakeCall(method="PUT", path=f"{entity_path}/train", params={"create_version": True}, json={}), + FakeCall( + method="PUT", path=f"{entity_path}/train", params={"create_version": True}, json={} + ) ] pc.train(predictor.uid) @@ -284,22 +299,24 @@ def test_list(valid_graph_predictor_data, valid_graph_predictor_data_empty): collection = PredictorCollection(uuid.uuid4(), session) session.set_responses( { - 'response': [valid_graph_predictor_data, valid_graph_predictor_data_empty], - 'page': 1, - 'per_page': 25 + "response": [valid_graph_predictor_data, valid_graph_predictor_data_empty], + "page": 1, + "per_page": 25, }, basic_predictor_report_data, - basic_predictor_report_data + basic_predictor_report_data, ) # When predictors = list(collection.list(per_page=25)) # Then - expected_call = FakeCall(method='GET', - path='/projects/{}/predictors'.format(collection.project_id), - params={'per_page': 25, 'page': 1, 'archived': False}, - version="v4") + expected_call = FakeCall( + method="GET", + path=f"/projects/{collection.project_id}/predictors", + params={"per_page": 25, "page": 1, "archived": False}, + version="v4", + ) assert 1 == session.num_calls, session.calls assert expected_call == session.calls[0] assert len(predictors) == 2 @@ -310,19 +327,21 @@ def test_list_all(valid_graph_predictor_data, valid_graph_predictor_data_empty): session = FakeSession() collection = PredictorCollection(uuid.uuid4(), session) session.set_responses( - {'response': [valid_graph_predictor_data, valid_graph_predictor_data_empty]}, + {"response": [valid_graph_predictor_data, valid_graph_predictor_data_empty]}, + basic_predictor_report_data, basic_predictor_report_data, - basic_predictor_report_data ) # When predictors = list(collection.list_all(per_page=25)) # Then - expected_call = FakeCall(method='GET', - path='/projects/{}/predictors'.format(collection.project_id), - params={'per_page': 25, 'page': 1}, - version="v4") + expected_call = FakeCall( + method="GET", + path=f"/projects/{collection.project_id}/predictors", + params={"per_page": 25, "page": 1}, + version="v4", + ) assert 1 == session.num_calls, session.calls assert expected_call == session.calls[0] assert len(predictors) == 2 @@ -331,7 +350,7 @@ def test_list_all(valid_graph_predictor_data, valid_graph_predictor_data_empty): def test_list_archived(valid_graph_predictor_data): # Given session = FakeSession() - session.set_response({'response': [valid_graph_predictor_data]}) + session.set_response({"response": [valid_graph_predictor_data]}) pc = PredictorCollection(uuid.uuid4(), session) # When @@ -339,10 +358,12 @@ def test_list_archived(valid_graph_predictor_data): # Then assert session.num_calls == 1 - assert session.last_call == FakeCall(method='GET', - path=f"/projects/{pc.project_id}/predictors", - params={'per_page': 20, 'page': 1, 'archived': True}, - version="v4") + assert session.last_call == FakeCall( + method="GET", + path=f"/projects/{pc.project_id}/predictors", + params={"per_page": 20, "page": 1, "archived": True}, + version="v4", + ) def test_get(valid_graph_predictor_data): @@ -359,9 +380,9 @@ def test_get(valid_graph_predictor_data): # Then expected_call = FakeCall( - method='GET', - path=f'/projects/{pc.project_id}/predictors/{id}/versions/{version}', - params={} + method="GET", + path=f"/projects/{pc.project_id}/predictors/{id}/versions/{version}", + params={}, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -390,7 +411,9 @@ def test_check_update_none(): # then assert update_check is None - expected_call = FakeCall(method='GET', path='/projects/{}/predictors/{}/update-check'.format(pc.project_id, predictor_id)) + expected_call = FakeCall( + method="GET", path=f"/projects/{pc.project_id}/predictors/{predictor_id}/update-check" + ) assert session.calls[0] == expected_call @@ -399,21 +422,25 @@ def test_check_update_some(): # given session = FakeSession() desc = RealDescriptor("spam", lower_bound=0, upper_bound=1, units="kg") - response = GraphPredictor.wrap_instance({ - "type": "Graph", - "name": "foo", - "description": "bar", - "predictors": [ - { - "type": "AnalyticExpression", - "name": "foo", - "description": "bar", - "expression": "2 * x", - "output": RealDescriptor("spam", lower_bound=0, upper_bound=1, units="kg").dump(), - "aliases": {} - } - ] - }) + response = GraphPredictor.wrap_instance( + { + "type": "Graph", + "name": "foo", + "description": "bar", + "predictors": [ + { + "type": "AnalyticExpression", + "name": "foo", + "description": "bar", + "expression": "2 * x", + "output": RealDescriptor( + "spam", lower_bound=0, upper_bound=1, units="kg" + ).dump(), + "aliases": {}, + } + ], + } + ) session.set_responses({"updatable": True, **response}) pc = PredictorCollection(uuid.uuid4(), session) predictor_id = uuid.uuid4() @@ -422,13 +449,11 @@ def test_check_update_some(): update_check = pc.check_for_update(predictor_id) # then - assert pc._api_version == 'v3' - exp = ExpressionPredictor("foo", description="bar", expression="2 * x", output=desc, aliases={}) - expected = GraphPredictor( - name="foo", - description="bar", - predictors=[exp] + assert pc._api_version == "v3" + exp = ExpressionPredictor( + "foo", description="bar", expression="2 * x", output=desc, aliases={} ) + expected = GraphPredictor(name="foo", description="bar", predictors=[exp]) assert update_check.dump() == expected.dump() assert update_check.uid == predictor_id @@ -441,9 +466,15 @@ def test_unexpected_pattern(): # Then with pytest.raises(ValueError): - pc.create_default(training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), pattern="yogurt") + pc.create_default( + training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), + pattern="yogurt", + ) with pytest.raises(ValueError): - pc.create_default_async(training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), pattern="yogurt") + pc.create_default_async( + training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), + pattern="yogurt", + ) def test_create_default_mode_pattern(valid_graph_predictor_data): @@ -458,11 +489,14 @@ def test_create_default_mode_pattern(valid_graph_predictor_data): pc = PredictorCollection(uuid.uuid4(), session) # When - pc.create_default(training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), pattern=AutoConfigureMode.INFER) + pc.create_default( + training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), + pattern=AutoConfigureMode.INFER, + ) # Then - assert (session.calls[0].json['pattern'] == "INFER") - assert (session.calls[0].json['prefer_valid'] == True) + assert session.calls[0].json["pattern"] == "INFER" + assert session.calls[0].json["prefer_valid"] == True def test_returned_predictor(valid_graph_predictor_data): @@ -477,7 +511,9 @@ def test_returned_predictor(valid_graph_predictor_data): pc = PredictorCollection(uuid.uuid4(), session) # When - result = pc.create_default(training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), pattern="PLAIN") + result = pc.create_default( + training_data=GemTableDataSource(table_id=uuid.uuid4(), table_version=0), pattern="PLAIN" + ) # Then the response is parsed in a predictor assert result.name == valid_graph_predictor_data["data"]["name"] @@ -500,7 +536,9 @@ def test_list_versions(valid_graph_predictor_data): predictor_v2 = deepcopy(valid_graph_predictor_data) predictor_v2["metadata"]["version"] = 2 - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) session.set_response(paging_response(predictor_v1, predictor_v2)) @@ -508,7 +546,9 @@ def test_list_versions(valid_graph_predictor_data): listed_predictors = list(pc.list_versions(pred_id, per_page=20)) # Then - assert session.calls == [FakeCall(method='GET', path=versions_path, params={'per_page': 20, 'page': 1})] + assert session.calls == [ + FakeCall(method="GET", path=versions_path, params={"per_page": 20, "page": 1}) + ] assert len(listed_predictors) == 2 @@ -524,7 +564,9 @@ def test_list_archived_versions(valid_graph_predictor_data): predictor_v2 = deepcopy(valid_graph_predictor_data) predictor_v2["metadata"]["version"] = 2 - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) session.set_response(paging_response(predictor_v1, predictor_v2)) @@ -532,8 +574,8 @@ def test_list_archived_versions(valid_graph_predictor_data): listed_predictors = list(pc.list_archived_versions(pred_id, per_page=20)) # Then - expected_params = {'per_page': 20, "filter": "archived eq 'true'", 'page': 1} - assert session.calls == [FakeCall(method='GET', path=versions_path, params=expected_params)] + expected_params = {"per_page": 20, "filter": "archived eq 'true'", "page": 1} + assert session.calls == [FakeCall(method="GET", path=versions_path, params=expected_params)] assert len(listed_predictors) == 2 @@ -543,13 +585,17 @@ def test_archive_version(valid_graph_predictor_data, version): pc = PredictorCollection(uuid.uuid4(), session) pred_id = valid_graph_predictor_data["id"] - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) session.set_response(valid_graph_predictor_data) pc.archive_version(pred_id, version=version) - assert session.calls == [FakeCall(method='PUT', path=f"{versions_path}/{version}/archive", json={})] + assert session.calls == [ + FakeCall(method="PUT", path=f"{versions_path}/{version}/archive", json={}) + ] @pytest.mark.parametrize("version", (2, "1", "latest", "most_recent")) @@ -558,13 +604,17 @@ def test_restore_version(valid_graph_predictor_data, version): pc = PredictorCollection(uuid.uuid4(), session) pred_id = valid_graph_predictor_data["id"] - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) session.set_response(valid_graph_predictor_data) pc.restore_version(pred_id, version=version) - assert session.calls == [FakeCall(method='PUT', path=f"{versions_path}/{version}/restore", json={})] + assert session.calls == [ + FakeCall(method="PUT", path=f"{versions_path}/{version}/restore", json={}) + ] @pytest.mark.parametrize("version", (-2, 0, "1.5", "draft")) @@ -593,18 +643,17 @@ def test_is_stale(valid_graph_predictor_data, is_stale): pc = PredictorCollection(uuid.uuid4(), session) pred_id = valid_graph_predictor_data["id"] pred_version = valid_graph_predictor_data["metadata"]["version"] - response = { - "id": pred_id, - "version": pred_version, - "status": "READY", - "is_stale": is_stale - } + response = {"id": pred_id, "version": pred_version, "status": "READY", "is_stale": is_stale} session.set_response(response) resp = pc.is_stale(pred_id, version=pred_version) - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) - assert session.calls == [FakeCall(method='GET', path=f"{versions_path}/{pred_version}/is-stale")] + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) + assert session.calls == [ + FakeCall(method="GET", path=f"{versions_path}/{pred_version}/is-stale") + ] assert resp == is_stale @@ -621,8 +670,12 @@ def test_retrain_stale(valid_graph_predictor_data): pc.retrain_stale(pred_id, version=pred_version) - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) - assert session.calls == [FakeCall(method='PUT', path=f"{versions_path}/{pred_version}/retrain-stale", json={})] + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) + assert session.calls == [ + FakeCall(method="PUT", path=f"{versions_path}/{pred_version}/retrain-stale", json={}) + ] def test_unsupported_archive(): @@ -639,15 +692,17 @@ def test_create_default_async(): session = FakeSession() pc = PredictorCollection(uuid.uuid4(), session) predictors_path = PredictorCollection._path_template.format(project_id=pc.project_id) - + mode = "PLAIN" prefer_valid = False ds = GemTableDataSource(table_id=uuid.uuid4(), table_version=1) - data_source_payload = TableDataSourceDataFactory(table_id=str(ds.table_id), table_version=ds.table_version) + data_source_payload = TableDataSourceDataFactory( + table_id=str(ds.table_id), table_version=ds.table_version + ) expected_payload = { "data_source": data_source_payload, "pattern": mode, - "prefer_valid": prefer_valid + "prefer_valid": prefer_valid, } metadata = AsyncDefaultPredictorResponseMetadataFactory(data_source=data_source_payload) @@ -655,7 +710,9 @@ def test_create_default_async(): pc.create_default_async(training_data=ds, pattern=mode, prefer_valid=prefer_valid) - assert session.calls == [FakeCall(method="POST", path=f"{predictors_path}/default-async", json=expected_payload)] + assert session.calls == [ + FakeCall(method="POST", path=f"{predictors_path}/default-async", json=expected_payload) + ] def test_get_default_async(valid_graph_predictor_data): @@ -693,9 +750,9 @@ def test_get_featurized_training_data(example_hierarchical_design_material): # Then expected_call = FakeCall( - method='GET', - path=f'/projects/{pc.project_id}/predictors/{id}/versions/{version}/featurized-training-data', - params={} + method="GET", + path=f"/projects/{pc.project_id}/predictors/{id}/versions/{version}/featurized-training-data", + params={}, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -714,9 +771,15 @@ def test_rename(valid_graph_predictor_data): session.set_response(valid_graph_predictor_data) pc.rename(pred_id, version=pred_version, name=new_name, description=new_description) # Then - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) expected_payload = {"name": new_name, "description": new_description} - assert session.calls == [FakeCall(method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload)] + assert session.calls == [ + FakeCall( + method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload + ) + ] def test_rename_name_only(valid_graph_predictor_data): @@ -733,9 +796,16 @@ def test_rename_name_only(valid_graph_predictor_data): pc.rename(pred_id, version=pred_version, name=new_name) # Then - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) expected_payload = {"name": new_name, "description": None} - assert session.calls == [FakeCall(method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload)] + assert session.calls == [ + FakeCall( + method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload + ) + ] + def test_rename_description_only(valid_graph_predictor_data): pred_id = valid_graph_predictor_data["id"] @@ -751,6 +821,12 @@ def test_rename_description_only(valid_graph_predictor_data): pc.rename(pred_id, version=pred_version, description=new_description) # Then - versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) + versions_path = _PredictorVersionCollection._path_template.format( + project_id=pc.project_id, uid=pred_id + ) expected_payload = {"name": None, "description": new_description} - assert session.calls == [FakeCall(method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload)] + assert session.calls == [ + FakeCall( + method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload + ) + ] diff --git a/tests/resources/test_predictor_evaluations.py b/tests/resources/test_predictor_evaluations.py index fed853601..78358e347 100644 --- a/tests/resources/test_predictor_evaluations.py +++ b/tests/resources/test_predictor_evaluations.py @@ -1,15 +1,18 @@ -from copy import deepcopy import uuid +from copy import deepcopy import pytest -from citrine.resources.predictor_evaluation import PredictorEvaluationCollection from citrine.informatics.executions.predictor_evaluation import PredictorEvaluationRequest from citrine.informatics.predictors import GraphPredictor from citrine.jobs.waiting import wait_while_executing - -from tests.utils.factories import CrossValidationEvaluatorFactory, PredictorEvaluationDataFactory,\ - PredictorEvaluationFactory, PredictorInstanceDataFactory, PredictorRefFactory +from citrine.resources.predictor_evaluation import PredictorEvaluationCollection +from tests.utils.factories import ( + CrossValidationEvaluatorFactory, + PredictorEvaluationDataFactory, + PredictorEvaluationFactory, + PredictorRefFactory, +) from tests.utils.session import FakeCall, FakeSession @@ -23,15 +26,13 @@ def test_get(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(evaluation_response) pec.get(id) expected_call = FakeCall( - method='GET', - path=f'/projects/{pec.project_id}/predictor-evaluations/{id}', - params={} + method="GET", path=f"/projects/{pec.project_id}/predictor-evaluations/{id}", params={} ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -43,15 +44,15 @@ def test_archived(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(evaluation_response) pec.archive(id) expected_call = FakeCall( - method='PUT', - path=f'/projects/{pec.project_id}/predictor-evaluations/{id}/archive', - json={} + method="PUT", + path=f"/projects/{pec.project_id}/predictor-evaluations/{id}/archive", + json={}, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -63,15 +64,15 @@ def test_restore(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(evaluation_response) pec.restore(id) expected_call = FakeCall( - method='PUT', - path=f'/projects/{pec.project_id}/predictor-evaluations/{id}/restore', - json={} + method="PUT", + path=f"/projects/{pec.project_id}/predictor-evaluations/{id}/restore", + json={}, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -84,15 +85,21 @@ def test_list(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(paging_response(evaluation_response)) evaluations = list(pec.list(predictor_id=pred_id, predictor_version=pred_ver)) expected_call = FakeCall( - method='GET', - path=f'/projects/{pec.project_id}/predictor-evaluations', - params={"page": 1, "per_page": 100, "predictor_id": str(pred_id), "predictor_version": pred_ver, "archived": False} + method="GET", + path=f"/projects/{pec.project_id}/predictor-evaluations", + params={ + "page": 1, + "per_page": 100, + "predictor_id": str(pred_id), + "predictor_version": pred_ver, + "archived": False, + }, ) assert session.num_calls == 1 @@ -107,15 +114,21 @@ def test_list_archived(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(paging_response(evaluation_response)) evaluations = list(pec.list_archived(predictor_id=pred_id, predictor_version=pred_ver)) expected_call = FakeCall( - method='GET', - path=f'/projects/{pec.project_id}/predictor-evaluations', - params={"page": 1, "per_page": 100, "predictor_id": str(pred_id), "predictor_version": pred_ver, "archived": True} + method="GET", + path=f"/projects/{pec.project_id}/predictor-evaluations", + params={ + "page": 1, + "per_page": 100, + "predictor_id": str(pred_id), + "predictor_version": pred_ver, + "archived": True, + }, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -129,15 +142,21 @@ def test_list_all(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(paging_response(*evaluations)) evaluations = list(pec.list_all(predictor_id=pred_id, predictor_version=pred_ver)) expected_call = FakeCall( - method='GET', - path=f'/projects/{pec.project_id}/predictor-evaluations', - params={"page": 1, "per_page": 100, "predictor_id": str(pred_id), "predictor_version": pred_ver, "archived": None} + method="GET", + path=f"/projects/{pec.project_id}/predictor-evaluations", + params={ + "page": 1, + "per_page": 100, + "predictor_id": str(pred_id), + "predictor_version": pred_ver, + "archived": None, + }, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -151,18 +170,24 @@ def test_trigger(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(evaluation_response) - pec.trigger(predictor_id=pred_ref["predictor_id"], predictor_version=pred_ref["predictor_version"], evaluators=evaluators) + pec.trigger( + predictor_id=pred_ref["predictor_id"], + predictor_version=pred_ref["predictor_version"], + evaluators=evaluators, + ) - expected_payload = PredictorEvaluationRequest(evaluators=evaluators, - predictor_id=pred_ref["predictor_id"], - predictor_version=pred_ref["predictor_version"]) + expected_payload = PredictorEvaluationRequest( + evaluators=evaluators, + predictor_id=pred_ref["predictor_id"], + predictor_version=pred_ref["predictor_version"], + ) expected_call = FakeCall( - method='POST', - path=f'/projects/{pec.project_id}/predictor-evaluations/trigger', - json=expected_payload.dump() + method="POST", + path=f"/projects/{pec.project_id}/predictor-evaluations/trigger", + json=expected_payload.dump(), ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -174,15 +199,17 @@ def test_trigger_default(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(evaluation_response) - pec.trigger_default(predictor_id=pred_ref["predictor_id"], predictor_version=pred_ref["predictor_version"]) + pec.trigger_default( + predictor_id=pred_ref["predictor_id"], predictor_version=pred_ref["predictor_version"] + ) expected_call = FakeCall( - method='POST', - path=f'/projects/{pec.project_id}/predictor-evaluations/trigger-default', - json=pred_ref + method="POST", + path=f"/projects/{pec.project_id}/predictor-evaluations/trigger-default", + json=pred_ref, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -194,36 +221,39 @@ def test_default(): session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(response) - default_evaluators = pec.default(predictor_id=pred_ref["predictor_id"], predictor_version=pred_ref["predictor_version"]) + default_evaluators = pec.default( + predictor_id=pred_ref["predictor_id"], predictor_version=pred_ref["predictor_version"] + ) expected_call = FakeCall( - method='POST', - path=f'/projects/{pec.project_id}/predictor-evaluations/default', - json=pred_ref + method="POST", + path=f"/projects/{pec.project_id}/predictor-evaluations/default", + json=pred_ref, ) assert session.num_calls == 1 assert expected_call == session.last_call assert len(default_evaluators) == len(response["evaluators"]) + def test_default_from_config(valid_graph_predictor_data): response = PredictorEvaluationDataFactory() config = GraphPredictor.build(valid_graph_predictor_data) - payload = config.dump()['instance'] + payload = config.dump()["instance"] session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + session.set_response(response) default_evaluators = pec.default_from_config(config) expected_call = FakeCall( - method='POST', - path=f'/projects/{pec.project_id}/predictor-evaluations/default-from-config', - json=payload + method="POST", + path=f"/projects/{pec.project_id}/predictor-evaluations/default-from-config", + json=payload, ) assert session.num_calls == 1 assert expected_call == session.last_call @@ -252,14 +282,16 @@ def test_delete_not_implemented(): def test_wait(): - in_progress_response = PredictorEvaluationFactory(metadata__status={"major": "INPROGRESS", "minor": "EXECUTING", "detail": []}) + in_progress_response = PredictorEvaluationFactory( + metadata__status={"major": "INPROGRESS", "minor": "EXECUTING", "detail": []} + ) completed_response = deepcopy(in_progress_response) completed_response["metadata"]["status"]["major"] = "SUCCEEDED" completed_response["metadata"]["status"]["minor"] = "COMPLETED" session = FakeSession() pec = PredictorEvaluationCollection(uuid.uuid4(), session) - + # wait_while_executing makes two additional calls once it's done polling. responses = 4 * [in_progress_response] + 3 * [completed_response] session.set_responses(*responses) @@ -268,7 +300,7 @@ def test_wait(): wait_while_executing(collection=pec, execution=evaluation, interval=0.1) expected_call = FakeCall( - method='GET', - path=f'/projects/{pec.project_id}/predictor-evaluations/{in_progress_response["id"]}' + method="GET", + path=f"/projects/{pec.project_id}/predictor-evaluations/{in_progress_response['id']}", ) - assert (len(responses) * [expected_call]) == session.calls + assert len(responses) * [expected_call] == session.calls diff --git a/tests/resources/test_process_run.py b/tests/resources/test_process_run.py index 58fb8c1fd..b03875fbe 100644 --- a/tests/resources/test_process_run.py +++ b/tests/resources/test_process_run.py @@ -4,7 +4,7 @@ from citrine.resources.process_run import ProcessRunCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.session import FakeCall, FakeSession +from tests.utils.session import FakeSession @pytest.fixture @@ -15,15 +15,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> ProcessRunCollection: return ProcessRunCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + session=session, + ) def test_list_by_spec(collection: ProcessRunCollection): run_noop_gemd_relation_search_test( - search_for='process-runs', - search_with='process-specs', + search_for="process-runs", + search_with="process-specs", collection=collection, search_fn=collection.list_by_spec, ) @@ -31,19 +32,12 @@ def test_list_by_spec(collection: ProcessRunCollection): def test_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.process_run import ProcessRun as CitrineProcessRun from gemd.entity.object import ProcessRun as GEMDProcessRun - gemd_obj = GEMDProcessRun( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) - citrine_obj = CitrineProcessRun( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) + from citrine.resources.process_run import ProcessRun as CitrineProcessRun + + gemd_obj = GEMDProcessRun(name="My Name", notes="I have notes", tags=["tag!"]) + citrine_obj = CitrineProcessRun(name="My Name", notes="I have notes", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" diff --git a/tests/resources/test_process_spec.py b/tests/resources/test_process_spec.py index f997a8129..b7a73712c 100644 --- a/tests/resources/test_process_spec.py +++ b/tests/resources/test_process_spec.py @@ -1,12 +1,12 @@ from uuid import UUID import pytest - from gemd.entity.object import ProcessSpec as GEMDProcessSpec -from citrine.resources.process_spec import ProcessSpec as CitrineProcesssSpec, ProcessSpecCollection +from citrine.resources.process_spec import ProcessSpec as CitrineProcesssSpec +from citrine.resources.process_spec import ProcessSpecCollection from tests.resources.test_data_concepts import run_noop_gemd_relation_search_test -from tests.utils.session import FakeCall, FakeSession +from tests.utils.session import FakeSession @pytest.fixture @@ -17,15 +17,16 @@ def session() -> FakeSession: @pytest.fixture def collection(session) -> ProcessSpecCollection: return ProcessSpecCollection( - dataset_id=UUID('8da51e93-8b55-4dd3-8489-af8f65d4ad9a'), - team_id = UUID('6b608f78-e341-422c-8076-35adc8828000'), - session=session) + dataset_id=UUID("8da51e93-8b55-4dd3-8489-af8f65d4ad9a"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828000"), + session=session, + ) def test_list_by_template(collection: ProcessSpecCollection): run_noop_gemd_relation_search_test( - search_for='process-specs', - search_with='process-templates', + search_for="process-specs", + search_with="process-templates", collection=collection, search_fn=collection.list_by_template, ) @@ -33,16 +34,8 @@ def test_list_by_template(collection: ProcessSpecCollection): def test_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - gemd_obj = GEMDProcessSpec( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) - citrine_obj = CitrineProcesssSpec( - name="My Name", - notes="I have notes", - tags=["tag!"] - ) + gemd_obj = GEMDProcessSpec(name="My Name", notes="I have notes", tags=["tag!"]) + citrine_obj = CitrineProcesssSpec(name="My Name", notes="I have notes", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.notes = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index 162931fc3..785922a22 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -1,23 +1,17 @@ -import json import uuid from unittest import mock import pytest from dateutil.parser import parse -from gemd.entity.link_by_uid import LinkByUID -from citrine.exceptions import NotFound, ModuleRegistrationFailedException +from citrine.exceptions import ModuleRegistrationFailedException, NotFound from citrine.informatics.predictors import GraphPredictor -from citrine.resources.api_error import ApiError, ValidationError -from citrine.resources.dataset import Dataset, DatasetCollection +from citrine.resources.dataset import Dataset from citrine.resources.gemtables import GemTableCollection -from citrine.resources.process_spec import ProcessSpec from citrine.resources.project import Project, ProjectCollection -from citrine.resources.project_member import ProjectMember -from citrine.resources.project_roles import MEMBER, LEAD, WRITE -from tests.utils.factories import ProjectDataFactory, UserDataFactory, TeamDataFactory -from tests.utils.session import FakeSession, FakeCall, FakePaginatedSession, FakeRequestResponse -from citrine.resources.team import READ, TeamMember +from citrine.resources.team import READ, TeamMember +from tests.utils.factories import ProjectDataFactory, TeamDataFactory, UserDataFactory +from tests.utils.session import FakeCall, FakePaginatedSession, FakeRequestResponse, FakeSession @pytest.fixture @@ -32,19 +26,17 @@ def paginated_session() -> FakePaginatedSession: @pytest.fixture def paginated_collection(paginated_session) -> ProjectCollection: - return ProjectCollection( - session=paginated_session - ) + return ProjectCollection(session=paginated_session) @pytest.fixture def project(session) -> Project: project = Project( - name='Test Project', + name="Test Project", session=session, - team_id=uuid.UUID('11111111-8baf-433b-82eb-8c7fada847da') + team_id=uuid.UUID("11111111-8baf-433b-82eb-8c7fada847da"), ) - project.uid = uuid.UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + project.uid = uuid.UUID("16fd2706-8baf-433b-82eb-8c7fada847da") return project @@ -54,10 +46,10 @@ def collection(session) -> ProjectCollection: def test_get_team_id_from_project(session): - team_id = uuid.UUID('6b608f78-e341-422c-8076-35adc8828000') - check_project = {'project': {'team': {'id': team_id}}} + team_id = uuid.UUID("6b608f78-e341-422c-8076-35adc8828000") + check_project = {"project": {"team": {"id": team_id}}} session.set_response(check_project) - p = Project(name='Test Project', session=session) + p = Project(name="Test Project", session=session) assert p.team_id == team_id @@ -72,11 +64,9 @@ def test_publish_resource(project, session): assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'/projects/{project.uid}/published-resources/MODULE/batch-publish', - json={ - 'ids': [str(predictor.uid)] - } + method="POST", + path=f"/projects/{project.uid}/published-resources/MODULE/batch-publish", + json={"ids": [str(predictor.uid)]}, ) assert expected_call == session.last_call @@ -94,11 +84,9 @@ def test_pull_in_resource(project, session): assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'/teams/{project.team_id}/projects/{project.uid}/outside-resources/MODULE/batch-pull-in', - json={ - 'ids': [str(predictor.uid)] - } + method="POST", + path=f"/teams/{project.team_id}/projects/{project.uid}/outside-resources/MODULE/batch-pull-in", + json={"ids": [str(predictor.uid)]}, ) assert expected_call == session.last_call @@ -116,11 +104,9 @@ def test_un_publish_resource(project, session): assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'/projects/{project.uid}/published-resources/MODULE/batch-un-publish', - json={ - 'ids': [str(predictor.uid)] - } + method="POST", + path=f"/projects/{project.uid}/published-resources/MODULE/batch-un-publish", + json={"ids": [str(predictor.uid)]}, ) assert expected_call == session.last_call @@ -171,13 +157,14 @@ def test_ara_definitions_get_project_id(project): def test_failed_register(): team_id = uuid.uuid4() session = mock.Mock() - session.post_resource.side_effect = NotFound(f'/teams/{team_id}/projects', - FakeRequestResponse(400)) + session.post_resource.side_effect = NotFound( + f"/teams/{team_id}/projects", FakeRequestResponse(400) + ) project_collection = ProjectCollection(session=session, team_id=team_id) with pytest.raises(ModuleRegistrationFailedException) as e: project_collection.register("Project") assert 'The "Project" failed to register.' in str(e.value) - assert f'/teams/{team_id}/projects' in str(e.value) + assert f"/teams/{team_id}/projects" in str(e.value) def test_failed_register_no_team(session): @@ -188,66 +175,62 @@ def test_failed_register_no_team(session): def test_project_registration(collection: ProjectCollection, session): # Given - create_time = parse('2019-09-10T00:00:00+00:00') + create_time = parse("2019-09-10T00:00:00+00:00") project_data = ProjectDataFactory( - name='testing', - description='A sample project', - created_at=int(create_time.timestamp() * 1000) # The lib expects ms since epoch, which is really odd + name="testing", + description="A sample project", + # The lib expects ms since epoch, which is really odd + created_at=int(create_time.timestamp() * 1000), ) - session.set_response({'project': project_data}) + session.set_response({"project": project_data}) team_id = collection.team_id # When - created_project = collection.register('testing') + created_project = collection.register("testing") # Then assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path=f'teams/{team_id}/projects', - json={ - 'name': 'testing' - } + method="POST", path=f"teams/{team_id}/projects", json={"name": "testing"} ) assert expected_call == session.last_call - assert 'A sample project' == created_project.description - assert 'CREATED' == created_project.status + assert "A sample project" == created_project.description + assert "CREATED" == created_project.status assert create_time == created_project.created_at def test_get_project(collection: ProjectCollection, session): # Given - project_data = ProjectDataFactory(name='single project') - session.set_response({'project': project_data}) + project_data = ProjectDataFactory(name="single project") + session.set_response({"project": project_data}) # When - created_project = collection.get(project_data['id']) + created_project = collection.get(project_data["id"]) # Then assert 1 == session.num_calls - expected_call = FakeCall( - method='GET', - path='/projects/{}'.format(project_data['id']), - ) + expected_call = FakeCall(method="GET", path="/projects/{}".format(project_data["id"])) assert expected_call == session.last_call - assert 'single project' == created_project.name + assert "single project" == created_project.name def test_list_projects(collection, session): # Given projects_data = ProjectDataFactory.create_batch(5) - session.set_response({'projects': projects_data}) + session.set_response({"projects": projects_data}) # When projects = list(collection.list()) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='GET', - path=f'/teams/{collection.team_id}/projects', - params={'per_page': 1000, 'page': 1}, - version="v3") + expected_call = FakeCall( + method="GET", + path=f"/teams/{collection.team_id}/projects", + params={"per_page": 1000, "page": 1}, + version="v3", + ) assert expected_call == session.last_call assert 5 == len(projects) @@ -255,17 +238,19 @@ def test_list_projects(collection, session): def test_list_archived_projects(collection, session): # Given projects_data = ProjectDataFactory.create_batch(5) - session.set_response({'projects': projects_data}) + session.set_response({"projects": projects_data}) # When projects = list(collection.list_archived()) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='GET', - path=f'/teams/{collection.team_id}/projects', - params={'per_page': 1000, 'page': 1, 'archived': "true"}, - version="v3") + expected_call = FakeCall( + method="GET", + path=f"/teams/{collection.team_id}/projects", + params={"per_page": 1000, "page": 1, "archived": "true"}, + version="v3", + ) assert expected_call == session.last_call assert 5 == len(projects) @@ -273,17 +258,19 @@ def test_list_archived_projects(collection, session): def test_list_active_projects(collection, session): # Given projects_data = ProjectDataFactory.create_batch(5) - session.set_response({'projects': projects_data}) + session.set_response({"projects": projects_data}) # When projects = list(collection.list_active()) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='GET', - path=f'/teams/{collection.team_id}/projects', - params={'per_page': 1000, 'page': 1, 'archived': "false"}, - version="v3") + expected_call = FakeCall( + method="GET", + path=f"/teams/{collection.team_id}/projects", + params={"per_page": 1000, "page": 1, "archived": "false"}, + version="v3", + ) assert expected_call == session.last_call assert 5 == len(projects) @@ -291,12 +278,12 @@ def test_list_active_projects(collection, session): def test_list_no_team(session): project_collection = ProjectCollection(session=session) projects_data = ProjectDataFactory.create_batch(5) - session.set_response({'projects': projects_data}) + session.set_response({"projects": projects_data}) projects = list(project_collection.list()) assert 1 == session.num_calls - expected_call = FakeCall(method='GET', path='/projects', params={'per_page': 1000, 'page': 1}) + expected_call = FakeCall(method="GET", path="/projects", params={"per_page": 1000, "page": 1}) assert expected_call == session.last_call assert 5 == len(projects) @@ -304,81 +291,90 @@ def test_list_no_team(session): def test_list_projects_with_page_params(collection, session): # Given project_data = ProjectDataFactory() - session.set_response({'projects': [project_data]}) + session.set_response({"projects": [project_data]}) # When list(collection.list(per_page=10)) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='GET', path=f'/teams/{collection.team_id}/projects', params={'per_page': 10, 'page': 1}) + expected_call = FakeCall( + method="GET", + path=f"/teams/{collection.team_id}/projects", + params={"per_page": 10, "page": 1}, + ) assert expected_call == session.last_call + def test_search_all_no_team(session): project_collection = ProjectCollection(session=session) projects_data = ProjectDataFactory.create_batch(2) - project_name_to_match = projects_data[0]['name'] + project_name_to_match = projects_data[0]["name"] - search_params = { - 'name': { - 'value': project_name_to_match, - 'search_method': 'EXACT'}} + search_params = {"name": {"value": project_name_to_match, "search_method": "EXACT"}} expected_response = [p for p in projects_data if p["name"] == project_name_to_match] - project_collection.session.set_response({'projects': expected_response}) + project_collection.session.set_response({"projects": expected_response}) # Then results = list(project_collection.search_all(search_params=search_params)) - expected_call = FakeCall(method='POST', path='/projects/search', params={'userId': ''}, json={'search_params': search_params}) + expected_call = FakeCall( + method="POST", + path="/projects/search", + params={"userId": ""}, + json={"search_params": search_params}, + ) assert 1 == project_collection.session.num_calls assert expected_call == project_collection.session.last_call assert 1 == len(results) + def test_search_all(collection: ProjectCollection): # Given projects_data = ProjectDataFactory.create_batch(2) - project_name_to_match = projects_data[0]['name'] + project_name_to_match = projects_data[0]["name"] - search_params = { - 'name': { - 'value': project_name_to_match, - 'search_method': 'EXACT'}} + search_params = {"name": {"value": project_name_to_match, "search_method": "EXACT"}} expected_response = [p for p in projects_data if p["name"] == project_name_to_match] - collection.session.set_response({'projects': expected_response}) + collection.session.set_response({"projects": expected_response}) # Then results = list(collection.search_all(search_params=search_params)) - expected_call = FakeCall(method='POST', - path=f'/teams/{collection.team_id}/projects/search', - params={'userId': ''}, - json={'search_params': { - 'name': { - 'value': project_name_to_match, - 'search_method': 'EXACT'}}}) + expected_call = FakeCall( + method="POST", + path=f"/teams/{collection.team_id}/projects/search", + params={"userId": ""}, + json={ + "search_params": {"name": {"value": project_name_to_match, "search_method": "EXACT"}} + }, + ) assert 1 == collection.session.num_calls assert expected_call == collection.session.last_call assert 1 == len(results) + def test_search_all_no_search_params(collection: ProjectCollection): # Given projects_data = ProjectDataFactory.create_batch(2) expected_response = projects_data - collection.session.set_response({'projects': expected_response}) + collection.session.set_response({"projects": expected_response}) # Then result = list(collection.search_all(search_params=None)) - expected_call = FakeCall(method='POST', - path=f'/teams/{collection.team_id}/projects/search', - params={'userId': ''}, - json={}) + expected_call = FakeCall( + method="POST", + path=f"/teams/{collection.team_id}/projects/search", + params={"userId": ""}, + json={}, + ) assert 1 == collection.session.num_calls assert expected_call == collection.session.last_call @@ -388,43 +384,47 @@ def test_search_all_no_search_params(collection: ProjectCollection): def test_search_projects(collection: ProjectCollection): # Given projects_data = ProjectDataFactory.create_batch(2) - project_name_to_match = projects_data[0]['name'] + project_name_to_match = projects_data[0]["name"] - search_params = { - 'name': { - 'value': project_name_to_match, - 'search_method': 'EXACT'}} + search_params = {"name": {"value": project_name_to_match, "search_method": "EXACT"}} expected_response = [p for p in projects_data if p["name"] == project_name_to_match] - collection.session.set_response({'projects': expected_response}) + collection.session.set_response({"projects": expected_response}) # Then result = list(collection.search(search_params=search_params)) - expected_call = FakeCall(method='POST', - path=f'/teams/{collection.team_id}/projects/search', - params={'userId': ''}, - json={'search_params': { - 'name': { - 'value': project_name_to_match, - 'search_method': 'EXACT'}}}) + expected_call = FakeCall( + method="POST", + path=f"/teams/{collection.team_id}/projects/search", + params={"userId": ""}, + json={ + "search_params": {"name": {"value": project_name_to_match, "search_method": "EXACT"}} + }, + ) assert 1 == collection.session.num_calls assert expected_call == collection.session.last_call assert 1 == len(result) + def test_search_projects_no_search_params(collection: ProjectCollection): # Given projects_data = ProjectDataFactory.create_batch(2) expected_response = projects_data - collection.session.set_response({'projects': expected_response}) + collection.session.set_response({"projects": expected_response}) # Then result = list(collection.search()) - expected_call = FakeCall(method='POST', path=f'/teams/{collection.team_id}/projects/search', params={'userId': ''}, json={}) + expected_call = FakeCall( + method="POST", + path=f"/teams/{collection.team_id}/projects/search", + params={"userId": ""}, + json={}, + ) assert 1 == collection.session.num_calls assert expected_call == collection.session.last_call @@ -433,40 +433,40 @@ def test_search_projects_no_search_params(collection: ProjectCollection): def test_archive_project(collection, session): # Given - uid = '151199ec-e9aa-49a1-ac8e-da722aaf74c4' + uid = "151199ec-e9aa-49a1-ac8e-da722aaf74c4" # When collection.archive(uid) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='POST', path=f'/projects/{uid}/archive') + expected_call = FakeCall(method="POST", path=f"/projects/{uid}/archive") assert expected_call == session.last_call def test_restore_project(collection, session): # Given - uid = '151199ec-e9aa-49a1-ac8e-da722aaf74c4' + uid = "151199ec-e9aa-49a1-ac8e-da722aaf74c4" # When collection.restore(uid) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='POST', path=f'/projects/{uid}/restore') + expected_call = FakeCall(method="POST", path=f"/projects/{uid}/restore") assert expected_call == session.last_call def test_delete_project(collection, session): # Given - uid = '151199ec-e9aa-49a1-ac8e-da722aaf74c4' + uid = "151199ec-e9aa-49a1-ac8e-da722aaf74c4" # When collection.delete(uid) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='DELETE', path=f'/projects/{uid}') + expected_call = FakeCall(method="DELETE", path=f"/projects/{uid}") assert expected_call == session.last_call @@ -482,22 +482,17 @@ def test_list_members(project, session): user["actions"] = READ user.pop("position") - team_data = TeamDataFactory( - id=str(project.team_id), - ) + team_data = TeamDataFactory(id=str(project.team_id)) - session.set_responses( - {'team': team_data}, - {'users': [user]} - ) + session.set_responses({"team": team_data}, {"users": [user]}) # When members = project.list_members() # Then assert 2 == session.num_calls - expect_call_1 = FakeCall(method='GET', path=f'/teams/{team_data["id"]}') - expect_call_2 = FakeCall(method='GET', path=f'/teams/{project.team_id}/users') + expect_call_1 = FakeCall(method="GET", path=f"/teams/{team_data['id']}") + expect_call_2 = FakeCall(method="GET", path=f"/teams/{project.team_id}/users") assert expect_call_1 == session.calls[0] assert expect_call_2 == session.calls[1] assert isinstance(members[0], TeamMember) diff --git a/tests/resources/test_project_member.py b/tests/resources/test_project_member.py index 3eb4e3cdc..601ba2249 100644 --- a/tests/resources/test_project_member.py +++ b/tests/resources/test_project_member.py @@ -23,5 +23,6 @@ def project_member(user, project) -> ProjectMember: def test_string_representation(project_member): - assert project_member.__str__() == ""\ - .format(project_member.user.screen_name, project_member.project.name) + cast = str(project_member) + assert project_member.user.screen_name in cast + assert project_member.project.name in cast diff --git a/tests/resources/test_report.py b/tests/resources/test_report.py index 5d913703f..7dda093fc 100644 --- a/tests/resources/test_report.py +++ b/tests/resources/test_report.py @@ -1,27 +1,25 @@ """Tests getting a report""" + import random import uuid -import pytest - from citrine.resources.report import ReportResource - from tests.utils.session import FakeCall, FakeSession def test_get_report(): project_id = uuid.uuid4() predictor_id = uuid.uuid4() - report_path = f'/projects/{project_id}/predictors/{predictor_id}/versions/most_recent/report' + report_path = f"/projects/{project_id}/predictors/{predictor_id}/versions/most_recent/report" session = FakeSession() - session.set_response(dict(status='PENDING', - report=dict(descriptors=[], models=[]), - uid=str(str(uuid.uuid4())))) + session.set_response( + dict(status="PENDING", report=dict(descriptors=[], models=[]), uid=str(str(uuid.uuid4()))) + ) report = ReportResource(project_id, session).get(predictor_id=predictor_id) - assert report.status == 'PENDING' + assert report.status == "PENDING" assert session.calls == [FakeCall(method="GET", path=report_path)] @@ -29,14 +27,18 @@ def test_get_report_with_version(): project_id = uuid.uuid4() predictor_id = uuid.uuid4() predictor_version = random.randint(1, 10) - report_path = f'/projects/{project_id}/predictors/{predictor_id}/versions/{predictor_version}/report' + report_path = ( + f"/projects/{project_id}/predictors/{predictor_id}/versions/{predictor_version}/report" + ) session = FakeSession() - session.set_response(dict(status='PENDING', - report=dict(descriptors=[], models=[]), - uid=str(str(uuid.uuid4())))) + session.set_response( + dict(status="PENDING", report=dict(descriptors=[], models=[]), uid=str(str(uuid.uuid4()))) + ) - report = ReportResource(project_id, session).get(predictor_id=predictor_id, predictor_version=predictor_version) + report = ReportResource(project_id, session).get( + predictor_id=predictor_id, predictor_version=predictor_version + ) - assert report.status == 'PENDING' + assert report.status == "PENDING" assert session.calls == [FakeCall(method="GET", path=report_path)] diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py index 18a263f15..779f7f6a2 100644 --- a/tests/resources/test_resources.py +++ b/tests/resources/test_resources.py @@ -10,7 +10,10 @@ from citrine.resources.material_template import MaterialTemplate, MaterialTemplateCollection from citrine.resources.measurement_run import MeasurementRun, MeasurementRunCollection from citrine.resources.measurement_spec import MeasurementSpec, MeasurementSpecCollection -from citrine.resources.measurement_template import MeasurementTemplate, MeasurementTemplateCollection +from citrine.resources.measurement_template import ( + MeasurementTemplate, + MeasurementTemplateCollection, +) from citrine.resources.parameter_template import ParameterTemplate, ParameterTemplateCollection from citrine.resources.process_run import ProcessRun, ProcessRunCollection from citrine.resources.process_spec import ProcessSpec, ProcessSpecCollection @@ -22,19 +25,31 @@ resource_string_data = [ (IngredientRun, {}, ""), - (IngredientSpec, {'name': 'foo'}, ""), - (MaterialSpec, {'name': 'foo'}, ""), - (MaterialTemplate, {'name': 'foo'}, ""), - (MeasurementRun, {'name': 'foo'}, ""), - (MeasurementSpec, {'name': 'foo'}, ""), - (MeasurementTemplate, {'name': 'foo'}, ""), - (ParameterTemplate, {'name': 'foo', 'bounds': RealBounds(0, 1, '')}, ""), - (ProcessRun, {'name': 'foo'}, ""), - (ProcessSpec, {'name': 'foo'}, ""), - (ProcessTemplate, {'name': 'foo'}, ""), - (PropertyTemplate, {'name': 'foo', 'bounds': RealBounds(0, 1, '')}, ""), - (ConditionTemplate, {'name': 'foo', 'bounds': RealBounds(0, 1, '')}, ""), - (Response, {'status_code': 200}, "") + (IngredientSpec, {"name": "foo"}, ""), + (MaterialSpec, {"name": "foo"}, ""), + (MaterialTemplate, {"name": "foo"}, ""), + (MeasurementRun, {"name": "foo"}, ""), + (MeasurementSpec, {"name": "foo"}, ""), + (MeasurementTemplate, {"name": "foo"}, ""), + ( + ParameterTemplate, + {"name": "foo", "bounds": RealBounds(0, 1, "")}, + "", + ), + (ProcessRun, {"name": "foo"}, ""), + (ProcessSpec, {"name": "foo"}, ""), + (ProcessTemplate, {"name": "foo"}, ""), + ( + PropertyTemplate, + {"name": "foo", "bounds": RealBounds(0, 1, "")}, + "", + ), + ( + ConditionTemplate, + {"name": "foo", "bounds": RealBounds(0, 1, "")}, + "", + ), + (Response, {"status_code": 200}, ""), ] resource_type_data = [ @@ -54,11 +69,11 @@ ] -@pytest.mark.parametrize('resource_type,kwargs,val', resource_string_data) +@pytest.mark.parametrize("resource_type,kwargs,val", resource_string_data) def test_str_representation(resource_type, kwargs, val): assert val == str(resource_type(**kwargs)) -@pytest.mark.parametrize('collection_type,resource_type', resource_type_data) +@pytest.mark.parametrize("collection_type,resource_type", resource_type_data) def test_collection_type(collection_type, resource_type): assert resource_type == collection_type.get_type() diff --git a/tests/resources/test_response.py b/tests/resources/test_response.py index 3fb28a63c..0370a6abc 100644 --- a/tests/resources/test_response.py +++ b/tests/resources/test_response.py @@ -14,7 +14,7 @@ def test_empty_response_repr(): def test_empty_body_present_code(): """Tests that the repr output expresses the absence of body and presence of - status code correctly.""" + status code correctly.""" resp_with_code = Response(status_code=404) no_body_found = re.search("No body available", resp_with_code.__repr__()) status_code_found = re.search("404", resp_with_code.__repr__()) @@ -24,7 +24,7 @@ def test_empty_body_present_code(): def test_empty_body_present_code(): """Tests that the repr output expresses the presence of body and presence of - status code correctly.""" + status code correctly.""" resp_with_code_and_body = Response(status_code=404, body={"message": "a quick message"}) body_found = re.search("a quick message", resp_with_code_and_body.__repr__()) status_code_found = re.search("404", resp_with_code_and_body.__repr__()) diff --git a/tests/resources/test_sample_design_space.py b/tests/resources/test_sample_design_space.py index c27ecb0da..131e04d0b 100644 --- a/tests/resources/test_sample_design_space.py +++ b/tests/resources/test_sample_design_space.py @@ -1,11 +1,12 @@ -import pytest import uuid -from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace +import pytest + from citrine.informatics.design_spaces.sample_design_space import SampleDesignSpaceInput +from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution from citrine.resources.sample_design_space_execution import SampleDesignSpaceExecutionCollection -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -23,7 +24,9 @@ def collection(session) -> SampleDesignSpaceExecutionCollection: @pytest.fixture -def sample_design_space_execution(collection: SampleDesignSpaceExecutionCollection, sample_design_space_execution_dict) -> SampleDesignSpaceExecution: +def sample_design_space_execution( + collection: SampleDesignSpaceExecutionCollection, sample_design_space_execution_dict +) -> SampleDesignSpaceExecution: return collection.build(sample_design_space_execution_dict) @@ -46,40 +49,42 @@ def test_build_new_execution(collection, sample_design_space_execution_dict): assert execution.in_progress() and not execution.succeeded() and not execution.failed() -def test_trigger_execution(collection: SampleDesignSpaceExecutionCollection, sample_design_space_execution_dict, session): +def test_trigger_execution( + collection: SampleDesignSpaceExecutionCollection, sample_design_space_execution_dict, session +): # Given session.set_response(sample_design_space_execution_dict) - sample_design_space_execution_input = SampleDesignSpaceInput( - n_candidates=10 - ) + sample_design_space_execution_input = SampleDesignSpaceInput(n_candidates=10) # When actual_execution = collection.trigger(sample_design_space_execution_input) # Then assert str(actual_execution.uid) == sample_design_space_execution_dict["id"] - expected_path = '/projects/{}/design-spaces/{}/sample'.format( - collection.project_id, collection.design_space_id + expected_path = ( + f"/projects/{collection.project_id}/design-spaces/{collection.design_space_id}/sample" ) assert session.last_call == FakeCall( - method='POST', + method="POST", path=expected_path, - json={ - 'n_candidates': sample_design_space_execution_input.n_candidates, - } + json={"n_candidates": sample_design_space_execution_input.n_candidates}, ) def test_execution_completes(): data_success = { - 'id': str(uuid.uuid4()), - 'status': {'major': 'SUCCEEDED', 'minor': 'COMPLETED', 'detail': [], 'info': []}, + "id": str(uuid.uuid4()), + "status": {"major": "SUCCEEDED", "minor": "COMPLETED", "detail": [], "info": []}, } execution_success = SampleDesignSpaceExecution.build(data_success) assert execution_success.succeeded() -def test_sample_design_space_execution_results(sample_design_space_execution: SampleDesignSpaceExecution, session, example_sample_design_space_response): +def test_sample_design_space_execution_results( + sample_design_space_execution: SampleDesignSpaceExecution, + session, + example_sample_design_space_response, +): # Given session.set_response(example_sample_design_space_response) @@ -87,30 +92,27 @@ def test_sample_design_space_execution_results(sample_design_space_execution: Sa list(sample_design_space_execution.results(per_page=4)) # Then - expected_path = '/projects/{}/design-spaces/{}/sample/{}/results'.format( - sample_design_space_execution.project_id, - sample_design_space_execution.design_space_id, - sample_design_space_execution.uid, + expected_path = f"/projects/{sample_design_space_execution.project_id}/design-spaces/{sample_design_space_execution.design_space_id}/sample/{sample_design_space_execution.uid}/results" + assert session.last_call == FakeCall( + method="GET", path=expected_path, params={"page": 1, "per_page": 4} ) - assert session.last_call == FakeCall(method='GET', path=expected_path, params={"page": 1, "per_page": 4}) -def test_sample_design_space_execution_result(sample_design_space_execution: SampleDesignSpaceExecution, session, example_sample_design_space_response): +def test_sample_design_space_execution_result( + sample_design_space_execution: SampleDesignSpaceExecution, + session, + example_sample_design_space_response, +): # Given session.set_response(example_sample_design_space_response["response"][0]) # When - result_id=example_sample_design_space_response["response"][0]["id"] + result_id = example_sample_design_space_response["response"][0]["id"] sample_design_space_execution.result(result_id=result_id) # Then - expected_path = '/projects/{}/design-spaces/{}/sample/{}/results/{}'.format( - sample_design_space_execution.project_id, - sample_design_space_execution.design_space_id, - sample_design_space_execution.uid, - result_id, - ) - assert session.last_call == FakeCall(method='GET', path=expected_path) + expected_path = f"/projects/{sample_design_space_execution.project_id}/design-spaces/{sample_design_space_execution.design_space_id}/sample/{sample_design_space_execution.uid}/results/{result_id}" + assert session.last_call == FakeCall(method="GET", path=expected_path) def test_list(collection: SampleDesignSpaceExecutionCollection, session): @@ -118,14 +120,11 @@ def test_list(collection: SampleDesignSpaceExecutionCollection, session): lst = list(collection.list(per_page=4)) assert len(lst) == 0 - expected_path = '/projects/{}/design-spaces/{}/sample'.format( - collection.project_id, - collection.design_space_id, + expected_path = ( + f"/projects/{collection.project_id}/design-spaces/{collection.design_space_id}/sample" ) assert session.last_call == FakeCall( - method='GET', - path=expected_path, - params={"page": 1, "per_page": 4} + method="GET", path=expected_path, params={"page": 1, "per_page": 4} ) diff --git a/tests/resources/test_table_config.py b/tests/resources/test_table_config.py index 4e11ed504..61aa55681 100644 --- a/tests/resources/test_table_config.py +++ b/tests/resources/test_table_config.py @@ -1,26 +1,43 @@ from uuid import UUID, uuid4 -import pytest +import pytest from gemd.entity.link_by_uid import LinkByUID + from citrine.gemd_queries.gemd_query import GemdQuery -from citrine.gemtables.columns import MeanColumn, OriginalUnitsColumn, StdDevColumn, IdentityColumn +from citrine.gemtables.columns import IdentityColumn, MeanColumn, OriginalUnitsColumn, StdDevColumn from citrine.gemtables.rows import MaterialRunByTemplate -from citrine.gemtables.variables import AttributeByTemplate, TerminalMaterialInfo, \ - IngredientQuantityDimension, IngredientQuantityByProcessAndName, \ - IngredientIdentifierByProcessTemplateAndName, TerminalMaterialIdentifier, \ - IngredientQuantityInOutput, IngredientIdentifierInOutput, \ - IngredientLabelsSetByProcessAndName, IngredientLabelsSetInOutput -from citrine.resources.table_config import TableConfig, TableConfigCollection, TableBuildAlgorithm, \ - TableFromGemdQueryAlgorithm +from citrine.gemtables.variables import ( + AttributeByTemplate, + IngredientIdentifierByProcessTemplateAndName, + IngredientIdentifierInOutput, + IngredientLabelsSetByProcessAndName, + IngredientLabelsSetInOutput, + IngredientQuantityByProcessAndName, + IngredientQuantityDimension, + IngredientQuantityInOutput, + TerminalMaterialIdentifier, + TerminalMaterialInfo, +) from citrine.resources.data_concepts import CITRINE_SCOPE from citrine.resources.material_run import MaterialRun -from citrine.resources.project import Project from citrine.resources.process_template import ProcessTemplate +from citrine.resources.project import Project +from citrine.resources.table_config import ( + TableBuildAlgorithm, + TableConfig, + TableConfigCollection, + TableFromGemdQueryAlgorithm, +) from citrine.resources.team import Team from citrine.seeding.find_or_create import create_or_update -from tests.utils.factories import TableConfigResponseDataFactory, ListTableConfigResponseDataFactory, \ - GemdQueryDataFactory, TableConfigDataFactory, DatasetDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.factories import ( + DatasetDataFactory, + GemdQueryDataFactory, + ListTableConfigResponseDataFactory, + TableConfigDataFactory, + TableConfigResponseDataFactory, +) +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -30,40 +47,32 @@ def session() -> FakeSession: @pytest.fixture def team(session) -> Team: - team = Team(name='Test Team', session=session) - team.uid = UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + team = Team(name="Test Team", session=session) + team.uid = UUID("16fd2706-8baf-433b-82eb-8c7fada847da") return team @pytest.fixture def project(session, team) -> Project: - project = Project( - name="Test GEM Table project", - session=session, - team_id=team.uid - ) - project.uid = UUID('6b608f78-e341-422c-8076-35adc8828545') - session.set_response({ - 'project': { - 'team': { - 'id': str(team.uid) - } - } - }) + project = Project(name="Test GEM Table project", session=session, team_id=team.uid) + project.uid = UUID("6b608f78-e341-422c-8076-35adc8828545") + session.set_response({"project": {"team": {"id": str(team.uid)}}}) return project @pytest.fixture def collection(session) -> TableConfigCollection: return TableConfigCollection( - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - project_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=session + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + project_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + session=session, ) def empty_defn() -> TableConfig: - return TableConfig(name="empty", description="empty", datasets=[], rows=[], variables=[], columns=[]) + return TableConfig( + name="empty", description="empty", datasets=[], rows=[], variables=[], columns=[] + ) def test_get_table_config(collection, session): @@ -81,8 +90,7 @@ def test_get_table_config(collection, session): # Then assert 1 == session.num_calls expect_call = FakeCall( - method="GET", - path=collection._get_path(defn_id, action=["versions", ver_number]) + method="GET", path=collection._get_path(defn_id, action=["versions", ver_number]) ) assert session.last_call == expect_call assert str(retrieved_table_config.config_uid) == defn_id @@ -91,7 +99,9 @@ def test_get_table_config(collection, session): # Given table_configs_response = ListTableConfigResponseDataFactory() defn_id = table_configs_response["definition"]["id"] - version_number = max([version_dict["version_number"] for version_dict in table_configs_response["versions"]]) + version_number = max( + [version_dict["version_number"] for version_dict in table_configs_response["versions"]] + ) session.set_response(table_configs_response) # When @@ -99,10 +109,7 @@ def test_get_table_config(collection, session): # Then assert 2 == session.num_calls - expect_call = FakeCall( - method="GET", - path=collection._get_path(defn_id) - ) + expect_call = FakeCall(method="GET", path=collection._get_path(defn_id)) assert session.last_call == expect_call assert str(retrieved_table_config.config_uid) == defn_id assert retrieved_table_config.version_number == version_number @@ -115,15 +122,18 @@ def test_get_table_config_raises(collection): def test_init_table_config(): - table_config = TableConfig(name="foo", description="bar", rows=[], columns=[], variables=[], datasets=[]) + table_config = TableConfig( + name="foo", description="bar", rows=[], columns=[], variables=[], datasets=[] + ) assert table_config.config_uid is None assert table_config.version_number is None def test_uid_aliases_config_uid(): """Test that uid returns config_uid attribute""" - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], - columns=[]) + table_config = TableConfig( + name="name", description="description", datasets=[], rows=[], variables=[], columns=[] + ) table_config.config_uid = uuid4() assert table_config.uid == table_config.config_uid @@ -135,7 +145,12 @@ def test_uid_aliases_config_uid(): def test_create_or_update_config(collection, session): initial_config = TableConfig( - name="Test Config", description="description", datasets=[], rows=[], variables=[], columns=[] + name="Test Config", + description="description", + datasets=[], + rows=[], + variables=[], + columns=[], ) # Fake table config data response @@ -143,16 +158,10 @@ def test_create_or_update_config(collection, session): retrieved_config_response["definition"]["name"] = "Test Config" retrieved_id = retrieved_config_response["definition"]["id"] retrieved_version = retrieved_config_response["version"]["version_number"] - session.set_responses( - {'definitions': [retrieved_config_response]}, - retrieved_config_response - ) + session.set_responses({"definitions": [retrieved_config_response]}, retrieved_config_response) # Create or update with mocked list, return just fake response - updated_table_config = create_or_update( - collection=collection, - resource=initial_config - ) + updated_table_config = create_or_update(collection=collection, resource=initial_config) # Updated config should have UID set from response data assert 2 == session.num_calls @@ -163,22 +172,30 @@ def test_dup_names(): """Make sure that variable name and headers are unique across a table config""" with pytest.raises(ValueError) as excinfo: TableConfig( - name="foo", description="bar", datasets=[], rows=[], columns=[], + name="foo", + description="bar", + datasets=[], + rows=[], + columns=[], variables=[ TerminalMaterialInfo(name="foo", headers=["foo", "bar"], field="name"), - TerminalMaterialInfo(name="foo", headers=["foo", "baz"], field="name") - ] + TerminalMaterialInfo(name="foo", headers=["foo", "baz"], field="name"), + ], ) assert "Multiple" in str(excinfo.value) assert "foo" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: TableConfig( - name="foo", description="bar", datasets=[], rows=[], columns=[], + name="foo", + description="bar", + datasets=[], + rows=[], + columns=[], variables=[ TerminalMaterialInfo(name="foo", headers=["spam", "eggs"], field="name"), - TerminalMaterialInfo(name="bar", headers=["spam", "eggs"], field="name") - ] + TerminalMaterialInfo(name="bar", headers=["spam", "eggs"], field="name"), + ], ) assert "Multiple" in str(excinfo.value) assert "spam" in str(excinfo.value) @@ -188,10 +205,12 @@ def test_missing_variable(): """Make sure that every data_source matches a name of a variable""" with pytest.raises(ValueError) as excinfo: TableConfig( - name="foo", description="bar", datasets=[], rows=[], variables=[], - columns=[ - MeanColumn(data_source="density") - ] + name="foo", + description="bar", + datasets=[], + rows=[], + variables=[], + columns=[MeanColumn(data_source="density")], ) assert "must match" in str(excinfo.value) assert "density" in str(excinfo.value) @@ -201,7 +220,7 @@ def test_dump_example(): density = AttributeByTemplate( name="density", headers=["Slice", "Density"], - template=LinkByUID(scope="templates", id="density") + template=LinkByUID(scope="templates", id="density"), ) table_config = TableConfig( name="Example Table", @@ -213,7 +232,7 @@ def test_dump_example(): MeanColumn(data_source=density.name), StdDevColumn(data_source=density.name), OriginalUnitsColumn(data_source=density.name), - ] + ], ) @@ -225,7 +244,7 @@ def test_preview(collection, session): expect_call = FakeCall( method="POST", path=f"teams/{collection.team_id}/ara-definitions/preview", - json={"definition": empty_defn().dump(), "rows": []} + json={"definition": empty_defn().dump(), "rows": []}, ) assert session.last_call == expect_call @@ -234,64 +253,61 @@ def test_default_for_material(collection: TableConfigCollection, session): """Test that default for material hits the right route""" # Given dummy_resp = { - 'config': TableConfigDataFactory(), - 'ambiguous': [ + "config": TableConfigDataFactory(), + "ambiguous": [ [ - TerminalMaterialIdentifier(name='foo', headers=['foo'], scope='id').dump(), - IdentityColumn(data_source='foo').dump(), + TerminalMaterialIdentifier(name="foo", headers=["foo"], scope="id").dump(), + IdentityColumn(data_source="foo").dump(), ] ], } session.responses.append(dummy_resp) collection.default_for_material( - material='my_id', - name='my_name', - description='my_description', - algorithm=TableBuildAlgorithm.SINGLE_ROW + material="my_id", + name="my_name", + description="my_description", + algorithm=TableBuildAlgorithm.SINGLE_ROW, ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path=f"teams/{collection.team_id}/table-configs/default", params={ - 'id': 'my_id', - 'scope': CITRINE_SCOPE, - 'algorithm': TableBuildAlgorithm.SINGLE_ROW.value, - 'name': 'my_name', - 'description': 'my_description' - } + "id": "my_id", + "scope": CITRINE_SCOPE, + "algorithm": TableBuildAlgorithm.SINGLE_ROW.value, + "name": "my_name", + "description": "my_description", + }, ) # We allowed for the more forgiving call structure, so test it. session.calls.clear() session.responses.append(dummy_resp) collection.default_for_material( - material=MaterialRun('foo', uids={'scope': 'id'}), + material=MaterialRun("foo", uids={"scope": "id"}), algorithm=TableBuildAlgorithm.FORMULATIONS.value, - name='my_name', - description='my_description', + name="my_name", + description="my_description", ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path=f"teams/{collection.team_id}/table-configs/default", params={ - 'id': 'id', - 'scope': 'scope', - 'algorithm': TableBuildAlgorithm.FORMULATIONS.value, - 'name': 'my_name', - 'description': 'my_description' - } + "id": "id", + "scope": "scope", + "algorithm": TableBuildAlgorithm.FORMULATIONS.value, + "name": "my_name", + "description": "my_description", + }, ) def test_default_for_material_failure(collection: TableConfigCollection): with pytest.raises(ValueError): - collection.default_for_material( - material=MaterialRun('foo'), - name='foo' - ) + collection.default_for_material(material=MaterialRun("foo"), name="foo") def test_from_query(collection: TableConfigCollection, session): @@ -300,31 +316,31 @@ def test_from_query(collection: TableConfigCollection, session): config = TableConfigDataFactory() config_resp = { - 'config': config, - 'ambiguous': [ + "config": config, + "ambiguous": [ [ - TerminalMaterialIdentifier(name='foo', headers=['foo'], scope='id').dump(), - IdentityColumn(data_source='foo').dump(), + TerminalMaterialIdentifier(name="foo", headers=["foo"], scope="id").dump(), + IdentityColumn(data_source="foo").dump(), ] ], } session.responses.append(config_resp) fake_call = FakeCall( - method='POST', - path=f'teams/{collection.team_id}/table-configs/from-query', + method="POST", + path=f"teams/{collection.team_id}/table-configs/from-query", params={ - 'name': config['name'], - 'description': config['description'], - 'algorithm': TableFromGemdQueryAlgorithm.MULTISTEP_MATERIALS, + "name": config["name"], + "description": config["description"], + "algorithm": TableFromGemdQueryAlgorithm.MULTISTEP_MATERIALS, }, json=query, ) collection.from_query( - name=config['name'], - description=config['description'], + name=config["name"], + description=config["description"], gemd_query=GemdQuery.build(query), - algorithm=TableFromGemdQueryAlgorithm.MULTISTEP_MATERIALS + algorithm=TableFromGemdQueryAlgorithm.MULTISTEP_MATERIALS, ) assert 1 == session.num_calls assert session.last_call.method == fake_call.method @@ -341,15 +357,17 @@ def test_from_query(collection: TableConfigCollection, session): def test_from_nameless_query_and_register(collection: TableConfigCollection, session): """Test that default for material hits the right route""" query = GemdQueryDataFactory() - config = TableConfigDataFactory(generation_algorithm=TableFromGemdQueryAlgorithm.MULTISTEP_MATERIALS) + config = TableConfigDataFactory( + generation_algorithm=TableFromGemdQueryAlgorithm.MULTISTEP_MATERIALS + ) - dataset_resps = [DatasetDataFactory(id=dataset) for dataset in query['datasets']] + dataset_resps = [DatasetDataFactory(id=dataset) for dataset in query["datasets"]] config_resp = { - 'config': config, - 'ambiguous': [ + "config": config, + "ambiguous": [ [ - TerminalMaterialIdentifier(name='foo', headers=['foo'], scope='id').dump(), - IdentityColumn(data_source='foo').dump(), + TerminalMaterialIdentifier(name="foo", headers=["foo"], scope="id").dump(), + IdentityColumn(data_source="foo").dump(), ] ], } @@ -360,11 +378,9 @@ def test_from_nameless_query_and_register(collection: TableConfigCollection, ses session.responses.append(register_resp) generated, _ = collection.from_query( - gemd_query=GemdQuery.build(query), - description='my_description', - register_config=True + gemd_query=GemdQuery.build(query), description="my_description", register_config=True ) - assert session.num_calls == len(query['datasets']) + 1 + 1 + assert session.num_calls == len(query["datasets"]) + 1 + 1 assert generated != TableConfig.build(config) # Because it has ids assert generated.variables == TableConfig.build(config).variables @@ -378,16 +394,18 @@ def test_add_columns(): with pytest.raises(ValueError) as excinfo: empty.add_columns( variable=TerminalMaterialInfo(name="foo", headers=["bar"], field="name"), - columns=[IdentityColumn(data_source="bar")] + columns=[IdentityColumn(data_source="bar")], ) assert "data_source must be" in str(excinfo.value) # Check desired behavior with_name_col = empty.add_columns( variable=TerminalMaterialInfo(name="name", headers=["bar"], field="name"), - columns=[IdentityColumn(data_source="name")] + columns=[IdentityColumn(data_source="name")], ) - assert with_name_col.variables == [TerminalMaterialInfo(name="name", headers=["bar"], field="name")] + assert with_name_col.variables == [ + TerminalMaterialInfo(name="name", headers=["bar"], field="name") + ] assert with_name_col.columns == [IdentityColumn(data_source="name")] assert with_name_col.config_uid == empty.config_uid @@ -395,7 +413,7 @@ def test_add_columns(): with pytest.raises(ValueError) as excinfo: with_name_col.add_columns( variable=TerminalMaterialInfo(name="name", headers=["bar"], field="name"), - columns=[IdentityColumn(data_source="name")] + columns=[IdentityColumn(data_source="name")], ) assert "already used" in str(excinfo.value) @@ -403,102 +421,122 @@ def test_add_columns(): def test_add_all_ingredients_via_team(session, team): """Test the behavior of AraDefinition.add_all_ingredients.""" # GIVEN - process_id = '3a308f78-e341-f39c-8076-35a2c88292ad' - process_name = 'mixing' + process_id = "3a308f78-e341-f39c-8076-35a2c88292ad" + process_name = "mixing" allowed_names = ["gold nanoparticles", "methanol", "acetone"] - process_link = LinkByUID('id', process_id) + process_link = LinkByUID("id", process_id) session.set_response( - ProcessTemplate(process_name, uids={'id': process_id}, allowed_names=allowed_names).dump() + ProcessTemplate(process_name, uids={"id": process_id}, allowed_names=allowed_names).dump() ) # WHEN we add all ingredients in a volume basis empty = empty_defn() - def1 = empty.add_all_ingredients(process_template=process_link, team=team, - quantity_dimension=IngredientQuantityDimension.VOLUME) + def1 = empty.add_all_ingredients( + process_template=process_link, + team=team, + quantity_dimension=IngredientQuantityDimension.VOLUME, + ) def1.config_uid = uuid4() # THEN there should be 3 variables and columns for each name, one for id, quantity, and labels assert len(def1.variables) == len(allowed_names) * 3 assert len(def1.columns) == len(def1.variables) for name in allowed_names: - assert next((var for var in def1.variables if name in var.headers - and isinstance(var, IngredientQuantityByProcessAndName)), None) is not None - assert next((var for var in def1.variables if name in var.headers - and isinstance(var, IngredientIdentifierByProcessTemplateAndName)), None) is not None - assert next((var for var in def1.variables if name in var.headers - and isinstance(var, IngredientLabelsSetByProcessAndName)), None) is not None + assert any( + var + for var in def1.variables + if name in var.headers and isinstance(var, IngredientQuantityByProcessAndName) + ) + assert any( + var + for var in def1.variables + if name in var.headers + and isinstance(var, IngredientIdentifierByProcessTemplateAndName) + ) + assert any( + var + for var in def1.variables + if name in var.headers and isinstance(var, IngredientLabelsSetByProcessAndName) + ) session.set_response( - ProcessTemplate(process_name, uids={'id': process_id}, allowed_names=allowed_names).dump() + ProcessTemplate(process_name, uids={"id": process_id}, allowed_names=allowed_names).dump() ) # WHEN we add all ingredients to the same Table Config as absolute quantities - def2 = def1.add_all_ingredients(process_template=process_link, team=team, - quantity_dimension=IngredientQuantityDimension.ABSOLUTE, - unit='kg') + def2 = def1.add_all_ingredients( + process_template=process_link, + team=team, + quantity_dimension=IngredientQuantityDimension.ABSOLUTE, + unit="kg", + ) # THEN there should be 1 new variable for each name, corresponding to the quantity # There is already a variable for id and labels # There should be 2 new columns for each name, one for the quantity and one for the units - new_variables = def2.variables[len(def1.variables):] - new_columns = def2.columns[len(def1.columns):] + new_variables = def2.variables[len(def1.variables) :] + new_columns = def2.columns[len(def1.columns) :] assert len(new_variables) == len(allowed_names) assert len(new_columns) == len(allowed_names) * 2 assert def2.config_uid == def1.config_uid for name in allowed_names: - assert next((var for var in new_variables if name in var.headers - and isinstance(var, IngredientQuantityByProcessAndName)), None) is not None + assert any( + var + for var in new_variables + if name in var.headers and isinstance(var, IngredientQuantityByProcessAndName) + ) session.set_response( - ProcessTemplate(process_name, uids={'id': process_id}, allowed_names=allowed_names).dump() + ProcessTemplate(process_name, uids={"id": process_id}, allowed_names=allowed_names).dump() ) # WHEN we add all ingredients to the same Table Config in a volume basis # THEN it raises an exception because these variables and columns already exist with pytest.raises(ValueError): - def2.add_all_ingredients(process_template=process_link, team=team, - quantity_dimension=IngredientQuantityDimension.VOLUME) + def2.add_all_ingredients( + process_template=process_link, + team=team, + quantity_dimension=IngredientQuantityDimension.VOLUME, + ) # If the process template has an empty allowed_names list then an error should be raised - session.set_response( - ProcessTemplate(process_name, uids={'id': process_id}).dump() - ) + session.set_response(ProcessTemplate(process_name, uids={"id": process_id}).dump()) with pytest.raises(RuntimeError): - empty_defn().add_all_ingredients(process_template=process_link, team=team, - quantity_dimension=IngredientQuantityDimension.VOLUME) + empty_defn().add_all_ingredients( + process_template=process_link, + team=team, + quantity_dimension=IngredientQuantityDimension.VOLUME, + ) def test_add_all_ingredients_no_principal(session): """Test the behavior of AraDefinition.add_all_ingredients.""" - process_link = LinkByUID('id', '3a308f78-e341-f39c-8076-35a2c88292ad') + process_link = LinkByUID("id", "3a308f78-e341-f39c-8076-35a2c88292ad") with pytest.raises(TypeError): - empty_defn().add_all_ingredients(process_template=process_link, - quantity_dimension=IngredientQuantityDimension.VOLUME) + empty_defn().add_all_ingredients( + process_template=process_link, quantity_dimension=IngredientQuantityDimension.VOLUME + ) def test_add_all_ingredients_in_output_via_team(session, team): """Test the behavior of TableConfig.add_all_ingredients_in_output.""" # GIVEN - process1_id = '3a308f78-e341-f39c-8076-35a2c88292ad' - process1_name = 'mixing' + process1_id = "3a308f78-e341-f39c-8076-35a2c88292ad" + process1_name = "mixing" allowed_names1 = ["gold nanoparticles", "methanol", "acetone"] - process1_link = LinkByUID('id', process1_id) + process1_link = LinkByUID("id", process1_id) - process2_id = '519ab440-fbda-4768-ad63-5e09b420285c' - process2_name = 'solvent_mixing' + process2_id = "519ab440-fbda-4768-ad63-5e09b420285c" + process2_name = "solvent_mixing" allowed_names2 = ["methanol", "acetone", "ethanol", "water"] - process2_link = LinkByUID('id', process2_id) + process2_link = LinkByUID("id", process2_id) union_allowed_names = list(set(allowed_names1) | set(allowed_names2)) session.set_responses( ProcessTemplate( - process1_name, - uids={'id': process1_id}, - allowed_names=allowed_names1 + process1_name, uids={"id": process1_id}, allowed_names=allowed_names1 ).dump(), ProcessTemplate( - process2_name, - uids={'id': process2_id}, - allowed_names=allowed_names2 - ).dump() + process2_name, uids={"id": process2_id}, allowed_names=allowed_names2 + ).dump(), ) # WHEN we add all ingredients in a volume basis @@ -506,7 +544,7 @@ def test_add_all_ingredients_in_output_via_team(session, team): def1 = empty.add_all_ingredients_in_output( process_templates=[process1_link, process2_link], team=team, - quantity_dimension=IngredientQuantityDimension.VOLUME + quantity_dimension=IngredientQuantityDimension.VOLUME, ) def1.config_uid = uuid4() @@ -514,55 +552,59 @@ def test_add_all_ingredients_in_output_via_team(session, team): assert len(def1.variables) == len(union_allowed_names) * 3 assert len(def1.columns) == len(def1.variables) for name in union_allowed_names: - assert next((var for var in def1.variables if name in var.headers - and isinstance(var, IngredientQuantityInOutput)), None) is not None - assert next((var for var in def1.variables if name in var.headers - and isinstance(var, IngredientIdentifierInOutput)), None) is not None - assert next((var for var in def1.variables if name in var.headers - and isinstance(var, IngredientLabelsSetInOutput)), None) is not None + assert any( + var + for var in def1.variables + if name in var.headers and isinstance(var, IngredientQuantityInOutput) + ) + assert any( + var + for var in def1.variables + if name in var.headers and isinstance(var, IngredientIdentifierInOutput) + ) + assert any( + var + for var in def1.variables + if name in var.headers and isinstance(var, IngredientLabelsSetInOutput) + ) session.set_responses( ProcessTemplate( - process1_name, - uids={'id': process1_id}, - allowed_names=allowed_names1 + process1_name, uids={"id": process1_id}, allowed_names=allowed_names1 ).dump(), ProcessTemplate( - process2_name, - uids={'id': process2_id}, - allowed_names=allowed_names2 - ).dump() + process2_name, uids={"id": process2_id}, allowed_names=allowed_names2 + ).dump(), ) # WHEN we add all ingredients to the same Table Config as absolute quantities def2 = def1.add_all_ingredients_in_output( process_templates=[process1_link, process2_link], team=team, quantity_dimension=IngredientQuantityDimension.ABSOLUTE, - unit='kg' + unit="kg", ) # THEN there should be 1 new variable for each name, corresponding to the quantity # There is already a variable for id and labels # There should be 2 new columns for each name, one for the quantity and one for the units - new_variables = def2.variables[len(def1.variables):] - new_columns = def2.columns[len(def1.columns):] + new_variables = def2.variables[len(def1.variables) :] + new_columns = def2.columns[len(def1.columns) :] assert len(new_variables) == len(union_allowed_names) assert len(new_columns) == len(union_allowed_names) * 2 assert def2.config_uid == def1.config_uid for name in union_allowed_names: - assert next((var for var in new_variables if name in var.headers - and isinstance(var, IngredientQuantityInOutput)), None) is not None + assert any( + var + for var in new_variables + if name in var.headers and isinstance(var, IngredientQuantityInOutput) + ) session.set_responses( ProcessTemplate( - process1_name, - uids={'id': process1_id}, - allowed_names=allowed_names1 + process1_name, uids={"id": process1_id}, allowed_names=allowed_names1 ).dump(), ProcessTemplate( - process2_name, - uids={'id': process2_id}, - allowed_names=allowed_names2 - ).dump() + process2_name, uids={"id": process2_id}, allowed_names=allowed_names2 + ).dump(), ) # WHEN we add all ingredients to the same Table Config in a volume basis # THEN it raises an exception because these variables and columns already exist @@ -570,42 +612,40 @@ def test_add_all_ingredients_in_output_via_team(session, team): def2.add_all_ingredients_in_output( process_templates=[process1_link, process2_link], team=team, - quantity_dimension=IngredientQuantityDimension.VOLUME + quantity_dimension=IngredientQuantityDimension.VOLUME, ) # If the process template has an empty allowed_names list then an error should be raised session.set_responses( - ProcessTemplate( - process1_name, - uids={'id': process1_id}, - ).dump(), - ProcessTemplate( - process2_name, - uids={'id': process2_id}, - ).dump() + ProcessTemplate(process1_name, uids={"id": process1_id}).dump(), + ProcessTemplate(process2_name, uids={"id": process2_id}).dump(), ) with pytest.raises(RuntimeError): empty_defn().add_all_ingredients_in_output( process_templates=[process1_link, process2_link], team=team, - quantity_dimension=IngredientQuantityDimension.VOLUME + quantity_dimension=IngredientQuantityDimension.VOLUME, ) def test_add_all_ingredients_in_output_no_principal(session): """Test the behavior of AraDefinition.add_all_ingredients.""" - process_link1 = LinkByUID('id', '3a308f78-e341-f39c-8076-35a2c88292ad') - process_link2 = LinkByUID('id', '519ab440-fbda-4768-ad63-5e09b420285c') + process_link1 = LinkByUID("id", "3a308f78-e341-f39c-8076-35a2c88292ad") + process_link2 = LinkByUID("id", "519ab440-fbda-4768-ad63-5e09b420285c") with pytest.raises(TypeError): - empty_defn().add_all_ingredients_in_output(process_templates=[process_link1, process_link2], - quantity_dimension=IngredientQuantityDimension.VOLUME) + empty_defn().add_all_ingredients_in_output( + process_templates=[process_link1, process_link2], + quantity_dimension=IngredientQuantityDimension.VOLUME, + ) def test_register_new(collection, session): """Test the behavior of AraDefinitionCollection.register() on an unregistered AraDefinition""" # Given - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], columns=[]) + table_config = TableConfig( + name="name", description="description", datasets=[], rows=[], variables=[], columns=[] + ) table_config_response = TableConfigResponseDataFactory() defn_uid = table_config_response["definition"]["id"] @@ -628,7 +668,9 @@ def test_register_new(collection, session): def test_register_existing(collection, session): """Test the behavior of AraDefinitionCollection.register() on a registered AraDefinition""" # Given - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], columns=[]) + table_config = TableConfig( + name="name", description="description", datasets=[], rows=[], variables=[], columns=[] + ) table_config.config_uid = uuid4() table_config_response = TableConfigResponseDataFactory() @@ -644,14 +686,17 @@ def test_register_existing(collection, session): assert session.num_calls == 1 # Ensure we PUT if we were called with a table config id + url = f"projects/{collection.project_id}/ara-definitions/{table_config.config_uid}" assert session.last_call.method == "PUT" - assert session.last_call.path == f"projects/{collection.project_id}/ara-definitions/{table_config.config_uid}" + assert session.last_call.path == url def test_update(collection, session): """Test the behavior of AraDefinitionCollection.update() on a registered AraDefinition""" # Given - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], columns=[]) + table_config = TableConfig( + name="name", description="description", datasets=[], rows=[], variables=[], columns=[] + ) table_config.config_uid = uuid4() table_config_response = TableConfigResponseDataFactory() @@ -668,8 +713,9 @@ def test_update(collection, session): assert session.num_calls == 1 # Ensure we POST if we weren't created with a table config id + url = f"projects/{collection.project_id}/ara-definitions/{table_config.config_uid}" assert session.last_call.method == "PUT" - assert session.last_call.path == f"projects/{collection.project_id}/ara-definitions/{table_config.config_uid}" + assert session.last_call.path == url def test_update_unregistered_fail(collection, session): @@ -677,7 +723,9 @@ def test_update_unregistered_fail(collection, session): # Given - table_config = TableConfig(name="name", description="description", datasets=[], rows=[], variables=[], columns=[]) + table_config = TableConfig( + name="name", description="description", datasets=[], rows=[], variables=[], columns=[] + ) # When with pytest.raises(ValueError, match="Cannot update Table Config without a config_uid."): diff --git a/tests/resources/test_team.py b/tests/resources/test_team.py index a0aebec7d..2f43f1f4f 100644 --- a/tests/resources/test_team.py +++ b/tests/resources/test_team.py @@ -1,6 +1,5 @@ import json import uuid -from uuid import UUID import pytest from dateutil.parser import parse @@ -8,12 +7,12 @@ from citrine._rest.resource import ResourceTypeEnum from citrine.resources.api_error import ApiError -from citrine.resources.dataset import Dataset, DatasetCollection +from citrine.resources.dataset import Dataset from citrine.resources.process_spec import ProcessSpec -from citrine.resources.team import Team, TeamCollection, SHARE, READ, WRITE, TeamMember +from citrine.resources.team import READ, SHARE, WRITE, Team, TeamCollection, TeamMember from citrine.resources.user import User -from tests.utils.factories import UserDataFactory, TeamDataFactory, DatasetDataFactory -from tests.utils.session import FakeSession, FakeCall, FakePaginatedSession +from tests.utils.factories import TeamDataFactory, UserDataFactory +from tests.utils.session import FakeCall, FakePaginatedSession, FakeSession @pytest.fixture @@ -30,20 +29,14 @@ def paginated_session() -> FakePaginatedSession: @pytest.fixture def team(session) -> Team: - team = Team( - name='Test Team', - session=session - ) - team.uid = uuid.UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + team = Team(name="Test Team", session=session) + team.uid = uuid.UUID("16fd2706-8baf-433b-82eb-8c7fada847da") return team @pytest.fixture def other_team(session) -> Team: - team = Team( - name='Test Team', - session=session - ) + team = Team(name="Test Team", session=session) team.uid = uuid.uuid4() return team @@ -55,12 +48,11 @@ def collection(session) -> TeamCollection: def test_team_member_string_representation(team): user = User.build(UserDataFactory()) - team_member = TeamMember( - user=user, - team=team, - actions=[READ] - ) - assert team_member.__str__() == ''.format(user.screen_name, team_member.actions, team.name) + team_member = TeamMember(user=user, team=team, actions=[READ]) + cast = str(team_member) + assert user.screen_name in cast + assert all(a in cast for a in team_member.actions) + assert team.name in cast def test_string_representation(team): @@ -74,78 +66,69 @@ def test_team_project_session(team): def test_team_registration(collection: TeamCollection, session): # Given - create_time = parse('2019-09-10T00:00:00+00:00') + create_time = parse("2019-09-10T00:00:00+00:00") team_data = TeamDataFactory( - name='testing', - description='A sample team', - created_at=int(create_time.timestamp() * 1000) # The lib expects ms since epoch, which is really odd + name="testing", + description="A sample team", + # The lib expects ms since epoch, which is really odd + created_at=int(create_time.timestamp() * 1000), ) user = UserDataFactory() session.set_responses( - {'team': team_data}, - user, - {'id': user['id'], 'actions': ['READ', 'WRITE', 'SHARE']} + {"team": team_data}, user, {"id": user["id"], "actions": ["READ", "WRITE", "SHARE"]} ) # When - created_team = collection.register('testing') + created_team = collection.register("testing") # Then assert 3 == session.num_calls expected_call_1 = FakeCall( - method='POST', - path='/teams', - json={ - 'name': 'testing', - 'description': '', - 'id': None, - 'created_at': None, - } + method="POST", + path="/teams", + json={"name": "testing", "description": "", "id": None, "created_at": None}, ) - expected_call_2 = FakeCall( - method="GET", - path='/users/me' + expected_call_2 = FakeCall(method="GET", path="/users/me") + expected_call_3 = FakeCall( + method="PUT", + path=f"/teams/{created_team.uid}/users", + json={"id": user["id"], "actions": [READ, WRITE, SHARE]}, ) - expected_call_3 = FakeCall(method="PUT", path="/teams/{}/users".format(created_team.uid), - json={'id': user["id"], 'actions': [READ, WRITE, SHARE]}) assert expected_call_1 == session.calls[0] assert expected_call_2 == session.calls[1] assert expected_call_3 == session.calls[2] - assert 'A sample team' == created_team.description + assert "A sample team" == created_team.description assert create_time == created_team.created_at def test_get_team(collection: TeamCollection, session): # Given - team_data = TeamDataFactory(name='single team') - session.set_response({'team': team_data}) + team_data = TeamDataFactory(name="single team") + session.set_response({"team": team_data}) # When - created_team = collection.get(team_data['id']) + created_team = collection.get(team_data["id"]) # Then assert 1 == session.num_calls - expected_call = FakeCall( - method='GET', - path='/teams/{}'.format(team_data['id']), - ) + expected_call = FakeCall(method="GET", path="/teams/{}".format(team_data["id"])) assert expected_call == session.last_call - assert 'single team' == created_team.name + assert "single team" == created_team.name def test_list_teams(collection, session): # Given teams_data = TeamDataFactory.create_batch(5) - session.set_response({'teams': teams_data}) + session.set_response({"teams": teams_data}) # When teams = list(collection.list()) # Then assert 1 == session.num_calls - expected_call = FakeCall(method='GET', path='/teams', params={'per_page': 100, 'page': 1}) + expected_call = FakeCall(method="GET", path="/teams", params={"per_page": 100, "page": 1}) assert expected_call == session.last_call assert 5 == len(teams) @@ -161,9 +144,7 @@ def test_list_teams_as_admin(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method="GET", - path="/teams", - params={"per_page": 100, "page": 1, "as_admin": "true"}, + method="GET", path="/teams", params={"per_page": 100, "page": 1, "as_admin": "true"} ) assert expected_call == session.last_call assert 5 == len(teams) @@ -171,7 +152,7 @@ def test_list_teams_as_admin(collection, session): def test_update_team(collection: TeamCollection, team, session): team.name = "updated name" - session.set_response({'team': team.dump()}) + session.set_response({"team": team.dump()}) result = collection.update(team) assert result.name == team.name @@ -181,14 +162,14 @@ def test_list_members(team, session): user = UserDataFactory() user["actions"] = READ user.pop("position") - session.set_response({'users': [user]}) + session.set_response({"users": [user]}) # When members = team.list_members() # Then assert 1 == session.num_calls - expect_call = FakeCall(method='GET', path='/teams/{}/users'.format(team.uid)) + expect_call = FakeCall(method="GET", path=f"/teams/{team.uid}/users") assert expect_call == session.last_call assert isinstance(members[0], TeamMember) @@ -199,14 +180,14 @@ def test_me(team, session): member = user.copy() member["actions"] = [READ] member.pop("position") - session.set_responses({**user}, {'user': member}) + session.set_responses({**user}, {"user": member}) # When member = team.me() # Then assert 2 == session.num_calls - member_call = FakeCall(method='GET', path='/teams/{}/users/{}'.format(team.uid, user["id"])) + member_call = FakeCall(method="GET", path="/teams/{}/users/{}".format(team.uid, user["id"])) assert member_call == session.last_call assert isinstance(member, TeamMember) @@ -214,15 +195,20 @@ def test_me(team, session): def test_update_user_actions(team, session): # Given user = UserDataFactory() - session.set_response({'id': user['id'], 'actions': ['READ']}) + session.set_response({"id": user["id"], "actions": ["READ"]}) # When - update_user_role_response = team.update_user_action(user_id=User.build(user), actions=[WRITE, SHARE]) + update_user_role_response = team.update_user_action( + user_id=User.build(user), actions=[WRITE, SHARE] + ) # Then assert 1 == session.num_calls - expect_call = FakeCall(method="PUT", path="/teams/{}/users".format(team.uid), - json={'id': user["id"], 'actions': [WRITE, SHARE]}) + expect_call = FakeCall( + method="PUT", + path=f"/teams/{team.uid}/users", + json={"id": user["id"], "actions": [WRITE, SHARE]}, + ) assert expect_call == session.last_call assert update_user_role_response is True @@ -230,17 +216,16 @@ def test_update_user_actions(team, session): def test_add_user(team, session): # Given user = UserDataFactory() - session.set_response({'id': user["id"], 'actions': ['READ']}) + session.set_response({"id": user["id"], "actions": ["READ"]}) # When add_user_response = team.add_user(User.build(user)) # Then assert 1 == session.num_calls - expect_call = FakeCall(method="PUT", path='/teams/{}/users'.format(team.uid), json={ - "id": user["id"], - "actions": ["READ"] - }) + expect_call = FakeCall( + method="PUT", path=f"/teams/{team.uid}/users", json={"id": user["id"], "actions": ["READ"]} + ) assert expect_call == session.last_call assert add_user_response is True @@ -248,17 +233,18 @@ def test_add_user(team, session): def test_add_user_with_actions(team, session): # Given user = UserDataFactory() - session.set_response({'id': user["id"], 'actions': ['READ', 'WRITE']}) + session.set_response({"id": user["id"], "actions": ["READ", "WRITE"]}) # When - add_user_response = team.add_user(user["id"], actions=['READ', 'WRITE']) + add_user_response = team.add_user(user["id"], actions=["READ", "WRITE"]) # Then assert 1 == session.num_calls - expect_call = FakeCall(method="PUT", path='/teams/{}/users'.format(team.uid), json={ - "id": user["id"], - "actions": ["READ", "WRITE"] - }) + expect_call = FakeCall( + method="PUT", + path=f"/teams/{team.uid}/users", + json={"id": user["id"], "actions": ["READ", "WRITE"]}, + ) assert expect_call == session.last_call assert add_user_response is True @@ -266,7 +252,7 @@ def test_add_user_with_actions(team, session): def test_remove_user(team, session): # Given user = UserDataFactory() - session.set_response({'ids': [user["id"]]}) + session.set_response({"ids": [user["id"]]}) # When remove_user_response = team.remove_user(User.build(user)) @@ -274,9 +260,7 @@ def test_remove_user(team, session): # Then assert 1 == session.num_calls expect_call = FakeCall( - method="POST", - path="/teams/{}/users/batch-remove".format(team.uid), - json={"ids": [user["id"]]} + method="POST", path=f"/teams/{team.uid}/users/batch-remove", json={"ids": [user["id"]]} ) assert expect_call == session.last_call assert remove_user_response is True @@ -294,12 +278,12 @@ def test_share(team, other_team, session): assert 1 == session.num_calls expect_call = FakeCall( method="POST", - path="/teams/{}/shared-resources".format(team.uid), + path=f"/teams/{team.uid}/shared-resources", json={ "resource_type": "DATASET", "resource_id": str(dataset.uid), - "target_team_id": str(other_team.uid) - } + "target_team_id": str(other_team.uid), + }, ) assert expect_call == session.last_call assert share_response is True @@ -318,26 +302,26 @@ def test_un_share(team, other_team, session): expect_call = FakeCall( method="DELETE", path="/teams/{}/shared-resources/{}/{}".format(team.uid, "DATASET", str(dataset.uid)), - json={ - "target_team_id": str(other_team.uid) - } + json={"target_team_id": str(other_team.uid)}, ) assert expect_call == session.last_call assert share_response is True -@pytest.mark.parametrize("resource_type,method", +@pytest.mark.parametrize( + "resource_type,method", [ (ResourceTypeEnum.DATASET, "dataset_ids"), (ResourceTypeEnum.MODULE, "module_ids"), (ResourceTypeEnum.TABLE, "table_ids"), - (ResourceTypeEnum.TABLE_DEFINITION, "table_definition_ids") - ]) + (ResourceTypeEnum.TABLE_DEFINITION, "table_definition_ids"), + ], +) def test_list_resource_ids(team, session, resource_type, method): # Given - read_response = {'ids': [uuid.uuid4(), uuid.uuid4()]} - write_response = {'ids': [uuid.uuid4(), uuid.uuid4()]} - share_response = {'ids': [uuid.uuid4(), uuid.uuid4()]} + read_response = {"ids": [uuid.uuid4(), uuid.uuid4()]} + write_response = {"ids": [uuid.uuid4(), uuid.uuid4()]} + share_response = {"ids": [uuid.uuid4(), uuid.uuid4()]} # When # This is equivalent to team.dataset_ids, team.module_ids, etc. @@ -354,47 +338,61 @@ def test_list_resource_ids(team, session, resource_type, method): # Then assert session.num_calls == 3 - assert session.calls[0] == FakeCall(method='GET', - path=f'/{resource_type.value}/authorized-ids', - params={"domain": f"/teams/{team.uid}", "action": READ}) - assert session.calls[1] == FakeCall(method='GET', - path=f'/{resource_type.value}/authorized-ids', - params={"domain": f"/teams/{team.uid}", "action": WRITE}) - assert session.calls[2] == FakeCall(method='GET', - path=f'/{resource_type.value}/authorized-ids', - params={"domain": f"/teams/{team.uid}", "action": SHARE}) - assert readable_ids == read_response['ids'] - assert writeable_ids == write_response['ids'] - assert shareable_ids == share_response['ids'] + assert session.calls[0] == FakeCall( + method="GET", + path=f"/{resource_type.value}/authorized-ids", + params={"domain": f"/teams/{team.uid}", "action": READ}, + ) + assert session.calls[1] == FakeCall( + method="GET", + path=f"/{resource_type.value}/authorized-ids", + params={"domain": f"/teams/{team.uid}", "action": WRITE}, + ) + assert session.calls[2] == FakeCall( + method="GET", + path=f"/{resource_type.value}/authorized-ids", + params={"domain": f"/teams/{team.uid}", "action": SHARE}, + ) + assert readable_ids == read_response["ids"] + assert writeable_ids == write_response["ids"] + assert shareable_ids == share_response["ids"] def test_analyses_get_team_id(team): assert team.uid == team.analyses.team_id + def test_owned_dataset_ids(team): # Create a set of datasets in the project ids = {uuid.uuid4() for _ in range(5)} for d_id in ids: - dataset = Dataset(name=f"Test Dataset - {d_id}", summary="Test Dataset", description="Test Dataset") + dataset = Dataset( + name=f"Test Dataset - {d_id}", summary="Test Dataset", description="Test Dataset" + ) team.datasets.register(dataset) # Set the session response to have the list of dataset IDs - team.session.set_response({'ids': list(ids)}) + team.session.set_response({"ids": list(ids)}) # Fetch the list of UUID owned by the current project owned_ids = team.owned_dataset_ids() # Let's mock our expected API call so we can compare and ensure that the one made is the same - expect_call = FakeCall(method='GET', - path='/DATASET/authorized-ids', - params={'userId': '', - 'domain': '/teams/16fd2706-8baf-433b-82eb-8c7fada847da', - 'action': 'WRITE'}) + expect_call = FakeCall( + method="GET", + path="/DATASET/authorized-ids", + params={ + "userId": "", + "domain": "/teams/16fd2706-8baf-433b-82eb-8c7fada847da", + "action": "WRITE", + }, + ) # Compare our calls assert expect_call == team.session.last_call assert team.session.num_calls == len(ids) + 1 assert ids == set(owned_ids) + def test_datasets_get_team_id(team): assert team.uid == team.datasets.team_id @@ -460,34 +458,34 @@ def test_gemd_resource_get_team_id(team): def test_team_batch_delete_no_errors(team, session): - job_resp = { - 'job_id': '1234' - } + job_resp = {"job_id": "1234"} # Actual response-like data - note there is no 'failures' array within 'output' successful_job_resp = { - 'job_type': 'batch_delete', - 'status': 'Success', - 'tasks': [ + "job_type": "batch_delete", + "status": "Success", + "tasks": [ { - "id": "7b6bafd9-f32a-4567-b54c-7ce594edc018", "task_type": "batch_delete", - "status": "Success", "dependencies": [] - } - ], - 'output': {} + "id": "7b6bafd9-f32a-4567-b54c-7ce594edc018", + "task_type": "batch_delete", + "status": "Success", + "dependencies": [], + } + ], + "output": {}, } session.set_responses(job_resp, successful_job_resp) # When - del_resp = team.gemd_batch_delete([uuid.UUID('16fd2706-8baf-433b-82eb-8c7fada847da')]) + del_resp = team.gemd_batch_delete([uuid.UUID("16fd2706-8baf-433b-82eb-8c7fada847da")]) # Then assert len(del_resp) == 0 # When trying with entities session.set_responses(job_resp, successful_job_resp) - entity = ProcessSpec(name="proc spec", uids={'id': '16fd2706-8baf-433b-82eb-8c7fada847da'}) + entity = ProcessSpec(name="proc spec", uids={"id": "16fd2706-8baf-433b-82eb-8c7fada847da"}) del_resp = team.gemd_batch_delete([entity]) # Then @@ -495,42 +493,34 @@ def test_team_batch_delete_no_errors(team, session): def test_team_batch_delete(team, session): - job_resp = { - 'job_id': '1234' - } + job_resp = {"job_id": "1234"} - failures_escaped_json = json.dumps([ - { - "id": { - 'scope': 'somescope', - 'id': 'abcd-1234' - }, - 'cause': { - "code": 400, - "message": "", - "validation_errors": [ - { - "failure_message": "fail msg", - "failure_id": "identifier.coreid.missing" - } - ] + failures_escaped_json = json.dumps( + [ + { + "id": {"scope": "somescope", "id": "abcd-1234"}, + "cause": { + "code": 400, + "message": "", + "validation_errors": [ + {"failure_message": "fail msg", "failure_id": "identifier.coreid.missing"} + ], + }, } - } - ]) + ] + ) failed_job_resp = { - 'job_type': 'batch_delete', - 'status': 'Success', - 'tasks': [], - 'output': { - 'failures': failures_escaped_json - } + "job_type": "batch_delete", + "status": "Success", + "tasks": [], + "output": {"failures": failures_escaped_json}, } session.set_responses(job_resp, failed_job_resp, job_resp, failed_job_resp) # When - del_resp = team.gemd_batch_delete([uuid.UUID('16fd2706-8baf-433b-82eb-8c7fada847da')]) + del_resp = team.gemd_batch_delete([uuid.UUID("16fd2706-8baf-433b-82eb-8c7fada847da")]) # Then assert 2 == session.num_calls @@ -538,21 +528,25 @@ def test_team_batch_delete(team, session): assert len(del_resp) == 1 first_failure = del_resp[0] - expected_api_error = ApiError.build({ - "code": "400", - "message": "", - "validation_errors": [{"failure_message": "fail msg", "failure_id": "identifier.coreid.missing"}] - }) + expected_api_error = ApiError.build( + { + "code": "400", + "message": "", + "validation_errors": [ + {"failure_message": "fail msg", "failure_id": "identifier.coreid.missing"} + ], + } + ) - assert first_failure[0] == LinkByUID('somescope', 'abcd-1234') + assert first_failure[0] == LinkByUID("somescope", "abcd-1234") assert first_failure[1].dump() == expected_api_error.dump() # And again with tuples of (scope, id) - del_resp = team.gemd_batch_delete([LinkByUID('id', '16fd2706-8baf-433b-82eb-8c7fada847da')]) + del_resp = team.gemd_batch_delete([LinkByUID("id", "16fd2706-8baf-433b-82eb-8c7fada847da")]) assert len(del_resp) == 1 first_failure = del_resp[0] - assert first_failure[0] == LinkByUID('somescope', 'abcd-1234') + assert first_failure[0] == LinkByUID("somescope", "abcd-1234") assert first_failure[1].dump() == expected_api_error.dump() diff --git a/tests/resources/test_templates.py b/tests/resources/test_templates.py index 4bf4bec17..e85eeabb2 100644 --- a/tests/resources/test_templates.py +++ b/tests/resources/test_templates.py @@ -1,27 +1,28 @@ """Test that templates show expected behavior.""" -import pytest + from uuid import uuid4 +import pytest +from gemd.entity.attribute.condition import Condition +from gemd.entity.bounds.categorical_bounds import CategoricalBounds +from gemd.entity.bounds.integer_bounds import IntegerBounds +from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.value.nominal_real import NominalReal + +from citrine.exceptions import BadRequest +from citrine.resources.condition_template import ConditionTemplate from citrine.resources.material_template import MaterialTemplate from citrine.resources.measurement_template import MeasurementTemplate -from citrine.resources.process_template import ProcessTemplate +from citrine.resources.parameter_template import ParameterTemplate from citrine.resources.process_spec import ProcessSpec +from citrine.resources.process_template import ProcessTemplate from citrine.resources.property_template import PropertyTemplate, PropertyTemplateCollection -from citrine.resources.condition_template import ConditionTemplate -from citrine.resources.parameter_template import ParameterTemplate -from citrine.exceptions import BadRequest -from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.bounds.integer_bounds import IntegerBounds -from gemd.entity.bounds.categorical_bounds import CategoricalBounds -from gemd.entity.value.nominal_real import NominalReal -from gemd.entity.attribute.condition import Condition - -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeSession def test_object_template_validation(): """Test that attribute templates are validated against given bounds.""" - length_template = PropertyTemplate("Length", bounds=RealBounds(2.0, 3.5, 'cm')) + length_template = PropertyTemplate("Length", bounds=RealBounds(2.0, 3.5, "cm")) dial_template = ConditionTemplate("dial", bounds=IntegerBounds(0, 5)) color_template = ParameterTemplate("Color", bounds=CategoricalBounds(["red", "green", "blue"])) @@ -29,11 +30,11 @@ def test_object_template_validation(): MaterialTemplate() with pytest.raises(ValueError): - MaterialTemplate("Block", properties=[[length_template, RealBounds(3.0, 4.0, 'cm')]]) + MaterialTemplate("Block", properties=[[length_template, RealBounds(3.0, 4.0, "cm")]]) with pytest.raises(ValueError): ProcessTemplate("a process", conditions=[[color_template, CategoricalBounds(["zz"])]]) - + with pytest.raises(ValueError): MeasurementTemplate("A measurement", parameters=[[dial_template, IntegerBounds(-3, -1)]]) @@ -42,8 +43,11 @@ def test_template_assignment(): """Test that an object and its attributes can both be assigned templates.""" humidity_template = ConditionTemplate("Humidity", bounds=RealBounds(0.5, 0.75, "")) template = ProcessTemplate("Dry", conditions=[[humidity_template, RealBounds(0.5, 0.65, "")]]) - ProcessSpec("Dry a polymer", template=template, conditions=[ - Condition("Humidity", value=NominalReal(0.6, ""), template=humidity_template)]) + ProcessSpec( + "Dry a polymer", + template=template, + conditions=[Condition("Humidity", value=NominalReal(0.6, ""), template=humidity_template)], + ) def test_automatic_async_update(): @@ -51,40 +55,42 @@ def test_automatic_async_update(): session = FakeSession() collection = PropertyTemplateCollection(team_id=uuid4(), dataset_id=uuid4(), session=session) this_id = str(uuid4()) - template = PropertyTemplate("dummy template", bounds=RealBounds(0.0, 0.5, ''), uids={'id': this_id}) + template = PropertyTemplate( + "dummy template", bounds=RealBounds(0.0, 0.5, ""), uids={"id": this_id} + ) session.set_responses( - BadRequest(""), # Attempted POST throws BadRequest because, for example, the template bounds are being narrowed. + # Attempted POST throws BadRequest because, e.g., the template bounds are being narrowed. + BadRequest(""), {"job_id": str(uuid4())}, # Call async route, returning a job_id. {"job_type": "", "status": "Success", "tasks": []}, # Check job status, it succeeded. - template.dump() # Get the resource. + template.dump(), # Get the resource. ) new_template = collection.update(template) assert new_template == template # Check that resource is returned. # First call should be an attempt to POST the resource + post_url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/property-templates" assert session.calls[0].method == "POST" - assert session.calls[0].path == f"teams/{collection.team_id}/datasets/{collection.dataset_id}/property-templates" + + assert session.calls[0].path == post_url # Second call should be a PUT to the async route + put_url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/property-templates/id/{this_id}/async" assert session.calls[1].method == "PUT" - assert session.calls[1].path == f"teams/{collection.team_id}/datasets/{collection.dataset_id}/property-templates/id/{this_id}/async" + assert session.calls[1].path == put_url # Last call should get the resource + get_url = f"teams/{collection.team_id}/datasets/{collection.dataset_id}/property-templates/id/{this_id}" assert session.last_call.method == "GET" - assert session.last_call.path == f"teams/{collection.team_id}/datasets/{collection.dataset_id}/property-templates/id/{this_id}" + assert session.last_call.path == get_url def test_process_template_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.process_template import ProcessTemplate as CitrineProcessTemplate from gemd.entity.template import ProcessTemplate as GEMDProcessTemplate - gemd_obj = GEMDProcessTemplate( - name="My Name", - tags=["tag!"] - ) - citrine_obj = CitrineProcessTemplate( - name="My Name", - tags=["tag!"] - ) + from citrine.resources.process_template import ProcessTemplate as CitrineProcessTemplate + + gemd_obj = GEMDProcessTemplate(name="My Name", tags=["tag!"]) + citrine_obj = CitrineProcessTemplate(name="My Name", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.name = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" @@ -92,17 +98,12 @@ def test_process_template_equals(): def test_material_template_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.material_template import MaterialTemplate as CitrineMaterialTemplate from gemd.entity.template import MaterialTemplate as GEMDMaterialTemplate - gemd_obj = GEMDMaterialTemplate( - name="My Name", - tags=["tag!"] - ) - citrine_obj = CitrineMaterialTemplate( - name="My Name", - tags=["tag!"] - ) + from citrine.resources.material_template import MaterialTemplate as CitrineMaterialTemplate + + gemd_obj = GEMDMaterialTemplate(name="My Name", tags=["tag!"]) + citrine_obj = CitrineMaterialTemplate(name="My Name", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.name = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" @@ -110,17 +111,14 @@ def test_material_template_equals(): def test_measurement_template_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.measurement_template import MeasurementTemplate as CitrineMeasurementTemplate from gemd.entity.template import MeasurementTemplate as GEMDMeasurementTemplate - gemd_obj = GEMDMeasurementTemplate( - name="My Name", - tags=["tag!"] - ) - citrine_obj = CitrineMeasurementTemplate( - name="My Name", - tags=["tag!"] + from citrine.resources.measurement_template import ( + MeasurementTemplate as CitrineMeasurementTemplate, ) + + gemd_obj = GEMDMeasurementTemplate(name="My Name", tags=["tag!"]) + citrine_obj = CitrineMeasurementTemplate(name="My Name", tags=["tag!"]) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.name = "Something else" assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference" @@ -128,18 +126,15 @@ def test_measurement_template_equals(): def test_condition_template_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.condition_template import ConditionTemplate as CitrineConditionTemplate from gemd.entity.template import ConditionTemplate as GEMDConditionTemplate + from citrine.resources.condition_template import ConditionTemplate as CitrineConditionTemplate + gemd_obj = GEMDConditionTemplate( - name="My Name", - bounds=CategoricalBounds(categories=["1"]), - tags=["tag!"] + name="My Name", bounds=CategoricalBounds(categories=["1"]), tags=["tag!"] ) citrine_obj = CitrineConditionTemplate( - name="My Name", - bounds=CategoricalBounds(categories=["1"]), - tags=["tag!"] + name="My Name", bounds=CategoricalBounds(categories=["1"]), tags=["tag!"] ) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.name = "Something else" @@ -148,18 +143,15 @@ def test_condition_template_equals(): def test_parameter_template_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.parameter_template import ParameterTemplate as CitrineParameterTemplate from gemd.entity.template import ParameterTemplate as GEMDParameterTemplate + from citrine.resources.parameter_template import ParameterTemplate as CitrineParameterTemplate + gemd_obj = GEMDParameterTemplate( - name="My Name", - bounds=CategoricalBounds(categories=["1"]), - tags=["tag!"] + name="My Name", bounds=CategoricalBounds(categories=["1"]), tags=["tag!"] ) citrine_obj = CitrineParameterTemplate( - name="My Name", - bounds=CategoricalBounds(categories=["1"]), - tags=["tag!"] + name="My Name", bounds=CategoricalBounds(categories=["1"]), tags=["tag!"] ) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.name = "Something else" @@ -168,18 +160,15 @@ def test_parameter_template_equals(): def test_property_template_equals(): """Test basic equality. Complex relationships are tested in test_material_run.test_deep_equals().""" - from citrine.resources.property_template import PropertyTemplate as CitrinePropertyTemplate from gemd.entity.template import PropertyTemplate as GEMDPropertyTemplate + from citrine.resources.property_template import PropertyTemplate as CitrinePropertyTemplate + gemd_obj = GEMDPropertyTemplate( - name="My Name", - bounds=CategoricalBounds(categories=["1"]), - tags=["tag!"] + name="My Name", bounds=CategoricalBounds(categories=["1"]), tags=["tag!"] ) citrine_obj = CitrinePropertyTemplate( - name="My Name", - bounds=CategoricalBounds(categories=["1"]), - tags=["tag!"] + name="My Name", bounds=CategoricalBounds(categories=["1"]), tags=["tag!"] ) assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence" citrine_obj.name = "Something else" diff --git a/tests/resources/test_user.py b/tests/resources/test_user.py index 9ca16cc22..8f01e27ac 100644 --- a/tests/resources/test_user.py +++ b/tests/resources/test_user.py @@ -4,7 +4,7 @@ from citrine.resources.user import User, UserCollection from tests.utils.factories import UserDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession @pytest.fixture @@ -14,13 +14,8 @@ def session() -> FakeSession: @pytest.fixture def user() -> User: - user = User( - screen_name='Test User', - email="test@user.io", - position="QA", - is_admin=False - ) - user.uid = UUID('16fd2706-8baf-433b-82eb-8c7fada847da') + user = User(screen_name="Test User", email="test@user.io", position="QA", is_admin=False) + user.uid = UUID("16fd2706-8baf-433b-82eb-8c7fada847da") return user @@ -30,12 +25,7 @@ def collection(session) -> UserCollection: def test_user_str_representation(): - user = User( - screen_name='joe', - email='joe@somewhere.com', - position='President', - is_admin=False - ) + user = User(screen_name="joe", email="joe@somewhere.com", position="President", is_admin=False) assert "" == str(user) @@ -49,50 +39,46 @@ def test_user_registration(collection, session): # given user = UserDataFactory() - session.set_response({'user': user}) + session.set_response({"user": user}) # When created_user = collection.register( screen_name=user["screen_name"], email=user["email"], position=user["position"], - is_admin=user["is_admin"] + is_admin=user["is_admin"], ) # Then assert 1 == session.num_calls expected_call = FakeCall( - method='POST', - path='/users', + method="POST", + path="/users", json={ - 'screen_name': user["screen_name"], - 'position': user["position"], - 'email': user["email"], - 'is_admin': user["is_admin"], - } + "screen_name": user["screen_name"], + "position": user["position"], + "email": user["email"], + "is_admin": user["is_admin"], + }, ) - assert expected_call.json['screen_name'] == created_user.screen_name - assert expected_call.json['email'] == created_user.email - assert expected_call.json['position'] == created_user.position - assert expected_call.json['is_admin'] == created_user.is_admin + assert expected_call.json["screen_name"] == created_user.screen_name + assert expected_call.json["email"] == created_user.email + assert expected_call.json["position"] == created_user.position + assert expected_call.json["is_admin"] == created_user.is_admin def test_list_users(collection, session): # Given user_data = UserDataFactory.create_batch(5) - session.set_response({'users': user_data}) + session.set_response({"users": user_data}) # When users = list(collection.list()) # Then assert 1 == session.num_calls - expected_call = FakeCall( - method='GET', - path='/users', - params={'per_page': 100, 'page': 1} - ) + expected_call = FakeCall(method="GET", path="/users", params={"per_page": 100, "page": 1}) assert expected_call == session.last_call assert len(users) == 5 @@ -101,7 +87,7 @@ def test_list_users(collection, session): def test_list_users_as_admin(collection, session): # Given user_data = UserDataFactory.create_batch(5) - session.set_response({'users': user_data}) + session.set_response({"users": user_data}) # When users = list(collection.list(as_admin=True)) @@ -109,9 +95,7 @@ def test_list_users_as_admin(collection, session): # Then assert 1 == session.num_calls expected_call = FakeCall( - method='GET', - path='/users', - params={'per_page': 100, 'page': 1, 'as_admin': 'true'} + method="GET", path="/users", params={"per_page": 100, "page": 1, "as_admin": "true"} ) assert expected_call == session.last_call @@ -120,7 +104,7 @@ def test_list_users_as_admin(collection, session): def test_get_users(collection, session): # Given - uid = '151199ec-e9aa-49a1-ac8e-da722aaf74c4' + uid = "151199ec-e9aa-49a1-ac8e-da722aaf74c4" # When with pytest.raises(KeyError): @@ -132,13 +116,10 @@ def test_delete_user(collection, session): user = UserDataFactory() # When - collection.delete(user['id']) + collection.delete(user["id"]) - session.set_response({'message': 'User was deleted'}) - expected_call = FakeCall( - method="DELETE", - path='/users/{}'.format(user["id"]), - ) + session.set_response({"message": "User was deleted"}) + expected_call = FakeCall(method="DELETE", path="/users/{}".format(user["id"])) assert 1 == session.num_calls assert expected_call == session.last_call @@ -150,13 +131,10 @@ def test_get_me(collection, session): session.set_response(user) # When - current_user = collection.me() + _ = collection.me() # Then - expected_call = FakeCall( - method="GET", - path='/users/me' - ) + expected_call = FakeCall(method="GET", path="/users/me") assert 1 == session.num_calls assert expected_call == session.last_call diff --git a/tests/resources/test_workflow.py b/tests/resources/test_workflow.py index dafffd672..008625d17 100644 --- a/tests/resources/test_workflow.py +++ b/tests/resources/test_workflow.py @@ -5,37 +5,36 @@ from citrine.informatics.workflows.design_workflow import DesignWorkflow from citrine.resources.design_workflow import DesignWorkflowCollection - from tests.utils.factories import BranchDataFactory -from tests.utils.session import FakeSession, FakeCall +from tests.utils.session import FakeCall, FakeSession -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def basic_design_workflow_data(): return { - 'id': str(uuid.uuid4()), - 'name': 'Test Workflow', - 'status': 'SUCCEEDED', - 'status_description': 'READY', - 'design_space_id': str(uuid.uuid4()), - 'predictor_id': str(uuid.uuid4()), - 'branch_id': str(uuid.uuid4()), - 'module_type': 'DESIGN_WORKFLOW', - 'create_time': datetime(2020, 1, 1, 1, 1, 1, 1).isoformat("T"), - 'created_by': str(uuid.uuid4()), + "id": str(uuid.uuid4()), + "name": "Test Workflow", + "status": "SUCCEEDED", + "status_description": "READY", + "design_space_id": str(uuid.uuid4()), + "predictor_id": str(uuid.uuid4()), + "branch_id": str(uuid.uuid4()), + "module_type": "DESIGN_WORKFLOW", + "create_time": datetime(2020, 1, 1, 1, 1, 1, 1).isoformat("T"), + "created_by": str(uuid.uuid4()), } -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def failed_design_workflow_data(basic_design_workflow_data): return { **basic_design_workflow_data, - 'status': 'FAILED', - 'status_description': 'ERROR', - 'status_detail': [ - {'level': 'WARNING', 'msg': 'Something is wrong'}, - {'level': 'Error', 'msg': 'Very wrong'} - ] + "status": "FAILED", + "status_description": "ERROR", + "status_detail": [ + {"level": "WARNING", "msg": "Something is wrong"}, + {"level": "Error", "msg": "Very wrong"}, + ], } @@ -61,16 +60,19 @@ def test_build_design_workflow(session, basic_design_workflow_data): def test_list_workflows(session, basic_design_workflow_data): - #Given + # Given workflow_collection = DesignWorkflowCollection(project_id=uuid.uuid4(), session=session) - session.set_responses({'response': [basic_design_workflow_data], 'page': 1, 'per_page': 20}) + session.set_responses({"response": [basic_design_workflow_data], "page": 1, "per_page": 20}) # When workflows = list(workflow_collection.list(per_page=20)) # Then - expected_design_call = FakeCall(method='GET', path='/projects/{}/modules'.format(workflow_collection.project_id), - params={'per_page': 20, 'module_type': 'DESIGN_WORKFLOW'}) + expected_design_call = FakeCall( + method="GET", + path=f"/projects/{workflow_collection.project_id}/modules", + params={"per_page": 20, "module_type": "DESIGN_WORKFLOW"}, + ) assert 1 == session.num_calls assert len(workflows) == 1 assert isinstance(workflows[0], DesignWorkflow) diff --git a/tests/rest/test_ingredient_rest.py b/tests/rest/test_ingredient_rest.py index 12acab09c..0df1469a4 100644 --- a/tests/rest/test_ingredient_rest.py +++ b/tests/rest/test_ingredient_rest.py @@ -1,4 +1,5 @@ """Test RESTful actions on ingredient runs""" + import pytest from citrine.resources.ingredient_run import IngredientRun @@ -8,23 +9,34 @@ @pytest.fixture def valid_data(): """Return valid data used for these tests.""" - return {"type": "ingredient_run", - "material": {"type": "link_by_uid", "id": "5c913611-c304-4254-bad2-4797c952a3b3", "scope": "ID"}, - "process": {"type": "link_by_uid", "id": "5c913611-c304-4254-bad2-4797c952a3b4", "scope": "ID"}, - "spec": {"type": "link_by_uid", "id": "5c913611-c304-4254-bad2-4797c952a3b5", "scope": "ID"}, - "name": "Good Ingredient Run", - "labels": [], - "mass_fraction": {'nominal': 0.5, 'units': 'dimensionless', 'type': 'nominal_real'}, - "volume_fraction": None, - "number_fraction": None, - "absolute_quantity": {'nominal': 2, 'units': 'g', 'type': 'nominal_real'}, - "uids": { - "id": "09145273-1ff2-4fbd-ba56-404c0408eb49" - }, - "tags": [], - "notes": "Ingredients!", - "file_links": [] - } + return { + "type": "ingredient_run", + "material": { + "type": "link_by_uid", + "id": "5c913611-c304-4254-bad2-4797c952a3b3", + "scope": "ID", + }, + "process": { + "type": "link_by_uid", + "id": "5c913611-c304-4254-bad2-4797c952a3b4", + "scope": "ID", + }, + "spec": { + "type": "link_by_uid", + "id": "5c913611-c304-4254-bad2-4797c952a3b5", + "scope": "ID", + }, + "name": "Good Ingredient Run", + "labels": [], + "mass_fraction": {"nominal": 0.5, "units": "dimensionless", "type": "nominal_real"}, + "volume_fraction": None, + "number_fraction": None, + "absolute_quantity": {"nominal": 2, "units": "g", "type": "nominal_real"}, + "uids": {"id": "09145273-1ff2-4fbd-ba56-404c0408eb49"}, + "tags": [], + "notes": "Ingredients!", + "file_links": [], + } def test_ingredient_build(valid_data): diff --git a/tests/rest/test_paginator.py b/tests/rest/test_paginator.py index 9edfd7974..3d973c3ca 100644 --- a/tests/rest/test_paginator.py +++ b/tests/rest/test_paginator.py @@ -1,8 +1,7 @@ """Test the Paginator""" -from uuid import uuid4 -from mock import Mock -import pytest +from unittest.mock import Mock +from uuid import uuid4 from citrine._rest.paginator import Paginator @@ -47,7 +46,9 @@ def test_pagination_stops_when_initial_item_repeated(): def test_pagination_deduplicates_repeated_intermediate_values(): - result = Paginator().paginate(mocked_fetcher(a, b, b, b, b, b, b, c, c), lambda x: x, per_page=1) + result = Paginator().paginate( + mocked_fetcher(a, b, b, b, b, b, b, c, c), lambda x: x, per_page=1 + ) assert list(result) == [a, b, c] diff --git a/tests/seeding/test_find_or_create.py b/tests/seeding/test_find_or_create.py index 944563ceb..062af2f70 100644 --- a/tests/seeding/test_find_or_create.py +++ b/tests/seeding/test_find_or_create.py @@ -1,25 +1,30 @@ -from typing import Callable, Optional, Union +from collections.abc import Callable from uuid import UUID, uuid4 import pytest + from citrine._rest.collection import Collection +from citrine.informatics.predictors import AutoMLPredictor, GraphPredictor 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.process_spec import ProcessSpec, ProcessSpecCollection from citrine.resources.project import ProjectCollection from citrine.resources.team import TeamCollection -from citrine.informatics.predictors import AutoMLPredictor, GraphPredictor -from citrine.seeding.find_or_create import (find_collection, get_by_name_or_create, - get_by_name_or_raise_error, - find_or_create_project, find_or_create_dataset, - create_or_update, find_or_create_team) +from citrine.seeding.find_or_create import ( + create_or_update, + find_collection, + find_or_create_dataset, + find_or_create_project, + find_or_create_team, + get_by_name_or_create, + get_by_name_or_raise_error, +) 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_dataset_collection import FakeDatasetCollection from tests.utils.fakes.fake_project_collection import FakeProjectCollection from tests.utils.fakes.fake_team_collection import FakeTeamCollection - from tests.utils.session import FakeSession duplicate_name = "duplicate" @@ -46,18 +51,20 @@ def register(self, model: ProcessSpec, dry_run=False) -> ProcessSpec: self.resources.append(model) return model - def list(self, page: Optional[int] = None, per_page: int = 100): + def list(self, page: int | None = None, per_page: int = 100): if page is None: return self.resources else: - return self.resources[(page - 1)*per_page:page*per_page] + return self.resources[(page - 1) * per_page : page * per_page] - collection = FakeCollection(dataset_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), - session=FakeSession()) - for i in range(0, 5): + collection = FakeCollection( + dataset_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), + session=FakeSession(), + ) + for i in range(5): collection.register(ProcessSpec("resource " + str(i))) - for i in range(0, 2): + for i in range(2): collection.register(ProcessSpec(duplicate_name)) return collection @@ -70,11 +77,11 @@ def session() -> FakeSession: @pytest.fixture def project_collection() -> Callable[[bool], ProjectCollection]: - def _make_project(search_implemented: bool = True, team_id: Optional[Union[UUID, str]] = uuid4()): + def _make_project(search_implemented: bool = True, team_id: UUID | str | None = uuid4()): projects = FakeProjectCollection(search_implemented, team_id) - for i in range(0, 5): + for i in range(5): projects.register("project " + str(i)) - for i in range(0, 2): + for i in range(2): projects.register(duplicate_name) return projects @@ -86,9 +93,9 @@ def team_collection() -> Callable[[bool], TeamCollection]: def _make_team(): teams = FakeTeamCollection(True) - for i in range(0, 5): + for i in range(5): teams.register("team " + str(i)) - for i in range(0, 2): + for i in range(2): teams.register(duplicate_name) return teams @@ -97,34 +104,45 @@ def _make_team(): @pytest.fixture def dataset_collection() -> DatasetCollection: - datasets = FakeDatasetCollection(team_id=UUID('6b608f78-e341-422c-8076-35adc8828545'), session=FakeSession()) - for i in range(0, 5): + datasets = FakeDatasetCollection( + team_id=UUID("6b608f78-e341-422c-8076-35adc8828545"), session=FakeSession() + ) + for i in range(5): num_string = str(i) - datasets.register(Dataset("dataset " + num_string, summary="summ " + num_string, description="desc " + num_string)) - for i in range(0, 2): + datasets.register( + Dataset( + "dataset " + num_string, + summary="summ " + num_string, + description="desc " + num_string, + ) + ) + for i in range(2): datasets.register(Dataset(duplicate_name, summary="dup", description="duplicate")) return datasets + @pytest.fixture def predictor_collection() -> PredictorCollection: - predictors = FakePredictorCollection(UUID('6b608f78-e341-422c-8076-35adc8828545'), FakeSession()) + predictors = FakePredictorCollection( + UUID("6b608f78-e341-422c-8076-35adc8828545"), FakeSession() + ) # Adding a few predictors in the collection to have something to update - for i in range(0, 5): + for i in range(5): pred = GraphPredictor( name=f"resource {i}", description="", - predictors=[AutoMLPredictor(name="", description="", inputs=[], outputs=[])] + predictors=[AutoMLPredictor(name="", description="", inputs=[], outputs=[])], ) predictors.register(pred) # Adding a few predictors with the same name ("resource {0,1}" were made above) # this is used to test behavior if there are duplicates - for i in range(0, 2): + for i in range(2): pred = GraphPredictor( name=f"resource {i}", description="", - predictors=[AutoMLPredictor(name="", description="", inputs=[], outputs=[])] + predictors=[AutoMLPredictor(name="", description="", inputs=[], outputs=[])], ) predictors.register(pred) return predictors @@ -152,7 +170,9 @@ def test_get_by_name_or_create_no_exist(fake_collection): # test when name doesn't exist default_provider = lambda: fake_collection.register(ProcessSpec("New Resource")) old_resource_count = len(list(fake_collection.list())) - result = get_by_name_or_create(collection=fake_collection, name="New Resource", default_provider=default_provider) + result = get_by_name_or_create( + collection=fake_collection, name="New Resource", default_provider=default_provider + ) new_resource_count = len(list(fake_collection.list())) assert result.name == "New Resource" assert new_resource_count == old_resource_count + 1 @@ -163,7 +183,9 @@ def test_get_by_name_or_create_exist(fake_collection): resource_name = "resource 2" default_provider = lambda: fake_collection.register(ProcessSpec("New Resource")) old_resource_count = len(list(fake_collection.list())) - result = get_by_name_or_create(collection=fake_collection, name=resource_name, default_provider=default_provider) + result = get_by_name_or_create( + collection=fake_collection, name=resource_name, default_provider=default_provider + ) new_resource_count = len(list(fake_collection.list())) assert result.name == resource_name assert new_resource_count == old_resource_count @@ -204,7 +226,9 @@ def test_find_or_create_team_exist(team_collection): def test_find_or_create_raise_error_team_no_exist(team_collection): # test when team doesn't exist and raise_error flag is on with pytest.raises(ValueError): - find_or_create_team(team_collection=team_collection(), team_name=absent_name, raise_error=True) + find_or_create_team( + team_collection=team_collection(), team_name=absent_name, raise_error=True + ) def test_find_or_create_project_no_exist(project_collection): @@ -240,20 +264,26 @@ 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.raises(ValueError): - find_or_create_project(project_collection=project_collection(), project_name=duplicate_name) + 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.raises(ValueError): - find_or_create_project(project_collection=project_collection(), project_name=absent_name, raise_error=True) + 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())) - 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 @@ -262,7 +292,9 @@ 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.raises(ValueError): - find_or_create_project(project_collection=project_collection(), project_name=duplicate_name, raise_error=True) + 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): @@ -275,7 +307,9 @@ def test_find_or_create_project_no_team(project_collection): def test_find_or_create_dataset_no_exist(dataset_collection): # test when dataset doesn't exist old_dataset_count = len(list(dataset_collection.list())) - result = find_or_create_dataset(dataset_collection=dataset_collection, dataset_name=absent_name) + result = find_or_create_dataset( + dataset_collection=dataset_collection, dataset_name=absent_name + ) new_dataset_count = len(list(dataset_collection.list())) assert result.name == absent_name assert new_dataset_count == old_dataset_count + 1 @@ -284,7 +318,9 @@ def test_find_or_create_dataset_no_exist(dataset_collection): def test_find_or_create_dataset_exist(dataset_collection): # test when dataset exists old_dataset_count = len(list(dataset_collection.list())) - result = find_or_create_dataset(dataset_collection=dataset_collection, dataset_name="dataset 2") + result = find_or_create_dataset( + dataset_collection=dataset_collection, dataset_name="dataset 2" + ) new_dataset_count = len(list(dataset_collection.list())) assert result.name == "dataset 2" assert new_dataset_count == old_dataset_count @@ -299,13 +335,17 @@ def test_find_or_create_dataset_exist_multiple(dataset_collection): def test_find_or_create_dataset_raise_error_no_exist(dataset_collection): # test when dataset doesn't exist and raise_error flag is on with pytest.raises(ValueError): - find_or_create_dataset(dataset_collection=dataset_collection, dataset_name=absent_name, raise_error=True) + find_or_create_dataset( + dataset_collection=dataset_collection, dataset_name=absent_name, raise_error=True + ) def test_find_or_create_dataset_raise_error_exist(dataset_collection): # test when dataset exists and raise_error flag is on old_dataset_count = len(list(dataset_collection.list())) - result = find_or_create_dataset(dataset_collection=dataset_collection, dataset_name="dataset 3", raise_error=True) + result = find_or_create_dataset( + dataset_collection=dataset_collection, dataset_name="dataset 3", raise_error=True + ) new_dataset_count = len(list(dataset_collection.list())) assert result.name == "dataset 3" assert new_dataset_count == old_dataset_count @@ -314,32 +354,34 @@ def test_find_or_create_dataset_raise_error_exist(dataset_collection): def test_find_or_create_dataset_raise_error_exist_multiple(dataset_collection): # test when dataset exists multiple times and raise_error flag is on with pytest.raises(ValueError): - find_or_create_dataset(dataset_collection=dataset_collection, dataset_name=duplicate_name, raise_error=True) + find_or_create_dataset( + dataset_collection=dataset_collection, dataset_name=duplicate_name, raise_error=True + ) def test_create_or_update_none_found(predictor_collection): # test when resource doesn't exist with listed name and check if new one is created assert not [r for r in list(predictor_collection.list()) if r.name == absent_name] - aml = AutoMLPredictor(name=absent_name, description='', inputs=[], outputs=[]) - pred = GraphPredictor(name=absent_name, description='', predictors=[aml]) - #verify that the returned object is updated + aml = AutoMLPredictor(name=absent_name, description="", inputs=[], outputs=[]) + pred = GraphPredictor(name=absent_name, description="", predictors=[aml]) + # verify that the returned object is updated returned_pred = create_or_update(collection=predictor_collection, resource=pred) assert returned_pred.uid == pred.uid assert returned_pred.name == pred.name assert returned_pred.description == pred.description - #verify that the collection is also updated + # verify that the collection is also updated assert any([r for r in list(predictor_collection.list()) if r.name == absent_name]) def test_create_or_update_unique_found(predictor_collection): # test when there is a single unique resource that exists with the listed name and update - aml = AutoMLPredictor(name="", description='', inputs=[], outputs=[]) + aml = AutoMLPredictor(name="", description="", inputs=[], outputs=[]) pred = GraphPredictor(name="resource 4", description="I am updated!", predictors=[aml]) - #verify that the returned object is updated + # verify that the returned object is updated returned_pred = create_or_update(collection=predictor_collection, resource=pred) assert returned_pred.name == pred.name assert returned_pred.description == pred.description - #verify that the collection is also updated + # verify that the collection is also updated updated_pred = [r for r in list(predictor_collection.list()) if r.name == "resource 4"][0] assert updated_pred.description == "I am updated!" @@ -360,20 +402,25 @@ def test_create_or_update_unique_found_design_workflow(session): dw2_dict, # Return the updated design workflow ) - collection = LocalDesignWorkflowCollection(project_id=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 + # 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"] + expected_root_id = UUID(branch_data["metadata"]["root_id"]) + assert returned_dw.branch_root_id == collection.branch_root_id == expected_root_id + expected_version = branch_data["metadata"]["version"] + assert returned_dw.branch_version == collection.branch_version == expected_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 - aml = AutoMLPredictor(name="", description='', inputs=[], outputs=[]) + aml = AutoMLPredictor(name="", description="", inputs=[], outputs=[]) pred = GraphPredictor(name="resource 1", description="I am updated!", predictors=[aml]) with pytest.raises(ValueError): create_or_update(collection=predictor_collection, resource=pred) diff --git a/tests/seeding/test_sort_gems.py b/tests/seeding/test_sort_gems.py index 72a0137cc..e851380a1 100644 --- a/tests/seeding/test_sort_gems.py +++ b/tests/seeding/test_sort_gems.py @@ -1,9 +1,10 @@ +from gemd.entity.bounds.categorical_bounds import CategoricalBounds + from citrine.resources.condition_template import ConditionTemplate from citrine.resources.measurement_spec import MeasurementSpec from citrine.resources.process_spec import ProcessSpec from citrine.resources.property_template import PropertyTemplate from citrine.seeding.sort_gems import split_templates_from_objects -from gemd.entity.bounds.categorical_bounds import CategoricalBounds def test_no_templates(): @@ -14,18 +15,22 @@ def test_no_templates(): def test_no_data_objects(): - objs = [PropertyTemplate("pt", bounds=CategoricalBounds()), - ConditionTemplate("ct", bounds=CategoricalBounds())] + objs = [ + PropertyTemplate("pt", bounds=CategoricalBounds()), + ConditionTemplate("ct", bounds=CategoricalBounds()), + ] templates, data_objects = split_templates_from_objects(objs) assert len(templates) == 2 assert len(data_objects) == 0 def test_both_present(): - objs = [ProcessSpec("ps"), - PropertyTemplate("pt", bounds=CategoricalBounds()), - MeasurementSpec("ms"), - ConditionTemplate("ct", bounds=CategoricalBounds())] + objs = [ + ProcessSpec("ps"), + PropertyTemplate("pt", bounds=CategoricalBounds()), + MeasurementSpec("ms"), + ConditionTemplate("ct", bounds=CategoricalBounds()), + ] templates, data_objects = split_templates_from_objects(objs) assert len(templates) == 2 assert len(data_objects) == 2 diff --git a/tests/serialization/__init__.py b/tests/serialization/__init__.py index ac519d948..467286009 100644 --- a/tests/serialization/__init__.py +++ b/tests/serialization/__init__.py @@ -2,7 +2,7 @@ def valid_serialization_output(valid_data): - exclude_fields = ['status', 'status_detail'] + exclude_fields = ["status", "status_detail"] return {x: y for x, y in valid_data.items() if x not in exclude_fields} diff --git a/tests/serialization/test_attribute_template.py b/tests/serialization/test_attribute_template.py index 673d650ba..eafe7d1f9 100644 --- a/tests/serialization/test_attribute_template.py +++ b/tests/serialization/test_attribute_template.py @@ -1,18 +1,21 @@ """Tests of the attribute template schema.""" -import pytest + +from gemd.entity.bounds.categorical_bounds import CategoricalBounds +from gemd.entity.bounds.integer_bounds import IntegerBounds +from gemd.entity.bounds.real_bounds import RealBounds +from gemd.json import dumps, loads + from citrine.resources.condition_template import ConditionTemplate from citrine.resources.parameter_template import ParameterTemplate from citrine.resources.property_template import PropertyTemplate -from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.bounds.integer_bounds import IntegerBounds -from gemd.entity.bounds.categorical_bounds import CategoricalBounds -from gemd.json import loads, dumps def test_condition_template(): """Test creation and serde of condition templates.""" - bounds = RealBounds(2.5, 10.0, default_units='cm') - template = ConditionTemplate("Chamber width", tags=[], bounds=bounds, description="width of chamber") + bounds = RealBounds(2.5, 10.0, default_units="cm") + template = ConditionTemplate( + "Chamber width", tags=[], bounds=bounds, description="width of chamber" + ) assert template.uids is not None # uids should be added automatically # Take template through a serde cycle and ensure that it is unchanged @@ -32,6 +35,6 @@ def test_parameter_template(): def test_property_template(): """Test creation and serde of condition templates.""" - bounds = CategoricalBounds(['solid', 'liquid', 'gas']) - template = PropertyTemplate("State", bounds=bounds, uids={'my_id': '0'}) + bounds = CategoricalBounds(["solid", "liquid", "gas"]) + template = PropertyTemplate("State", bounds=bounds, uids={"my_id": "0"}) assert PropertyTemplate.build(template.dump()) == template diff --git a/tests/serialization/test_constraints.py b/tests/serialization/test_constraints.py index 8bf647a35..85fbaa112 100644 --- a/tests/serialization/test_constraints.py +++ b/tests/serialization/test_constraints.py @@ -1,39 +1,37 @@ """Tests for citrine.informatics.constraints.""" + import pytest -from citrine.informatics.constraints import Constraint, ScalarRangeConstraint, \ - AcceptableCategoriesConstraint +from citrine.informatics.constraints import ( + AcceptableCategoriesConstraint, + Constraint, + ScalarRangeConstraint, +) @pytest.fixture def scalar_range_constraint() -> ScalarRangeConstraint: """Build a ScalarRangeConstraint.""" return ScalarRangeConstraint( - descriptor_key='z', - lower_bound=1.0, - upper_bound=10.0, - lower_inclusive=False + descriptor_key="z", lower_bound=1.0, upper_bound=10.0, lower_inclusive=False ) @pytest.fixture def acceptable_categories_constraint() -> AcceptableCategoriesConstraint: """Build a CategoricalConstraint.""" - return AcceptableCategoriesConstraint( - descriptor_key='x', - acceptable_categories=['y', 'z'] - ) + return AcceptableCategoriesConstraint(descriptor_key="x", acceptable_categories=["y", "z"]) def test_scalar_range_dumps(scalar_range_constraint): """Ensure values are persisted through deser.""" result = scalar_range_constraint.dump() - assert result['type'] == 'ScalarRange' - assert result['descriptor_key'] == 'z' - assert result['min'] == 1.0 - assert result['max'] == 10.0 - assert not result['min_inclusive'] - assert result['max_inclusive'] + assert result["type"] == "ScalarRange" + assert result["descriptor_key"] == "z" + assert result["min"] == 1.0 + assert result["max"] == 10.0 + assert not result["min_inclusive"] + assert result["max_inclusive"] def test_get_scalar_range_type(scalar_range_constraint): @@ -45,9 +43,9 @@ def test_get_scalar_range_type(scalar_range_constraint): def test_categorical_dumps(acceptable_categories_constraint): """Ensure values are persisted through deser.""" result = acceptable_categories_constraint.dump() - assert result['type'] == 'AcceptableCategoriesConstraint' - assert result['descriptor_key'] == 'x' - assert result['acceptable_classes'] == ['y', 'z'] + assert result["type"] == "AcceptableCategoriesConstraint" + assert result["descriptor_key"] == "x" + assert result["acceptable_classes"] == ["y", "z"] def test_get_categorical_type(acceptable_categories_constraint): diff --git a/tests/serialization/test_dataset.py b/tests/serialization/test_dataset.py index f9fa33158..248359aab 100644 --- a/tests/serialization/test_dataset.py +++ b/tests/serialization/test_dataset.py @@ -1,8 +1,11 @@ """Tests of the Dataset schema.""" + +from uuid import UUID, uuid4 + +import arrow import pytest -from uuid import uuid4, UUID + from citrine.resources.dataset import Dataset -import arrow @pytest.fixture @@ -10,10 +13,10 @@ def valid_data(): """Return valid data used for these tests.""" return dict( id=str(uuid4()), - name='Dataset 1', + name="Dataset 1", unique_name=None, - summary='The first dataset', - description='A dummy dataset for performing unit tests', + summary="The first dataset", + description="A dummy dataset for performing unit tests", deleted=True, created_by=None, updated_by=None, @@ -21,19 +24,19 @@ def valid_data(): create_time=1559933807392, update_time=None, delete_time=None, - public=False + public=False, ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Dataset looks sane.""" dataset: Dataset = Dataset.build(valid_data) - assert dataset.uid == UUID(valid_data['id']) - assert dataset.name == 'Dataset 1' - assert dataset.summary == 'The first dataset' - assert dataset.description == 'A dummy dataset for performing unit tests' + assert dataset.uid == UUID(valid_data["id"]) + assert dataset.name == "Dataset 1" + assert dataset.summary == "The first dataset" + assert dataset.description == "A dummy dataset for performing unit tests" assert dataset.deleted - assert dataset.create_time == arrow.get(valid_data['create_time'] / 1000).datetime + assert dataset.create_time == arrow.get(valid_data["create_time"] / 1000).datetime def test_serialization(valid_data): diff --git a/tests/serialization/test_descriptors.py b/tests/serialization/test_descriptors.py index d5c41f178..aabdd9e57 100644 --- a/tests/serialization/test_descriptors.py +++ b/tests/serialization/test_descriptors.py @@ -1,26 +1,27 @@ """Tests for citrine.informatics.descriptors serialization.""" + import pytest -from citrine.informatics.descriptors import RealDescriptor, Descriptor +from citrine.informatics.descriptors import Descriptor, RealDescriptor @pytest.fixture def valid_data(): """Produce valid descriptor data.""" - return dict( - type='Real', - descriptor_key='alpha', - units='', - lower_bound=5.0, - upper_bound=10.0, - ) + return { + "type": "Real", + "descriptor_key": "alpha", + "units": "", + "lower_bound": 5.0, + "upper_bound": 10.0, + } def test_simple_deserialization(valid_data): """Ensure a deserialized RealDescriptor looks sane.""" descriptor = RealDescriptor.build(valid_data) - assert descriptor.key == 'alpha' - assert descriptor.units == '' + assert descriptor.key == "alpha" + assert descriptor.units == "" assert descriptor.lower_bound == 5.0 assert descriptor.upper_bound == 10.0 @@ -28,8 +29,8 @@ def test_simple_deserialization(valid_data): def test_polymorphic_deserialization(valid_data): """Ensure a polymorphically deserialized RealDescriptor looks sane.""" descriptor: RealDescriptor = Descriptor.build(valid_data) - assert descriptor.key == 'alpha' - assert descriptor.units == '' + assert descriptor.key == "alpha" + assert descriptor.units == "" assert descriptor.lower_bound == 5.0 assert descriptor.upper_bound == 10.0 diff --git a/tests/serialization/test_design_spaces.py b/tests/serialization/test_design_spaces.py index 008eceb54..c46442591 100644 --- a/tests/serialization/test_design_spaces.py +++ b/tests/serialization/test_design_spaces.py @@ -1,31 +1,36 @@ """Tests for citrine.informatics.design_spaces serialization.""" -from copy import copy, deepcopy -from uuid import UUID + +from copy import deepcopy import pytest -from . import design_space_serialization_check, valid_serialization_output from citrine.informatics.constraints import IngredientCountConstraint -from citrine.informatics.descriptors import CategoricalDescriptor, RealDescriptor, ChemicalFormulaDescriptor,\ - FormulationDescriptor -from citrine.informatics.design_spaces import DesignSpace, DesignSubspace, FormulationDesignSpace, ProductDesignSpace, TopLevelDesignSpace +from citrine.informatics.descriptors import FormulationDescriptor +from citrine.informatics.design_spaces import ( + DesignSubspace, + FormulationDesignSpace, + ProductDesignSpace, + TopLevelDesignSpace, +) from citrine.informatics.dimensions import ContinuousDimension, EnumeratedDimension +from . import design_space_serialization_check + def test_product_deserialization(valid_product_design_space_data): """Ensure that a deserialized ProductDesignSpace looks sane.""" for designSpaceClass in [ProductDesignSpace, TopLevelDesignSpace]: data = deepcopy(valid_product_design_space_data) design_space: ProductDesignSpace = designSpaceClass.build(data) - assert design_space.name == 'my design space' - assert design_space.description == 'does some things' + assert design_space.name == "my design space" + assert design_space.description == "does some things" assert type(design_space.dimensions[0]) == ContinuousDimension assert design_space.dimensions[0].lower_bound == 6.0 assert type(design_space.dimensions[1]) == EnumeratedDimension - assert design_space.dimensions[1].values == ['red'] + assert design_space.dimensions[1].values == ["red"] assert type(design_space.subspaces[0]) == FormulationDesignSpace assert type(design_space.subspaces[1]) == FormulationDesignSpace - assert design_space.subspaces[1].ingredients == {'baz'} + assert design_space.subspaces[1].ingredients == {"baz"} def test_product_serialization(valid_product_design_space_data): @@ -33,9 +38,11 @@ def test_product_serialization(valid_product_design_space_data): original_data = deepcopy(valid_product_design_space_data) design_space = ProductDesignSpace.build(valid_product_design_space_data) serialized = design_space.dump() - serialized['id'] = valid_product_design_space_data['id'] - assert serialized['instance']['subspaces'][0] == original_data['data']['instance']['subspaces'][0] - assert serialized['instance']['subspaces'][1] == original_data['data']['instance']['subspaces'][1] + serialized["id"] = valid_product_design_space_data["id"] + serialized_subspaces = serialized["instance"]["subspaces"] + original_subspaces = original_data["data"]["instance"]["subspaces"] + assert serialized_subspaces[0] == original_subspaces[0] + assert serialized_subspaces[1] == original_subspaces[1] def test_formulation_deserialization(valid_formulation_design_space_data): @@ -45,18 +52,18 @@ def test_formulation_deserialization(valid_formulation_design_space_data): """ expected_descriptor = FormulationDescriptor.hierarchical() expected_constraint = IngredientCountConstraint( - formulation_descriptor=expected_descriptor, - min=0, - max=1 + formulation_descriptor=expected_descriptor, min=0, max=1 ) for designSpaceClass in [DesignSubspace, FormulationDesignSpace]: - design_space: FormulationDesignSpace = designSpaceClass.build(valid_formulation_design_space_data) - assert design_space.name == 'formulation design space' - assert design_space.description == 'formulates some things' + design_space: FormulationDesignSpace = designSpaceClass.build( + valid_formulation_design_space_data + ) + assert design_space.name == "formulation design space" + assert design_space.description == "formulates some things" assert design_space.formulation_descriptor.key == expected_descriptor.key - assert design_space.ingredients == {'foo'} - assert design_space.labels == {'bar': {'foo'}} - assert design_space.untested_ingredients == {'qux'} + assert design_space.ingredients == {"foo"} + assert design_space.labels == {"bar": {"foo"}} + assert design_space.untested_ingredients == {"qux"} assert len(design_space.constraints) == 1 actual_constraint: IngredientCountConstraint = next(iter(design_space.constraints)) assert actual_constraint.formulation_descriptor == expected_descriptor @@ -77,7 +84,7 @@ def test_formulation_without_untested_ingredients(valid_formulation_design_space field must stay optional, so older payloads without the key don't fail to build. """ data = deepcopy(valid_formulation_design_space_data) - del data['untested_ingredients'] + del data["untested_ingredients"] design_space: FormulationDesignSpace = FormulationDesignSpace.build(data) assert design_space.untested_ingredients is None diff --git a/tests/serialization/test_dimensions.py b/tests/serialization/test_dimensions.py index 76af408af..ce234ee17 100644 --- a/tests/serialization/test_dimensions.py +++ b/tests/serialization/test_dimensions.py @@ -1,41 +1,40 @@ """Tests for citrine.informatics.dimensions serialization.""" -import uuid import pytest -from citrine.informatics.descriptors import RealDescriptor, CategoricalDescriptor -from citrine.informatics.dimensions import Dimension, ContinuousDimension, EnumeratedDimension +from citrine.informatics.descriptors import CategoricalDescriptor, RealDescriptor +from citrine.informatics.dimensions import ContinuousDimension, Dimension, EnumeratedDimension @pytest.fixture def valid_continuous_data(): """Produce valid continuous dimension data.""" - return dict( - type='ContinuousDimension', - descriptor=dict( - type='Real', - descriptor_key='alpha', - units='', - lower_bound=5.0, - upper_bound=10.0, - ), - lower_bound=6.0, - upper_bound=7.0 - ) + return { + "type": "ContinuousDimension", + "descriptor": { + "type": "Real", + "descriptor_key": "alpha", + "units": "", + "lower_bound": 5.0, + "upper_bound": 10.0, + }, + "lower_bound": 6.0, + "upper_bound": 7.0, + } @pytest.fixture def valid_enumerated_data(): """Produce valid enumerated dimension data.""" - return dict( - type='EnumeratedDimension', - descriptor=dict( - type='Categorical', - descriptor_key='color', - descriptor_values=['blue', 'green', 'red'], - ), - list=['red'] - ) + return { + "type": "EnumeratedDimension", + "descriptor": { + "type": "Categorical", + "descriptor_key": "color", + "descriptor_values": ["blue", "green", "red"], + }, + "list": ["red"], + } def test_simple_continuous_deserialization(valid_continuous_data): @@ -67,7 +66,7 @@ def test_simple_enumerated_deserialization(valid_enumerated_data): """Ensure that a deserialized EnumeratedDimension looks sane.""" dimension: EnumeratedDimension = EnumeratedDimension.build(valid_enumerated_data) assert type(dimension) == EnumeratedDimension - assert dimension.values == ['red'] + assert dimension.values == ["red"] assert type(dimension.descriptor) == CategoricalDescriptor @@ -75,7 +74,7 @@ def test_polymorphic_enumerated_deserialization(valid_enumerated_data): """Ensure that a polymorphically deserialized EnumeratedDimension looks sane.""" dimension: EnumeratedDimension = Dimension.build(valid_enumerated_data) assert type(dimension) == EnumeratedDimension - assert dimension.values == ['red'] + assert dimension.values == ["red"] assert type(dimension.descriptor) == CategoricalDescriptor diff --git a/tests/serialization/test_file_link.py b/tests/serialization/test_file_link.py index ec4b4ca69..d19b313fd 100644 --- a/tests/serialization/test_file_link.py +++ b/tests/serialization/test_file_link.py @@ -1,18 +1,19 @@ """Tests of FileLink serialization and deserialization.""" + from citrine.resources.file_link import FileLink from tests.utils.factories import FileLinkDataFactory def test_simple_deserialization(): """Ensure that a deserialized File Link looks sane.""" - valid_data = FileLinkDataFactory(url='www.citrine.io', filename='materials.txt') + valid_data = FileLinkDataFactory(url="www.citrine.io", filename="materials.txt") file_link = FileLink.build(valid_data) - assert file_link.url == 'www.citrine.io' - assert file_link.filename == 'materials.txt' + assert file_link.url == "www.citrine.io" + assert file_link.filename == "materials.txt" def test_serialization(): """Ensure that a serialized File Link looks sane.""" - valid_data = FileLinkDataFactory(url='www.citrine.io', filename='materials.txt') + valid_data = FileLinkDataFactory(url="www.citrine.io", filename="materials.txt") file_link = FileLink.build(valid_data) assert file_link.dump() == valid_data diff --git a/tests/serialization/test_gem_table.py b/tests/serialization/test_gem_table.py index 5a9cf1e6f..58da32e85 100644 --- a/tests/serialization/test_gem_table.py +++ b/tests/serialization/test_gem_table.py @@ -1,7 +1,7 @@ -from uuid import uuid4, UUID +from random import randrange +from uuid import UUID, uuid4 import pytest -from random import randrange from citrine.resources.gemtables import GemTable @@ -12,14 +12,14 @@ def valid_data(): return dict( id=str(uuid4()), version=randrange(10), - signed_download_url="https://s3.amazonaws.citrine.io/bucketboi" + signed_download_url="https://s3.amazonaws.citrine.io/bucketboi", ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Table looks normal.""" table: GemTable = GemTable.build(valid_data) - assert table.uid == UUID(valid_data['id']) + assert table.uid == UUID(valid_data["id"]) assert table.version == valid_data["version"] assert table.download_url == "https://s3.amazonaws.citrine.io/bucketboi" diff --git a/tests/serialization/test_ingredient_run.py b/tests/serialization/test_ingredient_run.py index 7c294bc5c..ee4d1a6d1 100644 --- a/tests/serialization/test_ingredient_run.py +++ b/tests/serialization/test_ingredient_run.py @@ -1,54 +1,63 @@ """Tests of the ingredient run schema.""" -import pytest + from uuid import uuid4 +import pytest +from gemd.entity.value.nominal_real import NominalReal +from gemd.entity.value.normal_real import NormalReal + from citrine.resources.ingredient_run import IngredientRun from citrine.resources.material_run import MaterialRun -from gemd.entity.value.normal_real import NormalReal -from gemd.entity.value.nominal_real import NominalReal @pytest.fixture def valid_data(): """Return valid data used for these tests.""" return dict( - uids={'id': str(uuid4())}, + uids={"id": str(uuid4())}, tags=[], notes=None, - material={'type': 'material_run', 'name': 'flour', 'uids': {'id': str(uuid4())}, - 'tags': [], 'file_links': [], 'notes': None, - 'process': None, 'sample_type': 'unknown', 'spec': None, - }, + material={ + "type": "material_run", + "name": "flour", + "uids": {"id": str(uuid4())}, + "tags": [], + "file_links": [], + "notes": None, + "process": None, + "sample_type": "unknown", + "spec": None, + }, process=None, - mass_fraction={'type': 'normal_real', 'mean': 0.5, 'std': 0.1, 'units': 'dimensionless'}, + mass_fraction={"type": "normal_real", "mean": 0.5, "std": 0.1, "units": "dimensionless"}, volume_fraction=None, number_fraction=None, absolute_quantity=None, - name='flour', - labels=['fine', 'bleached'], + name="flour", + labels=["fine", "bleached"], spec=None, file_links=[], - type='ingredient_run' + type="ingredient_run", ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Ingredient Run looks sane.""" ingredient_run: IngredientRun = IngredientRun.build(valid_data) - assert ingredient_run.uids == {'id': valid_data['uids']['id']} + assert ingredient_run.uids == {"id": valid_data["uids"]["id"]} assert ingredient_run.tags == [] assert ingredient_run.notes is None - assert ingredient_run.material.dump() == valid_data['material'] + assert ingredient_run.material.dump() == valid_data["material"] assert ingredient_run.process is None - assert ingredient_run.mass_fraction == NormalReal(0.5, 0.1, '') + assert ingredient_run.mass_fraction == NormalReal(0.5, 0.1, "") assert ingredient_run.volume_fraction is None assert ingredient_run.number_fraction is None assert ingredient_run.absolute_quantity is None - assert ingredient_run.name == 'flour' - assert ingredient_run.labels == ['fine', 'bleached'] + assert ingredient_run.name == "flour" + assert ingredient_run.labels == ["fine", "bleached"] assert ingredient_run.spec is None assert ingredient_run.file_links == [] - assert ingredient_run.typ == 'ingredient_run' + assert ingredient_run.typ == "ingredient_run" def test_serialization(valid_data): @@ -64,8 +73,8 @@ def test_material_attachment(): Check that the ingredient can be built, and that the connection survives ser/de. """ - flour = MaterialRun("flour", sample_type='unknown') - flour_ingredient = IngredientRun(material=flour, absolute_quantity=NominalReal(500, 'g')) + flour = MaterialRun("flour", sample_type="unknown") + flour_ingredient = IngredientRun(material=flour, absolute_quantity=NominalReal(500, "g")) flour_ingredient_copy = IngredientRun.build(flour_ingredient.dump()) assert flour_ingredient_copy == flour_ingredient diff --git a/tests/serialization/test_material_run.py b/tests/serialization/test_material_run.py index 0ed9acee8..b15563389 100644 --- a/tests/serialization/test_material_run.py +++ b/tests/serialization/test_material_run.py @@ -1,44 +1,45 @@ """Tests of the Material Run schema.""" + import json -from typing import Optional, Iterable +from collections.abc import Iterable -from citrine.resources.material_run import MaterialRun -from citrine.resources.material_spec import MaterialSpec -from citrine.resources.measurement_spec import MeasurementSpec -from citrine.resources.process_run import ProcessRun -from citrine.resources.ingredient_run import IngredientRun -from citrine.resources.ingredient_spec import IngredientSpec -from citrine.resources.measurement_run import MeasurementRun -from gemd.entity.link_by_uid import LinkByUID -from gemd.json import GEMDJson from gemd.demo.cake import make_cake -from gemd.entity.object import MeasurementRun as GEMDMeasurementRun +from gemd.entity.file_link import FileLink +from gemd.entity.link_by_uid import LinkByUID from gemd.entity.object import MaterialRun as GEMDMaterialRun from gemd.entity.object import MaterialSpec as GEMDMaterialSpec +from gemd.entity.object import MeasurementRun as GEMDMeasurementRun from gemd.entity.object import MeasurementSpec as GEMDMeasurementSpec -from gemd.entity.object import ProcessSpec as GEMDProcessSpec from gemd.entity.object import ProcessRun as GEMDProcessRun -from gemd.entity.object.ingredient_spec import IngredientSpec as GEMDIngredientSpec +from gemd.entity.object import ProcessSpec as GEMDProcessSpec from gemd.entity.object.ingredient_run import IngredientRun as GEMDIngredientRun -from gemd.entity.file_link import FileLink +from gemd.entity.object.ingredient_spec import IngredientSpec as GEMDIngredientSpec +from gemd.json import GEMDJson +from citrine.resources.ingredient_run import IngredientRun +from citrine.resources.ingredient_spec import IngredientSpec +from citrine.resources.material_run import MaterialRun +from citrine.resources.material_spec import MaterialSpec +from citrine.resources.measurement_run import MeasurementRun +from citrine.resources.measurement_spec import MeasurementSpec +from citrine.resources.process_run import ProcessRun from tests.utils.factories import MaterialRunDataFactory def test_simple_deserialization(): """Ensure that a deserialized Material Run looks sane.""" - valid_data: dict = MaterialRunDataFactory(name='Cake 1', notes=None, spec=None) + valid_data: dict = MaterialRunDataFactory(name="Cake 1", notes=None, spec=None) material_run: MaterialRun = MaterialRun.build(valid_data) assert isinstance(material_run, MaterialRun) - assert material_run.uids == valid_data['uids'] - assert material_run.name == valid_data['name'] - assert material_run.tags == valid_data['tags'] + assert material_run.uids == valid_data["uids"] + assert material_run.name == valid_data["name"] + assert material_run.tags == valid_data["tags"] assert material_run.notes is None - assert material_run.process == LinkByUID.build(valid_data['process']) - assert material_run.sample_type == valid_data['sample_type'] + assert material_run.process == LinkByUID.build(valid_data["process"]) + assert material_run.sample_type == valid_data["sample_type"] assert material_run.template is None assert material_run.spec is None - assert material_run.file_links == [FileLink.build(x) for x in valid_data['file_links']] + assert material_run.file_links == [FileLink.build(x) for x in valid_data["file_links"]] def test_serialization(): @@ -51,15 +52,15 @@ def test_serialization(): def test_process_attachment(): """Test that a process can be attached to a material, and that the connection survives serde""" - cake = MaterialRun('Final cake') - cake.process = ProcessRun('Icing', uids={'id': '12345'}) + cake = MaterialRun("Final cake") + cake.process = ProcessRun("Icing", uids={"id": "12345"}) cake_data = cake.dump() cake_copy = MaterialRun.build(cake_data).as_dict() - assert cake_copy['name'] == cake.name - assert cake_copy['uids'] == cake.uids - assert cake.process.uids['id'] == cake_copy['process'].uids['id'] + assert cake_copy["name"] == cake.name + assert cake_copy["uids"] == cake.uids + assert cake.process.uids["id"] == cake_copy["process"].uids["id"] reconstituted_cake = MaterialRun.build(cake_copy) assert isinstance(reconstituted_cake, MaterialRun) @@ -74,22 +75,22 @@ def make_ingredient(material: MaterialRun): return IngredientRun(material=material) icing = ProcessRun(name="Icing") - cake = MaterialRun(name='Final cake', process=icing) + cake = MaterialRun(name="Final cake", process=icing) - cake.process.ingredients.append(make_ingredient(MaterialRun('Baked Cake'))) - cake.process.ingredients.append(make_ingredient(MaterialRun('Frosting'))) + cake.process.ingredients.append(make_ingredient(MaterialRun("Baked Cake"))) + cake.process.ingredients.append(make_ingredient(MaterialRun("Frosting"))) baked = cake.process.ingredients[0].material - baked.process = ProcessRun(name='Baking') - baked.process.ingredients.append(make_ingredient(MaterialRun('Batter'))) + baked.process = ProcessRun(name="Baking") + baked.process.ingredients.append(make_ingredient(MaterialRun("Batter"))) batter = baked.process.ingredients[0].material - batter.process = ProcessRun(name='Mixing batter') + batter.process = ProcessRun(name="Mixing batter") - batter.process.ingredients.append(make_ingredient(material=MaterialRun('Butter'))) - batter.process.ingredients.append(make_ingredient(material=MaterialRun('Sugar'))) - batter.process.ingredients.append(make_ingredient(material=MaterialRun('Flour'))) - batter.process.ingredients.append(make_ingredient(material=MaterialRun('Milk'))) + batter.process.ingredients.append(make_ingredient(material=MaterialRun("Butter"))) + batter.process.ingredients.append(make_ingredient(material=MaterialRun("Sugar"))) + batter.process.ingredients.append(make_ingredient(material=MaterialRun("Flour"))) + batter.process.ingredients.append(make_ingredient(material=MaterialRun("Milk"))) cake.dump() @@ -99,51 +100,64 @@ def test_measurement_material_connection_rehydration(): starting_mat_spec = GEMDMaterialSpec("starting material") starting_mat = GEMDMaterialRun("starting material", spec=starting_mat_spec) meas_spec = GEMDMeasurementSpec("measurement spec") - meas1 = GEMDMeasurementRun("measurement on starting material", - spec=meas_spec, material=starting_mat) + meas1 = GEMDMeasurementRun( + "measurement on starting material", spec=meas_spec, material=starting_mat + ) process_spec = GEMDProcessSpec("Transformative process") process = GEMDProcessRun("Transformative process", spec=process_spec) - ingredient_spec = GEMDIngredientSpec(name="ingredient", material=starting_mat_spec, - process=process_spec) + ingredient_spec = GEMDIngredientSpec( + name="ingredient", material=starting_mat_spec, process=process_spec + ) ingredient = GEMDIngredientRun(material=starting_mat, process=process, spec=ingredient_spec) ending_mat_spec = GEMDMaterialSpec("ending material", process=process_spec) ending_mat = GEMDMaterialRun("ending material", process=process, spec=ending_mat_spec) - meas2 = GEMDMeasurementRun("measurement on ending material", - spec=meas_spec, material=ending_mat) + meas2 = GEMDMeasurementRun( + "measurement on ending material", spec=meas_spec, material=ending_mat + ) copy = MaterialRun.build(json.loads(GEMDJson().dumps(ending_mat))) assert isinstance(copy, MaterialRun), "copy of ending_mat should be a MaterialRun" assert len(copy.measurements) == 1, "copy of ending_mat should have one measurement" - assert isinstance(copy.measurements[0], MeasurementRun), \ + assert isinstance(copy.measurements[0], MeasurementRun), ( "copy of ending_mat should have a measurement that is a MeasurementRun" - assert isinstance(copy.measurements[0].spec, MeasurementSpec), \ + ) + assert isinstance(copy.measurements[0].spec, MeasurementSpec), ( "copy of ending_mat should have a measurement that has a spec that is a MeasurementSpec" + ) assert isinstance(copy.process, ProcessRun), "copy of ending_mat should have a process" - assert len(copy.process.ingredients) == 1, \ + assert len(copy.process.ingredients) == 1, ( "copy of ending_mat should have a process with one ingredient" + ) assert isinstance(copy.spec, MaterialSpec), "copy of ending_mat should have a spec" - assert len(copy.spec.process.ingredients) == 1, \ + assert len(copy.spec.process.ingredients) == 1, ( "copy of ending_mat should have a spec with a process that has one ingredient" - assert isinstance(copy.process.spec.ingredients[0], IngredientSpec), \ - "copy of ending_mat should have a spec with a process that has an ingredient " \ + ) + assert isinstance(copy.process.spec.ingredients[0], IngredientSpec), ( + "copy of ending_mat should have a spec with a process that has an ingredient " "that is an IngredientRun" + ) copy_ingredient = copy.process.ingredients[0] - assert isinstance(copy_ingredient, IngredientRun), \ + assert isinstance(copy_ingredient, IngredientRun), ( "copy of ending_mat should have a process with an ingredient that is an IngredientRun" - assert isinstance(copy_ingredient.material, MaterialRun), \ + ) + assert isinstance(copy_ingredient.material, MaterialRun), ( "copy of ending_mat should have a process with an ingredient that links to a MaterialRun" - assert len(copy_ingredient.material.measurements) == 1, \ - "copy of ending_mat should have a process with an ingredient derived from a material " \ + ) + assert len(copy_ingredient.material.measurements) == 1, ( + "copy of ending_mat should have a process with an ingredient derived from a material " "that has one measurement performed on it" - assert isinstance(copy_ingredient.material.measurements[0], MeasurementRun), \ - "copy of ending_mat should have a process with an ingredient derived from a material " \ + ) + assert isinstance(copy_ingredient.material.measurements[0], MeasurementRun), ( + "copy of ending_mat should have a process with an ingredient derived from a material " "that has one measurement that gets deserialized as a MeasurementRun" - assert isinstance(copy_ingredient.material.measurements[0].spec, MeasurementSpec), \ - "copy of ending_mat should have a process with an ingredient derived from a material " \ + ) + assert isinstance(copy_ingredient.material.measurements[0].spec, MeasurementSpec), ( + "copy of ending_mat should have a process with an ingredient derived from a material " "that has one measurement that has a spec" + ) def test_cake(): @@ -158,13 +172,15 @@ def test_cake(): """ gemd_cake = make_cake() cake = MaterialRun.build(json.loads(GEMDJson().dumps(gemd_cake))) - assert [ingred.name for ingred in cake.process.ingredients] == \ - [ingred.name for ingred in gemd_cake.process.ingredients] - assert [ingred.labels for ingred in cake.process.ingredients] == \ - [ingred.labels for ingred in gemd_cake.process.ingredients] + assert [ingred.name for ingred in cake.process.ingredients] == [ + ingred.name for ingred in gemd_cake.process.ingredients + ] + assert [ingred.labels for ingred in cake.process.ingredients] == [ + ingred.labels for ingred in gemd_cake.process.ingredients + ] assert gemd_cake == cake - def _by_name(start: MaterialRun, names: Iterable[str]) -> Optional[MaterialRun]: + def _by_name(start: MaterialRun, names: Iterable[str]) -> MaterialRun | None: if isinstance(names, str): names = [names] while names: diff --git a/tests/serialization/test_material_spec.py b/tests/serialization/test_material_spec.py index 9bb04c1fa..8eb1afaf0 100644 --- a/tests/serialization/test_material_spec.py +++ b/tests/serialization/test_material_spec.py @@ -1,77 +1,74 @@ """Tests of the material spec schema.""" -import pytest + from uuid import uuid4 -from citrine.resources.material_spec import MaterialSpec +import pytest from gemd.entity.attribute.condition import Condition from gemd.entity.attribute.property import Property from gemd.entity.attribute.property_and_conditions import PropertyAndConditions from gemd.entity.value.nominal_categorical import NominalCategorical from gemd.entity.value.nominal_real import NominalReal +from citrine.resources.material_spec import MaterialSpec + @pytest.fixture def valid_data(): """Return valid data used for these tests.""" return dict( - name='spec of material', - uids={'id': str(uuid4())}, + name="spec of material", + uids={"id": str(uuid4())}, tags=[], notes=None, process=None, template=None, properties=[ { - 'type': 'property_and_conditions', - 'property': + "type": "property_and_conditions", + "property": { + "type": "property", + "origin": "specified", + "name": "color", + "template": None, + "notes": None, + "value": {"category": "tan", "type": "nominal_categorical"}, + "file_links": [], + }, + "conditions": [ { - 'type': 'property', - 'origin': 'specified', - 'name': 'color', - 'template': None, - 'notes': None, - 'value': {'category': 'tan', 'type': 'nominal_categorical'}, - 'file_links': [] - }, - 'conditions': - [ - { - 'type': 'condition', - 'origin': 'specified', - 'name': 'temperature', - 'template': None, - 'notes': None, - 'value': { - 'type': 'nominal_real', - 'nominal': 300.0, - 'units': 'kelvin' - }, - 'file_links': [] - } - ] + "type": "condition", + "origin": "specified", + "name": "temperature", + "template": None, + "notes": None, + "value": {"type": "nominal_real", "nominal": 300.0, "units": "kelvin"}, + "file_links": [], + } + ], } ], file_links=[], - type='material_spec' + type="material_spec", ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Material Spec looks sane.""" material_spec: MaterialSpec = MaterialSpec.build(valid_data) - assert material_spec.uids == {'id': valid_data['uids']['id']} - assert material_spec.name == 'spec of material' + assert material_spec.uids == {"id": valid_data["uids"]["id"]} + assert material_spec.name == "spec of material" assert material_spec.tags == [] assert material_spec.notes is None assert material_spec.process is None - assert material_spec.properties[0] == \ - PropertyAndConditions(Property("color", origin='specified', - value=NominalCategorical("tan")), - conditions=[Condition('temperature', origin='specified', - value=NominalReal(300, units='kelvin'))]) + assert material_spec.properties[0] == PropertyAndConditions( + Property("color", origin="specified", value=NominalCategorical("tan")), + conditions=[ + Condition("temperature", origin="specified", value=NominalReal(300, units="kelvin")) + ], + ) assert material_spec.template is None assert material_spec.file_links == [] - assert material_spec.typ == 'material_spec' + assert material_spec.typ == "material_spec" def test_serialization(valid_data): diff --git a/tests/serialization/test_measurement_run.py b/tests/serialization/test_measurement_run.py index 9dc6953f1..b3d5d9c76 100644 --- a/tests/serialization/test_measurement_run.py +++ b/tests/serialization/test_measurement_run.py @@ -1,56 +1,72 @@ """Tests of the Measurement Run schema""" -import pytest -from uuid import uuid4, UUID -from datetime import datetime +from uuid import UUID, uuid4 + +import pytest from gemd.entity.attribute.property import Property from gemd.entity.value.nominal_integer import NominalInteger -from citrine.resources.measurement_run import MeasurementRun + from citrine.resources.material_run import MaterialRun +from citrine.resources.measurement_run import MeasurementRun @pytest.fixture def valid_data(): """Return valid data used for these tests.""" return dict( - uids={'id': str(uuid4())}, - name='Taste test', + uids={"id": str(uuid4())}, + name="Taste test", tags=[], notes=None, conditions=[], parameters=[], - properties=[{'name': 'sweetness', 'type': 'property', 'template': None, 'notes': None, - 'origin': 'measured', 'file_links': [], - 'value': {'type': 'nominal_integer', 'nominal': 7}}, - {'type': 'property', 'name': 'fluffiness', 'template': None, 'notes': None, - 'origin': 'measured', 'file_links': [], - 'value': {'type': 'nominal_integer', 'nominal': 10} - }], + properties=[ + { + "name": "sweetness", + "type": "property", + "template": None, + "notes": None, + "origin": "measured", + "file_links": [], + "value": {"type": "nominal_integer", "nominal": 7}, + }, + { + "type": "property", + "name": "fluffiness", + "template": None, + "notes": None, + "origin": "measured", + "file_links": [], + "value": {"type": "nominal_integer", "nominal": 10}, + }, + ], material={ - 'uids': {'id': str(uuid4())}, - 'name': 'sponge cake', - 'tags': [], - 'notes': None, - 'process': None, - 'sample_type': 'experimental', - 'spec': None, - 'file_links': [], - 'type': 'material_run', - 'audit_info': { - 'created_by': str(uuid4()), 'created_at': 1559933807392, - 'updated_by': str(uuid4()), 'updated_at': 1560033807392 + "uids": {"id": str(uuid4())}, + "name": "sponge cake", + "tags": [], + "notes": None, + "process": None, + "sample_type": "experimental", + "spec": None, + "file_links": [], + "type": "material_run", + "audit_info": { + "created_by": str(uuid4()), + "created_at": 1559933807392, + "updated_by": str(uuid4()), + "updated_at": 1560033807392, }, - 'dataset': str(uuid4()), + "dataset": str(uuid4()), }, spec=None, file_links=[], - type='measurement_run', + type="measurement_run", source={ "type": "performed_source", "performed_by": "Marie Curie", - "performed_date": "1898-07-01" + "performed_date": "1898-07-01", }, - audit_info={'created_by': str(uuid4()), 'created_at': 1560133807392}, + audit_info={"created_by": str(uuid4()), "created_at": 1560133807392}, dataset=str(uuid4()), ) @@ -58,27 +74,34 @@ def valid_data(): def test_simple_deserialization(valid_data): """Ensure that a deserialized Measurement Run looks sane.""" measurement_run: MeasurementRun = MeasurementRun.build(valid_data) - assert measurement_run.uids == {'id': valid_data['uids']['id']} - assert measurement_run.name == 'Taste test' + assert measurement_run.uids == {"id": valid_data["uids"]["id"]} + assert measurement_run.name == "Taste test" assert measurement_run.notes is None assert measurement_run.tags == [] assert measurement_run.conditions == [] assert measurement_run.parameters == [] - assert measurement_run.properties[0] == Property('sweetness', origin="measured", - value=NominalInteger(7)) - assert measurement_run.properties[1] == Property('fluffiness', origin="measured", - value=NominalInteger(10)) + assert measurement_run.properties[0] == Property( + "sweetness", origin="measured", value=NominalInteger(7) + ) + assert measurement_run.properties[1] == Property( + "fluffiness", origin="measured", value=NominalInteger(10) + ) assert measurement_run.file_links == [] assert measurement_run.template is None - assert measurement_run.material == MaterialRun('sponge cake', tags=[], - uids={'id': valid_data['material']['uids']['id']}, - sample_type='experimental') - assert measurement_run.material.audit_info.created_by == UUID(valid_data['material']['audit_info']['created_by']) - assert measurement_run.material.dataset == UUID(valid_data['material']['dataset']) + assert measurement_run.material == MaterialRun( + "sponge cake", + tags=[], + uids={"id": valid_data["material"]["uids"]["id"]}, + sample_type="experimental", + ) + assert measurement_run.material.audit_info.created_by == UUID( + valid_data["material"]["audit_info"]["created_by"] + ) + assert measurement_run.material.dataset == UUID(valid_data["material"]["dataset"]) assert measurement_run.spec is None - assert measurement_run.typ == 'measurement_run' - assert measurement_run.audit_info.created_by == UUID(valid_data['audit_info']['created_by']) - assert measurement_run.dataset == UUID(valid_data['dataset']) + assert measurement_run.typ == "measurement_run" + assert measurement_run.audit_info.created_by == UUID(valid_data["audit_info"]["created_by"]) + assert measurement_run.dataset == UUID(valid_data["dataset"]) def test_serialization(valid_data): @@ -86,17 +109,17 @@ def test_serialization(valid_data): measurement_run: MeasurementRun = MeasurementRun.build(valid_data) serialized = measurement_run.dump() # Audit info & dataset are not included in the dump - serialized['audit_info'] = valid_data['audit_info'] - serialized['dataset'] = valid_data['dataset'] - serialized['material']['audit_info'] = valid_data['material']['audit_info'] - serialized['material']['dataset'] = valid_data['material']['dataset'] + serialized["audit_info"] = valid_data["audit_info"] + serialized["dataset"] = valid_data["dataset"] + serialized["material"]["audit_info"] = valid_data["material"]["audit_info"] + serialized["material"]["dataset"] = valid_data["material"]["dataset"] assert serialized == valid_data def test_material_attachment(): """Test that a material can be attached to a measurement, and the connection survives serde.""" - cake = MaterialRun('Final Cake') - mass = MeasurementRun('Weigh cake', material=cake) + cake = MaterialRun("Final Cake") + mass = MeasurementRun("Weigh cake", material=cake) mass_data = mass.dump() mass_copy = MeasurementRun.build(mass_data) assert mass_copy == mass diff --git a/tests/serialization/test_object_template.py b/tests/serialization/test_object_template.py index cfb815045..818f550a5 100644 --- a/tests/serialization/test_object_template.py +++ b/tests/serialization/test_object_template.py @@ -1,75 +1,87 @@ """Tests of the object template schema.""" + from uuid import uuid4 +from gemd.entity.bounds.categorical_bounds import CategoricalBounds +from gemd.entity.bounds.integer_bounds import IntegerBounds +from gemd.entity.bounds.real_bounds import RealBounds +from gemd.entity.link_by_uid import LinkByUID + +from citrine.resources.condition_template import ConditionTemplate from citrine.resources.material_template import MaterialTemplate from citrine.resources.measurement_template import MeasurementTemplate +from citrine.resources.parameter_template import ParameterTemplate from citrine.resources.process_template import ProcessTemplate from citrine.resources.property_template import PropertyTemplate -from citrine.resources.condition_template import ConditionTemplate -from citrine.resources.parameter_template import ParameterTemplate -from gemd.entity.link_by_uid import LinkByUID -from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.bounds.integer_bounds import IntegerBounds -from gemd.entity.bounds.categorical_bounds import CategoricalBounds def test_object_template_serde(): """Test serde of an object template.""" - length_template = PropertyTemplate("Length", bounds=RealBounds(2.0, 3.5, 'cm')) - sub_bounds = RealBounds(2.5, 3.0, 'cm') + length_template = PropertyTemplate("Length", bounds=RealBounds(2.0, 3.5, "cm")) + sub_bounds = RealBounds(2.5, 3.0, "cm") color_template = PropertyTemplate("Color", bounds=CategoricalBounds(["red", "green", "blue"])) # Properties are a mixture of property templates and [template, bounds], pairs - block_template = MaterialTemplate("Block", properties=[[length_template, sub_bounds], - color_template]) + block_template = MaterialTemplate( + "Block", properties=[[length_template, sub_bounds], color_template] + ) copy_template = MaterialTemplate.build(block_template.dump()) assert copy_template == block_template # Tests below exercise similar code, but for measurement and process templates - pressure_template = ConditionTemplate("pressure", bounds=RealBounds(0.1, 0.11, 'MPa')) + pressure_template = ConditionTemplate("pressure", bounds=RealBounds(0.1, 0.11, "MPa")) index_template = ParameterTemplate("index", bounds=IntegerBounds(2, 10)) - meas_template = MeasurementTemplate("A measurement of length", properties=[length_template], - conditions=[pressure_template], description="Description", - parameters=[index_template], tags=["foo"]) + meas_template = MeasurementTemplate( + "A measurement of length", + properties=[length_template], + conditions=[pressure_template], + description="Description", + parameters=[index_template], + tags=["foo"], + ) assert MeasurementTemplate.build(meas_template.dump()) == meas_template - proc_template = ProcessTemplate("Make an object", parameters=[index_template], - conditions=[pressure_template], allowed_labels=["Label"], - allowed_names=["first sample", "second sample"]) + proc_template = ProcessTemplate( + "Make an object", + parameters=[index_template], + conditions=[pressure_template], + allowed_labels=["Label"], + allowed_names=["first sample", "second sample"], + ) assert ProcessTemplate.build(proc_template.dump()) == proc_template # Check that serde still works if the template is a LinkByUID - pressure_template.uids['id'] = '12345' # uids['id'] not populated by default - proc_template.conditions[0][0] = LinkByUID('id', pressure_template.uids['id']) + pressure_template.uids["id"] = "12345" # uids['id'] not populated by default + proc_template.conditions[0][0] = LinkByUID("id", pressure_template.uids["id"]) assert ProcessTemplate.build(proc_template.dump()) == proc_template def test_bounds_optional(): """Test that each object template can have passthrough bounds for any of its attributes.""" + def link(): return LinkByUID(id=str(uuid4()), scope=str(uuid4())) + for template_type, attribute_args in [ - (MaterialTemplate, [ - ('properties', PropertyTemplate), - ]), - (ProcessTemplate, [ - ('conditions', ConditionTemplate), - ('parameters', ParameterTemplate), - ]), - (MeasurementTemplate, [ - ('properties', PropertyTemplate), - ('conditions', ConditionTemplate), - ('parameters', ParameterTemplate), - ]), + (MaterialTemplate, [("properties", PropertyTemplate)]), + (ProcessTemplate, [("conditions", ConditionTemplate), ("parameters", ParameterTemplate)]), + ( + MeasurementTemplate, + [ + ("properties", PropertyTemplate), + ("conditions", ConditionTemplate), + ("parameters", ParameterTemplate), + ], + ), ]: kwargs = {} for name, attribute_type in attribute_args: kwargs[name] = [ [link(), IntegerBounds(0, 10)], link(), - attribute_type('foo', bounds=IntegerBounds(0, 10)), - (link(), None) + attribute_type("foo", bounds=IntegerBounds(0, 10)), + (link(), None), ] - template = template_type(name='foo', **kwargs) + template = template_type(name="foo", **kwargs) for name, _ in attribute_args: attributes = getattr(template, name) assert len(attributes) == 4 diff --git a/tests/serialization/test_objectives.py b/tests/serialization/test_objectives.py index 69f135669..65e285fbd 100644 --- a/tests/serialization/test_objectives.py +++ b/tests/serialization/test_objectives.py @@ -1,4 +1,5 @@ """Tests for citrine.informatics.objectives.""" + import pytest from citrine.informatics.objectives import Objective, ScalarMaxObjective, ScalarMinObjective @@ -7,17 +8,13 @@ @pytest.fixture def scalar_max_objective() -> ScalarMaxObjective: """Build a ScalarMaxObjective.""" - return ScalarMaxObjective( - descriptor_key="z" - ) + return ScalarMaxObjective(descriptor_key="z") @pytest.fixture def scalar_min_objective() -> ScalarMinObjective: """Build a ScalarMinObjective.""" - return ScalarMinObjective( - descriptor_key="z" - ) + return ScalarMinObjective(descriptor_key="z") def test_scalar_max_dumps(scalar_max_objective): diff --git a/tests/serialization/test_predictors.py b/tests/serialization/test_predictors.py index bc456a445..eee17047c 100644 --- a/tests/serialization/test_predictors.py +++ b/tests/serialization/test_predictors.py @@ -1,20 +1,20 @@ """Tests for citrine.informatics.predictors serialization.""" + from copy import deepcopy -from uuid import UUID import pytest -from . import predictor_serialization_check, valid_serialization_output, \ - predictor_node_serialization_check from citrine.informatics.descriptors import RealDescriptor from citrine.informatics.predictors import * +from . import predictor_node_serialization_check, valid_serialization_output + def test_auto_ml_deserialization(valid_auto_ml_predictor_data): """Ensure that a deserialized SimplePredictor looks sane.""" predictor: AutoMLPredictor = AutoMLPredictor.build(valid_auto_ml_predictor_data) - assert predictor.name == 'AutoML predictor' - assert predictor.description == 'Predicts z from input x' + assert predictor.name == "AutoML predictor" + assert predictor.description == "Predicts z from input x" assert len(predictor.inputs) == 1 assert predictor.inputs[0] == RealDescriptor("x", lower_bound=0, upper_bound=100, units="") assert len(predictor.outputs) == 1 @@ -24,8 +24,8 @@ def test_auto_ml_deserialization(valid_auto_ml_predictor_data): def test_polymorphic_auto_ml_deserialization(valid_auto_ml_predictor_data): """Ensure that a polymorphically deserialized SimplePredictor looks sane.""" predictor: AutoMLPredictor = PredictorNode.build(valid_auto_ml_predictor_data) - assert predictor.name == 'AutoML predictor' - assert predictor.description == 'Predicts z from input x' + assert predictor.name == "AutoML predictor" + assert predictor.description == "Predicts z from input x" assert len(predictor.inputs) == 1 assert predictor.inputs[0] == RealDescriptor("x", lower_bound=0, upper_bound=100, units="") assert len(predictor.outputs) == 1 @@ -42,8 +42,10 @@ def test_graph_serialization(valid_graph_predictor_data): graph_data_copy = deepcopy(valid_graph_predictor_data) predictor = GraphPredictor.build(valid_graph_predictor_data) serialized = predictor.dump() - assert serialized['instance']['predictors'] == graph_data_copy['data']['instance']['predictors'] - assert serialized == valid_serialization_output(graph_data_copy['data']) + serialized_predictors = serialized["instance"]["predictors"] + expected_predictors = graph_data_copy["data"]["instance"]["predictors"] + assert serialized_predictors == expected_predictors + assert serialized == valid_serialization_output(graph_data_copy["data"]) def test_expression_serialization(valid_expression_predictor_data): @@ -53,7 +55,9 @@ def test_expression_serialization(valid_expression_predictor_data): def test_ing_to_formulation_serialization(valid_ing_formulation_predictor_data): """Ensure that a serialized IngredientsToFormulationPredictor looks sane.""" - predictor_node_serialization_check(valid_ing_formulation_predictor_data, IngredientsToFormulationPredictor) + predictor_node_serialization_check( + valid_ing_formulation_predictor_data, IngredientsToFormulationPredictor + ) def test_mean_property_serialization(valid_mean_property_predictor_data): @@ -67,16 +71,20 @@ def test_simple_mixture_predictor_serialization(valid_simple_mixture_predictor_d def test_label_fractions_serialization(valid_label_fractions_predictor_data): """Ensure that a serialized LabelFractionPredictor looks sane.""" - predictor_node_serialization_check(valid_label_fractions_predictor_data, LabelFractionsPredictor) + predictor_node_serialization_check( + valid_label_fractions_predictor_data, LabelFractionsPredictor + ) def test_ingredient_fractions_serialization(valid_ingredient_fractions_predictor_data): - """"Ensure that a serialized IngredientsFractionsPredictor looks sane.""" - predictor_node_serialization_check(valid_ingredient_fractions_predictor_data, IngredientFractionsPredictor) + """ "Ensure that a serialized IngredientsFractionsPredictor looks sane.""" + predictor_node_serialization_check( + valid_ingredient_fractions_predictor_data, IngredientFractionsPredictor + ) def test_auto_ml_serialization(valid_auto_ml_predictor_data): - """"Ensure that a serialized AutoMLPredictor looks sane.""" + """ "Ensure that a serialized AutoMLPredictor looks sane.""" predictor_node_serialization_check(valid_auto_ml_predictor_data, AutoMLPredictor) diff --git a/tests/serialization/test_process_run.py b/tests/serialization/test_process_run.py index 06d538470..153b191a2 100644 --- a/tests/serialization/test_process_run.py +++ b/tests/serialization/test_process_run.py @@ -1,10 +1,12 @@ """Tests of the Process Run schema""" -import pytest + from uuid import uuid4 +import pytest from gemd.entity.attribute.condition import Condition from gemd.entity.value.nominal_real import NominalReal from gemd.entity.value.uniform_real import UniformReal + from citrine.resources.process_run import ProcessRun from citrine.resources.process_spec import ProcessSpec @@ -13,56 +15,77 @@ def valid_data(): """Return valid data used for these tests.""" return dict( - uids={'id': str(uuid4()), 'my_id': 'process1-v1'}, - name='Process 1', - tags=['baking::cakes', 'danger::low'], - notes='make sure to use oven mitts', - conditions=[{'name': 'oven temp', 'type': 'condition', 'notes': None, - 'template': None, 'origin': 'measured', 'file_links': [], - 'value': {'nominal': 203.0, 'units': 'dimensionless', 'type': 'nominal_real'} - }], + uids={"id": str(uuid4()), "my_id": "process1-v1"}, + name="Process 1", + tags=["baking::cakes", "danger::low"], + notes="make sure to use oven mitts", + conditions=[ + { + "name": "oven temp", + "type": "condition", + "notes": None, + "template": None, + "origin": "measured", + "file_links": [], + "value": {"nominal": 203.0, "units": "dimensionless", "type": "nominal_real"}, + } + ], parameters=[], - spec={'type': 'process_spec', 'name': 'Spec for proc 1', - 'uids': {'id': str(uuid4())}, 'file_links': [], 'notes': None, - 'conditions': [{'type': 'condition', 'name': 'oven temp', 'origin': 'specified', - 'template': None, 'notes': None, 'file_links': [], - 'value': {'type': 'uniform_real', 'units': 'dimensionless', - 'lower_bound': 175, 'upper_bound': 225 - } - }], - 'template': None, 'tags': [], 'parameters': [] - }, + spec={ + "type": "process_spec", + "name": "Spec for proc 1", + "uids": {"id": str(uuid4())}, + "file_links": [], + "notes": None, + "conditions": [ + { + "type": "condition", + "name": "oven temp", + "origin": "specified", + "template": None, + "notes": None, + "file_links": [], + "value": { + "type": "uniform_real", + "units": "dimensionless", + "lower_bound": 175, + "upper_bound": 225, + }, + } + ], + "template": None, + "tags": [], + "parameters": [], + }, file_links=[], - type='process_run', - source={ - "type": "performed_source", - "performed_by": "Marie Curie", - "performed_date": None - } + type="process_run", + source={"type": "performed_source", "performed_by": "Marie Curie", "performed_date": None}, ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Process Run looks sane.""" process_run: ProcessRun = ProcessRun.build(valid_data) - assert process_run.uids == {'id': valid_data['uids']['id'], 'my_id': 'process1-v1'} - assert process_run.tags == ['baking::cakes', 'danger::low'] - assert process_run.conditions[0] == Condition(name='oven temp', - value=NominalReal(203.0, ''), - origin='measured') + assert process_run.uids == {"id": valid_data["uids"]["id"], "my_id": "process1-v1"} + assert process_run.tags == ["baking::cakes", "danger::low"] + assert process_run.conditions[0] == Condition( + name="oven temp", value=NominalReal(203.0, ""), origin="measured" + ) assert process_run.parameters == [] assert process_run.file_links == [] assert process_run.template is None assert process_run.output_material is None - assert process_run.spec == \ - ProcessSpec(name="Spec for proc 1", tags=[], - uids={'id': valid_data['spec']['uids']['id']}, - conditions=[Condition(name='oven temp', value=UniformReal(175, 225, ''), - origin='specified')] - ) - assert process_run.name == 'Process 1' - assert process_run.notes == 'make sure to use oven mitts' - assert process_run.typ == 'process_run' + assert process_run.spec == ProcessSpec( + name="Spec for proc 1", + tags=[], + uids={"id": valid_data["spec"]["uids"]["id"]}, + conditions=[ + Condition(name="oven temp", value=UniformReal(175, 225, ""), origin="specified") + ], + ) + assert process_run.name == "Process 1" + assert process_run.notes == "make sure to use oven mitts" + assert process_run.typ == "process_run" def test_serialization(valid_data): diff --git a/tests/serialization/test_process_spec.py b/tests/serialization/test_process_spec.py index bf0f8b3e2..82c107d43 100644 --- a/tests/serialization/test_process_spec.py +++ b/tests/serialization/test_process_spec.py @@ -1,88 +1,115 @@ """Tests of the Process Run schema""" -import pytest -from uuid import uuid4, UUID +from uuid import UUID, uuid4 + +import pytest from gemd.entity.attribute.parameter import Parameter from gemd.entity.bounds.real_bounds import RealBounds -from gemd.entity.value.uniform_real import UniformReal from gemd.entity.file_link import FileLink +from gemd.entity.value.uniform_real import UniformReal + +from citrine.resources.parameter_template import ParameterTemplate from citrine.resources.process_spec import ProcessSpec from citrine.resources.process_template import ProcessTemplate -from citrine.resources.parameter_template import ParameterTemplate @pytest.fixture def valid_data(): """Return valid data used for these tests.""" return dict( - uids={'id': str(uuid4())}, - name='Process 1', - tags=['baking::cakes', 'danger::low'], - notes='make sure to use oven mitts', - parameters=[{'name': 'oven temp', 'type': 'parameter', - 'template': None, 'origin': 'specified', 'notes': None, 'file_links': [], - 'value': {'lower_bound': 195, 'upper_bound': 205, - 'units': 'dimensionless', 'type': 'uniform_real'} - }], + uids={"id": str(uuid4())}, + name="Process 1", + tags=["baking::cakes", "danger::low"], + notes="make sure to use oven mitts", + parameters=[ + { + "name": "oven temp", + "type": "parameter", + "template": None, + "origin": "specified", + "notes": None, + "file_links": [], + "value": { + "lower_bound": 195, + "upper_bound": 205, + "units": "dimensionless", + "type": "uniform_real", + }, + } + ], conditions=[], template={ - 'name': 'the template', - 'tags': [], - 'uids': {'id': str(uuid4())}, - 'type': 'process_template', - 'conditions': [], - 'parameters': [ + "name": "the template", + "tags": [], + "uids": {"id": str(uuid4())}, + "type": "process_template", + "conditions": [], + "parameters": [ [ { - 'type': 'parameter_template', - 'name': 'oven temp template', - 'tags': [], - 'bounds': {'type': 'real_bounds', 'lower_bound': 175, 'upper_bound': 225, 'default_units': 'dimensionless'}, - 'uids': {'id': str(uuid4())}, - 'description': None, + "type": "parameter_template", + "name": "oven temp template", + "tags": [], + "bounds": { + "type": "real_bounds", + "lower_bound": 175, + "upper_bound": 225, + "default_units": "dimensionless", + }, + "uids": {"id": str(uuid4())}, + "description": None, }, { - 'type': 'real_bounds', - 'lower_bound': 175, 'upper_bound': 225, 'default_units': 'dimensionless' - } + "type": "real_bounds", + "lower_bound": 175, + "upper_bound": 225, + "default_units": "dimensionless", + }, ] ], - 'allowed_labels': ['a', 'b'], - 'allowed_names': ['a name'], - 'description': 'a long description', + "allowed_labels": ["a", "b"], + "allowed_names": ["a name"], + "description": "a long description", }, - file_links=[{'type': 'file_link', 'filename': 'cake_recipe.txt', 'url': 'www.baking.com'}], - audit_info={'created_by': str(uuid4()), 'created_at': 1559933807392}, - type='process_spec' + file_links=[{"type": "file_link", "filename": "cake_recipe.txt", "url": "www.baking.com"}], + audit_info={"created_by": str(uuid4()), "created_at": 1559933807392}, + type="process_spec", ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Process Spec looks sane.""" process_spec: ProcessSpec = ProcessSpec.build(valid_data) - assert process_spec.uids == {'id': valid_data['uids']['id']} - assert process_spec.tags == ['baking::cakes', 'danger::low'] - assert process_spec.parameters[0] == Parameter(name='oven temp', - value=UniformReal(195, 205, ''), - origin='specified') + assert process_spec.uids == {"id": valid_data["uids"]["id"]} + assert process_spec.tags == ["baking::cakes", "danger::low"] + assert process_spec.parameters[0] == Parameter( + name="oven temp", value=UniformReal(195, 205, ""), origin="specified" + ) assert process_spec.conditions == [] - assert process_spec.template == \ - ProcessTemplate('the template', tags=[], - uids={'id': valid_data['template']['uids']['id']}, - parameters=[ - [ParameterTemplate('oven temp template', tags=[], - bounds=RealBounds(175, 225, ''), - uids={'id': valid_data['template']['parameters'][0][0]['uids']['id']}), - RealBounds(175, 225, '')] - ], - description='a long description', - allowed_labels=['a', 'b'], - allowed_names=['a name']) - assert process_spec.name == 'Process 1' - assert process_spec.notes == 'make sure to use oven mitts' - assert process_spec.file_links == [FileLink('cake_recipe.txt', 'www.baking.com')] - assert process_spec.typ == 'process_spec' - assert process_spec.audit_info.created_by == UUID(valid_data['audit_info']['created_by']) + assert process_spec.template == ProcessTemplate( + "the template", + tags=[], + uids={"id": valid_data["template"]["uids"]["id"]}, + parameters=[ + [ + ParameterTemplate( + "oven temp template", + tags=[], + bounds=RealBounds(175, 225, ""), + uids={"id": valid_data["template"]["parameters"][0][0]["uids"]["id"]}, + ), + RealBounds(175, 225, ""), + ] + ], + description="a long description", + allowed_labels=["a", "b"], + allowed_names=["a name"], + ) + assert process_spec.name == "Process 1" + assert process_spec.notes == "make sure to use oven mitts" + assert process_spec.file_links == [FileLink("cake_recipe.txt", "www.baking.com")] + assert process_spec.typ == "process_spec" + assert process_spec.audit_info.created_by == UUID(valid_data["audit_info"]["created_by"]) def test_serialization(valid_data): @@ -90,5 +117,5 @@ def test_serialization(valid_data): process_spec: ProcessSpec = ProcessSpec.build(valid_data) serialized = process_spec.dump() # Audit info & dataset are not included in the dump - serialized['audit_info'] = valid_data['audit_info'] + serialized["audit_info"] = valid_data["audit_info"] assert serialized == valid_data diff --git a/tests/serialization/test_project.py b/tests/serialization/test_project.py index 271a81b8e..edd419239 100644 --- a/tests/serialization/test_project.py +++ b/tests/serialization/test_project.py @@ -1,8 +1,11 @@ """Tests of the Project schema.""" + +from uuid import UUID, uuid4 + +import arrow import pytest -from uuid import uuid4, UUID + from citrine.resources.project import Project -import arrow @pytest.fixture @@ -11,19 +14,19 @@ def valid_data(): return dict( id=str(uuid4()), created_at=1559933807392, - name='my project', - description='a good project', - status='in-progress' + name="my project", + description="a good project", + status="in-progress", ) def test_simple_deserialization(valid_data): """Ensure that a deserialized Project looks sane.""" project: Project = Project.build(valid_data) - assert project.uid == UUID(valid_data['id']) - assert project.created_at == arrow.get(valid_data['created_at'] / 1000).datetime - assert project.name == 'my project' - assert project.status == 'in-progress' + assert project.uid == UUID(valid_data["id"]) + assert project.created_at == arrow.get(valid_data["created_at"] / 1000).datetime + assert project.name == "my project" + assert project.status == "in-progress" def test_serialization(valid_data): diff --git a/tests/serialization/test_reports.py b/tests/serialization/test_reports.py index b895c3d83..9c80fc45d 100644 --- a/tests/serialization/test_reports.py +++ b/tests/serialization/test_reports.py @@ -1,21 +1,21 @@ """Tests for citrine.informatics.reports serialization.""" -import logging -import pytest +import logging from copy import deepcopy -import warnings from uuid import UUID +import pytest + from citrine.informatics.descriptors import RealDescriptor -from citrine.informatics.reports import Report, ModelSummary, FeatureImportanceReport +from citrine.informatics.reports import ModelSummary, Report def test_predictor_report_build(valid_predictor_report_data): """Build a predictor report and verify its structure.""" report = Report.build(valid_predictor_report_data) - assert report.status == 'OK' - assert str(report.uid) == valid_predictor_report_data['id'] + assert report.status == "OK" + assert str(report.uid) == valid_predictor_report_data["id"] x = RealDescriptor("x", lower_bound=0, upper_bound=1, units="") y = RealDescriptor("y", lower_bound=0, upper_bound=100, units="") @@ -23,40 +23,38 @@ def test_predictor_report_build(valid_predictor_report_data): assert report.descriptors == [x, y, z] lolo_model: ModelSummary = report.model_summaries[0] - assert lolo_model.name == 'GeneralLoloModel_1' - assert lolo_model.type_ == 'ML Model' + assert lolo_model.name == "GeneralLoloModel_1" + assert lolo_model.type_ == "ML Model" assert lolo_model.inputs == [x] assert lolo_model.outputs == [y] assert lolo_model.model_settings == { - 'Algorithm': 'Ensemble of non-linear estimators', - 'Number of estimators': 64, - 'Leaf model': 'Mean', - 'Use jackknife': True + "Algorithm": "Ensemble of non-linear estimators", + "Number of estimators": 64, + "Leaf model": "Mean", + "Use jackknife": True, } feature_importance = lolo_model.feature_importances[0] assert feature_importance.importances == {"x": 1.0} assert feature_importance.output_key == "y" - assert lolo_model.predictor_name == 'Predict y from x with ML' + assert lolo_model.predictor_name == "Predict y from x with ML" assert lolo_model.predictor_uid is None exp_model: ModelSummary = report.model_summaries[1] - assert exp_model.name == 'GeneralLosslessModel_2' - assert exp_model.type_ == 'Analytic Model' + assert exp_model.name == "GeneralLosslessModel_2" + assert exp_model.type_ == "Analytic Model" assert exp_model.inputs == [x, y] assert exp_model.outputs == [z] - assert exp_model.model_settings == { - "Expression": "(z) <- (x + y)" - } + assert exp_model.model_settings == {"Expression": "(z) <- (x + y)"} assert exp_model.feature_importances == [] - assert exp_model.predictor_name == 'Expression for z' + assert exp_model.predictor_name == "Expression for z" assert exp_model.predictor_uid == UUID("249bf32c-6f3d-4a93-9387-94cc877f170c") def test_empty_report_build(): """Build a predictor report when the 'report' field is somehow unfilled.""" - Report.build(dict(id='7c2dda5d-675a-41b6-829c-e485163f0e43', status='PENDING')) - Report.build(dict(id='7c2dda5d-675a-41b6-829c-e485163f0e43', status='PENDING', report=None)) - Report.build(dict(id='7c2dda5d-675a-41b6-829c-e485163f0e43', status='PENDING', report=dict())) + Report.build(dict(id="7c2dda5d-675a-41b6-829c-e485163f0e43", status="PENDING")) + Report.build(dict(id="7c2dda5d-675a-41b6-829c-e485163f0e43", status="PENDING", report=None)) + Report.build(dict(id="7c2dda5d-675a-41b6-829c-e485163f0e43", status="PENDING", report=dict())) def test_bad_predictor_report_build(caplog, valid_predictor_report_data): @@ -64,7 +62,7 @@ def test_bad_predictor_report_build(caplog, valid_predictor_report_data): too_many_descriptors = deepcopy(valid_predictor_report_data) # Multiple descriptors with the same key other_x = RealDescriptor("x", lower_bound=0, upper_bound=100, units="") - too_many_descriptors['report']['descriptors'].append(other_x.dump()) + too_many_descriptors["report"]["descriptors"].append(other_x.dump()) with caplog.at_level(logging.WARNING): caplog.clear() Report.build(too_many_descriptors) @@ -73,9 +71,9 @@ def test_bad_predictor_report_build(caplog, valid_predictor_report_data): # A key that appears in inputs and/or outputs, but there is no corresponding descriptor. # This is done twice for coverage, once to catch a missing input and once for a missing output. too_few_descriptors = deepcopy(valid_predictor_report_data) - too_few_descriptors['report']['descriptors'].pop() + too_few_descriptors["report"]["descriptors"].pop() with pytest.raises(RuntimeError): Report.build(too_few_descriptors) - too_few_descriptors['report']['descriptors'] = [] + too_few_descriptors["report"]["descriptors"] = [] with pytest.raises(RuntimeError): Report.build(too_few_descriptors) diff --git a/tests/serialization/test_scorers.py b/tests/serialization/test_scorers.py index 8de71ff94..d73339382 100644 --- a/tests/serialization/test_scorers.py +++ b/tests/serialization/test_scorers.py @@ -1,8 +1,10 @@ """Tests for citrine.informatics.scores.""" + from citrine.informatics.objectives import ScalarMaxObjective -from citrine.informatics.scores import Score, EIScore, LIScore +from citrine.informatics.scores import EIScore, LIScore, Score -from tests.informatics.test_scores import li_score, ei_score +# Imported for use as pytest fixtures; not referenced directly. +from tests.informatics.test_scores import ei_score, li_score # noqa: F401 def test_li_dumps(li_score): diff --git a/tests/serialization/test_user.py b/tests/serialization/test_user.py index ee96ae108..931947627 100644 --- a/tests/serialization/test_user.py +++ b/tests/serialization/test_user.py @@ -1,6 +1,9 @@ """Tests of the Project schema.""" -import pytest + from uuid import uuid4 + +import pytest + from citrine.resources.user import User @@ -9,19 +12,19 @@ def valid_data(): """Return valid data used for these tests.""" return dict( id=str(uuid4()), - screen_name='bob', - position='the builder', - email='bob@thebuilder.com', - is_admin=True + screen_name="bob", + position="the builder", + email="bob@thebuilder.com", + is_admin=True, ) def test_simple_deserialization(valid_data): """Ensure a deserialized User looks sane.""" user: User = User.build(valid_data) - assert user.screen_name == 'bob' - assert user.position == 'the builder' - assert user.email == 'bob@thebuilder.com' + assert user.screen_name == "bob" + assert user.position == "the builder" + assert user.email == "bob@thebuilder.com" assert user.is_admin diff --git a/tests/serialization/test_workflow.py b/tests/serialization/test_workflow.py index 3a25a82bf..4154f0742 100644 --- a/tests/serialization/test_workflow.py +++ b/tests/serialization/test_workflow.py @@ -1,7 +1,10 @@ """Tests of the Project schema.""" -import pytest + from datetime import datetime -from uuid import uuid4, UUID +from uuid import UUID, uuid4 + +import pytest + from citrine.informatics.workflows import DesignWorkflow @@ -10,47 +13,50 @@ def valid_data(): """Return valid data used for these tests.""" return dict( id=str(uuid4()), - name='A rad new workflow', - description='All about my workflow', - status='SUCCEEDED', - status_description='READY', - status_detail=[{'level': 'Info', 'msg': 'Things are looking good'}], + name="A rad new workflow", + description="All about my workflow", + status="SUCCEEDED", + status_description="READY", + status_detail=[{"level": "Info", "msg": "Things are looking good"}], archived=False, design_space_id=str(uuid4()), predictor_id=str(uuid4()), created_by=str(uuid4()), - create_time=datetime(2020, 1, 1, 1, 1, 1, 1).isoformat("T") + create_time=datetime(2020, 1, 1, 1, 1, 1, 1).isoformat("T"), ) @pytest.fixture def valid_serialization_output(valid_data): - return {x: y for x, y in valid_data.items() if x not in - ['status', 'status_detail', 'status_description', 'created_by', 'create_time']} + return { + x: y + for x, y in valid_data.items() + if x not in ["status", "status_detail", "status_description", "created_by", "create_time"] + } def test_simple_deserialization(valid_data): """Ensure a deserialized DesignWorkflow looks sane.""" workflow: DesignWorkflow = DesignWorkflow.build(valid_data) - assert workflow.design_space_id == UUID(valid_data['design_space_id']) - assert workflow.predictor_id == UUID(valid_data['predictor_id']) + assert workflow.design_space_id == UUID(valid_data["design_space_id"]) + assert workflow.predictor_id == UUID(valid_data["predictor_id"]) def test_deserialization_missing_created_by(valid_data): """Ensure a DesignWorkflow can be deserialized with no created_by field.""" - valid_data['created_by'] = None + valid_data["created_by"] = None workflow: DesignWorkflow = DesignWorkflow.build(valid_data) - assert workflow.design_space_id == UUID(valid_data['design_space_id']) + assert workflow.design_space_id == UUID(valid_data["design_space_id"]) assert workflow.created_by is None def test_deserialization_missing_create_time(valid_data): """Ensure a DesignWorkflow can be deserialized with no created_by field.""" - valid_data['create_time'] = None + valid_data["create_time"] = None workflow: DesignWorkflow = DesignWorkflow.build(valid_data) - assert workflow.design_space_id == UUID(valid_data['design_space_id']) + assert workflow.design_space_id == UUID(valid_data["design_space_id"]) assert workflow.create_time is None @@ -58,7 +64,7 @@ def test_serialization(valid_data, valid_serialization_output): """Ensure a serialized DesignWorkflow looks sane.""" workflow: DesignWorkflow = DesignWorkflow.build(valid_data) serialized = workflow.dump() - serialized['id'] = valid_data['id'] + serialized["id"] = valid_data["id"] # we can have extra fields in the output of `dump` # these support forwards and backwards compatibility for k in valid_serialization_output: diff --git a/tests/test_citrine.py b/tests/test_citrine.py index 1b24c9221..7d07d4c07 100644 --- a/tests/test_citrine.py +++ b/tests/test_citrine.py @@ -10,10 +10,9 @@ def refresh_token(expiration: datetime = None) -> dict: token = jwt.encode( - payload={'exp': expiration.timestamp()}, - key='Actually_Triangle_Perhaps_Finally' + payload={"exp": expiration.timestamp()}, key="Actually_Triangle_Perhaps_Finally" ) - return {'access_token': token} + return {"access_token": token} token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc)) @@ -21,19 +20,19 @@ def refresh_token(expiration: datetime = None) -> dict: def test_citrine_creation(): with requests_mock.Mocker() as m: - m.post('https://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) + m.post("https://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) - assert '1234' == Citrine(api_key='1234', host='citrine-testing.fake').session.refresh_token + assert "1234" == Citrine(api_key="1234", host="citrine-testing.fake").session.refresh_token def test_citrine_signature(monkeypatch): with requests_mock.Mocker() as m: - m.post('http://citrine-testing.fake:8080/api/v1/tokens/refresh', json=token_refresh_response) + m.post( + "http://citrine-testing.fake:8080/api/v1/tokens/refresh", json=token_refresh_response + ) - assert '1234' == Citrine(api_key='1234', - scheme='http', - host='citrine-testing.fake', - port="8080").session.refresh_token + citrine = Citrine(api_key="1234", scheme="http", host="citrine-testing.fake", port="8080") + assert citrine.session.refresh_token == "1234" # Validate defaults with requests_mock.Mocker() as m: @@ -41,7 +40,7 @@ def test_citrine_signature(monkeypatch): patched_host = "monkeypatch.citrine-testing.fake" monkeypatch.setenv("CITRINE_API_KEY", patched_key) monkeypatch.setenv("CITRINE_API_HOST", patched_host) - m.post(f'https://{patched_host}/api/v1/tokens/refresh', json=token_refresh_response) + m.post(f"https://{patched_host}/api/v1/tokens/refresh", json=token_refresh_response) assert patched_key == Citrine().session.refresh_token assert patched_key == Citrine(api_key=patched_key).session.refresh_token @@ -55,45 +54,45 @@ def test_citrine_signature(monkeypatch): def test_citrine_project_session(): with requests_mock.Mocker() as m: - m.post('https://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) + m.post("https://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) - citrine = Citrine(api_key='foo', host='citrine-testing.fake') + citrine = Citrine(api_key="foo", host="citrine-testing.fake") assert citrine.session == citrine.projects.session def test_citrine_user_session(): with requests_mock.Mocker() as m: - m.post('https://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - citrine = Citrine(api_key='foo', host='citrine-testing.fake') + m.post("https://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + citrine = Citrine(api_key="foo", host="citrine-testing.fake") assert citrine.session == citrine.users.session def test_citrine_team_session(): with requests_mock.Mocker() as m: - m.post('https://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - citrine = Citrine(api_key='foo', host='citrine-testing.fake') + m.post("https://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + citrine = Citrine(api_key="foo", host="citrine-testing.fake") assert citrine.session == citrine.teams.session def test_citrine_catalyst_session(): with requests_mock.Mocker() as m: - m.post('https://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - citrine = Citrine(api_key='foo', host='citrine-testing.fake') + m.post("https://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + citrine = Citrine(api_key="foo", host="citrine-testing.fake") assert citrine.session == citrine.catalyst.session def test_citrine_user_agent(): with requests_mock.Mocker() as m: - m.post('https://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - citrine = Citrine(api_key='foo', host='citrine-testing.fake') + m.post("https://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + citrine = Citrine(api_key="foo", host="citrine-testing.fake") - agent_parts = citrine.session.headers['User-Agent'].split() - python_impls = {'CPython', 'IronPython', 'Jython', 'PyPy'} - expected_products = {'python-requests', 'citrine-python'} + agent_parts = citrine.session.headers["User-Agent"].split() + python_impls = {"CPython", "IronPython", "Jython", "PyPy"} + expected_products = {"python-requests", "citrine-python"} for product in agent_parts: - product_name, product_version = product.split('/') + product_name, product_version = product.split("/") assert product_name in {*python_impls, *expected_products} if product_name in python_impls: @@ -102,4 +101,4 @@ def test_citrine_user_agent(): # Check that the version is major.minor.patch but don't # enforce them to be ints. It's common to see strings used # as the patch version - assert len(product_version.split('.')) == 3 + assert len(product_version.split(".")) == 3 diff --git a/tests/test_session.py b/tests/test_session.py index e85e83d69..0da6a701d 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,10 +1,10 @@ -import jwt from datetime import datetime, timedelta, timezone +from unittest import mock -import mock +import jwt +import pytest import requests import requests_mock -import pytest from citrine._session import Session from citrine.exceptions import ( @@ -13,32 +13,26 @@ NonRetryableException, NotFound, RetryableException, - WorkflowNotReadyException, Unauthorized, - UnauthorizedRefreshToken, + UnauthorizedRefreshToken, + WorkflowNotReadyException, ) - from tests.utils.session import make_fake_cursor_request_function def refresh_token(expiration: datetime = None) -> dict: token = jwt.encode( - payload={'exp': expiration.timestamp()}, - key='Actually_Triangle_Perhaps_Finally' + payload={"exp": expiration.timestamp()}, key="Actually_Triangle_Perhaps_Finally" ) - return {'access_token': token} + return {"access_token": token} @pytest.fixture def session(): token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc)) with requests_mock.Mocker() as m: - m.post('http://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - session = Session( - refresh_token='12345', - scheme='http', - host='citrine-testing.fake' - ) + m.post("http://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + session = Session(refresh_token="12345", scheme="http", host="citrine-testing.fake") # Default behavior is to *not* require a refresh - those tests can clear this out # As rule of thumb, we should be using freezegun or similar to never rely on the system clock # for these scenarios, but I thought this is light enough to postpone that for the time being @@ -50,12 +44,14 @@ def session(): def test_session_signature(monkeypatch): token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc)) with requests_mock.Mocker() as m: - m.post('ftp://citrine-testing.fake:8080/api/v1/tokens/refresh', json=token_refresh_response) + m.post( + "ftp://citrine-testing.fake:8080/api/v1/tokens/refresh", json=token_refresh_response + ) - assert '1234' == Session(refresh_token='1234', - scheme='ftp', - host='citrine-testing.fake', - port="8080").refresh_token + session = Session( + refresh_token="1234", scheme="ftp", host="citrine-testing.fake", port="8080" + ) + assert "1234" == session.refresh_token # Validate defaults with requests_mock.Mocker() as m: @@ -63,7 +59,7 @@ def test_session_signature(monkeypatch): patched_host = "monkeypatch.citrine-testing.fake" monkeypatch.setenv("CITRINE_API_KEY", patched_key) monkeypatch.setenv("CITRINE_API_HOST", patched_host) - m.post(f'https://{patched_host}/api/v1/tokens/refresh', json=token_refresh_response) + m.post(f"https://{patched_host}/api/v1/tokens/refresh", json=token_refresh_response) assert patched_key == Session().refresh_token assert patched_key == Session(refresh_token=patched_key).refresh_token @@ -80,14 +76,16 @@ def test_get_refreshes_token(session: Session): token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc)) with requests_mock.Mocker() as m: - m.post('http://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - m.get('http://citrine-testing.fake/api/v1/foo', - json={'foo': 'bar'}, - headers={'content-type': "application/json"}) + m.post("http://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + m.get( + "http://citrine-testing.fake/api/v1/foo", + json={"foo": "bar"}, + headers={"content-type": "application/json"}, + ) - resp = session.get_resource('/foo') + resp = session.get_resource("/foo") - assert {'foo': 'bar'} == resp + assert {"foo": "bar"} == resp assert datetime(2019, 3, 14, tzinfo=timezone.utc) == session.access_token_expiration @@ -95,37 +93,41 @@ def test_get_refresh_token_failure(session: Session): session.access_token_expiration = datetime.now(timezone.utc) - timedelta(minutes=1) with requests_mock.Mocker() as m: - m.post('http://citrine-testing.fake/api/v1/tokens/refresh', status_code=401) + m.post("http://citrine-testing.fake/api/v1/tokens/refresh", status_code=401) with pytest.raises(UnauthorizedRefreshToken): - session.get_resource('/foo') + session.get_resource("/foo") def test_get_no_refresh(session: Session): with requests_mock.Mocker() as m: - m.get('http://citrine-testing.fake/api/v1/foo', json={'foo': 'bar'}, headers={'content-type': "application/json"}) - resp = session.get_resource('/foo') + m.get( + "http://citrine-testing.fake/api/v1/foo", + json={"foo": "bar"}, + headers={"content-type": "application/json"}, + ) + resp = session.get_resource("/foo") - assert {'foo': 'bar'} == resp + assert {"foo": "bar"} == resp def test_get_not_found(session: Session): with requests_mock.Mocker() as m: - m.get('http://citrine-testing.fake/api/v1/foo', status_code=404) + m.get("http://citrine-testing.fake/api/v1/foo", status_code=404) with pytest.raises(NotFound): - session.get_resource('/foo') + session.get_resource("/foo") def test_status_code_409(session: Session): with requests_mock.Mocker() as m: - url = '/foo' - conflict_message = 'you have a conflict' + url = "/foo" + conflict_message = "you have a conflict" resp_json = { - 'code': 409, - 'message': 'a message', - 'validation_errors': [{'failure_message': conflict_message}] + "code": 409, + "message": "a message", + "validation_errors": [{"failure_message": conflict_message}], } - m.get('http://citrine-testing.fake/api/v1/foo', status_code=409, json=resp_json) + m.get("http://citrine-testing.fake/api/v1/foo", status_code=409, json=resp_json) with pytest.raises(Conflict) as einfo: session.get_resource(url) @@ -136,62 +138,64 @@ def test_status_code_409(session: Session): def test_status_code_425(session: Session): with requests_mock.Mocker() as m: - m.get('http://citrine-testing.fake/api/v1/foo', status_code=425) + m.get("http://citrine-testing.fake/api/v1/foo", status_code=425) with pytest.raises(RetryableException): - session.get_resource('/foo') + session.get_resource("/foo") with pytest.raises(WorkflowNotReadyException): - session.get_resource('/foo') + session.get_resource("/foo") def test_status_code_400(session: Session): with requests_mock.Mocker() as m: resp_json = { - 'code': 400, - 'message': 'a message', - 'validation_errors': [ - { - 'failure_message': 'you have failed', - }, - ], + "code": 400, + "message": "a message", + "validation_errors": [{"failure_message": "you have failed"}], } - m.get('http://citrine-testing.fake/api/v1/foo', - status_code=400, - json=resp_json - ) + m.get("http://citrine-testing.fake/api/v1/foo", status_code=400, json=resp_json) with pytest.raises(BadRequest) as einfo: - session.get_resource('/foo') - assert einfo.value.api_error.validation_errors[0].failure_message \ - == resp_json['validation_errors'][0]['failure_message'] + session.get_resource("/foo") + actual = einfo.value.api_error.validation_errors[0].failure_message + expected = resp_json["validation_errors"][0]["failure_message"] + assert actual == expected def test_status_code_401(session: Session): with requests_mock.Mocker() as m: - m.get('http://citrine-testing.fake/api/v1/foo', status_code=401) + m.get("http://citrine-testing.fake/api/v1/foo", status_code=401) with pytest.raises(NonRetryableException): - session.get_resource('/foo') + session.get_resource("/foo") with pytest.raises(Unauthorized): - session.get_resource('/foo') + session.get_resource("/foo") def test_status_code_404(session: Session): with requests_mock.Mocker() as m: - m.get('http://citrine-testing.fake/api/v1/foo', status_code=404) + m.get("http://citrine-testing.fake/api/v1/foo", status_code=404) with pytest.raises(NonRetryableException): - session.get_resource('/foo') + session.get_resource("/foo") def test_connection_error(session: Session): - data = {'stuff': 'not_used'} + data = {"stuff": "not_used"} # Simulate a request using a stale session that raises # a ConnectionError then works on the second call. with requests_mock.Mocker() as m: - m.register_uri('GET', - 'http://citrine-testing.fake/api/v1/foo', - [{'exc': requests.exceptions.ConnectionError}, - {'json': data, 'status_code': 200, 'headers': {'content-type': "application/json"}}]) + m.register_uri( + "GET", + "http://citrine-testing.fake/api/v1/foo", + [ + {"exc": requests.exceptions.ConnectionError}, + { + "json": data, + "status_code": 200, + "headers": {"content-type": "application/json"}, + }, + ], + ) - resp = session.get_resource('/foo') + resp = session.get_resource("/foo") assert resp == data @@ -199,58 +203,62 @@ def test_post_refreshes_token_when_denied(session: Session): token_refresh_response = refresh_token(datetime(2019, 3, 14, tzinfo=timezone.utc)) with requests_mock.Mocker() as m: - m.post('http://citrine-testing.fake/api/v1/tokens/refresh', json=token_refresh_response) - m.register_uri('POST', 'http://citrine-testing.fake/api/v1/foo', [ - {'status_code': 401, 'json': {'reason': 'invalid-token'}}, - {'json': {'foo': 'bar'}, 'headers': {'content-type': "application/json"}} - ]) + m.post("http://citrine-testing.fake/api/v1/tokens/refresh", json=token_refresh_response) + m.register_uri( + "POST", + "http://citrine-testing.fake/api/v1/foo", + [ + {"status_code": 401, "json": {"reason": "invalid-token"}}, + {"json": {"foo": "bar"}, "headers": {"content-type": "application/json"}}, + ], + ) - resp = session.post_resource('/foo', json={'data': 'hi'}) + resp = session.post_resource("/foo", json={"data": "hi"}) - assert {'foo': 'bar'} == resp + assert {"foo": "bar"} == resp assert datetime(2019, 3, 14, tzinfo=timezone.utc) == session.access_token_expiration # this test exists to provide 100% coverage for the legacy 401 status on Unauthorized responses def test_delete_unauthorized_without_json_legacy(session: Session): with requests_mock.Mocker() as m: - m.delete('http://citrine-testing.fake/api/v1/bar/something', status_code=401) + m.delete("http://citrine-testing.fake/api/v1/bar/something", status_code=401) with pytest.raises(Unauthorized): - session.delete_resource('/bar/something') + session.delete_resource("/bar/something") def test_delete_unauthorized_with_str_json_legacy(session: Session): with requests_mock.Mocker() as m: m.delete( - 'http://citrine-testing.fake/api/v1/bar/something', + "http://citrine-testing.fake/api/v1/bar/something", status_code=401, - json='an error string' + json="an error string", ) with pytest.raises(Unauthorized): - session.delete_resource('/bar/something') + session.delete_resource("/bar/something") def test_delete_unauthorized_without_json(session: Session): with requests_mock.Mocker() as m: - m.delete('http://citrine-testing.fake/api/v1/bar/something', status_code=403) + m.delete("http://citrine-testing.fake/api/v1/bar/something", status_code=403) with pytest.raises(Unauthorized): - session.delete_resource('/bar/something') + session.delete_resource("/bar/something") def test_failed_put_with_stacktrace(session: Session): with mock.patch("time.sleep", return_value=None): with requests_mock.Mocker() as m: m.put( - 'http://citrine-testing.fake/api/v1/bad-endpoint', + "http://citrine-testing.fake/api/v1/bad-endpoint", status_code=500, - json={'debug_stacktrace': 'blew up!'} + json={"debug_stacktrace": "blew up!"}, ) with pytest.raises(Exception) as e: - session.put_resource('/bad-endpoint', json={}) + session.put_resource("/bad-endpoint", json={}) assert '{"debug_stacktrace": "blew up!"}' == str(e.value) @@ -261,37 +269,45 @@ def test_cursor_paged_resource(): fake_request = make_fake_cursor_request_function(full_result_set) # varying page size should not affect final result - assert list(Session.cursor_paged_resource(fake_request, 'foo', forward=True, per_page=10)) == full_result_set - assert list(Session.cursor_paged_resource(fake_request, 'foo', forward=True, per_page=26)) == full_result_set - assert list(Session.cursor_paged_resource(fake_request, 'foo', forward=True, per_page=40)) == full_result_set + for per_page in (10, 26, 40): + result = list( + Session.cursor_paged_resource(fake_request, "foo", forward=True, per_page=per_page) + ) + assert result == full_result_set def test_bad_json_response(session: Session): with requests_mock.Mocker() as m: - m.delete('http://citrine-testing.fake/api/v1/bar/something', - status_code=200, - headers={'content-type': "application/json"}) - response_json = session.delete_resource('/bar/something') + m.delete( + "http://citrine-testing.fake/api/v1/bar/something", + status_code=200, + headers={"content-type": "application/json"}, + ) + response_json = session.delete_resource("/bar/something") assert response_json == {} def test_good_json_response(session: Session): with requests_mock.Mocker() as m: json_to_validate = {"bar": "something"} - m.put('http://citrine-testing.fake/api/v1/bar/something', - status_code=200, - json=json_to_validate, - headers={'content-type': "application/json"}) - response_json = session.put_resource('bar/something', {"ignored": "true"}) + m.put( + "http://citrine-testing.fake/api/v1/bar/something", + status_code=200, + json=json_to_validate, + headers={"content-type": "application/json"}, + ) + response_json = session.put_resource("bar/something", {"ignored": "true"}) assert response_json == json_to_validate def test_patch(session: Session): with requests_mock.Mocker() as m: json_to_validate = {"bar": "something"} - m.patch('http://citrine-testing.fake/api/v1/bar/something', - status_code=200, - json=json_to_validate, - headers={'content-type': "application/json"}) - response_json = session.patch_resource('bar/something', {"ignored": "true"}) + m.patch( + "http://citrine-testing.fake/api/v1/bar/something", + status_code=200, + json=json_to_validate, + headers={"content-type": "application/json"}, + ) + response_json = session.patch_resource("bar/something", {"ignored": "true"}) assert response_json == json_to_validate diff --git a/tests/utils/factories.py b/tests/utils/factories.py index f897b9910..0ea4c8270 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -4,15 +4,17 @@ # Naming convention here is to use "*DataFactory" for dictionaries used as API input/out, and # Factory for the domain objects themselves +from random import randint, random + import arrow import factory from faker.providers.date_time import Provider -from random import random, randint -from typing import Set, Optional +from gemd import EmpiricalFormula, FileLink, LinkByUID +from gemd.enumeration import SampleType -from citrine.gemd_queries.gemd_query import * from citrine.gemd_queries.criteria import * from citrine.gemd_queries.filter import * +from citrine.gemd_queries.gemd_query import * from citrine.informatics.scores import LIScore from citrine.informatics.workflows import DesignWorkflow from citrine.jobs.job import JobStatus @@ -25,12 +27,9 @@ from citrine.resources.process_template import ProcessTemplate from citrine.resources.table_config import TableConfigInitiator -from gemd import LinkByUID, EmpiricalFormula, FileLink -from gemd.enumeration import SampleType - class AugmentedProvider(Provider): - def random_formula(self, count: int = None, elements: Set[str] = None) -> str: + def random_formula(self, count: int = None, elements: set[str] = None) -> str: """Generate a random, well-formed chemical formula. Likely non-physical.""" if not elements: # None or empty elements = list(EmpiricalFormula.all_elements()) # Must be Sequence @@ -38,7 +37,9 @@ def random_formula(self, count: int = None, elements: Set[str] = None) -> str: count = self.generator.random.randrange(1, 5) components = sorted(self.generator.random.sample(elements, count)) # Use weights to bias toward looking more real-ish - amounts = self.generator.random.choices([1, 2, 3, 4, 5], weights=[40, 40, 10, 10, 2], k=count) + amounts = self.generator.random.choices( + [1, 2, 3, 4, 5], weights=[40, 40, 10, 10, 2], k=count + ) return "".join(f"({c}){a}" for c, a in zip(components, amounts)) def random_smiles(self) -> str: @@ -53,7 +54,7 @@ def random_smiles(self) -> str: "F": 1, "Cl": 1, "Br": 1, - "I": 1 + "I": 1, } valence = { "B": 3, @@ -65,9 +66,9 @@ def random_smiles(self) -> str: "F": 1, "Cl": 1, "Br": 1, - "I": 1 + "I": 1, } - bonds = ['', '=', '#', '$'] + bonds = ["", "=", "#", "$"] elements = list(element_weights) weights = list(element_weights.values()) @@ -83,10 +84,12 @@ def random_smiles(self) -> str: else: atom = self.generator.random.choices(elements, weights=weights)[0] max_bond = max(valence[atom], remain[-1]) - bond = 1 + self.generator.random.choices( - range(max_bond), - weights=[0.1 ** i for i in range(max_bond)] - )[0] + bond = ( + 1 + + self.generator.random.choices( + range(max_bond), weights=[0.1**i for i in range(max_bond)] + )[0] + ) remain[-1] -= bond if remain[-1] > 1 and self.generator.random.randrange(3 ** len(remain)) == 0: # Branch @@ -98,9 +101,7 @@ def random_smiles(self) -> str: return smiles[:-1] # Always has a superfluous ) at the end def unix_milliseconds( - self, - end_milliseconds: Optional[int] = None, - start_milliseconds: Optional[int] = None, + self, end_milliseconds: int | None = None, start_milliseconds: int | None = None ) -> float: """ Get a timestamp in milliseconds between January 1, 1970 and now, unless @@ -131,19 +132,19 @@ class UserTimestampDataFactory(factory.DictFactory): class TeamDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') - name = factory.Faker('company') - description = factory.Faker('catch_phrase') + id = factory.Faker("uuid4") + name = factory.Faker("company") + description = factory.Faker("catch_phrase") created_at = factory.Faker("unix_milliseconds") class ProjectDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') - name = factory.Faker('company') - description = factory.Faker('catch_phrase') - status = 'CREATED' + id = factory.Faker("uuid4") + name = factory.Faker("company") + description = factory.Faker("catch_phrase") + status = "CREATED" created_at = factory.Faker("unix_milliseconds") - team_id = factory.Faker('uuid4') + team_id = factory.Faker("uuid4") class DataVersionUpdateFactory(factory.DictFactory): @@ -152,8 +153,8 @@ class DataVersionUpdateFactory(factory.DictFactory): class PredictorRefFactory(factory.DictFactory): - predictor_id = factory.Faker('uuid4') - predictor_version = factory.Faker('random_digit_not_null') + predictor_id = factory.Faker("uuid4") + predictor_version = factory.Faker("random_digit_not_null") class BranchDataUpdateFactory(factory.DictFactory): @@ -167,47 +168,47 @@ class NextBranchVersionFactory(factory.DictFactory): class BranchDataFieldFactory(factory.DictFactory): - name = factory.Faker('company') + name = factory.Faker("company") class BranchMetadataFieldFactory(factory.DictFactory): - root_id = factory.Faker('uuid4') - archived = factory.Faker('boolean') - version = factory.Faker('random_digit_not_null') + root_id = factory.Faker("uuid4") + archived = factory.Faker("boolean") + version = factory.Faker("random_digit_not_null") created = factory.SubFactory(UserTimestampDataFactory) updated = factory.SubFactory(UserTimestampDataFactory) class BranchDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") data = factory.SubFactory(BranchDataFieldFactory) metadata = factory.SubFactory(BranchMetadataFieldFactory) class BranchVersionRefFactory(factory.DictFactory): - id = factory.Faker('uuid4') - version = factory.Faker('random_digit_not_null') + id = factory.Faker("uuid4") + version = factory.Faker("random_digit_not_null") class BranchRootMetadataFieldFactory(factory.DictFactory): latest_branch_version = factory.SubFactory(BranchVersionRefFactory) - archived = factory.Faker('boolean') + archived = factory.Faker("boolean") created = factory.SubFactory(UserTimestampDataFactory) updated = factory.SubFactory(UserTimestampDataFactory) class BranchRootDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") data = factory.SubFactory(BranchDataFieldFactory) metadata = factory.SubFactory(BranchRootMetadataFieldFactory) class UserDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') - screen_name = factory.Faker('name') - position = factory.Faker('job') - email = factory.Faker('email') - is_admin = factory.Faker('boolean') + id = factory.Faker("uuid4") + screen_name = factory.Faker("name") + position = factory.Faker("job") + email = factory.Faker("email") + is_admin = factory.Faker("boolean") class GemTableDataFactory(factory.DictFactory): @@ -218,9 +219,9 @@ class GemTableDataFactory(factory.DictFactory): * table-configs/{table_config_uid_str}/gem-tables """ - id = factory.Faker('uuid4') - version = factory.Faker('random_digit_not_null') - signed_download_url = factory.Faker('uri') + id = factory.Faker("uuid4") + version = factory.Faker("random_digit_not_null") + signed_download_url = factory.Faker("uri") class ListGemTableVersionsDataFactory(factory.DictFactory): @@ -230,17 +231,20 @@ class ListGemTableVersionsDataFactory(factory.DictFactory): * gem-tables/ * gem-tables/{table_identity_id} """ + # Explicitly set version numbers so that they are distinct - tables = factory.List([ - factory.SubFactory(GemTableDataFactory, version=1), - factory.SubFactory(GemTableDataFactory, version=4), - factory.SubFactory(GemTableDataFactory, version=2), - ]) + tables = factory.List( + [ + factory.SubFactory(GemTableDataFactory, version=1), + factory.SubFactory(GemTableDataFactory, version=4), + factory.SubFactory(GemTableDataFactory, version=2), + ] + ) class RealFilterDataFactory(factory.DictFactory): type = AllRealFilter.typ - unit = 'dimensionless' + unit = "dimensionless" lower = factory.LazyAttribute(lambda o: min(0, 2 * o.upper) + random() * o.upper) upper = factory.Faker("pyfloat") @@ -253,114 +257,114 @@ class IntegerFilterDataFactory(factory.DictFactory): class CategoryFilterDataFactory(factory.DictFactory): type = NominalCategoricalFilter.typ - categories = factory.Faker('words', unique=True) + categories = factory.Faker("words", unique=True) class PropertiesCriteriaDataFactory(factory.DictFactory): type = PropertiesCriteria.typ - property_templates_filter = factory.List([factory.Faker('uuid4')]) + property_templates_filter = factory.List([factory.Faker("uuid4")]) value_type_filter = factory.SubFactory(RealFilterDataFactory) class Params: - integer = factory.Trait( - value_type_filter=factory.SubFactory(IntegerFilterDataFactory) - ) - category = factory.Trait( - value_type_filter=factory.SubFactory(CategoryFilterDataFactory) - ) + integer = factory.Trait(value_type_filter=factory.SubFactory(IntegerFilterDataFactory)) + category = factory.Trait(value_type_filter=factory.SubFactory(CategoryFilterDataFactory)) class NameCriteriaDataFactory(factory.DictFactory): type = NameCriteria.typ - name = factory.Faker('word') - search_type = factory.Faker('enum', enum_cls=TextSearchType) + name = factory.Faker("word") + search_type = factory.Faker("enum", enum_cls=TextSearchType) class MaterialRunClassificationCriteriaDataFactory(factory.DictFactory): type = MaterialRunClassificationCriteria.typ classifications = factory.Faker( - 'random_elements', - elements=[str(x) for x in MaterialClassification], - unique=True + "random_elements", elements=[str(x) for x in MaterialClassification], unique=True ) class MaterialTemplatesCriteriaDataFactory(factory.DictFactory): type = MaterialTemplatesCriteria.typ - material_templates_identifiers = factory.List([factory.Faker('uuid4')]) - tag_filters = factory.Faker('words', unique=True) + material_templates_identifiers = factory.List([factory.Faker("uuid4")]) + tag_filters = factory.Faker("words", unique=True) class ConnectivityClassCriteriaDataFactory(factory.DictFactory): type = ConnectivityClassCriteria.typ - is_consumed = factory.Faker('boolean') - is_produced = factory.Faker('boolean') + is_consumed = factory.Faker("boolean") + is_produced = factory.Faker("boolean") class TagsCriteriaDataFactory(factory.DictFactory): type = TagsCriteria.typ - tags = factory.Faker('words', unique=True) - filter_type = factory.Faker('enum', enum_cls=TagFilterType) + tags = factory.Faker("words", unique=True) + filter_type = factory.Faker("enum", enum_cls=TagFilterType) class AndOperatorCriteriaDataFactory(factory.DictFactory): type = AndOperator.typ - criteria = factory.List([ - factory.SubFactory(NameCriteriaDataFactory), - factory.SubFactory(MaterialRunClassificationCriteriaDataFactory), - factory.SubFactory(MaterialTemplatesCriteriaDataFactory) - ]) + criteria = factory.List( + [ + factory.SubFactory(NameCriteriaDataFactory), + factory.SubFactory(MaterialRunClassificationCriteriaDataFactory), + factory.SubFactory(MaterialTemplatesCriteriaDataFactory), + ] + ) class OrOperatorCriteriaDataFactory(factory.DictFactory): type = OrOperator.typ - criteria = factory.List([ - factory.SubFactory(PropertiesCriteriaDataFactory), - factory.SubFactory(PropertiesCriteriaDataFactory, integer=True), - factory.SubFactory(PropertiesCriteriaDataFactory, category=True), - factory.SubFactory(AndOperatorCriteriaDataFactory) - ]) + criteria = factory.List( + [ + factory.SubFactory(PropertiesCriteriaDataFactory), + factory.SubFactory(PropertiesCriteriaDataFactory, integer=True), + factory.SubFactory(PropertiesCriteriaDataFactory, category=True), + factory.SubFactory(AndOperatorCriteriaDataFactory), + ] + ) class GemdQueryDataFactory(factory.DictFactory): criteria = factory.List([factory.SubFactory(OrOperatorCriteriaDataFactory)]) - datasets = factory.List([factory.Faker('uuid4')]) + datasets = factory.List([factory.Faker("uuid4")]) object_types = factory.List([str(x) for x in GemdObjectType]) schema_version = 1 class TableConfigMainMetaDataDataFactory(factory.DictFactory): """This is the metadata for the primary definition ID of the TableConfig.""" - id = factory.Faker('uuid4') + + id = factory.Faker("uuid4") deleted = False create_time = factory.Faker("unix_milliseconds") - created_by = factory.Faker('uuid4') + created_by = factory.Faker("uuid4") update_time = factory.Faker("unix_milliseconds") - updated_by = factory.Faker('uuid4') + updated_by = factory.Faker("uuid4") class TableConfigDataFactory(factory.DictFactory): """This is simply the Blob stored in a Table Config Version.""" + name = factory.Faker("company") - description = factory.Faker('bs') + description = factory.Faker("bs") # TODO Create factories for definitions rows = [] columns = [] variables = [] - datasets = factory.List([factory.Faker('uuid4')]) + datasets = factory.List([factory.Faker("uuid4")]) gemd_query = factory.SubFactory(GemdQueryDataFactory) class TableConfigVersionMetaDataDataFactory(factory.DictFactory): ara_definition = factory.SubFactory(TableConfigDataFactory) - id = factory.Faker('uuid4') - definition_id = factory.Faker('uuid4') - version_number = factory.Faker('random_digit_not_null') + id = factory.Faker("uuid4") + definition_id = factory.Faker("uuid4") + version_number = factory.Faker("random_digit_not_null") deleted = False create_time = factory.Faker("unix_milliseconds") - created_by = factory.Faker('uuid4') + created_by = factory.Faker("uuid4") update_time = factory.Faker("unix_milliseconds") - updated_by = factory.Faker('uuid4') + updated_by = factory.Faker("uuid4") initiator = str(TableConfigInitiator.CITRINE_PYTHON) @@ -371,28 +375,34 @@ class TableConfigResponseDataFactory(factory.DictFactory): * projects/{project_id}/display-tables/{uid}/versions/{version}/definition """ + definition = factory.SubFactory(TableConfigMainMetaDataDataFactory) version = factory.SubFactory(TableConfigVersionMetaDataDataFactory) class ListTableConfigResponseDataFactory(factory.DictFactory): """This encapsulates all of the versions of a table config object.""" + definition = factory.SubFactory(TableConfigMainMetaDataDataFactory) # Explicitly set version numbers so that they are distinct - versions = factory.List([ - factory.SubFactory(TableConfigVersionMetaDataDataFactory, version_number=1), - factory.SubFactory(TableConfigVersionMetaDataDataFactory, version_number=4), - factory.SubFactory(TableConfigVersionMetaDataDataFactory, version_number=2), - ]) + versions = factory.List( + [ + factory.SubFactory(TableConfigVersionMetaDataDataFactory, version_number=1), + factory.SubFactory(TableConfigVersionMetaDataDataFactory, version_number=4), + factory.SubFactory(TableConfigVersionMetaDataDataFactory, version_number=2), + ] + ) class TableDataSourceDataFactory(factory.DictFactory): type = "hosted_table_data_source" table_id = factory.Faker("uuid4") - table_version = factory.Faker('random_digit_not_null') + table_version = factory.Faker("random_digit_not_null") + from citrine.informatics.data_sources import GemTableDataSource + class TableDataSourceFactory(factory.Factory): class Meta: model = GemTableDataSource @@ -436,7 +446,7 @@ class PredictorDataDataFactory(factory.DictFactory): class PredictorEntityDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") data = factory.SubFactory(PredictorDataDataFactory) metadata = factory.SubFactory(PredictorMetadataDataFactory) @@ -458,7 +468,7 @@ class AsyncDefaultPredictorResponseDataFactory(factory.DictFactory): class AsyncDefaultPredictorResponseFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") metadata = factory.SubFactory(AsyncDefaultPredictorResponseMetadataFactory) data = factory.SubFactory(AsyncDefaultPredictorResponseDataFactory) @@ -493,9 +503,9 @@ class AreaUnderROCFactory(factory.DictFactory): class CoverageProbabilityFactory(factory.DictFactory): class Meta: - exclude = ("_level", ) + exclude = ("_level",) - _level = factory.Faker('pyfloat', max_value=1, min_value=0) + _level = factory.Faker("pyfloat", max_value=1, min_value=0) coverage_level = factory.LazyAttribute(lambda o: str(o._level)) type = "CoverageProbability" @@ -503,17 +513,21 @@ class Meta: class CrossValidationEvaluatorFactory(factory.DictFactory): name = factory.Faker("company") description = factory.Faker("catch_phrase") - responses = factory.List(3 * [factory.Faker('company')]) - n_folds = factory.Faker('random_digit_not_null') - n_trials = factory.Faker('random_digit_not_null') - metrics = factory.List([factory.SubFactory(RMSEFactory), - factory.SubFactory(NDMEFactory), - factory.SubFactory(RSquaredFactory), - factory.SubFactory(StandardRMSEFactory), - factory.SubFactory(PVALFactory), - factory.SubFactory(F1Factory), - factory.SubFactory(AreaUnderROCFactory), - factory.SubFactory(CoverageProbabilityFactory)]) + responses = factory.List(3 * [factory.Faker("company")]) + n_folds = factory.Faker("random_digit_not_null") + n_trials = factory.Faker("random_digit_not_null") + metrics = factory.List( + [ + factory.SubFactory(RMSEFactory), + factory.SubFactory(NDMEFactory), + factory.SubFactory(RSquaredFactory), + factory.SubFactory(StandardRMSEFactory), + factory.SubFactory(PVALFactory), + factory.SubFactory(F1Factory), + factory.SubFactory(AreaUnderROCFactory), + factory.SubFactory(CoverageProbabilityFactory), + ] + ) type = "CrossValidationEvaluator" @@ -523,24 +537,24 @@ class PredictorEvaluationDataFactory(factory.DictFactory): class PredictorEvaluationMetadataFactory(factory.DictFactory): class Meta: - exclude = ('is_archived', ) + exclude = ("is_archived",) created = factory.SubFactory(UserTimestampDataFactory) updated = factory.SubFactory(UserTimestampDataFactory) - archived = factory.Maybe('is_archived', factory.SubFactory(UserTimestampDataFactory), None) + archived = factory.Maybe("is_archived", factory.SubFactory(UserTimestampDataFactory), None) predictor_id = factory.Faker("uuid4") predictor_version = factory.Faker("random_digit_not_null") status = {"major": "SUCCEEDED", "minor": "READY", "detail": []} class PredictorEvaluationFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") data = factory.SubFactory(PredictorEvaluationDataFactory) metadata = factory.SubFactory(PredictorEvaluationMetadataFactory) class DesignSpaceConfigDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") name = factory.Faker("company") descriptor = factory.Faker("catch_phrase") subspaces = [] # TODO Create SubspaceDataFactory @@ -550,7 +564,7 @@ class DesignSpaceConfigDataFactory(factory.DictFactory): class DesignSpaceDataFactory(factory.DictFactory): config = factory.SubFactory(DesignSpaceConfigDataFactory) - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") display_name = factory.Faker("company") archived = False module_type = "DESIGN_SPACE" @@ -565,28 +579,28 @@ class Params: branch = factory.SubFactory(BranchDataFactory) times = factory.List([factory.Faker("unix_milliseconds") for i in range(3)]) register = factory.Trait( - id = factory.Faker('uuid4'), - branch_id = factory.LazyAttribute(lambda o: o.branch["id"]), - created_by = factory.Faker('uuid4'), - updated_by = factory.LazyAttribute(lambda o: o.created_by), - create_time = factory.LazyAttribute(lambda o: sorted(o.times)[0]), - update_time = factory.LazyAttribute(lambda o: sorted(o.times)[0]), + id=factory.Faker("uuid4"), + branch_id=factory.LazyAttribute(lambda o: o.branch["id"]), + created_by=factory.Faker("uuid4"), + updated_by=factory.LazyAttribute(lambda o: o.created_by), + create_time=factory.LazyAttribute(lambda o: sorted(o.times)[0]), + update_time=factory.LazyAttribute(lambda o: sorted(o.times)[0]), # TODO: Create a Trait for statuses - status = "SUCCEEDED", - status_description = "READY", - status_info = [], - status_detail = [] + status="SUCCEEDED", + status_description="READY", + status_info=[], + status_detail=[], ) update = factory.Trait( - register = True, - updated_by = factory.Faker('uuid4'), - update_time = factory.LazyAttribute(lambda o: sorted(o.times)[1]) + register=True, + updated_by=factory.Faker("uuid4"), + update_time=factory.LazyAttribute(lambda o: sorted(o.times)[1]), ) archive = factory.Trait( - update = True, - archived = True, - archived_by = factory.Faker('uuid4'), - archive_time = factory.LazyAttribute(lambda o: sorted(o.times)[2]), + update=True, + archived=True, + archived_by=factory.Faker("uuid4"), + archive_time=factory.LazyAttribute(lambda o: sorted(o.times)[2]), ) type = DesignWorkflow.typ @@ -603,37 +617,33 @@ class Params: class IngestFilesResponseDataFactory(factory.DictFactory): - team_id = factory.Faker('uuid4') - dataset_id = factory.Faker('uuid4') - ingestion_id = factory.Faker('uuid4') + team_id = factory.Faker("uuid4") + dataset_id = factory.Faker("uuid4") + ingestion_id = factory.Faker("uuid4") class IngestionStatusResponseDataFactory(factory.DictFactory): - ingestion_id = factory.Faker('uuid4') + ingestion_id = factory.Faker("uuid4") status = IngestionStatusType.INGESTION_CREATED errors = factory.List([]) class JobSubmissionResponseDataFactory(factory.DictFactory): - job_id = factory.Faker('uuid4') + job_id = factory.Faker("uuid4") class TaskNodeDataFactory(factory.DictFactory): class Params: failure = False - id = factory.Faker('uuid4') - task_type = factory.Faker('word') + id = factory.Faker("uuid4") + task_type = factory.Faker("word") status = factory.Maybe( - "failure", - yes_declaration=JobStatus.FAILURE, - no_declaration=JobStatus.SUCCESS + "failure", yes_declaration=JobStatus.FAILURE, no_declaration=JobStatus.SUCCESS ) dependencies = factory.List([]) failure_reason = factory.Maybe( - "failure", - yes_declaration=factory.Faker('sentence'), - no_declaration=None + "failure", yes_declaration=factory.Faker("sentence"), no_declaration=None ) @@ -641,15 +651,13 @@ class JobStatusResponseDataFactory(factory.DictFactory): class Params: failure = False - job_type = factory.Faker('word') + job_type = factory.Faker("word") status = factory.Maybe( - "failure", - yes_declaration=JobStatus.FAILURE, - no_declaration=JobStatus.SUCCESS + "failure", yes_declaration=JobStatus.FAILURE, no_declaration=JobStatus.SUCCESS + ) + tasks = factory.List( + [factory.RelatedFactory(TaskNodeDataFactory, failure=factory.SelfAttribute("...failure"))] ) - tasks = factory.List([ - factory.RelatedFactory(TaskNodeDataFactory, failure=factory.SelfAttribute('...failure')) - ]) output = factory.Dict({}) @@ -657,14 +665,14 @@ class DatasetDataFactory(factory.DictFactory): class Params: times = factory.List([factory.Faker("unix_milliseconds") for i in range(3)]) - id = factory.Faker('uuid4') - name = factory.Faker('company') - summary = factory.Faker('catch_phrase') - description = factory.Faker('bs') + id = factory.Faker("uuid4") + name = factory.Faker("company") + summary = factory.Faker("catch_phrase") + description = factory.Faker("bs") deleted = False - created_by = factory.Faker('uuid4') - updated_by = factory.Faker('uuid4') - deleted_by = factory.Faker('uuid4') + created_by = factory.Faker("uuid4") + updated_by = factory.Faker("uuid4") + deleted_by = factory.Faker("uuid4") unique_name = None # TODO Update tests to include unique_name create_time = factory.LazyAttribute(lambda o: sorted(o.times)[0]) update_time = factory.LazyAttribute(lambda o: sorted(o.times)[1]) @@ -673,23 +681,23 @@ class Params: class IDDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") class LinkByUIDFactory(factory.Factory): class Meta: model = LinkByUID - scope = 'id' - id = factory.Faker('uuid4') + scope = "id" + id = factory.Faker("uuid4") class FileLinkFactory(factory.Factory): class Meta: model = FileLink - url = factory.Faker('uri') - filename = factory.Faker('file_name') + url = factory.Faker("uri") + filename = factory.Faker("file_name") class ProcessTemplateFactory(factory.Factory): @@ -697,9 +705,9 @@ class Meta: model = ProcessTemplate uids = factory.SubFactory(IDDataFactory) - name = factory.Faker('color_name') - tags = factory.List([factory.Faker('color_name'), factory.Faker('color_name')]) - description = factory.Faker('catch_phrase') + name = factory.Faker("color_name") + tags = factory.List([factory.Faker("color_name"), factory.Faker("color_name")]) + description = factory.Faker("catch_phrase") conditions = [] # TODO make a ConditionsTemplateFactory parameters = [] # TODO make a ParametersTemplateFactory @@ -709,10 +717,10 @@ class Meta: model = MaterialTemplate uids = factory.SubFactory(IDDataFactory) - name = factory.Faker('color_name') - tags = factory.List([factory.Faker('color_name'), factory.Faker('color_name')]) + name = factory.Faker("color_name") + tags = factory.List([factory.Faker("color_name"), factory.Faker("color_name")]) properties = [] # TODO make a PropertiesTemplateFactory - description = factory.Faker('catch_phrase') + description = factory.Faker("catch_phrase") class MaterialSpecFactory(factory.Factory): @@ -720,9 +728,9 @@ class Meta: model = MaterialSpec uids = factory.SubFactory(IDDataFactory) - name = factory.Faker('color_name') - tags = factory.List([factory.Faker('color_name'), factory.Faker('color_name')]) - notes = factory.Faker('catch_phrase') + name = factory.Faker("color_name") + tags = factory.List([factory.Faker("color_name"), factory.Faker("color_name")]) + notes = factory.Faker("catch_phrase") process = factory.SubFactory(LinkByUIDFactory) file_links = factory.List([factory.SubFactory(FileLinkFactory)]) template = factory.SubFactory(LinkByUIDFactory) @@ -734,9 +742,9 @@ class Meta: model = MaterialRun uids = factory.SubFactory(IDDataFactory) - name = factory.Faker('color_name') - tags = factory.List([factory.Faker('color_name'), factory.Faker('color_name')]) - notes = factory.Faker('catch_phrase') + name = factory.Faker("color_name") + tags = factory.List([factory.Faker("color_name"), factory.Faker("color_name")]) + notes = factory.Faker("catch_phrase") process = factory.SubFactory(LinkByUIDFactory) sample_type = factory.Faker("enum", enum_cls=SampleType) spec = factory.SubFactory(LinkByUIDFactory) @@ -746,13 +754,13 @@ class Meta: class LinkByUIDDataFactory(factory.DictFactory): id = LinkByUIDFactory.id scope = LinkByUIDFactory.scope - type = 'link_by_uid' + type = "link_by_uid" class FileLinkDataFactory(factory.DictFactory): url = FileLinkFactory.url filename = FileLinkFactory.filename - type = 'file_link' + type = "file_link" class MaterialSpecDataFactory(factory.DictFactory): @@ -764,7 +772,7 @@ class MaterialSpecDataFactory(factory.DictFactory): file_links = factory.List([factory.SubFactory(FileLinkDataFactory)]) template = factory.SubFactory(LinkByUIDDataFactory) properties = [] # TODO make a PropertiesDataFactory - type = 'material_spec' + type = "material_spec" class MaterialRunDataFactory(factory.DictFactory): @@ -776,16 +784,16 @@ class MaterialRunDataFactory(factory.DictFactory): sample_type = MaterialRunFactory.sample_type spec = factory.SubFactory(LinkByUIDDataFactory) file_links = factory.List([factory.SubFactory(FileLinkDataFactory)]) - type = 'material_run' + type = "material_run" class DatasetFactory(factory.Factory): class Meta: model = Dataset - name = factory.Faker('company') - summary = factory.Faker('catch_phrase') - description = factory.Faker('bs') + name = factory.Faker("company") + summary = factory.Faker("catch_phrase") + description = factory.Faker("bs") unique_name = None # TODO Update tests to include unique_name @@ -796,14 +804,14 @@ class Meta: # TODO Bring _Uploader in line with other library concepts @factory.post_generation def assign_values(obj, create, extracted): - obj.bucket = 'citrine-datasvc' - obj.object_key = '334455' - obj.upload_id = 'dea3a-555' - obj.region_name = 'us-west' - obj.aws_access_key_id = 'dkfjiejkcm' - obj.aws_secret_access_key = 'ifeemkdsfjeijie8759235u2wjr388' - obj.aws_session_token = 'fafjeijfi87834j87woa' - obj.s3_version = '2' + obj.bucket = "citrine-datasvc" + obj.object_key = "334455" + obj.upload_id = "dea3a-555" + obj.region_name = "us-west" + obj.aws_access_key_id = "dkfjiejkcm" + obj.aws_secret_access_key = "ifeemkdsfjeijie8759235u2wjr388" + obj.aws_session_token = "fafjeijfi87834j87woa" + obj.s3_version = "2" class MLIScoreFactory(factory.Factory): @@ -816,84 +824,113 @@ class Meta: class AnalysisPlotMetadataDataFactory(factory.DictFactory): - rank = factory.Faker('random_int', min=1, max=10) + rank = factory.Faker("random_int", min=1, max=10) created = factory.SubFactory(UserTimestampDataFactory) updated = factory.SubFactory(UserTimestampDataFactory) class AnalysisPlotDataDataFactory(factory.DictFactory): - name = factory.Faker('company') - description = factory.Faker('catch_phrase') - plot_type = factory.Faker('random_element', elements=('SCATTER', 'VIOLIN')) + name = factory.Faker("company") + description = factory.Faker("catch_phrase") + plot_type = factory.Faker("random_element", elements=("SCATTER", "VIOLIN")) config = {} class AnalysisPlotEntityDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") data = factory.SubFactory(AnalysisPlotDataDataFactory) metadata = factory.SubFactory(AnalysisPlotMetadataDataFactory) class LatestBuildDataFactory(factory.DictFactory): class Params: - is_failed = factory.LazyAttribute(lambda o: o.status == 'FAILED') + is_failed = factory.LazyAttribute(lambda o: o.status == "FAILED") - status = factory.Faker('random_element', elements=('INPROGRESS', 'SUCCEEDED', 'FAILED')) - failure_reason = factory.Maybe('is_failed', ['This is a test failure message'], []) + status = factory.Faker("random_element", elements=("INPROGRESS", "SUCCEEDED", "FAILED")) + failure_reason = factory.Maybe("is_failed", ["This is a test failure message"], []) query = factory.SubFactory(GemdQueryDataFactory) class AnalysisWorkflowMetadataDataFactory(factory.DictFactory): class Meta: - exclude = ('is_archived', 'has_build') + exclude = ("is_archived", "has_build") created = factory.SubFactory(UserTimestampDataFactory) updated = factory.SubFactory(UserTimestampDataFactory) - archived = factory.Maybe('is_archived', factory.SubFactory(UserTimestampDataFactory), None) - latest_build = factory.Maybe('has_build', factory.SubFactory(LatestBuildDataFactory), None) + archived = factory.Maybe("is_archived", factory.SubFactory(UserTimestampDataFactory), None) + latest_build = factory.Maybe("has_build", factory.SubFactory(LatestBuildDataFactory), None) class AnalysisWorkflowDataDataFactory(factory.DictFactory): class Meta: - exclude = ('has_snapshot', 'plot_count') + exclude = ("has_snapshot", "plot_count") class Params: plot_count = 1 - name = factory.Faker('company') - description = factory.Faker('catch_phrase') - snapshot_id = factory.Maybe('has_snapshot', factory.Faker('uuid4'), None) - plots = factory.LazyAttribute(lambda self: [AnalysisPlotEntityDataFactory() for _ in range(self.plot_count)]) + name = factory.Faker("company") + description = factory.Faker("catch_phrase") + snapshot_id = factory.Maybe("has_snapshot", factory.Faker("uuid4"), None) + plots = factory.LazyAttribute( + lambda self: [AnalysisPlotEntityDataFactory() for _ in range(self.plot_count)] + ) class AnalysisWorkflowEntityDataFactory(factory.DictFactory): - id = factory.Faker('uuid4') + id = factory.Faker("uuid4") data = factory.SubFactory(AnalysisWorkflowDataDataFactory) metadata = factory.SubFactory(AnalysisWorkflowMetadataDataFactory) class FeatureEffectsResponseResultFactory(factory.DictFactory): - materials = factory.List([ - factory.Faker('uuid4', cast_to=None), - factory.Faker('uuid4', cast_to=None), - factory.Faker('uuid4', cast_to=None) - ]) - outputs = factory.Dict({ - "output1": factory.Dict({ - "feature1": factory.List([factory.Faker("pyfloat"), factory.Faker("pyfloat"), factory.Faker("pyfloat")]) - }), - "output2": factory.Dict({ - "feature1": factory.List([factory.Faker("pyfloat"), factory.Faker("pyfloat"), factory.Faker("pyfloat")]), - "feature2": factory.List([factory.Faker("pyfloat"), factory.Faker("pyfloat"), factory.Faker("pyfloat")]) - }) - }) + materials = factory.List( + [ + factory.Faker("uuid4", cast_to=None), + factory.Faker("uuid4", cast_to=None), + factory.Faker("uuid4", cast_to=None), + ] + ) + outputs = factory.Dict( + { + "output1": factory.Dict( + { + "feature1": factory.List( + [ + factory.Faker("pyfloat"), + factory.Faker("pyfloat"), + factory.Faker("pyfloat"), + ] + ) + } + ), + "output2": factory.Dict( + { + "feature1": factory.List( + [ + factory.Faker("pyfloat"), + factory.Faker("pyfloat"), + factory.Faker("pyfloat"), + ] + ), + "feature2": factory.List( + [ + factory.Faker("pyfloat"), + factory.Faker("pyfloat"), + factory.Faker("pyfloat"), + ] + ), + } + ), + } + ) + class FeatureEffectsMetadataFactory(factory.DictFactory): - predictor_id = factory.Faker('uuid4') - predictor_version = factory.Faker('random_digit_not_null') + predictor_id = factory.Faker("uuid4") + predictor_version = factory.Faker("random_digit_not_null") created = factory.SubFactory(UserTimestampDataFactory) updated = factory.SubFactory(UserTimestampDataFactory) - status = 'SUCCEEDED' + status = "SUCCEEDED" class FeatureEffectsResponseFactory(factory.DictFactory): diff --git a/tests/utils/fakes/__init__.py b/tests/utils/fakes/__init__.py index 6176ea9f5..3328c8d95 100644 --- a/tests/utils/fakes/__init__.py +++ b/tests/utils/fakes/__init__.py @@ -1,3 +1,7 @@ +# isort: skip_file +# Import order is significant here: modules that define names re-imported by +# sibling modules (e.g. FakeDesignWorkflowCollection) must be imported first to +# avoid circular-import errors during package initialization. from .fake_collection import * from .fake_file_collection import * from .fake_dataset_collection import * diff --git a/tests/utils/fakes/fake_collection.py b/tests/utils/fakes/fake_collection.py index 01f035e70..ac0bda4be 100644 --- a/tests/utils/fakes/fake_collection.py +++ b/tests/utils/fakes/fake_collection.py @@ -1,16 +1,15 @@ -from uuid import uuid4, UUID -from typing import TypeVar, Optional, Union, Iterable +from collections.abc import Iterable +from typing import TypeVar +from uuid import UUID, uuid4 from citrine._rest.collection import Collection from citrine.exceptions import NotFound - from tests.utils.functions import normalize_uid -ResourceType = TypeVar('ResourceType', bound='Resource') +ResourceType = TypeVar("ResourceType", bound="Resource") class FakeCollection(Collection[ResourceType]): - def __init__(self): self._resources = {} @@ -19,18 +18,18 @@ def register(self, resource: ResourceType) -> ResourceType: resource.uid = uuid4() self._resources[resource.uid] = resource return resource - + def update(self, resource: ResourceType): self._resources.pop(resource.uid, None) return self.register(resource) - - def list(self, page: Optional[int] = None, per_page: int = 100) -> Iterable[ResourceType]: + + def list(self, page: int | None = None, per_page: int = 100) -> Iterable[ResourceType]: if page is None: return iter(list(self._resources.values())) else: - return iter(list(self._resources.values())[(page - 1)*per_page:page*per_page]) - - def get(self, uid: Union[UUID, str]) -> ResourceType: + return iter(list(self._resources.values())[(page - 1) * per_page : page * per_page]) + + def get(self, uid: UUID | str) -> ResourceType: if normalize_uid(uid) not in self._resources: raise NotFound("") return self._resources[normalize_uid(uid)] diff --git a/tests/utils/fakes/fake_dataset_collection.py b/tests/utils/fakes/fake_dataset_collection.py index 2346574eb..9718ff15d 100644 --- a/tests/utils/fakes/fake_dataset_collection.py +++ b/tests/utils/fakes/fake_dataset_collection.py @@ -1,12 +1,9 @@ -from typing import Optional - from citrine.resources.dataset import Dataset, DatasetCollection from citrine.resources.file_link import FileCollection from tests.utils.fakes.fake_file_collection import FakeFileCollection class FakeDataset(Dataset): - def __init__(self): pass @@ -16,7 +13,6 @@ def files(self) -> FileCollection: class FakeDatasetCollection(DatasetCollection): - def __init__(self, *, session, team_id): super().__init__(team_id=team_id, session=session) self.datasets = [] @@ -25,8 +21,8 @@ def register(self, model: Dataset) -> Dataset: self.datasets.append(model) return model - def list(self, page: Optional[int] = None, per_page: int = 100): + def list(self, page: int | None = None, per_page: int = 100): if page is None: return self.datasets else: - return self.datasets[(page - 1)*per_page:page*per_page] + return self.datasets[(page - 1) * per_page : page * per_page] diff --git a/tests/utils/fakes/fake_descriptor_methods.py b/tests/utils/fakes/fake_descriptor_methods.py index e3467b683..8b5c3abcc 100644 --- a/tests/utils/fakes/fake_descriptor_methods.py +++ b/tests/utils/fakes/fake_descriptor_methods.py @@ -1,11 +1,12 @@ -from typing import List, Union from uuid import uuid4 -from citrine.informatics.descriptors import Descriptor, RealDescriptor, CategoricalDescriptor +from citrine.informatics.descriptors import CategoricalDescriptor, Descriptor, RealDescriptor from citrine.informatics.predictors import ( ChemicalFormulaFeaturizer, + GraphPredictor, + MeanPropertyPredictor, MolecularStructureFeaturizer, - MeanPropertyPredictor, PredictorNode, GraphPredictor + PredictorNode, ) from citrine.resources.descriptors import DescriptorMethods from tests.utils.session import FakeSession @@ -18,16 +19,23 @@ def __init__(self, num_properties): self.num_properties = num_properties def from_predictor_responses( - self, - predictor: Union[PredictorNode, GraphPredictor], - inputs: List[Descriptor] + self, predictor: PredictorNode | GraphPredictor, inputs: list[Descriptor] ): if isinstance(predictor, (MolecularStructureFeaturizer, ChemicalFormulaFeaturizer)): input_descriptor = predictor.input_descriptor return [ - RealDescriptor(f"{input_descriptor.key} real property {i}", lower_bound=0, upper_bound=1, units="") - for i in range(self.num_properties) - ] + [CategoricalDescriptor(f"{input_descriptor.key} categorical property", categories=["cat1", "cat2"])] + RealDescriptor( + f"{input_descriptor.key} real property {i}", + lower_bound=0, + upper_bound=1, + units="", + ) + for i in range(self.num_properties) + ] + [ + CategoricalDescriptor( + f"{input_descriptor.key} categorical property", categories=["cat1", "cat2"] + ) + ] elif isinstance(predictor, MeanPropertyPredictor): label_str = predictor.label or "all ingredients" @@ -36,7 +44,7 @@ def from_predictor_responses( f"mean of {prop.key} for {label_str} in {predictor.input_descriptor.key}", lower_bound=0, upper_bound=1, - units="" + units="", ) for prop in predictor.properties - ] \ No newline at end of file + ] diff --git a/tests/utils/fakes/fake_execution_collection.py b/tests/utils/fakes/fake_execution_collection.py index 1719e4ece..8b3f65b56 100644 --- a/tests/utils/fakes/fake_execution_collection.py +++ b/tests/utils/fakes/fake_execution_collection.py @@ -1,15 +1,12 @@ -from uuid import UUID -from typing import Optional - from citrine.informatics.executions import DesignExecution from citrine.informatics.scores import Score - from citrine.resources.design_execution import DesignExecutionCollection class FakeDesignExecutionCollection(DesignExecutionCollection): - - def trigger(self, execution_input: Score, max_candidates: Optional[int] = None) -> DesignExecution: + def trigger( + self, execution_input: Score, max_candidates: int | None = None + ) -> DesignExecution: execution = DesignExecution() execution.score = execution_input execution.descriptors = [] diff --git a/tests/utils/fakes/fake_file_collection.py b/tests/utils/fakes/fake_file_collection.py index 10fe0b71c..699eaffc0 100644 --- a/tests/utils/fakes/fake_file_collection.py +++ b/tests/utils/fakes/fake_file_collection.py @@ -2,7 +2,6 @@ class FakeFileCollection(FileCollection): - def __init__(self): self.files = [] diff --git a/tests/utils/fakes/fake_module_collection.py b/tests/utils/fakes/fake_module_collection.py index ac261304b..5565d771b 100644 --- a/tests/utils/fakes/fake_module_collection.py +++ b/tests/utils/fakes/fake_module_collection.py @@ -1,6 +1,6 @@ from datetime import datetime -from typing import TypeVar, Union -from uuid import uuid4, UUID +from typing import TypeVar +from uuid import UUID, uuid4 from citrine._rest.collection import Collection from citrine._session import Session @@ -10,15 +10,13 @@ from citrine.informatics.predictors import GraphPredictor from citrine.resources.design_space import DesignSpaceCollection from citrine.resources.predictor import PredictorCollection - -from tests.utils.functions import normalize_uid from tests.utils.fakes import FakeCollection +from tests.utils.functions import normalize_uid -ModuleType = TypeVar('ModuleType', bound='Module') +ModuleType = TypeVar("ModuleType", bound="Module") class FakeModuleCollection(FakeCollection[ModuleType], Collection[ModuleType]): - def __init__(self, project_id, session): FakeCollection.__init__(self) self.project_id = project_id @@ -34,30 +32,20 @@ def archive(self, module_id: UUID): module.archive_time = datetime.now() return module -class FakeDesignSpaceCollection(FakeModuleCollection[DesignSpace], DesignSpaceCollection): +class FakeDesignSpaceCollection(FakeModuleCollection[DesignSpace], DesignSpaceCollection): def create_default(self, *, predictor_id: UUID) -> DesignSpace: return ProductDesignSpace( - f"Default design space", - description="", - dimensions=[], - subspaces=[] + "Default design space", description="", dimensions=[], subspaces=[] ) class FakePredictorCollection(FakeModuleCollection[GraphPredictor], PredictorCollection): - def create_default( - self, - *, - training_data: DataSource, - pattern="PLAIN", - prefer_valid=True + self, *, training_data: DataSource, pattern="PLAIN", prefer_valid=True ) -> GraphPredictor: return GraphPredictor( - name=f"Default {pattern.lower()} predictor", - description="", - predictors=[] + name=f"Default {pattern.lower()} predictor", description="", predictors=[] ) - + auto_configure = create_default diff --git a/tests/utils/fakes/fake_project_collection.py b/tests/utils/fakes/fake_project_collection.py index a0f6a657f..01d143330 100644 --- a/tests/utils/fakes/fake_project_collection.py +++ b/tests/utils/fakes/fake_project_collection.py @@ -1,35 +1,37 @@ -from typing import Optional, Union from uuid import UUID, uuid4 from citrine.exceptions import NotFound from citrine.resources.project import Project, ProjectCollection -from tests.utils.fakes import FakeDatasetCollection -from tests.utils.fakes import FakeDesignSpaceCollection, FakeDesignWorkflowCollection -from tests.utils.fakes import FakeGemTableCollection, FakeTableConfigCollection -from tests.utils.fakes import FakePredictorCollection -from tests.utils.fakes import FakeDescriptorMethods +from tests.utils.fakes import ( + FakeDatasetCollection, + FakeDescriptorMethods, + FakeDesignSpaceCollection, + FakeDesignWorkflowCollection, + FakeGemTableCollection, + FakePredictorCollection, + FakeTableConfigCollection, +) from tests.utils.session import FakeSession class FakeProjectCollection(ProjectCollection): - - def __init__(self, search_implemented: bool = True, team_id: Optional[Union[UUID, str]] = None): + def __init__(self, search_implemented: bool = True, team_id: UUID | str | None = None): super().__init__(session=FakeSession, team_id=team_id) self.projects = [] self.search_implemented = search_implemented - def register(self, name: str, description: Optional[str] = None) -> Project: + def register(self, name: str, description: str | None = None) -> Project: project = FakeProject(name=name) self.projects.append(project) return project - def list(self, page: Optional[int] = None, per_page: int = 100): + def list(self, page: int | None = None, per_page: int = 100): if page is None: return self.projects else: - return self.projects[(page - 1) * per_page:page * per_page] + return self.projects[(page - 1) * per_page : page * per_page] - def search(self, search_params: Optional[dict] = None, per_page: int = 100): + def search(self, search_params: dict | None = None, per_page: int = 100): if not self.search_implemented: raise NotFound("search") @@ -56,7 +58,6 @@ def delete(self, uuid): class FakeProject(Project): - def __init__(self, name="foo", description="bar", num_properties=3, session=FakeSession()): super().__init__(name=name, description=description, session=session) self.uid = uuid4() @@ -66,8 +67,12 @@ def __init__(self, name="foo", description="bar", num_properties=3, session=Fake self._descriptor_methods = FakeDescriptorMethods(num_properties) self._datasets = FakeDatasetCollection(team_id=self.team_id, session=self.session) self._predictors = FakePredictorCollection(self.uid, self.session) - self._tables = FakeGemTableCollection(team_id=self.team_id, project_id=self.uid, session=self.session) - self._table_configs = FakeTableConfigCollection(team_id=self.team_id, project_id=self.uid, session=self.session) + self._tables = FakeGemTableCollection( + team_id=self.team_id, project_id=self.uid, session=self.session + ) + self._table_configs = FakeTableConfigCollection( + team_id=self.team_id, project_id=self.uid, session=self.session + ) @property def datasets(self) -> FakeDatasetCollection: diff --git a/tests/utils/fakes/fake_table_collection.py b/tests/utils/fakes/fake_table_collection.py index 5fe5b2dde..202182e60 100644 --- a/tests/utils/fakes/fake_table_collection.py +++ b/tests/utils/fakes/fake_table_collection.py @@ -1,20 +1,20 @@ -from uuid import uuid4, UUID -from typing import List, Dict, Tuple, Optional, Union, Iterable, TypeVar, Generic +import builtins +from collections.abc import Iterable +from typing import Generic, TypeVar +from uuid import UUID, uuid4 from gemd.entity.link_by_uid import LinkByUID from citrine._session import Session from citrine.exceptions import NotFound -from citrine.resources.material_run import MaterialRun -from citrine.resources.gemtables import GemTable, GemTableCollection -from citrine.resources.table_config import TableConfig, TableConfigCollection, TableBuildAlgorithm - from citrine.gemtables.columns import Column from citrine.gemtables.variables import Variable - +from citrine.resources.gemtables import GemTable, GemTableCollection +from citrine.resources.material_run import MaterialRun +from citrine.resources.table_config import TableBuildAlgorithm, TableConfig, TableConfigCollection from tests.utils.functions import normalize_uid -ResourceType = TypeVar('ResourceType', bound='Resource') +ResourceType = TypeVar("ResourceType", bound="Resource") class VersionedResourceStorage(Generic[ResourceType]): @@ -24,7 +24,7 @@ def __init__(self): self._resources = {} @property - def resources(self) -> Dict[UUID, Dict[int, ResourceType]]: + def resources(self) -> dict[UUID, dict[int, ResourceType]]: return self._resources def register(self, resource: ResourceType, *, version: int) -> None: @@ -38,7 +38,7 @@ def _get_latest(self, uid: UUID) -> ResourceType: latest_version = max(versions.keys()) return versions[latest_version] - def get(self, uid: Union[str, UUID], *, version: Optional[int] = None) -> Optional[ResourceType]: + def get(self, uid: str | UUID, *, version: int | None = None) -> ResourceType | None: uid = normalize_uid(uid) if uid not in self.resources: return None @@ -48,23 +48,22 @@ def get(self, uid: Union[str, UUID], *, version: Optional[int] = None) -> Option versions = self.resources[uid] return versions.get(version, None) - def list_by_uid(self, uid: Union[str, UUID]) -> List[ResourceType]: + def list_by_uid(self, uid: str | UUID) -> list[ResourceType]: uid = normalize_uid(uid) versions = self.resources.get(uid, {}) sorted_versions = sorted(versions.keys()) return [versions[v] for v in sorted_versions] - def list_latest(self) -> List[ResourceType]: + def list_latest(self) -> list[ResourceType]: return [self._get_latest(uid) for uid in self.resources.keys()] class FakeTableConfigCollection(TableConfigCollection): - def __init__(self, team_id: UUID, project_id: UUID, session: Session): super().__init__(team_id=team_id, project_id=project_id, session=session) self._storage = VersionedResourceStorage[TableConfig]() - def get(self, uid: Union[UUID, str], *, version: Optional[int] = None): + def get(self, uid: UUID | str, *, version: int | None = None): config = self._storage.get(uid, version=version) if config is None: raise NotFound("") @@ -85,38 +84,41 @@ def register(self, table_config: TableConfig) -> TableConfig: return table_config - def list(self, page: Optional[int] = None, per_page: int = 100) -> Iterable[TableConfig]: + def list(self, page: int | None = None, per_page: int = 100) -> Iterable[TableConfig]: configs = self._storage.list_latest() if page is None: return iter(configs) else: - return iter(configs[(page - 1)*per_page:page*per_page]) + return iter(configs[(page - 1) * per_page : page * per_page]) def default_for_material( - self, *, - material: Union[MaterialRun, LinkByUID, str, UUID], + self, + *, + material: MaterialRun | LinkByUID | str | UUID, name: str, description: str = None, - algorithm: Optional[TableBuildAlgorithm] = None, - scope: str = None - ) -> Tuple[TableConfig, List[Tuple[Variable, Column]]]: + algorithm: TableBuildAlgorithm | None = None, + scope: str = None, + ) -> tuple[TableConfig, builtins.list[tuple[Variable, Column]]]: table_config = TableConfig( - name=name, description="", datasets=[], - rows=[], variables=[], columns=[] + name=name, description="", datasets=[], rows=[], variables=[], columns=[] ) return table_config, [] class FakeGemTableCollection(GemTableCollection): - def __init__(self, team_id: UUID, project_id: UUID, session: Session): super().__init__(team_id=team_id, project_id=project_id, session=session) self._config_map = {} # Map config UID to table UID self._table_storage = VersionedResourceStorage[GemTable]() - def build_from_config(self, config: Union[TableConfig, str, UUID], *, - version: Union[str, int] = None, - timeout: float = 15 * 60) -> GemTable: + def build_from_config( + self, + config: TableConfig | str | UUID, + *, + version: str | int = None, + timeout: float = 15 * 60, + ) -> GemTable: if isinstance(config, TableConfig): config_uid = config.config_uid else: @@ -136,9 +138,7 @@ def build_from_config(self, config: Union[TableConfig, str, UUID], *, return table - def list_by_config(self, table_config_uid: UUID, - *, - per_page: int = 100) -> Iterable[GemTable]: + def list_by_config(self, table_config_uid: UUID, *, per_page: int = 100) -> Iterable[GemTable]: config_uid = normalize_uid(table_config_uid) if config_uid not in self._config_map: return iter([]) @@ -146,9 +146,6 @@ def list_by_config(self, table_config_uid: UUID, table_id = self._config_map[config_uid] return self.list_versions(table_id, per_page=per_page) - def list_versions(self, - uid: UUID, - *, - per_page: int = 100) -> Iterable[GemTable]: + def list_versions(self, uid: UUID, *, per_page: int = 100) -> Iterable[GemTable]: tables = self._table_storage.list_by_uid(uid) return iter(tables) diff --git a/tests/utils/fakes/fake_team_collection.py b/tests/utils/fakes/fake_team_collection.py index c2b59eacd..2ba84900c 100644 --- a/tests/utils/fakes/fake_team_collection.py +++ b/tests/utils/fakes/fake_team_collection.py @@ -1,16 +1,12 @@ -from typing import Optional - from citrine.resources.team import Team, TeamCollection class FakeTeam(Team): - def __init__(self, name): self.name = name class FakeTeamCollection(TeamCollection): - def __init__(self, session): super().__init__(session=session) self.teams = [] @@ -20,8 +16,8 @@ def register(self, name: str) -> Team: self.teams.append(model) return model - def list(self, page: Optional[int] = None, per_page: int = 100): + def list(self, page: int | None = None, per_page: int = 100): if page is None: return self.teams else: - return self.teams[(page - 1) * per_page:page * per_page] + return self.teams[(page - 1) * per_page : page * per_page] diff --git a/tests/utils/fakes/fake_workflow_collection.py b/tests/utils/fakes/fake_workflow_collection.py index 6de507cf5..598b9baa4 100644 --- a/tests/utils/fakes/fake_workflow_collection.py +++ b/tests/utils/fakes/fake_workflow_collection.py @@ -1,17 +1,15 @@ -from typing import TypeVar, Union -from uuid import uuid4, UUID +from typing import TypeVar +from uuid import UUID from citrine._session import Session from citrine.informatics.workflows import DesignWorkflow from citrine.resources.design_workflow import DesignWorkflowCollection - from tests.utils.fakes import FakeCollection -WorkflowType = TypeVar('WorkflowType', bound='Workflow') +WorkflowType = TypeVar("WorkflowType", bound="Workflow") class FakeWorkflowCollection(FakeCollection[WorkflowType]): - def __init__(self, project_id, session: Session): FakeCollection.__init__(self) self.project_id = project_id @@ -22,7 +20,7 @@ def register(self, workflow: WorkflowType) -> WorkflowType: workflow.project_id = self.project_id return workflow - def archive(self, uid: Union[UUID, str]): + def archive(self, uid: UUID | str): # Search for workflow via UID to ensure exists # If found, flip archived=True with no return workflow = self.get(uid) @@ -30,5 +28,7 @@ def archive(self, uid: Union[UUID, str]): self.update(workflow) -class FakeDesignWorkflowCollection(FakeWorkflowCollection[DesignWorkflow], DesignWorkflowCollection): +class FakeDesignWorkflowCollection( + FakeWorkflowCollection[DesignWorkflow], DesignWorkflowCollection +): pass diff --git a/tests/utils/fakes/fake_workflows.py b/tests/utils/fakes/fake_workflows.py index e1afc1d44..7ece5740c 100644 --- a/tests/utils/fakes/fake_workflows.py +++ b/tests/utils/fakes/fake_workflows.py @@ -1,14 +1,13 @@ from citrine.informatics.workflows import DesignWorkflow - from tests.utils.fakes import FakeDesignExecutionCollection class FakeDesignWorkflow(DesignWorkflow): - @property def design_executions(self) -> FakeDesignExecutionCollection: """Return a resource representing all visible executions of this workflow.""" - if getattr(self, 'project_id', None) is None: - raise AttributeError('Cannot initialize execution without project reference!') + if getattr(self, "project_id", None) is None: + raise AttributeError("Cannot initialize execution without project reference!") return FakeDesignExecutionCollection( - project_id=self.project_id, session=self._session, workflow_id=self.uid) + project_id=self.project_id, session=self._session, workflow_id=self.uid + ) diff --git a/tests/utils/functions.py b/tests/utils/functions.py index 92f6cb9ce..2f9a5d392 100644 --- a/tests/utils/functions.py +++ b/tests/utils/functions.py @@ -1,8 +1,7 @@ from uuid import UUID -from typing import Union -def normalize_uid(uid: Union[UUID, str]) -> UUID: +def normalize_uid(uid: UUID | str) -> UUID: if isinstance(uid, str): return UUID(uid) else: diff --git a/tests/utils/session.py b/tests/utils/session.py index 0a6a7b97d..6aff3b3af 100644 --- a/tests/utils/session.py +++ b/tests/utils/session.py @@ -1,16 +1,18 @@ +from collections.abc import Callable, Iterator from json import dumps -from typing import Callable, Iterator, List from urllib.parse import urlencode +from citrine._session import Session from citrine.exceptions import NonRetryableHttpException from citrine.resources.api_error import ValidationError -from citrine._session import Session class FakeCall: """Encapsulates a call to a FakeSession.""" - def __init__(self, method, path, json=None, params: dict = None, version: str = None, **kwargs): + def __init__( + self, method, path, json=None, params: dict = None, version: str = None, **kwargs + ): self.method = method self.path = path self.json = json @@ -19,40 +21,43 @@ def __init__(self, method, path, json=None, params: dict = None, version: str = self.kwargs = kwargs def __repr__(self): - return f'FakeCall({self})' + return f"FakeCall({self})" def __str__(self) -> str: path = self.path if self.version: - path = path[1:] if path.startswith('/') else path - path = f'{self.version}/{path}' + path = path.removeprefix("/") + path = f"{self.version}/{path}" if self.params: - path = f'{path}?{urlencode(self.params)}' + path = f"{path}?{urlencode(self.params)}" - return f'{self.method} {path} : {dumps(self.json)}' + return f"{self.method} {path} : {dumps(self.json)}" def __eq__(self, other) -> bool: if not isinstance(other, FakeCall): return NotImplemented return ( - self.method == other.method and - self.path.lstrip('/') == other.path.lstrip('/') and # Leading slashes don't affect results - self.json == other.json and - self.params == other.params and - (not self.version or not other.version or self.version == other.version) # Allows users to check the URL version without forcing everyone to. + self.method == other.method + # Leading slashes don't affect results + and self.path.lstrip("/") == other.path.lstrip("/") + and self.json == other.json + and self.params == other.params + # Allows users to check the URL version without forcing everyone to. + and (not self.version or not other.version or self.version == other.version) ) class FakeSession(Session): """Fake version of Session used to test API interaction.""" + def __init__(self): self.calls = [] self.responses = [] self.s3_endpoint_url = None self.s3_use_ssl = True - self.s3_addressing_style = 'auto' + self.s3_addressing_style = "auto" self.use_idempotent_dataset_put = False def set_response(self, resp): @@ -85,23 +90,23 @@ def delete_resource(self, path: str, **kwargs) -> dict: return self.checked_delete(path, **kwargs) def checked_get(self, path: str, **kwargs) -> dict: - self.calls.append(FakeCall('GET', path, **kwargs)) + self.calls.append(FakeCall("GET", path, **kwargs)) return self._get_response() def checked_post(self, path: str, json: dict, **kwargs) -> dict: - self.calls.append(FakeCall('POST', path, json, **kwargs)) + self.calls.append(FakeCall("POST", path, json, **kwargs)) return self._get_response(default_response=json) def checked_put(self, path: str, json: dict, **kwargs) -> dict: - self.calls.append(FakeCall('PUT', path, json, **kwargs)) + self.calls.append(FakeCall("PUT", path, json, **kwargs)) return self._get_response(default_response=json) def checked_patch(self, path: str, json: dict, **kwargs) -> dict: - self.calls.append(FakeCall('PATCH', path, json, **kwargs)) + self.calls.append(FakeCall("PATCH", path, json, **kwargs)) return self._get_response(default_response=json) def checked_delete(self, path: str, **kwargs) -> dict: - self.calls.append(FakeCall('DELETE', path, **kwargs)) + self.calls.append(FakeCall("DELETE", path, **kwargs)) return self._get_response() def _get_response(self, default_response: dict = None): @@ -121,39 +126,45 @@ def _get_response(self, default_response: dict = None): return response @staticmethod - def cursor_paged_resource(base_method: Callable[..., dict], path: str, - forward: bool = True, per_page: int = 100, - version: str = 'v2', **kwargs) -> Iterator[dict]: + def cursor_paged_resource( + base_method: Callable[..., dict], + path: str, + forward: bool = True, + per_page: int = 100, + version: str = "v2", + **kwargs, + ) -> Iterator[dict]: """ Returns a flat generator of results for an API query. Results are fetched in chunks of size `per_page` and loaded lazily. """ - params = kwargs.get('params', {}) - params['forward'] = forward - params['ascending'] = forward - params['per_page'] = per_page - kwargs['params'] = params + params = kwargs.get("params", {}) + params["forward"] = forward + params["ascending"] = forward + params["per_page"] = per_page + kwargs["params"] = params while True: response_json = base_method(path, version=version, **kwargs) - for obj in response_json['contents']: + for obj in response_json["contents"]: yield obj - cursor = response_json.get('next') + cursor = response_json.get("next") if cursor is None: break - params['cursor'] = cursor + params["cursor"] = cursor class FakePaginatedSession(FakeSession): """Fake version of Session used to test API interaction, with support for pagination params page and per_page.""" + def checked_get(self, path: str, **kwargs) -> dict: - params = kwargs.get('params') - self.calls.append(FakeCall('GET', path, params=params)) + params = kwargs.get("params") + self.calls.append(FakeCall("GET", path, params=params)) return self._get_response(**params) def checked_post(self, path: str, json: dict, **kwargs) -> dict: - params = kwargs.get('params') - self.calls.append(FakeCall('POST', path, json, params=params)) + params = kwargs.get("params") + self.calls.append(FakeCall("POST", path, json, params=params)) return self._get_response(**params) def _get_response(self, **kwargs): @@ -162,23 +173,23 @@ def _get_response(self, **kwargs): """ if not self.responses: return {} - - page = kwargs.get('page', 1) - per_page = kwargs.get('per_page', 20) + + page = kwargs.get("page", 1) + per_page = kwargs.get("per_page", 20) start_idx = (page - 1) * per_page - # in case the response takes the shape of something like + # in case the response takes the shape of something like # {'projects': [Project1, Project2, etc.]} has_collection_key = isinstance(self.responses[0], dict) if has_collection_key: key = list(self.responses[0].keys())[0] - list_values = self.responses[0][key][start_idx:start_idx + per_page] + list_values = self.responses[0][key][start_idx : start_idx + per_page] return dict.fromkeys([key], list_values) else: - return self.responses[0][start_idx:start_idx + per_page] + return self.responses[0][start_idx : start_idx + per_page] class FakeS3Client: @@ -200,7 +211,7 @@ def put_object(self, *args, **kwargs): class FakeRequestResponse: """A fake version of a requests.request() response.""" - def __init__(self, status_code, content=None, text="", reason='BadRequest'): + def __init__(self, status_code, content=None, text="", reason="BadRequest"): self.content = content self.text = text self.status_code = status_code @@ -215,11 +226,19 @@ def json(self): # the method to FakeRequest. class FakeRequestResponseApiError: """A fake version of a requests.request() response that has an ApiError""" - def __init__(self, code: int, message: str, validation_errors: List[ValidationError], - reason: str = 'BadRequest'): - self.api_error_json = {"code": code, - "message": message, - "validation_errors": [ve.dump() for ve in validation_errors]} + + def __init__( + self, + code: int, + message: str, + validation_errors: list[ValidationError], + reason: str = "BadRequest", + ): + self.api_error_json = { + "code": code, + "message": message, + "validation_errors": [ve.dump() for ve in validation_errors], + } self.text = message self.status_code = code self.reason = reason @@ -245,18 +264,20 @@ def make_fake_cursor_request_function(all_results: list): all_results: list All results in the result set to simulate paging """ + # TODO add logic for `forward` and `ascending` def fake_cursor_request(*_, params=None, **__): - page_size = params['per_page'] - if 'cursor' in params: - cursor = int(params['cursor']) - contents = all_results[cursor + 1:cursor + page_size + 1] + page_size = params["per_page"] + if "cursor" in params: + cursor = int(params["cursor"]) + contents = all_results[cursor + 1 : cursor + page_size + 1] else: contents = all_results[:page_size] - response = {'contents': contents} + response = {"contents": contents} if contents: - response['next'] = str(all_results.index(contents[-1])) - if 'cursor' in params: - response['previous'] = str(all_results.index(contents[0])) + response["next"] = str(all_results.index(contents[-1])) + if "cursor" in params: + response["previous"] = str(all_results.index(contents[0])) return response + return fake_cursor_request diff --git a/tests/utils/wait.py b/tests/utils/wait.py index 68a010d86..ddc765c2f 100644 --- a/tests/utils/wait.py +++ b/tests/utils/wait.py @@ -1,5 +1,5 @@ -from time import time, sleep -from typing import List, Optional, Callable +from collections.abc import Callable +from time import sleep, time from citrine.resources.status_detail import StatusDetail @@ -16,7 +16,9 @@ def wait_until(condition, timeout=30, interval=0.5): return result -def generate_fake_wait_while(*, status: str, status_detail: Optional[List[StatusDetail]] = None) -> Callable: +def generate_fake_wait_while( + *, status: str, status_detail: list[StatusDetail] | None = None +) -> Callable: """Generate a wait_while function that mutates a resource with the specified status info.""" status_detail = status_detail or [] diff --git a/tox.ini b/tox.ini index bd3bd3207..4c9867300 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,8 @@ max-doc-length = 119 # D301: backslash is used in making docstrings for sphinx to parse # D401: Imperative mood requirement basically gets in the way # W503: Line breaks before a binary operator are best practice -ignore = D100,D104,D105,D107,D301,D401,W503 +# E203: Whitespace before ':' in slices conflicts with ruff/black formatting +ignore = D100,D104,D105,D107,D301,D401,W503,E203 # D101, D102 would result in redundant documentation for subclasses. per-file-ignores =