Ejemplo n.º 1
0
 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)
Ejemplo n.º 2
0
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()
Ejemplo n.º 3
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 7
0
    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
Ejemplo n.º 10
0
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
Ejemplo n.º 12
0
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')
Ejemplo n.º 13
0
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),
        ]
    )
Ejemplo n.º 14
0
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
Ejemplo n.º 15
0
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
Ejemplo n.º 16
0
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"
Ejemplo n.º 18
0
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
Ejemplo n.º 19
0
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
Ejemplo n.º 21
0
    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().")
Ejemplo n.º 22
0
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
Ejemplo n.º 23
0
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
Ejemplo n.º 24
0
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'
Ejemplo n.º 26
0
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
Ejemplo n.º 27
0
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)
Ejemplo n.º 28
0
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'}})
Ejemplo n.º 29
0
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))