Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"licence_keys": ""
}
},
{
"elementName": "SequenceElement",
"elementPath": "fiftyone_pipeline_core.sequenceelement"
},
{
"elementName": "JSONBundlerElement",
"elementPath": "fiftyone_pipeline_core.jsonbundler"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# *********************************************************************
# This Original Work is copyright of 51 Degrees Mobile Experts Limited.
# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
#
# This Original Work is licensed under the European Union Public Licence
# (EUPL) v.1.2 and is subject to its terms as set out below.
#
# If a copy of the EUPL was not distributed with this file, You can obtain
# one at https://opensource.org/licenses/EUPL-1.2.
#
# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
# amended by the European Commission) shall be deemed incompatible for
# the purposes of the Work and the provisions of the compatibility
# clause in Article 5 of the EUPL shall not apply.
#
# If using the Work as, or as part of, a network application, by
# including the attribution notice(s) required under Article 5 of the EUPL
# in the end user terms of the application under an appropriate heading,
# such notice(s) shall fulfill the requirements of that article.
# *********************************************************************

"""The on-premise web examples build their pipelines from config.json, which
must place a Sequence Element before the JSON Builder, or the JSON Builder
fails on every request. Only the pipeline core elements of each file are
built, so no data file is needed."""

import json
import unittest
from pathlib import Path

from fiftyone_pipeline_core.pipelinebuilder import PipelineBuilder

EXAMPLES = Path(__file__).resolve().parent.parent.joinpath(
"src", "fiftyone_devicedetection_examples", "onpremise")
WEB_EXAMPLES = ("gettingstarted_web", "useragentclienthints_web")
CORE = "fiftyone_pipeline_core."


class WebExampleConfigTests(unittest.TestCase):

def test_json_builder_has_a_sequence_number(self):
for example in WEB_EXAMPLES:
with self.subTest(example):
config = json.loads(
EXAMPLES.joinpath(example, "config.json").read_text())
options = config["PipelineOptions"]
options["Elements"] = [
element for element in options["Elements"]
if element["elementPath"].startswith(CORE)]
pipeline = PipelineBuilder().build_from_configuration(config)
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
flowdata.evidence.add("header.user-agent", "a browser")
flowdata.process()

self.assertEqual({}, flowdata.errors)
self.assertIn(
"json", flowdata.get("jsonbundler").as_dictionary())


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ def process_internal(self, flowdata):

"""

# The sequence number is the one the Sequence Element has put in the
# Evidence as a whole number. Text in the query string does not
# count, and without a number no JSON is set.
sequence = flowdata.evidence.get("query.sequence")
if isinstance(sequence, bool) or not isinstance(sequence, int):
raise Exception(
"The JSON Builder needs the sequence number that a Sequence "
"Element produces. A Sequence Element needs to be placed "
"before it in the Pipeline.")

# Get every property on every FlowElement
# Storing JavaScript properties in an extra section

Expand Down Expand Up @@ -188,9 +198,7 @@ def process_internal(self, flowdata):
if value is None:
output[flow_element.datakey.lower()][propertykey.lower() + "nullreason"] = null_reason

sequence = flowdata.evidence.get("query.sequence")

if sequence is None or sequence < 10:
if sequence < 10:

if "type" in propertymeta and propertymeta["type"].lower() == "javascript":

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,19 @@
from parameterized import parameterized

from fiftyone_pipeline_core import javascriptbuilder
from fiftyone_pipeline_core.javascriptbuilder import JavascriptBuilderElement
from fiftyone_pipeline_core.jsonbundler import JSONBundlerElement
from fiftyone_pipeline_core.pipelinebuilder import PipelineBuilder

NODE = shutil.which("node")


def _render(evidence=None, minify=False, sequence_element=False):
"""Render the script. With sequence_element False the pipeline has only
the JSON bundler and the JavaScript builder."""
def _render(evidence=None, minify=False, sequence_element=True):
"""Render the script. The JSON builder fails without a sequence number,
so the pipeline always has the SequenceElement."""

settings = {"javascript_builder_settings": {"minify": minify}}
if not sequence_element:
settings["add_javascript_builder"] = False
builder = PipelineBuilder(settings)
if not sequence_element:
builder.add(JSONBundlerElement())
builder.add(JavascriptBuilderElement({"minify": minify}))
pipeline = builder.build()
raise ValueError("The JSON builder needs the SequenceElement")
settings = {"javascript_builder_settings": {"minify": minify}}
pipeline = PipelineBuilder(settings).build()
flowdata = pipeline.create_flowdata()
for key, value in (evidence or {}).items():
flowdata.evidence.add(key, value)
Expand Down Expand Up @@ -92,54 +86,15 @@ def assert_parses(self, script):
"the rendered script does not parse: " + result.stderr)

@parameterized.expand([["unminified", False], ["minified", True]])
def test_script_parses_without_sequence_element(self, _, minify):
"""With no SequenceElement and no query evidence the script is
rendered with sequence 1 and an empty session id, and parses."""
def test_script_parses(self, _, minify):
"""With no query evidence the script is rendered with sequence 1, and
parses."""

script = _render(minify=minify)

self.assert_parses(script)
if not minify:
self.assertEqual(["var sequence = 1;"], _line(script, "sequence"))
self.assertEqual(
['var sessionId = "";'], _line(script, "sessionId"))

@parameterized.expand([
["text", "abc"],
["negative", "-1"],
["zero", "0"],
["too_large", "99999999999"],
["empty", ""],
["statement", "1;b"],
["decimal", "1.5"],
["underscore", "1_0"],
["just_too_large", "2147483648"],
["file_separator", chr(0x1C) + "5"],
["group_separator", chr(0x1D) + "5"],
["record_separator", chr(0x1E) + "5"],
["unit_separator", chr(0x1F) + "5"],
["trailing_file_separator", "5" + chr(0x1C)],
["null", chr(0) + "5"],
["no_break_space", chr(0xA0) + "5"],
["line_separator", chr(0x2028) + "5"],
["arabic_indic_digit", chr(0x665)],
["full_width_digit", chr(0xFF15)],
["five_thousand_digits", "9" * 5000],
["plus_alone", "+"],
["spaces_alone", " "],
["zeros", "0000"],
])
def test_unusable_sequence_from_query_becomes_one(self, _, value):
"""A query.sequence that is not a positive 32 bit integer is rendered
as 1, as the pipeline specification says, so the script parses and
nothing else is written in its place."""

script = _render({
"query.session-id": "abc",
"query.sequence": value})

self.assertEqual(["var sequence = 1;"], _line(script, "sequence"))
self.assert_parses(script)

@parameterized.expand([
["text", "abc"],
Expand Down Expand Up @@ -171,30 +126,8 @@ def test_unusable_sequence_with_sequence_element_becomes_one(
['var sessionId = "abc";'], _line(script, "sessionId"))
self.assert_parses(script)

@parameterized.expand([
["text", "7", 7],
["spaces", " 7 ", 7],
["number", 12, 12],
["largest", "2147483647", 2147483647],
])
def test_usable_sequence_from_query_is_rendered(self, _, value, expected):
script = _render({
"query.session-id": "abc",
"query.sequence": value})

self.assertEqual(
["var sequence = " + str(expected) + ";"],
_line(script, "sequence"))
self.assert_parses(script)

@parameterized.expand([
["without_sequence_element", False],
["with_sequence_element", True],
])
def test_session_id_from_query_is_rendered(self, _, sequence_element):
script = _render(
{"query.session-id": "abc-123"},
sequence_element=sequence_element)
def test_session_id_from_query_is_rendered(self):
script = _render({"query.session-id": "abc-123"})

self.assertEqual(
['var sessionId = "abc-123";'], _line(script, "sessionId"))
Expand Down Expand Up @@ -223,7 +156,7 @@ def test_longest_session_id_is_rendered(self):
["space", "a b", False],
["new_line", "ab\n", False],
]
for element in [False, True]
for element in [True]
])
def test_unsafe_session_id_is_rendered_empty(
self, _, value, check_text, sequence_element):
Expand Down Expand Up @@ -331,7 +264,7 @@ def test_any_sequence_value_reaches_the_script(self):
chr(0x1F) + "5", "5" + chr(0x1C), chr(0) + "5",
"9" * 5000, "+", " "]
for value in values:
for element in [False, True]:
for element in [True]:
with self.subTest(value=repr(value), element=element):
script = _render(
{"query.session-id": "abc",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@

"""
The JSON Builder fails with an error, and sets no JSON, when no Sequence
Element has produced a sequence number, as the JSON Builder specification
says.
"""

import unittest

from fiftyone_pipeline_core.basiclist_evidence_keyfilter import \
BasicListEvidenceKeyFilter
from fiftyone_pipeline_core.flowelement import FlowElement
from fiftyone_pipeline_core.jsonbundler import JSONBundlerElement
from fiftyone_pipeline_core.pipelinebuilder import PipelineBuilder


class KeepsTheSequence(FlowElement):
"""Accepts query.sequence and does nothing else, as an element of the
caller's that keeps the integer it adds would. The JSON Builder accepts
no evidence, so without such an element the pipeline sets the value
aside."""

def __init__(self):
super(KeepsTheSequence, self).__init__()
self.datakey = "keepsthesequence"

def get_evidence_key_filter(self):
return BasicListEvidenceKeyFilter(["query.sequence"])

def process_internal(self, flowdata):
pass


def bundler_only(suppress=False, keep_sequence=False):
builder = PipelineBuilder({
"add_javascript_builder": False,
"use_setheader_properties": False,
"suppress_process_exceptions": suppress})
if keep_sequence:
builder.add(KeepsTheSequence())
return builder.add(JSONBundlerElement()).build()


class JsonBuilderSequenceTests(unittest.TestCase):

def test_no_sequence_number_is_an_error_that_names_the_sequence_element(
self):
pipeline = bundler_only()
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
with self.assertRaises(Exception) as caught:
flowdata.process()
self.assertIn("Sequence Element", str(caught.exception))

def test_no_json_is_set_when_the_error_is_suppressed(self):
pipeline = bundler_only(suppress=True)
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
flowdata.process()
self.assertEqual(1, len(flowdata.errors))
self.assertNotIn("jsonbundler", flowdata.data)

def test_a_sequence_number_from_the_query_string_does_not_count(self):
for value in ("3", "", "abc", True, 3.0):
pipeline = bundler_only(suppress=True)
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
flowdata.evidence.add("query.sequence", value)
flowdata.process()
self.assertEqual(1, len(flowdata.errors), repr(value))
self.assertNotIn("jsonbundler", flowdata.data, repr(value))

def test_an_integer_sequence_number_in_the_evidence_counts(self):
pipeline = bundler_only(keep_sequence=True)
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
flowdata.evidence.add("query.sequence", 3)
flowdata.process()
self.assertIn("javascriptProperties", flowdata.jsonbundler.json)

def test_the_default_pipeline_has_a_sequence_number(self):
pipeline = PipelineBuilder({}).build()
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
flowdata.process()
self.assertEqual(0, len(flowdata.errors))
self.assertIn("javascriptProperties", flowdata.jsonbundler.json)

def test_a_sequence_of_ten_or_more_leaves_out_the_javascript_properties(
self):
pipeline = bundler_only(keep_sequence=True)
self.addCleanup(pipeline.close)
flowdata = pipeline.create_flowdata()
flowdata.evidence.add("query.sequence", 10)
flowdata.process()
self.assertEqual([], flowdata.jsonbundler.json["javascriptProperties"])


if __name__ == "__main__":
unittest.main()
Loading