def make_link(entity: BaseEntity): if len(entity.uids) == 0: raise ValueError("No UID for {}".format(entity)) elif native_uid and native_uid in entity.uids: return LinkByUID(native_uid, entity.uids[native_uid]) else: return LinkByUID.from_entity(entity)
def test_serialized_history(): """Test the serialization of a complete material history.""" # Create several runs and specs linked together buy_spec = LinkByUID("id", "pr723") cookie_dough_spec = MaterialSpec("cookie dough spec", process=buy_spec) buy_cookie_dough = ProcessRun("Buy cookie dough", uids={'id': '32283'}, spec=buy_spec) cookie_dough = MaterialRun("cookie dough", process=buy_cookie_dough, spec=cookie_dough_spec) bake = ProcessRun("bake cookie dough", conditions=[ Condition("oven temp", origin='measured', value=NominalReal(357, 'degF'))]) IngredientRun(material=cookie_dough, process=bake, number_fraction=NominalReal(1, '')) cookie = MaterialRun("cookie", process=bake, tags=["chocolate chip", "drop"]) MeasurementRun("taste", material=cookie, properties=[ Property("taste", value=DiscreteCategorical("scrumptious"))]) cookie_history = complete_material_history(cookie) # There are 7 entities in the serialized list: cookie dough (spec & run), buy cookie dough, # cookie dough ingredient, bake cookie dough, cookie, taste assert len(cookie_history) == 7 for entity in cookie_history: assert len(entity['uids']) > 0, "Serializing material history should assign uids." # Check that the measurement points to the material taste_dict = next(x for x in cookie_history if x.get('type') == 'measurement_run') cookie_dict = next(x for x in cookie_history if x.get('name') == 'cookie') scope = taste_dict.get('material').get('scope') assert taste_dict.get('material').get('id') == cookie_dict.get('uids').get(scope) # Check that both the material spec and the process run point to the same process spec. # Because that spec was initially a LinkByUID, this also tests the methods ability to # serialize a LinkByUID. cookie_dough_spec_dict = next(x for x in cookie_history if x.get('type') == 'material_spec') buy_cookie_dough_dict = next(x for x in cookie_history if x.get('name') == 'Buy cookie dough') assert cookie_dough_spec_dict.get('process') == buy_spec.as_dict() assert buy_cookie_dough_dict.get('spec') == buy_spec.as_dict()
def test_invalid_assignment(): """Test that invalid assignment throws a TypeError.""" with pytest.raises(TypeError): PropertyAndConditions(property=LinkByUID('id', 'a15')) with pytest.raises(TypeError): PropertyAndConditions(property=Property("property"), conditions=[Condition("condition"), LinkByUID('scope', 'id')])
def test_passthrough_bounds(): """Test that unspecified Bounds are accepted and set to None.""" template = ProcessTemplate('foo', conditions=[ (LinkByUID('1', '2'), None), [LinkByUID('3', '4'), None], LinkByUID('5', '6'), ConditionTemplate('foo', bounds=IntegerBounds( 0, 10)), ]) assert len(template.conditions) == 4 for _, bounds in template.conditions: assert bounds is None copied = loads(dumps(template)) assert len(copied.conditions) == 4 for _, bounds in copied.conditions: assert bounds is None from_dict = ProcessTemplate.build({ 'type': 'process_template', 'name': 'foo', 'conditions': [[ { 'scope': 'foo', 'id': 'bar', 'type': 'link_by_uid', }, None, ]], }) assert len(from_dict.conditions) == 1
def test_equality(): """Test that the __eq__ method performs as expected.""" link = LinkByUID(scope="foo", id="bar") assert link == ProcessRun("Good", uids={"foo": "bar"}) assert link != ProcessRun("Good", uids={"foo": "rab"}) assert link != ProcessRun("Good", uids={"oof": "bar"}) assert link != LinkByUID(scope="foo", id="rab") assert link == ("foo", "bar") assert link != ("foo", "bar", "baz") assert link != ("foo", "rab")
def test_dictionary_substitution(): """substitute_objects() should substitute LinkByUIDs that occur in dict keys and values.""" proc = ProcessRun("A process", uids={'id': '123'}) mat = MaterialRun("A material", uids={'generic id': '38f8jf'}) proc_link = LinkByUID.from_entity(proc) mat_link = LinkByUID.from_entity(mat) index = {(mat_link.scope.lower(), mat_link.id): mat, (proc_link.scope.lower(), proc_link.id): proc} test_dict = {LinkByUID.from_entity(proc): LinkByUID.from_entity(mat)} subbed = substitute_objects(test_dict, index) k, v = next((k, v) for k, v in subbed.items()) assert k == proc assert v == mat
def to_link(self, scope: Optional[str] = None, *, allow_fallback: bool = False) -> 'LinkByUID': # noqa: F821 """ Generate a LinkByUID for this object. Parameters ---------- scope: str, optional scope of the uid to get allow_fallback: bool whether to grab another scope/id if chosen scope is missing (Default: False). Returns ------- LinkByUID """ from gemd.entity.link_by_uid import LinkByUID if len(self.uids) == 0: raise ValueError( f"{type(self)} {self.name} does not have any uids.") if (scope is None) or (allow_fallback and scope not in self.uids): scope = next(x for x in self.uids) uid = self.uids.get(scope, None) if uid is None: raise ValueError( f"{type(self)} {self.name} has no uid with scope {scope}.") return LinkByUID(scope=scope, id=uid)
def test_process_id_link(): """Test that a process run can house a LinkByUID object, and that it survives serde.""" uid = str(uuid4()) proc_link = LinkByUID(scope='id', id=uid) mat_run = MaterialRun("Another cake", process=proc_link) copy_material = loads(dumps(mat_run)) assert dumps(copy_material) == dumps(mat_run)
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') 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]) 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')) 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"]) 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"]) 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']) assert ProcessTemplate.build(proc_template.dump()) == proc_template
def test_scope_substitution(): """Test that the native id gets serialized, when specified.""" native_id = 'id1' # Create measurement and material with two ids mat = MaterialRun("A material", uids={ native_id: str(uuid4()), "an_id": str(uuid4()), "another_id": str(uuid4())}) meas = MeasurementRun("A measurement", material=mat, uids={ "some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4())}) # Turn the material pointer into a LinkByUID using native_id subbed = substitute_links(meas, scope=native_id) assert subbed.material == LinkByUID.from_entity(mat, scope=native_id) # Put the measurement into a list and convert that into a LinkByUID using native_id measurements_list = [meas] subbed = substitute_links(measurements_list, scope=native_id) assert subbed == [LinkByUID.from_entity(meas, scope=native_id)]
def test_tuple_sub(): """substitute_objects() should correctly substitute tuple values.""" proc = ProcessRun('foo', uids={'id': '123'}) proc_link = LinkByUID.from_entity(proc) index = {(proc_link.scope, proc_link.id): proc} tup = (proc_link,) subbed = substitute_objects(tup, index) assert subbed[0] == proc
def test_object_key_substitution(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" spec = ProcessSpec("A process spec", uids={'id': str(uuid4()), 'auto': str(uuid4())}) run1 = ProcessRun("A process run", spec=spec, uids={'id': str(uuid4()), 'auto': str(uuid4())}) run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) process_dict = {spec: [run1, run2]} subbed = substitute_links(process_dict, scope='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(spec, scope='auto') assert LinkByUID.from_entity(run1, scope='auto') in value assert LinkByUID.from_entity(run2) in value reverse_process_dict = {run2: spec} subbed = substitute_links(reverse_process_dict, scope='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(run2) assert value == LinkByUID.from_entity(spec, scope='auto')
def test_dump_example(): density = AttributeByTemplate( name="density", headers=["Slice", "Density"], template=LinkByUID(scope="templates", id="density") ) table_config = TableConfig( name="Example Table", description="Illustrative example that's meant to show how Table Configs will look serialized", datasets=[uuid4()], variables=[density], rows=[MaterialRunByTemplate(templates=[LinkByUID(scope="templates", id="slices")])], columns=[ MeanColumn(data_source=density.name), StdDevColumn(data_source=density.name), OriginalUnitsColumn(data_source=density.name), ] )
def test_template_access(): """A material run's template should be equal to its spec's template.""" template = MaterialTemplate("material template", uids={'id': str(uuid4())}) spec = MaterialSpec("A spec", uids={'id': str(uuid4())}, template=template) mat = MaterialRun("A run", uids=['id', str(uuid4())], spec=spec) assert mat.template == template mat.spec = LinkByUID.from_entity(spec) assert mat.template is None
def test_template_access(): """A process run's template should be equal to its spec's template.""" template = ProcessTemplate("process template", uids={'id': str(uuid4())}) spec = ProcessSpec("A spec", uids={'id': str(uuid4())}, template=template) proc = ProcessRun("A run", uids={'id': str(uuid4())}, spec=spec) assert proc.template == template proc.spec = LinkByUID.from_entity(spec) assert proc.template is None
def test_add_all_ingredients(session, project): """Test the behavior of AraDefinition.add_all_ingredients.""" # GIVEN process_id = '3a308f78-e341-f39c-8076-35a2c88292ad' process_name = 'mixing' allowed_names = ["gold nanoparticles", "methanol", "acetone"] process_link = LinkByUID('id', process_id) session.set_response( ProcessTemplate(process_name, uids={'id': process_id}, allowed_names=allowed_names).dump() ) # WHEN we add all ingredients in a volume basis def1 = empty_defn().add_all_ingredients(process_template=process_link, project=project, quantity_dimension=IngredientQuantityDimension.VOLUME) # THEN there should be 2 variables and columns for each name, one for id and one for quantity assert len(def1.variables) == len(allowed_names) * 2 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 session.set_response( 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, project=project, quantity_dimension=IngredientQuantityDimension.ABSOLUTE) # THEN there should be 1 new variable for each name, corresponding to the quantity # There is already a variable for id # 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):] assert len(new_variables) == len(allowed_names) assert len(new_columns) == len(allowed_names) * 2 assert def2.config_uid == UUID("6b608f78-e341-422c-8076-35adc8828545") 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 session.set_response( 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, project=project, 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() ) with pytest.raises(RuntimeError): empty_defn().add_all_ingredients(process_template=process_link, project=project, quantity_dimension=IngredientQuantityDimension.VOLUME)
def test_quantity_dimension_serializes_to_string(): variable = IngredientQuantityByProcessAndName( name="ingredient quantity dimension", headers=["quantity"], process_template=LinkByUID(scope="template", id="process"), ingredient_name="ingredient", quantity_dimension=IngredientQuantityDimension.NUMBER) variable_data = variable.dump() assert variable_data["quantity_dimension"] == "number"
def test_link_by_uid(): """Test that linking works.""" root = MaterialRun(name='root', process=ProcessRun(name='root proc')) leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) IngredientRun(process=root.process, material=leaf) IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf)) copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_substitute_equivalence(): """PLA-6423: verify that substitutions match up.""" spec = ProcessSpec(name="old spec", uids={'scope': 'spec'}) run = ProcessRun(name="old run", uids={'scope': 'run'}, spec=LinkByUID(id='spec', scope="scope")) # make a dictionary from ids to objects, to be used in substitute_objects gem_index = make_index([run, spec]) substitute_objects(obj=run, index=gem_index, inplace=True) assert spec == run.spec
def test_name_persistance(): """Verify that a serialized IngredientRun doesn't lose its name.""" from gemd.entity.object import IngredientSpec from gemd.entity.link_by_uid import LinkByUID from gemd.json import GEMDJson je = GEMDJson() ms_link = LinkByUID(scope='local', id='mat_spec') mr_link = LinkByUID(scope='local', id='mat_run') ps_link = LinkByUID(scope='local', id='pro_spec') pr_link = LinkByUID(scope='local', id='pro_run') spec = IngredientSpec(name='Ingred', labels=['some', 'words'], process=ps_link, material=ms_link) run = IngredientRun(spec=spec, process=pr_link, material=mr_link) assert run.name == spec.name assert run.labels == spec.labels # Try changing them and make sure they change spec.name = 'Frank' spec.labels = ['other', 'words'] assert run.name == spec.name assert run.labels == spec.labels run.spec = LinkByUID(scope='local', id='ing_spec') # Name and labels are now stashed but not stored assert run == je.copy(run) assert run.name == spec.name assert run.labels == spec.labels # Test that serialization doesn't get confused after a deser and set spec_too = IngredientSpec(name='Jorge', labels=[], process=ps_link, material=ms_link) run.spec = spec_too assert run == je.copy(run) assert run.name == spec_too.name assert run.labels == spec_too.labels
def _deserialize(self, value: dict): if 'type' in value and value['type'] == LinkByUID.typ: if 'scope' in value and 'id' in value: value.pop('type') return LinkByUID(**value) else: raise ValueError( "LinkByUID dictionary must have both scope and id fields") raise Exception( "Serializable object that is being pointed to must have a self-contained " "build() method that does not call deserialize().")
def test_template_access(): """A measurement run's template should be equal to its spec's template.""" template = MeasurementTemplate("measurement template", uids={'id': str(uuid4())}) spec = MeasurementSpec("A spec", uids={'id': str(uuid4())}, template=template) meas = MeasurementRun("A run", uids={'id': str(uuid4())}, spec=spec) assert meas.template == template meas.spec = LinkByUID.from_entity(spec) assert meas.template is None
def test_link_by_uid(): """Test that linking works.""" root = MaterialRun(name='root', process=ProcessRun(name='root proc')) leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) IngredientRun(process=root.process, material=leaf) IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf, scope='id')) # Paranoid assertions about equality's symmetry since it's implemented in 2 places assert root.process.ingredients[0].material == root.process.ingredients[ 1].material assert root.process.ingredients[0].material.__eq__( root.process.ingredients[1].material) assert root.process.ingredients[1].material.__eq__( root.process.ingredients[0].material) # Verify hash collision on equal LinkByUIDs assert LinkByUID.from_entity(leaf) in {LinkByUID.from_entity(leaf)} copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_to_link(): """Test that to_link behaves as expected.""" obj = IngredientRun(uids={"Scope": "UID", "Second": "option"}) assert isinstance(obj.to_link(), LinkByUID), "Returns a useful LinkByUID" assert LinkByUID(scope="Scope", id="UID") == obj.to_link("Scope"), "Correct choice of UID" with pytest.raises(ValueError): IngredientRun().to_link(), "to_link on an object w/o IDs is fatal" with pytest.raises(ValueError): obj.to_link("Third"), "to_link with a scope that an object lacks is fatal" assert obj.to_link(scope="Third", allow_fallback=True).scope in obj.uids, \ "... unless allow_fallback is set"
def test_simple_deserialization(): """Ensure that a deserialized Material Run looks sane.""" valid_data: dict = MaterialRunDataFactory(name='Cake 1') material_run: MaterialRun = MaterialRun.build(valid_data) assert material_run.uids == {'id': valid_data['uids']['id']} assert material_run.name == 'Cake 1' assert material_run.tags == ["color"] assert material_run.notes is None assert material_run.process == LinkByUID('id', valid_data['process']['id']) assert material_run.sample_type == 'experimental' assert material_run.template is None assert material_run.spec is None assert material_run.file_links == [] assert material_run.typ == 'material_run'
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 not isinstance(uids, dict) or not uids: return json if CITRINE_SCOPE in uids: scope = CITRINE_SCOPE else: scope = next(iter(uids)) this_id = uids[scope] return LinkByUID(scope, this_id).as_dict() else: return json
def test_exceptions(): """Additional tests to get full coverage on exceptions.""" with pytest.raises(ValueError): add_edge(MaterialRun("Input"), make_node("Output")) with pytest.raises(ValueError): add_edge(make_node("Input"), MaterialRun("Output", spec=LinkByUID("Bad", "ID"))) with pytest.raises(ValueError): add_measurement(make_node('Material'), name='Measurement', attributes=[UnsupportedAttribute("Spider-man")]) with pytest.raises(ValueError): make_attribute(UnsupportedAttributeTemplate, 5)
def test_thin_dumps(): """Test that thin_dumps turns pointers into links.""" mat = MaterialRun("The actual material") meas_spec = MeasurementSpec("measurement", uids={'my_scope': '324324'}) meas = MeasurementRun("The measurement", spec=meas_spec, material=mat) thin_copy = MeasurementRun.build(json.loads(GEMDJson().thin_dumps(meas))) assert isinstance(thin_copy, MeasurementRun) assert isinstance(thin_copy.material, LinkByUID) assert isinstance(thin_copy.spec, LinkByUID) assert thin_copy.spec.id == meas_spec.uids['my_scope'] # Check that LinkByUID objects are correctly converted their JSON equivalent expected_json = '{"id": "my_id", "scope": "scope", "type": "link_by_uid"}' assert GEMDJson().thin_dumps(LinkByUID('scope', 'my_id')) == expected_json # Check that objects lacking .uid attributes will raise an exception when dumped with pytest.raises(TypeError): GEMDJson().thin_dumps({{'key': 'value'}})
def _poll_for_async_batch_delete_result( project_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). Parameters ---------- project_id: UUID The Project ID to use in the delete request. session: Session The Citrine session. job_id: str The asynchronous Job ID. timeout: float Amount of time to wait on the job (in seconds) before giving up. Note that this number has no effect on the underlying job itself, which can also time out server-side. polling_delay: float How long to delay between each polling retry attempt. Returns ------- List[Tuple[LinkByUID, ApiError]] A list of (LinkByUID, api_error) for each failure to delete an object. Note that this method doesn't raise an exception if an object fails to be deleted. """ response = _poll_for_job_completion(session, project_id, job_id, timeout=timeout, polling_delay=polling_delay) return [(LinkByUID(f['id']['scope'], f['id']['id']), ApiError.from_dict(f['cause'])) for f in json.loads(response.output.get('failures', '[]'))]
def get_history(self, scope: str, id: Union[str, UUID]) -> Type[MaterialRun]: """ Get the history associated with a terminal material. The history contains every single every process, ingredient and material that went into the terminal material as well as the measurements that were performed on all of those materials. The returned object is a material run with all of its fields fully populated. Parameters ---------- scope: str The scope used to locate the material. id: str The unique id corresponding to `scope`. The lookup will be most efficient if you use the Citrine ID (scope='id') of the material. Returns ------- MaterialRun A material run that has all of its fields fully populated with the processes, ingredients, measurements, and other materials that were involved in the history of the object. """ base_path = os.path.dirname(self._get_path(ignore_dataset=True)) path = base_path + "/material-history/{}/{}".format(scope, id) data = self.session.get_resource(path) # Add the root to the context and sort by writable order blob = dict() blob["context"] = sorted(data['context'] + [data['root']], key=lambda x: writable_sort_order(x["type"])) terminal_scope, terminal_id = next(iter(data['root']['uids'].items())) # Add a link to the root as the "object" blob["object"] = LinkByUID(scope=terminal_scope, id=terminal_id) # Serialize using normal json (with the GEMDEncoder) and then deserialize with the # GEMDEncoder encoder in order to rebuild the material history return MaterialRun.get_json_support().loads( json.dumps(blob, cls=GEMDEncoder, sort_keys=True))