diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/jsonbundler.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/jsonbundler.py index be38237c..00fff827 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/jsonbundler.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/jsonbundler.py @@ -61,6 +61,37 @@ def get_evidence_key_filter(self): return BasicListEvidenceKeyFilter([]) + @staticmethod + def record_json(record): + + """! + A record, being the Properties of one Element Data in a list, as an + object. A Property that has been assigned no value is null with its + nullreason beside it, and a Property that is null without one has no + entry. + @type record: dict + @param record: Property name to value or aspect property value + @rtype: dict + @return: The record as it is written to JSON + """ + + written = {} + + for key, value in record.items(): + key = str(key).lower() + + if hasattr(value, "has_value"): + if value.has_value(): + written[key] = value.value() + else: + written[key] = None + written[key + "nullreason"] = ( + value.no_value_message() or "Unknown") + elif value is not None: + written[key] = value + + return written + def process_internal(self, flowdata): """! @@ -134,6 +165,7 @@ def process_internal(self, flowdata): for propertykey, propertymeta in properties.items(): value = None null_reason = "Unknown" + no_value = False # Check if property has delayed execution and set in JSON if yes @@ -162,21 +194,28 @@ def process_internal(self, flowdata): value = value_container.value() else: value = None - null_reason = value_container.no_value_message() + no_value = True + null_reason = ( + value_container.no_value_message() + or null_reason) - # Check if list of aspect property values + # A list of records, such as the profiles of a hardware + # profile lookup, or of aspect property values - elif isinstance(value_container, list) and isinstance(value_container[0], object): + elif isinstance(value_container, list): - output = [] + items = [] for item in value_container: - if item.has_value(): - output.append(item.value()) + if isinstance(item, dict): + items.append(self.record_json(item)) + elif hasattr(item, "has_value"): + if item.has_value(): + items.append(item.value()) else: - null_reason = item.no_value_message + items.append(item) - value = output + value = items else: @@ -187,14 +226,18 @@ def process_internal(self, flowdata): # Catching missing property exceptions and other errors continue - + if (isinstance(value, str) and str(propertymeta.get("type", "")).lower() in WKT_TYPES): value = {"value": value} - output[flow_element.datakey.lower()][propertykey.lower()] = value - if value is None: - output[flow_element.datakey.lower()][propertykey.lower() + "nullreason"] = null_reason + # A null that was not assigned 'no value', which an element + # that is not an engine can give, has no entry of its own. A + # JavaScript Property is still listed below. + if value is not None or no_value: + output[flow_element.datakey.lower()][propertykey.lower()] = value + if value is None: + output[flow_element.datakey.lower()][propertykey.lower() + "nullreason"] = null_reason sequence = flowdata.evidence.get("query.sequence") diff --git a/pipeline/fiftyone_pipeline_core/tests/test_jsonbundler_records.py b/pipeline/fiftyone_pipeline_core/tests/test_jsonbundler_records.py new file mode 100644 index 00000000..e6ff5cf0 --- /dev/null +++ b/pipeline/fiftyone_pipeline_core/tests/test_jsonbundler_records.py @@ -0,0 +1,95 @@ + +""" +A Property whose value is a list of Element Data is written as a list of +objects, each following the null rules of the JSON Builder specification, and +the other Properties are written beside it. +""" + +import unittest + +from fiftyone_pipeline_core.aspectproperty_value import AspectPropertyValue +from fiftyone_pipeline_core.elementdata_dictionary import ElementDataDictionary +from fiftyone_pipeline_core.flowelement import FlowElement +from fiftyone_pipeline_core.pipelinebuilder import PipelineBuilder + + +class ProfilesElement(FlowElement): + + def __init__(self): + super(ProfilesElement, self).__init__() + self.datakey = "hardware" + self.properties = { + "profiles": {"name": "Profiles", "type": "array"}, + "vendor": {"name": "Vendor", "type": "string"}, + "model": {"name": "Model", "type": "string"}, + "colour": {"name": "Colour", "type": "string"}, + "script": {"name": "Script", "type": "javascript"}, + "size": {"name": "Size", "type": "string"}} + + def process_internal(self, flowdata): + records = [ + {"hardwarename": AspectPropertyValue(value="Phone"), + "hardwarevendor": AspectPropertyValue(no_value_message="No data"), + "hardwaremodel": None}, + {"hardwarename": AspectPropertyValue(value="Tablet")}, + {"hardwarename": AspectPropertyValue()}] + flowdata.set_element_data(ElementDataDictionary( + self, {"profiles": records, + "vendor": AspectPropertyValue(value="Acme"), + "model": None, + "colour": AspectPropertyValue( + no_value_message="No colour"), + "script": None, + "size": AspectPropertyValue()})) + + +class JsonBuilderRecordTests(unittest.TestCase): + + def process_json(self): + pipeline = PipelineBuilder({}).add(ProfilesElement()).build() + self.addCleanup(pipeline.close) + flowdata = pipeline.create_flowdata() + flowdata.process() + return flowdata.jsonbundler.json + + def process(self): + return self.process_json()["hardware"] + + def test_each_record_is_an_object(self): + self.assertEqual( + [{"hardwarename": "Phone", + "hardwarevendor": None, + "hardwarevendornullreason": "No data"}, + {"hardwarename": "Tablet"}, + {"hardwarename": None, "hardwarenamenullreason": "Unknown"}], + self.process()["profiles"]) + + def test_a_null_that_was_not_assigned_no_value_has_no_entry(self): + self.assertNotIn("hardwaremodel", self.process()["profiles"][0]) + + def test_the_other_properties_are_still_written(self): + self.assertEqual("Acme", self.process()["vendor"]) + + def test_a_top_level_null_not_assigned_no_value_has_no_entry(self): + hardware = self.process() + self.assertNotIn("model", hardware) + self.assertNotIn("modelnullreason", hardware) + + def test_a_null_javascript_property_is_still_listed(self): + json = self.process_json() + self.assertNotIn("script", json["hardware"]) + self.assertIn("hardware.script", json["javascriptProperties"]) + + def test_no_value_with_no_message_has_a_text_reason(self): + hardware = self.process() + self.assertIsNone(hardware["size"]) + self.assertEqual("Unknown", hardware["sizenullreason"]) + + def test_a_top_level_property_assigned_no_value_is_null(self): + hardware = self.process() + self.assertIsNone(hardware["colour"]) + self.assertEqual("No colour", hardware["colournullreason"]) + + +if __name__ == "__main__": + unittest.main()