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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/data_entry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The Citrine Python client implementations are consistent with the GEMD_ model sp

The Citrine Python client is built on top of and entirely interoperable with the gemd-python_ package.
Any method that accepts the Citrine Python client's implementations of data model objects should also accept those from GEMD.
A run may also reference its template directly through its ``template`` field, and an :class:`~citrine.resources.ingredient_run.IngredientRun` may carry its own ``name`` and ``labels``.

Identifying Data Model Objects
---------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/citrine/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "5.0.1"
__version__ = "5.1.0"
35 changes: 35 additions & 0 deletions src/citrine/resources/ingredient_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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_run import ProcessRun as GEMDProcessRun
from gemd.entity.setters import validate_list, validate_str
from gemd.entity.value.continuous_value import ContinuousValue

from citrine._rest.resource import GEMDResource
Expand Down Expand Up @@ -54,6 +55,10 @@ class IngredientRun(
The absolute quantity of the ingredient in the process.
spec: IngredientSpec
The specification of which this ingredient is a realization.
name: str, optional
The ingredient's own name. When unset, the name of the spec applies.
labels: list[str], optional
The ingredient's own labels. When unset, the labels of the spec apply.
file_links: list[FileLink], optional
Links to associated files, with resource paths into the files API.

Expand Down Expand Up @@ -88,6 +93,8 @@ def __init__(
number_fraction: ContinuousValue | None = None,
absolute_quantity: ContinuousValue | None = None,
spec: GEMDIngredientSpec | None = None,
name: str | None = None,
labels: list[str] | None = None,
file_links: list[FileLink] | None = None,
):
if uids is None:
Expand All @@ -107,6 +114,34 @@ def __init__(
spec=spec,
file_links=file_links,
)
self.name = name
self.labels = labels

@property
def name(self) -> str | None:
"""The ingredient's own name when set, otherwise the name of its spec."""
if self._name is not None:
return self._name
if isinstance(self.spec, GEMDIngredientSpec):
return self.spec.name
return None

@name.setter
def name(self, name: str | None):
self._name = None if name is None else validate_str(name)

@property
def labels(self) -> list[str] | None:
"""The ingredient's own labels when set, otherwise the labels of its spec."""
if self._labels is not None:
return self._labels
if isinstance(self.spec, GEMDIngredientSpec):
return self.spec.labels
return None

@labels.setter
def labels(self, labels: list[str] | None):
self._labels = None if labels is None else validate_list(labels, str)

def __str__(self):
return f"<Ingredient run {self.name!r}>"
Expand Down
17 changes: 14 additions & 3 deletions src/citrine/resources/material_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
from citrine.resources._default_labels import _inject_default_label_tags
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 citrine.resources.object_runs import ObjectRunCollection, TemplatedObjectRun


class MaterialRun(
GEMDResource["MaterialRun"], ObjectRun, GEMDMaterialRun, typ=GEMDMaterialRun.typ
GEMDResource["MaterialRun"], TemplatedObjectRun, GEMDMaterialRun, typ=GEMDMaterialRun.typ
):
"""
A material run.
Expand All @@ -46,6 +46,9 @@ class MaterialRun(
"unknown." Default is "unknown."
spec: MaterialSpec
The material specification of which this is an instance.
template: MaterialTemplate, optional
The material template this run is an instance of.
When unset, the template of the spec applies.
file_links: list[FileLink], optional
Links to associated files, with resource paths into the files API.
default_labels: list[str], optional
Expand All @@ -63,6 +66,7 @@ class MaterialRun(
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)
_template = Optional(LinkOrElse(GEMDMaterialTemplate), "template")

def __init__(
self,
Expand All @@ -74,13 +78,14 @@ def __init__(
process: GEMDProcessRun | None = None,
sample_type: str | None = "unknown",
spec: GEMDMaterialSpec | None = None,
template: GEMDMaterialTemplate | LinkByUID | 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__()
super(TemplatedObjectRun, self).__init__()
GEMDMaterialRun.__init__(
self,
name=name,
Expand All @@ -92,6 +97,12 @@ def __init__(
file_links=file_links,
notes=notes,
)
self.template = template

@staticmethod
def _template_type() -> type:
"""Return the template type this run accepts."""
return GEMDMaterialTemplate

def __str__(self):
return f"<Material run {self.name!r}>"
Expand Down
23 changes: 20 additions & 3 deletions src/citrine/resources/measurement_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@
from gemd.entity.object.measurement_run import MeasurementRun as GEMDMeasurementRun
from gemd.entity.object.measurement_spec import MeasurementSpec as GEMDMeasurementSpec
from gemd.entity.source.performed_source import PerformedSource
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_runs import ObjectRun, ObjectRunCollection
from citrine.resources.object_runs import ObjectRunCollection, TemplatedObjectRun


class MeasurementRun(
GEMDResource["MeasurementRun"], ObjectRun, GEMDMeasurementRun, typ=GEMDMeasurementRun.typ
GEMDResource["MeasurementRun"],
TemplatedObjectRun,
GEMDMeasurementRun,
typ=GEMDMeasurementRun.typ,
):
"""
A measurement run.
Expand All @@ -46,6 +52,9 @@ class MeasurementRun(
Properties that are measured during this measurement run.
spec: MeasurementSpec
The measurement specification of which this is an instance.
template: MeasurementTemplate, optional
A template bounding the valid values for this run's attributes.
When unset, the template of the spec applies.
material: MaterialRun
The material run being measured.
spec: MaterialSpec
Expand All @@ -64,6 +73,7 @@ class MeasurementRun(
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)
_template = Optional(LinkOrElse(GEMDMeasurementTemplate), "template")
material = Optional(LinkOrElse(GEMDMaterialRun), "material", override=True, use_init=True)
source = Optional(Object(PerformedSource), "source", override=True)

Expand All @@ -78,13 +88,14 @@ def __init__(
properties: list[Property] | None = None,
parameters: list[Parameter] | None = None,
spec: GEMDMeasurementSpec | None = None,
template: GEMDMeasurementTemplate | LinkByUID | 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__()
super(TemplatedObjectRun, self).__init__()
GEMDMeasurementRun.__init__(
self,
name=name,
Expand All @@ -99,6 +110,12 @@ def __init__(
notes=notes,
source=source,
)
self.template = template

@staticmethod
def _template_type() -> type:
"""Return the template type this run accepts."""
return GEMDMeasurementTemplate

def __str__(self):
return f"<Measurement run {self.name!r}>"
Expand Down
52 changes: 51 additions & 1 deletion src/citrine/resources/object_runs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Top-level class for all object run objects and collections thereof."""

from abc import ABC
from abc import ABC, abstractmethod
from typing import TypeVar

from gemd.entity.base_entity import BaseEntity
from gemd.entity.link_by_uid import LinkByUID
from gemd.entity.object.has_spec import HasSpec
from gemd.entity.object.has_template import HasTemplate
from gemd.entity.template.base_template import BaseTemplate

from citrine.resources.data_objects import DataObject, DataObjectCollection

Expand All @@ -16,6 +20,52 @@ class ObjectRun(DataObject, HasSpec, ABC):
"""


class TemplatedObjectRun(ObjectRun, ABC):
"""
An abstract object run that can reference its template directly.

The run's own template takes precedence over the template of its spec.
Only a template set on the run itself is sent to the platform; the template of the spec
is never copied onto the run.

Subclasses declare the serialized ``_template`` field and the accepted template type.
"""

@staticmethod
@abstractmethod
def _template_type() -> type:
"""Return the template type this run accepts."""

@property
def template(self) -> BaseTemplate | LinkByUID | None:
"""The run's own template when set, otherwise the template of its spec."""
if self._template is not None:
return self._template
return super().template

@template.setter
def template(self, template: BaseTemplate | LinkByUID | None):
if template is not None and not isinstance(template, (self._template_type(), LinkByUID)):
raise TypeError(
f"template must be a {self._template_type().__name__} or LinkByUID, "
f"not {type(template)}"
)
self._template = template

def _local_dependencies(self) -> set[BaseEntity | LinkByUID]:
"""Return the spec and the run's own template, when set."""
return {x for x in (self.spec, self._template) if x is not None}

def _dict_for_compare(self) -> dict:
"""Support for recursive equals."""
result = super()._dict_for_compare()
# A gemd run that mixes in HasTemplate has a template key in its as_dict; one that
# does not has no such key, so the key is dropped to keep the two comparable.
if not isinstance(self, HasTemplate):
result.pop("template", None)
return result


ObjectRunResourceType = TypeVar("ObjectRunResourceType", bound="ObjectRun")


Expand Down
20 changes: 17 additions & 3 deletions src/citrine/resources/process_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
from gemd.entity.object.process_run import ProcessRun as GEMDProcessRun
from gemd.entity.object.process_spec import ProcessSpec as GEMDProcessSpec
from gemd.entity.source.performed_source import PerformedSource
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_runs import ObjectRun, ObjectRunCollection
from citrine.resources.object_runs import ObjectRunCollection, TemplatedObjectRun


class ProcessRun(GEMDResource["ProcessRun"], ObjectRun, GEMDProcessRun, typ=GEMDProcessRun.typ):
class ProcessRun(
GEMDResource["ProcessRun"], TemplatedObjectRun, GEMDProcessRun, typ=GEMDProcessRun.typ
):
"""
A process run.

Expand All @@ -42,6 +45,9 @@ class ProcessRun(GEMDResource["ProcessRun"], ObjectRun, GEMDProcessRun, typ=GEMD
Parameters of this process run.
spec: ProcessSpec
Spec for this process run.
template: ProcessTemplate, optional
A template bounding the valid values for this run's conditions and parameters.
When unset, the template of the spec applies.
file_links: list[FileLink], optional
Links to associated files, with resource paths into the files API.
source: PerformedSource, optional
Expand All @@ -55,6 +61,7 @@ class ProcessRun(GEMDResource["ProcessRun"], ObjectRun, GEMDProcessRun, typ=GEMD
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)
_template = Optional(LinkOrElse(GEMDProcessTemplate), "template")
source = Optional(Object(PerformedSource), "source", override=True)

def __init__(
Expand All @@ -67,12 +74,13 @@ def __init__(
conditions: list[Condition] | None = None,
parameters: list[Parameter] | None = None,
spec: GEMDProcessSpec | None = None,
template: GEMDProcessTemplate | LinkByUID | None = None,
file_links: list[FileLink] | None = None,
source: PerformedSource | None = None,
):
if uids is None:
uids = dict()
super(ObjectRun, self).__init__()
super(TemplatedObjectRun, self).__init__()
GEMDProcessRun.__init__(
self,
name=name,
Expand All @@ -85,6 +93,12 @@ def __init__(
notes=notes,
source=source,
)
self.template = template

@staticmethod
def _template_type() -> type:
"""Return the template type this run accepts."""
return GEMDProcessTemplate

def __str__(self):
return f"<Process run {self.name!r}>"
Expand Down
37 changes: 37 additions & 0 deletions tests/resources/test_ingredient_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,40 @@ def test_equals():
assert gemd_obj == citrine_obj, "GEMD/Citrine equivalence"
citrine_obj.notes = "Something else"
assert gemd_obj != citrine_obj, "GEMD/Citrine detects difference"


def test_name_and_labels_precedence():
"""An ingredient run's own name and labels take precedence over the spec's."""
from citrine.resources.ingredient_run import IngredientRun as CitrineIngredientRun
from citrine.resources.ingredient_spec import IngredientSpec

bare = CitrineIngredientRun()
assert bare.name is None
assert bare.labels is None

spec = IngredientSpec(name="flour", labels=["dry"], uids={"id": "spec"})
run = CitrineIngredientRun(spec=spec, uids={"id": "run"})
assert run.name == "flour"
assert run.labels == ["dry"]
assert run.dump()["name"] is None, "Only the run's own name is serialized"
assert run.dump()["labels"] is None, "Only the run's own labels are serialized"

run.name = "bleached flour"
run.labels = ["dry", "white"]
assert run.name == "bleached flour"
assert run.labels == ["dry", "white"]
built = CitrineIngredientRun.build(run.dump())
assert built.name == "bleached flour"
assert built.labels == ["dry", "white"]

run.name = None
run.labels = None
assert run.name == "flour"
assert run.labels == ["dry"]

own = CitrineIngredientRun(name="salt", labels=["mineral"], spec=spec)
assert own.name == "salt"
assert own.labels == ["mineral"]

with pytest.raises(TypeError):
run.name = 5
Loading
Loading