From 769c76d48e76b6d52aece3698ba137de52e86b1f Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 16:17:22 +0000 Subject: [PATCH 1/7] Give each web integration element its own switch and put Sequence first The Sequence, JSON Builder, JavaScript Builder and Set Headers elements each have a switch that defaults to on, and add_javascript_builder and use_setheader_properties work as before. The Sequence Element is the first element of the pipeline and also writes the session-id and sequence Element Data, as well as its Evidence. --- .../tests/test_cloud.py | 6 +- .../tests/test_automatic_data_file_updates.py | 22 ++-- .../tests/test_devicedetection.py | 18 +-- .../fiftyone_pipeline_core/pipelinebuilder.py | 69 ++++++++--- .../fiftyone_pipeline_core/sequenceelement.py | 24 ++-- .../tests/test_web_integration_elements.py | 112 ++++++++++++++++++ 6 files changed, 201 insertions(+), 50 deletions(-) create mode 100644 pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py diff --git a/device-detection/fiftyone_devicedetection_cloud/tests/test_cloud.py b/device-detection/fiftyone_devicedetection_cloud/tests/test_cloud.py index 177a42ed..7d769b84 100644 --- a/device-detection/fiftyone_devicedetection_cloud/tests/test_cloud.py +++ b/device-detection/fiftyone_devicedetection_cloud/tests/test_cloud.py @@ -59,8 +59,8 @@ def test_pipeline_builder_cloud_engine_init(self): pipeline = DeviceDetectionCloudPipelineBuilder(resource_key = self.resource_key).build() - self.assertTrue(pipeline.flow_elements[0].datakey == "cloud") - self.assertTrue(pipeline.flow_elements[1].datakey == "device") + self.assertIn("cloud", pipeline.flow_elements_list) + self.assertIn("device", pipeline.flow_elements_list) def test_properties_cloud(self): """! @@ -69,7 +69,7 @@ def test_properties_cloud(self): pipeline = DeviceDetectionCloudPipelineBuilder(resource_key = self.resource_key).build() - properties = pipeline.flow_elements[1].get_properties() + properties = pipeline.get_element("device").get_properties() self.assertTrue(len(properties.keys()) > 0) diff --git a/device-detection/fiftyone_devicedetection_onpremise/tests/test_automatic_data_file_updates.py b/device-detection/fiftyone_devicedetection_onpremise/tests/test_automatic_data_file_updates.py index e1ac9944..47cfb443 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/tests/test_automatic_data_file_updates.py +++ b/device-detection/fiftyone_devicedetection_onpremise/tests/test_automatic_data_file_updates.py @@ -73,7 +73,7 @@ def test_on_premise_data_update_url(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertFalse(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -92,7 +92,7 @@ def test_on_premise_data_old_property_data_file_update_base_url(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertFalse(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -114,7 +114,7 @@ def test_on_premise_data_update_url_config(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertFalse(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -136,7 +136,7 @@ def test_on_premise_data_config_old_property_data_file_update_base_url(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertFalse(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -155,7 +155,7 @@ def test_on_premise_data_update_verify_md5(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertTrue(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -174,7 +174,7 @@ def test_on_premise_data_update_verify_md5_old_property(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertTrue(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -196,7 +196,7 @@ def test_on_premise_data_update_verify_md5_config(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertTrue(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -218,7 +218,7 @@ def test_on_premise_data_update_verify_md5_config_old_property(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertTrue(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) @@ -238,7 +238,7 @@ def test_on_premise_data_update_use_url_formatter(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self._assertNotSignedUrl(flow_element_data_file.get_update_url()) @@ -260,7 +260,7 @@ def test_on_premise_data_update_use_url_formatter_config(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self._assertNotSignedUrl(flow_element_data_file.get_update_url()) @@ -278,7 +278,7 @@ def test_on_premise_data_update_status_rate_limit(self): with pipeline.create_flowdata() as flowdata: flowdata.process() - flow_element_data_file = pipeline.flow_elements[0].data_file + flow_element_data_file = pipeline.get_element("device").data_file self.assertIsInstance(flow_element_data_file, DeviceDetectionDataFile) self.assertTrue(flow_element_data_file.verify_md5) self._assertSignedUrl(flow_element_data_file.get_update_url()) diff --git a/device-detection/fiftyone_devicedetection_onpremise/tests/test_devicedetection.py b/device-detection/fiftyone_devicedetection_onpremise/tests/test_devicedetection.py index 11ed21f6..af63494a 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/tests/test_devicedetection.py +++ b/device-detection/fiftyone_devicedetection_onpremise/tests/test_devicedetection.py @@ -63,7 +63,7 @@ def test_pipeline_builder_shareusage_init(self): data_file_path=data_file, licence_keys="").build() self.addCleanup(pipeline.close) - self.assertTrue(pipeline.flow_elements[0].datakey == "shareusage") + self.assertIn("shareusage", pipeline.flow_elements_list) def test_on_premise_engine_datafile(self): """! @@ -75,7 +75,7 @@ def test_on_premise_engine_datafile(self): self.addCleanup(pipeline.close) self.assertTrue(isinstance( - pipeline.flow_elements[0].data_file, DeviceDetectionDataFile)) + pipeline.get_element("device").data_file, DeviceDetectionDataFile)) def test_properties_onpremise(self): """! @@ -87,7 +87,7 @@ def test_properties_onpremise(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - properties = pipeline.flow_elements[0].get_properties() + properties = pipeline.get_element("device").get_properties() self.assertTrue(len(properties.keys()) > 0) @@ -100,7 +100,7 @@ def test_evidencekey_filter_onpremise(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - evidence_key_filter = pipeline.flow_elements[0].get_evidence_key_filter( + evidence_key_filter = pipeline.get_element("device").get_evidence_key_filter( ) self.assertFalse(evidence_key_filter.filter_evidence_key("test.test")) @@ -310,7 +310,7 @@ def test_evidencekey_filter_contains_user_agent(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - evidence_key_filter = pipeline.flow_elements[0].get_evidence_key_filter( + evidence_key_filter = pipeline.get_element("device").get_evidence_key_filter( ) self.assertTrue( @@ -325,7 +325,7 @@ def test_evidencekey_filter_contains_device_stock_ua(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - evidence_key_filter = pipeline.flow_elements[0].get_evidence_key_filter( + evidence_key_filter = pipeline.get_element("device").get_evidence_key_filter( ) self.assertTrue(evidence_key_filter.filter_evidence_key( @@ -340,7 +340,7 @@ def test_evidencekey_filter_contains_query_params(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - evidence_key_filter = pipeline.flow_elements[0].get_evidence_key_filter( + evidence_key_filter = pipeline.get_element("device").get_evidence_key_filter( ) self.assertTrue(evidence_key_filter.filter_evidence_key( @@ -357,7 +357,7 @@ def test_evidencekey_filter_case_insensitive_keys(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - evidence_key_filter = pipeline.flow_elements[0].get_evidence_key_filter( + evidence_key_filter = pipeline.get_element("device").get_evidence_key_filter( ) self.assertTrue( @@ -378,7 +378,7 @@ def test_evidencekey_filter_overrides(self): data_file_path=data_file, usage_sharing=False, licence_keys="").build() self.addCleanup(pipeline.close) - evidence_key_filter = pipeline.flow_elements[0].get_evidence_key_filter( + evidence_key_filter = pipeline.get_element("device").get_evidence_key_filter( ) self.assertTrue(evidence_key_filter.filter_evidence_key( diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/pipelinebuilder.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/pipelinebuilder.py index c3fa52e0..33e7829c 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/pipelinebuilder.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/pipelinebuilder.py @@ -42,12 +42,22 @@ def __init__(self, settings= {}): Pipeline Builder constructor. @type settings: dictionary @param settings : settings for the pipeline builder including: - `add_javascript_builder (Bool)` - Whether to add the JavaScriptBuilder to the pipeline - (default true) + `add_javascript_builder (Bool)` - Whether to add the JavaScriptBuilder, with the + JSONBundler and SequenceElement it works with, to the pipeline (default true). + Set to False to add none of the three. + `add_sequence_element (Bool)` - Whether to add the SequenceElement, as the first + element of the pipeline (default true) + `add_json_builder_element (Bool)` - Whether to add the JSONBundler (default true). + The JavaScriptBuilder reads its output, so a pipeline that keeps the + JavaScriptBuilder needs a JSONBundler, added here or by the caller. + `add_javascript_builder_element (Bool)` - Whether to add the JavaScriptBuilder + alone (default true) `javascript_builder_settings (dict)` - Settings for the JavaScriptBuilder engine @rtype: PipelineBuilder `use_setheader_properties (Bool)` - Whether to add the SetHeaderElement to the pipeline (default true) + `add_set_header_element (Bool)` - The same switch as use_setheader_properties. + The SetHeaderElement is added only when both are true (default true) `suppress_process_exceptions (Bool)` - If False (the default) the first error thrown while processing is re-raised from flow_data.process(); if True, errors are stored on flow_data.errors and process() returns normally. Recommended True for web apps so a @@ -67,6 +77,14 @@ def __init__(self, settings= {}): else: self.add_javaScriptbuilder = True + self.add_sequence_element = settings.get("add_sequence_element", True) + self.add_json_builder_element = settings.get( + "add_json_builder_element", True) + self.add_javascript_builder_element = settings.get( + "add_javascript_builder_element", True) + self.add_set_header_element = settings.get( + "add_set_header_element", True) + if "javascript_builder_settings" in settings: self.javascriptbuilder_settings = settings["javascript_builder_settings"] @@ -90,10 +108,25 @@ def __init__(self, settings= {}): else: self.auto_close_elements = True + def get_sequence_elements(self): + """ + The SequenceElement, to be the first element of the pipeline, unless + add_javascript_builder or add_sequence_element is false + @rtype: list + @return: Returns a list of FlowElements + """ + + if self.add_javaScriptbuilder and self.add_sequence_element: + return [SequenceElement()] + + return [] + def get_javascript_elements(self): """ - Adds the JavaScriptBuilder, JSONBundler and SequenceElement to the pipeline if - If add_javascript_builder is set to true (the default) + The JSONBundler and the JavaScriptBuilder, to be added after the other + elements. Neither is added if add_javascript_builder is false, and + each has its own switch (add_json_builder_element and + add_javascript_builder_element). @rtype: list @return: Returns a list of FlowElements """ @@ -102,27 +135,28 @@ def get_javascript_elements(self): if (self.add_javaScriptbuilder): - flow_elements.append(SequenceElement()) - flow_elements.append(JSONBundlerElement()) - - if (hasattr(self, "javascriptbuilder_settings")): - flow_elements.append(JavascriptBuilderElement(self.javascriptbuilder_settings)) - else: - flow_elements.append(JavascriptBuilderElement()) + if self.add_json_builder_element: + flow_elements.append(JSONBundlerElement()) + + if self.add_javascript_builder_element: + if (hasattr(self, "javascriptbuilder_settings")): + flow_elements.append(JavascriptBuilderElement(self.javascriptbuilder_settings)) + else: + flow_elements.append(JavascriptBuilderElement()) return flow_elements def get_setheader_elements(self): """ Adds the SetHeaderElement to the pipeline if - If use_setheader_properties is set to true (the default) + If use_setheader_properties and add_set_header_element are true (the default) @rtype: list @return: Returns a list of FlowElements """ flow_elements = [] - if (self.use_setheader_properties): + if self.use_setheader_properties and self.add_set_header_element: flow_elements.append(SetHeaderElement()) return flow_elements @@ -153,10 +187,13 @@ def build(self): """ - self.flow_elements.extend(self.get_javascript_elements()) - self.flow_elements.extend(self.get_setheader_elements()) + flow_elements = ( + self.get_sequence_elements() + + self.flow_elements + + self.get_javascript_elements() + + self.get_setheader_elements()) - return Pipeline(self.flow_elements, logger=self.logger, suppress_process_exceptions=self.suppress_process_exceptions, data_file_update_service=self.data_file_update_service, auto_close_elements=self.auto_close_elements) + return Pipeline(flow_elements, logger=self.logger, suppress_process_exceptions=self.suppress_process_exceptions, data_file_update_service=self.data_file_update_service, auto_close_elements=self.auto_close_elements) def add_logger(self, logger): """ diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py index c4056232..d72858a8 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py @@ -24,6 +24,7 @@ from .flowelement import FlowElement from .evidence_keyfilter import EvidenceKeyFilter from .javascriptbuilder import parse_sequence +from .elementdata_dictionary import ElementDataDictionary import uuid class SequenceElementEvidenceKeyFilter(EvidenceKeyFilter): @@ -66,7 +67,8 @@ def get_evidence_key_filter(self): def process_internal(self, flowdata): """! - The SequenceElement stores session data for requests for JavaScript + The SequenceElement stores session data for requests for JavaScript, + in its Element Data (session-id and sequence) and in Evidence @type flowdata: FlowData @param flowdata: The FlowData @@ -81,15 +83,15 @@ def process_internal(self, flowdata): # builder renders any value it cannot use as 1 in the same way. sequence = parse_sequence(flowdata.evidence.get("query.sequence")) - - flowdata.evidence.add( - "query.sequence", - 1 if sequence is None else sequence + 1) + sequence = 1 if sequence is None else sequence + 1 + session_id = flowdata.evidence.get("query.session-id") else: - flowdata.evidence.add( - "query.session-id", - str(uuid.uuid4()) - ) - - flowdata.evidence.add("query.sequence", 1) + session_id = str(uuid.uuid4()) + sequence = 1 + flowdata.evidence.add("query.session-id", session_id) + + flowdata.evidence.add("query.sequence", sequence) + + flowdata.set_element_data(ElementDataDictionary( + self, {"session-id": session_id, "sequence": sequence})) diff --git a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py new file mode 100644 index 00000000..141abaca --- /dev/null +++ b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py @@ -0,0 +1,112 @@ + +""" +The elements the web integration adds (Sequence, JSON Builder, JavaScript +Builder and Set Headers) each have a switch that defaults to on, and the +Sequence Element comes first, as the web integration specification says. +""" + +import unittest + +from fiftyone_pipeline_core.flowelement import FlowElement +from fiftyone_pipeline_core.pipelinebuilder import PipelineBuilder + + +class UserElement(FlowElement): + + def __init__(self): + super(UserElement, self).__init__() + self.datakey = "user" + + def process_internal(self, flowdata): + pass + + +def datakeys(settings=None): + builder = PipelineBuilder(settings or {}) + builder.add(UserElement()) + pipeline = builder.build() + return [element.datakey for element in pipeline.flow_elements] + + +class WebIntegrationElementSwitchTests(unittest.TestCase): + + def testAllElementsAreAddedByDefaultWithSequenceFirst(self): + self.assertEqual( + ["sequence", "user", "jsonbundler", "javascriptbuilder", + "set-headers"], + datakeys()) + + def testSequenceSwitch(self): + self.assertEqual( + ["user", "jsonbundler", "javascriptbuilder", "set-headers"], + datakeys({"add_sequence_element": False})) + + def testJsonBuilderSwitch(self): + self.assertEqual( + ["sequence", "user", "javascriptbuilder", "set-headers"], + datakeys({"add_json_builder_element": False})) + + def testJavaScriptBuilderSwitch(self): + self.assertEqual( + ["sequence", "user", "jsonbundler", "set-headers"], + datakeys({"add_javascript_builder_element": False})) + + def testSetHeadersSwitch(self): + self.assertEqual( + ["sequence", "user", "jsonbundler", "javascriptbuilder"], + datakeys({"add_set_header_element": False})) + + def testExistingJavaScriptSwitchStillDropsThreeElements(self): + self.assertEqual( + ["user", "set-headers"], + datakeys({"add_javascript_builder": False})) + + def testExistingSetHeaderSwitchStillDropsSetHeaders(self): + self.assertEqual( + ["sequence", "user", "jsonbundler", "javascriptbuilder"], + datakeys({"use_setheader_properties": False})) + + def testEverySwitchOff(self): + self.assertEqual( + ["user"], + datakeys({ + "add_sequence_element": False, + "add_json_builder_element": False, + "add_javascript_builder_element": False, + "add_set_header_element": False})) + + +# The Sequence Element writes Element Data as well as Evidence. +class SequenceElementDataTests(unittest.TestCase): + + def process(self, evidence): + pipeline = PipelineBuilder({}).build() + flowdata = pipeline.create_flowdata() + for key, value in evidence.items(): + flowdata.evidence.add(key, value) + flowdata.process() + return flowdata + + def testFirstRequestHasNewSessionAndSequenceOne(self): + flowdata = self.process({}) + session_id = flowdata.get("sequence").get("session-id") + self.assertTrue(session_id) + self.assertEqual(1, flowdata.get("sequence").get("sequence")) + self.assertEqual(session_id, flowdata.evidence.get("query.session-id")) + self.assertEqual(1, flowdata.evidence.get("query.sequence")) + + def testCallbackKeepsSessionAndAddsOne(self): + flowdata = self.process( + {"query.session-id": "abc-123", "query.sequence": "3"}) + self.assertEqual("abc-123", flowdata.get("sequence").get("session-id")) + self.assertEqual(4, flowdata.get("sequence").get("sequence")) + self.assertEqual("abc-123", flowdata.evidence.get("query.session-id")) + self.assertEqual(4, flowdata.evidence.get("query.sequence")) + + def testSequenceIsNotAddedToTheJsonSentToBrowsers(self): + flowdata = self.process({}) + self.assertNotIn("sequence", flowdata.jsonbundler.json) + + +if __name__ == "__main__": + unittest.main() From d0b91e230348e6e2d4024a3d49e7119fafa72f31 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:00:15 +0000 Subject: [PATCH 2/7] Include the Sequence Element in the JSON, declare its properties and say the Lite file has few Properties The JSON Builder specification requires a top-level entry for each Element Data and excludes only Set Headers, the Cloud Request Engine and Usage Sharing, so the JSON Builder no longer skips the Sequence Element. The Sequence Element declares session-id and sequence as the Sequence Element specification lists them. The Device Detection example comments now say what the required examples specification says of the Lite data file. --- .../onpremise/gettingstarted_console.py | 4 ++-- .../onpremise/match_metrics.py | 4 ++-- .../onpremise/metadata_console.py | 4 ++-- .../onpremise/offlineprocessing.py | 4 ++-- .../src/fiftyone_pipeline_core/jsonbundler.py | 1 - .../fiftyone_pipeline_core/sequenceelement.py | 3 +++ .../fiftyone_pipeline_core/tests/test_core.py | 3 ++- .../tests/test_web_integration_elements.py | 17 +++++++++++++++-- 8 files changed, 28 insertions(+), 12 deletions(-) diff --git a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/gettingstarted_console.py b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/gettingstarted_console.py index 4742eed1..2f7a6330 100644 --- a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/gettingstarted_console.py +++ b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/gettingstarted_console.py @@ -174,8 +174,8 @@ def main(argv): # somewhere in the project space, or you may specify another file as # a command line parameter. # - # Note that the Lite data file is only used for illustration, and has - # limited accuracy and capabilities. The example shows the properties + # Note that the Lite data file is only used for illustration, and has a small + # number of Properties. The example shows the properties # only the Enterprise data file has as needing it. Contact us for a copy # of the Enterprise data file: # https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-python&utm_content=fiftyone_devicedetection_examples-src-fiftyone_devicedetection_examples-onpremise-gettingstarted_console.py&utm_term=main diff --git a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/match_metrics.py b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/match_metrics.py index dd53ed68..a52b55f8 100644 --- a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/match_metrics.py +++ b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/match_metrics.py @@ -188,8 +188,8 @@ def main(argv): # somewhere in the project space, or you may specify another file as # a command line parameter. # - # Note that the Lite data file is only used for illustration, and has - # limited accuracy and capabilities. + # Note that the Lite data file is only used for illustration, and has a small + # number of Properties. # Contact us for a copy of the Enterprise data file: # https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-python&utm_content=fiftyone_devicedetection_examples-src-fiftyone_devicedetection_examples-onpremise-match_metrics.py&utm_term=main data_file = argv[0] if len(argv) > 0 else ExampleUtils.find_file(LITE_DATAFILE_NAME) diff --git a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/metadata_console.py b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/metadata_console.py index e3141e06..f6334c37 100644 --- a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/metadata_console.py +++ b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/metadata_console.py @@ -126,8 +126,8 @@ def main(argv): # somewhere in the project space, or you may specify another file as # a command line parameter. # - # Note that the Lite data file is only used for illustration, and has - # limited accuracy and capabilities. + # Note that the Lite data file is only used for illustration, and has a small + # number of Properties. # Contact us for a copy of the Enterprise data file: # https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-python&utm_content=fiftyone_devicedetection_examples-src-fiftyone_devicedetection_examples-onpremise-metadata_console.py&utm_term=main data_file = argv[0] if len(argv) > 0 else ExampleUtils.find_file(LITE_DATAFILE_NAME) diff --git a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/offlineprocessing.py b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/offlineprocessing.py index c4b437f5..9dcb71ec 100644 --- a/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/offlineprocessing.py +++ b/device-detection/fiftyone_devicedetection_examples/src/fiftyone_devicedetection_examples/onpremise/offlineprocessing.py @@ -222,8 +222,8 @@ def main(argv): # somewhere in the project space, or you may specify another file as # a command line parameter. # - # Note that the Lite data file is only used for illustration, and has - # limited accuracy and capabilities. + # Note that the Lite data file is only used for illustration, and has a small + # number of Properties. # Contact us for a copy of the Enterprise data file: # https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=51degrees-python&utm_content=fiftyone_devicedetection_examples-src-fiftyone_devicedetection_examples-onpremise-offlineprocessing.py&utm_term=main data_file = argv[0] if len(argv) > 0 else ExampleUtils.find_file(LITE_DATAFILE_NAME) 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 9f2a8ca7..b11f6fe2 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/jsonbundler.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/jsonbundler.py @@ -70,7 +70,6 @@ def process_internal(self, flowdata): for flow_element in flowdata.pipeline.flow_elements: if (flow_element.datakey == "jsonbundler" or - flow_element.datakey == "sequence" or flow_element.datakey == "javascriptbuilder" or flow_element.datakey == "set-headers"): continue diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py index d72858a8..834e6789 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py @@ -53,6 +53,9 @@ def __init__(self): super(SequenceElement, self).__init__() self.datakey = "sequence" + self.properties = { + "session-id": {"type": "string"}, + "sequence": {"type": "int"}} self.exclude_from_messages = True diff --git a/pipeline/fiftyone_pipeline_core/tests/test_core.py b/pipeline/fiftyone_pipeline_core/tests/test_core.py index 21407b33..9f0d88b9 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_core.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_core.py @@ -86,8 +86,9 @@ def testGet(self): def testGetWhere(self): testPipeline = TestPipeline() + # The example element's integer and the Sequence Element's sequence. getValue = len(testPipeline.flowdata.get_where("type", "int")) - self.assertTrue(getValue == 1) + self.assertTrue(getValue == 2) def testGetFromElement(self): diff --git a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py index 141abaca..93560523 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py @@ -103,9 +103,22 @@ def testCallbackKeepsSessionAndAddsOne(self): self.assertEqual("abc-123", flowdata.evidence.get("query.session-id")) self.assertEqual(4, flowdata.evidence.get("query.sequence")) - def testSequenceIsNotAddedToTheJsonSentToBrowsers(self): + # The JSON Builder specification requires a top-level entry for each + # Element Data, excluding only Set Headers, the Cloud Request Engine and + # Usage Sharing. + def testSequenceHasAnEntryInTheJsonSentToBrowsers(self): flowdata = self.process({}) - self.assertNotIn("sequence", flowdata.jsonbundler.json) + entry = flowdata.jsonbundler.json["sequence"] + self.assertEqual( + flowdata.get("sequence").get("session-id"), entry["session-id"]) + self.assertEqual(1, entry["sequence"]) + + def testSequencePropertiesAreDescribed(self): + element = PipelineBuilder({}).build().flow_elements[0] + self.assertEqual("sequence", element.datakey) + self.assertEqual( + {"session-id": {"type": "string"}, "sequence": {"type": "int"}}, + element.get_properties()) if __name__ == "__main__": From 230e097223ffa6407c031d531a7a9ccdb882185b Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:03:14 +0000 Subject: [PATCH 3/7] Write an empty session id to the Sequence Element Data when the script cannot be given it The Sequence Element's session id now goes into the JSON that is written into the script, so an id the script cannot take is empty there, as it is in the script's own sessionId. --- .../src/fiftyone_pipeline_core/sequenceelement.py | 8 ++++++-- .../tests/test_javascriptbuilder.py | 8 +++++++- .../tests/test_web_integration_elements.py | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py index 834e6789..39e4d767 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py @@ -23,7 +23,7 @@ from .flowelement import FlowElement from .evidence_keyfilter import EvidenceKeyFilter -from .javascriptbuilder import parse_sequence +from .javascriptbuilder import get_session_id, parse_sequence from .elementdata_dictionary import ElementDataDictionary import uuid @@ -96,5 +96,9 @@ def process_internal(self, flowdata): flowdata.evidence.add("query.sequence", sequence) + # The Element Data goes into the JSON that is written into the script, + # so a session id the script cannot be given is empty there, as it is + # in the script itself. flowdata.set_element_data(ElementDataDictionary( - self, {"session-id": session_id, "sequence": sequence})) + self, {"session-id": get_session_id(session_id), + "sequence": sequence})) diff --git a/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder.py b/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder.py index a2d80b01..03f4e68d 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder.py @@ -229,7 +229,13 @@ def testJSONBundler(self): } } - self.assertEqual(FlowData.jsonbundler.json, expected) + # The Sequence Element has an entry too. Its session id is new on + # every request. + actual = dict(FlowData.jsonbundler.json) + sequence = actual.pop('sequence') + self.assertTrue(sequence['session-id']) + self.assertEqual(1, sequence['sequence']) + self.assertEqual(actual, expected) def testJavaScriptBuilder_Minify(self): diff --git a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py index 93560523..0a0f64e6 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py @@ -113,6 +113,12 @@ def testSequenceHasAnEntryInTheJsonSentToBrowsers(self): flowdata.get("sequence").get("session-id"), entry["session-id"]) self.assertEqual(1, entry["sequence"]) + def testUnsafeSessionIdIsEmptyInTheElementDataAndTheJson(self): + flowdata = self.process({"query.session-id": ""}) + self.assertEqual("", flowdata.get("sequence").get("session-id")) + self.assertEqual("", flowdata.jsonbundler.json["sequence"]["session-id"]) + self.assertNotIn("", flowdata.javascriptbuilder.javascript) + def testSequencePropertiesAreDescribed(self): element = PipelineBuilder({}).build().flow_elements[0] self.assertEqual("sequence", element.datakey) From ff8f39a43fc12e7c28eb6ce60e9bd23324127f21 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:05:39 +0000 Subject: [PATCH 4/7] Expect the incremented sequence in the usage sharing record now that Sequence is first --- .../fiftyone_pipeline_engines_fiftyone/tests/test_usage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pipeline/fiftyone_pipeline_engines_fiftyone/tests/test_usage.py b/pipeline/fiftyone_pipeline_engines_fiftyone/tests/test_usage.py index 4f1bea40..b354cd82 100644 --- a/pipeline/fiftyone_pipeline_engines_fiftyone/tests/test_usage.py +++ b/pipeline/fiftyone_pipeline_engines_fiftyone/tests/test_usage.py @@ -293,7 +293,9 @@ def test_session_and_sequence(self): self.waitForUsageThreads() self.assertEqual(len(app.received), 1) self.assertIn("1", app.received[0]) - self.assertIn("1", app.received[0]) + # The Sequence Element is the first element, so it has added one to the + # sequence by the time usage sharing reads it. + self.assertIn("2", app.received[0]) def test_invalid_char(self): """! From a834a527c43c6602b851b32b8b40ad398efec589 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:25:28 +0000 Subject: [PATCH 5/7] Give the native engines evidence as text, as the Sequence Element now adds a number before them --- .../devicedetection_onpremise.py | 3 ++- .../ipintelligence_onpremise.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py b/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py index 632751ae..4f0c1802 100644 --- a/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py +++ b/device-detection/fiftyone_devicedetection_onpremise/src/fiftyone_devicedetection_onpremise/devicedetection_onpremise.py @@ -433,8 +433,9 @@ def process_internal(self, flow_data): evidence = EvidenceDeviceDetectionSwig() + # The native evidence takes text, and the Sequence Element adds a number. for key, value in flow_data.evidence.get_all().items(): - evidence.__setitem__(key, value) + evidence.__setitem__(key, str(value)) result = self.engine.process(evidence) diff --git a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py index 9f15784e..e7f9a01e 100644 --- a/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py +++ b/ip-intelligence/fiftyone_ipintelligence_onpremise/src/fiftyone_ipintelligence_onpremise/ipintelligence_onpremise.py @@ -400,8 +400,9 @@ def process_internal(self, flow_data): evidence = EvidenceIpiSwig() + # The native evidence takes text, and the Sequence Element adds a number. for key, value in flow_data.evidence.get_all().items(): - evidence.__setitem__(key, value) + evidence.__setitem__(key, str(value)) result = self.engine.process(evidence) From c96cd1dedabb37ae192cdf129cb5eb34b08ec3bd Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:50:44 +0000 Subject: [PATCH 6/7] Read the session id and the sequence independently in the Sequence Element A sequence with no session id now counts up with a new id. A session id that is not a non-empty string is absent, a sequence in text must be decimal digits, and the Evidence and the Element Data are written from the same values. The JSON written into the script escapes the characters that could end the script. --- .../javascriptbuilder.py | 8 ++- .../fiftyone_pipeline_core/sequenceelement.py | 39 ++++++----- .../tests/test_javascriptbuilder_sequence.py | 7 +- .../tests/test_web_integration_elements.py | 64 ++++++++++++++++++- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/javascriptbuilder.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/javascriptbuilder.py index bdb85c50..a97e2356 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/javascriptbuilder.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/javascriptbuilder.py @@ -351,7 +351,13 @@ def process_internal(self, flowdata): for key, value in self.settings.items(): variables[key] = value - variables["_jsonObject"] = json.dumps(flowdata.jsonbundler.json) + # The JSON is written into a script element, so the characters that + # could end the element or the script are written as escapes. + variables["_jsonObject"] = ( + json.dumps(flowdata.jsonbundler.json) + .replace("<", "\\u003c") + .replace(">", "\\u003e") + .replace("&", "\\u0026")) # Generate URL and autoUpdate params diff --git a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py index 39e4d767..c7cc2514 100644 --- a/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py +++ b/pipeline/fiftyone_pipeline_core/src/fiftyone_pipeline_core/sequenceelement.py @@ -23,10 +23,14 @@ from .flowelement import FlowElement from .evidence_keyfilter import EvidenceKeyFilter -from .javascriptbuilder import get_session_id, parse_sequence +from .javascriptbuilder import parse_sequence from .elementdata_dictionary import ElementDataDictionary +import re import uuid +# Text that is a sequence, in ASCII decimal digits and nothing else. +DIGITS_PATTERN = re.compile(r"[0-9]+") + class SequenceElementEvidenceKeyFilter(EvidenceKeyFilter): def filter(self, key): @@ -78,27 +82,22 @@ def process_internal(self, flowdata): """ - if flowdata.evidence.get("query.session-id"): - - # Get current sequence number. A value that is absent or is not - # a positive 32 bit integer is treated as no sequence at all, so - # the request is sequence 1 rather than failing. The JavaScript - # builder renders any value it cannot use as 1 in the same way. - - sequence = parse_sequence(flowdata.evidence.get("query.sequence")) - sequence = 1 if sequence is None else sequence + 1 - session_id = flowdata.evidence.get("query.session-id") - - else: + # The session id and the sequence are read independently. A session + # id that is not a non-empty string is absent, and gets a new id. A + # sequence that is not a whole number from 1 to 2147483647, written + # in decimal digits if it is text, is absent and gives 1. + session_id = flowdata.evidence.get("query.session-id") + if not isinstance(session_id, str) or session_id == "": session_id = str(uuid.uuid4()) - sequence = 1 - flowdata.evidence.add("query.session-id", session_id) + sequence = flowdata.evidence.get("query.sequence") + if isinstance(sequence, str) and not DIGITS_PATTERN.fullmatch(sequence): + sequence = None + sequence = parse_sequence(sequence) + sequence = 1 if sequence is None else sequence + 1 + + flowdata.evidence.add("query.session-id", session_id) flowdata.evidence.add("query.sequence", sequence) - # The Element Data goes into the JSON that is written into the script, - # so a session id the script cannot be given is empty there, as it is - # in the script itself. flowdata.set_element_data(ElementDataDictionary( - self, {"session-id": get_session_id(session_id), - "sequence": sequence})) + self, {"session-id": session_id, "sequence": sequence})) diff --git a/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder_sequence.py b/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder_sequence.py index 2e42c300..3c7093b8 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder_sequence.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_javascriptbuilder_sequence.py @@ -211,7 +211,9 @@ def test_longest_session_id_is_rendered(self): @parameterized.expand([ [name + "_" + ("with" if element else "without") - + "_sequence_element", value, check_text, element] + + "_sequence_element", value, + # The JSON of the Sequence Element's data holds the long id as it is. + check_text and not (element and name == "too_long"), element] for name, value, check_text in [ ["quote", 'a"b', True], ["backslash", "a\\b", True], @@ -231,7 +233,8 @@ def test_unsafe_session_id_is_rendered_empty( one that is not 1 to 64 ASCII letters, digits and hyphens is rendered as an empty string, as the pipeline specification says. The SequenceElement keeps a session id it is given, so the check is the - builder's in both pipelines.""" + builder's in both pipelines. The JSON in the script cannot be ended + early by the id either.""" script = _render( {"query.session-id": value}, diff --git a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py index 0a0f64e6..c610178e 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py @@ -113,11 +113,15 @@ def testSequenceHasAnEntryInTheJsonSentToBrowsers(self): flowdata.get("sequence").get("session-id"), entry["session-id"]) self.assertEqual(1, entry["sequence"]) - def testUnsafeSessionIdIsEmptyInTheElementDataAndTheJson(self): + # The JSON is written into the script, so a session id from the query + # string must not end the script. + def testSessionIdIsKeptButCannotEndTheScript(self): flowdata = self.process({"query.session-id": ""}) - self.assertEqual("", flowdata.get("sequence").get("session-id")) - self.assertEqual("", flowdata.jsonbundler.json["sequence"]["session-id"]) + self.assertEqual("", flowdata.get("sequence").get("session-id")) + self.assertEqual( + "", flowdata.jsonbundler.json["sequence"]["session-id"]) self.assertNotIn("", flowdata.javascriptbuilder.javascript) + self.assertIn('var sessionId = "";', flowdata.javascriptbuilder.javascript) def testSequencePropertiesAreDescribed(self): element = PipelineBuilder({}).build().flow_elements[0] @@ -127,5 +131,59 @@ def testSequencePropertiesAreDescribed(self): element.get_properties()) +# The session id and the sequence are read independently, as the Process +# section of the Sequence Element specification says, and the Evidence and the +# Element Data are written from the same values. +class SequenceElementProcessTests(unittest.TestCase): + + def process(self, evidence): + pipeline = PipelineBuilder({}).build() + flowdata = pipeline.create_flowdata() + for key, value in evidence.items(): + flowdata.evidence.add(key, value) + flowdata.process() + data = flowdata.get("sequence") + session_id = data.get("session-id") + sequence = data.get("sequence") + self.assertEqual(session_id, flowdata.evidence.get("query.session-id")) + self.assertEqual(sequence, flowdata.evidence.get("query.sequence")) + return session_id, sequence + + def testSequenceWithNoSessionIdCountsUpWithANewId(self): + session_id, sequence = self.process({"query.sequence": "3"}) + self.assertTrue(session_id) + self.assertEqual(4, sequence) + + def testSessionIdWithNoSequenceGivesOne(self): + self.assertEqual(("abc", 1), self.process({"query.session-id": "abc"})) + + def testIntegerSequenceCountsUp(self): + self.assertEqual( + ("abc", 8), + self.process({"query.session-id": "abc", "query.sequence": 7})) + + def testSessionIdThatIsNotANonEmptyStringIsAbsent(self): + for value in (123, "", None, ["abc"]): + session_id, sequence = self.process( + {"query.session-id": value, "query.sequence": "2"}) + self.assertIsInstance(session_id, str) + self.assertTrue(session_id) + self.assertEqual(3, sequence) + + def testSessionIdOfZeroIsKept(self): + self.assertEqual( + ("0", 3), + self.process({"query.session-id": "0", "query.sequence": "2"})) + + def testSequenceThatIsNotWholeDigitsIsAbsent(self): + for value in ("abc", "", " 5 ", "+5", "-5", "5.0", "0", True, 0, + 2 ** 31, "2147483648", "٣", None): + self.assertEqual( + ("abc", 1), + self.process( + {"query.session-id": "abc", "query.sequence": value}), + repr(value)) + + if __name__ == "__main__": unittest.main() From 86872f4fda5d02f5c0f87911b8af3c8048eda182 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 9 Nov 2026 18:54:48 +0000 Subject: [PATCH 7/7] Check the escaped script end in the session id test --- .../tests/test_web_integration_elements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py index c610178e..4be3fcd7 100644 --- a/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py +++ b/pipeline/fiftyone_pipeline_core/tests/test_web_integration_elements.py @@ -121,7 +121,8 @@ def testSessionIdIsKeptButCannotEndTheScript(self): self.assertEqual( "", flowdata.jsonbundler.json["sequence"]["session-id"]) self.assertNotIn("", flowdata.javascriptbuilder.javascript) - self.assertIn('var sessionId = "";', flowdata.javascriptbuilder.javascript) + self.assertIn( + "\\u003c/script\\u003e", flowdata.javascriptbuilder.javascript) def testSequencePropertiesAreDescribed(self): element = PipelineBuilder({}).build().flow_elements[0]