Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ def __init__(self, flowdata):
@param flowdata: Parent FlowData
"""
self.evidence = {}
# Each key in lower case to the key as first added, because keys
# ignore letter case.
self.keys = {}
self.evidence_filtered = False
self.flowdata = flowdata
self.unaccepted = {}
Expand All @@ -50,6 +53,11 @@ def add(self, key, value):
@param value: a piece of evidence
"""

folded = str(key).lower()
if folded in self.keys:
key = self.keys[folded]
else:
self.keys[folded] = key
self.evidence[key] = value

def add_from_dict(self, evidence_dictionary):
Expand Down Expand Up @@ -77,9 +85,10 @@ def get(self, key):
@return: A piece of evidence
"""

if key in self.evidence:
found = self.keys.get(str(key).lower())
if found is not None and found in self.evidence:

return self.evidence[key]
return self.evidence[found]

else:

Expand Down Expand Up @@ -127,5 +136,6 @@ def filter(self) -> None:
in self.flowdata.pipeline.flow_elements
]):
self.unaccepted[key] = self.evidence.pop(key)
del self.keys[str(key).lower()]

self.evidence_filtered = True
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,17 @@ def url_encode(value):
class JavaScriptBuilderEvidenceKeyFilter(EvidenceKeyFilter):

# The query string, which the script sends back to the callback, and the
# host and protocol the callback URL is built from.
# host and protocol the callback URL is built from. Evidence keys ignore
# letter case, so each is matched in lower case.
def filter_evidence_key(self, key):
if key.startswith("query."):
folded = str(key).lower()

if folded.startswith("query."):
return True

if key == "header.host" or key == "header.protocol":
if folded == "header.host" or folded == "header.protocol":
return True

return False


Expand Down Expand Up @@ -413,12 +416,22 @@ def process_internal(self, flowdata):
+ self.settings["_objName"] + "' is used.")

query_params = self.get_evidence_key_filter().filter_evidence(flowdata.evidence.get_all())
# Evidence keys ignore letter case, and get_all gives each key in
# the letters it was added with, so a key is matched here in lower
# case and keeps those letters where it is sent on.
folded = {str(key).lower(): key for key in query_params}

def query_param(name):
key = folded.get(name)
return None if key is None else query_params[key]

# Both are written into the script, so each always has a safe value.
# The session id is written inside quotes and is empty when absent or
# not safe, and the sequence is written as bare code, so it is always
# a positive number.
variables["_sessionId"] = get_session_id(query_params.get("query.session-id"))
variables["_sequence"] = get_sequence(query_params.get("query.sequence"))
variables["_sessionId"] = get_session_id(
query_param("query.session-id"))
variables["_sequence"] = get_sequence(query_param("query.sequence"))

# The session id and the sequence are left out, because the script
# appends both to its own request after it has taken the record of
Expand All @@ -435,8 +448,8 @@ def process_internal(self, flowdata):
(url_encode(param.split(".", 1)[1]),
url_encode(query_params[param]))
for param in query_params.keys()
if param.startswith("query.")
and param not in EXCLUDED_PARAMETERS
if str(param).lower().startswith("query.")
and str(param).lower() not in EXCLUDED_PARAMETERS
])
# Written without spaces, as the .NET builder writes it.
variables["_parameters"] = json.dumps(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
class SequenceElementEvidenceKeyFilter(EvidenceKeyFilter):

# The session id and the sequence. The result changes as the sequence
# grows, so they are part of the ETag.
# grows, so they are part of the ETag. Evidence keys ignore letter case,
# so each is matched in lower case.
def filter_evidence_key(self, key):

if (key == "query.sequence" or key == "query.session-id"):

folded = str(key).lower()

if (folded == "query.sequence" or folded == "query.session-id"):
return True

return False


Expand Down
122 changes: 122 additions & 0 deletions pipeline/fiftyone_pipeline_core/tests/test_evidence_key_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# *********************************************************************
# 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.
# *********************************************************************

"""Evidence keys ignore letter case, as the Evidence page of the pipeline
specification says, so header.User-Agent and header.user-agent are the same
key. get_all gives each key in the letters it was added with, so an element
that matches a key it holds matches it in lower case."""

import json
import unittest
from unittest import mock

from fiftyone_pipeline_core import javascriptbuilder
from fiftyone_pipeline_core.pipelinebuilder import PipelineBuilder
from fiftyone_pipeline_core.sequenceelement import SequenceElement

from .classes.exampleflowelement1 import ExampleFlowElement1


def flowdata():
return PipelineBuilder({
"add_javascript_builder": False,
"use_setheader_properties": False}) \
.add(ExampleFlowElement1()).build().create_flowdata()


class _Captured:
"""Records the variables of the last render and renders nothing."""

def __init__(self):
self.variables = None

def render(self, template, variables):
self.variables = dict(variables)
return ""


def rendered_variables(evidence):
"""The variables the JavaScript Builder renders its script from, after a
pipeline holding the Sequence Element has processed the evidence given."""

pipeline = PipelineBuilder({"javascript_builder_settings": {
"minify": False, "endpoint": "/json"}}).build()
data = pipeline.create_flowdata()
for key, value in evidence.items():
data.evidence.add(key, value)
captured = _Captured()
with mock.patch.object(
javascriptbuilder.chevron, "render", captured.render):
data.process()
return captured.variables


class EvidenceKeyCaseTests(unittest.TestCase):

def test_a_key_is_read_in_any_case(self):
data = flowdata()
data.evidence.add("header.User-Agent", "a browser")
self.assertEqual("a browser", data.evidence.get("header.user-agent"))
self.assertEqual("a browser", data.evidence.get("HEADER.USER-AGENT"))

def test_keys_that_differ_only_in_case_are_one_key(self):
data = flowdata()
data.evidence.add("header.User-Agent", "first")
data.evidence.add("header.user-agent", "second")
self.assertEqual(1, len(data.evidence.get_all()))
self.assertEqual("second", data.evidence.get("header.User-Agent"))

def test_a_key_in_any_case_survives_filtering(self):
data = flowdata()
data.evidence.add("Header.User-Agent", "a browser")
data.process()
self.assertEqual("a browser", data.evidence.get("header.user-agent"))

def test_the_sequence_element_accepts_its_keys_in_any_case(self):
"""Without this the two keys move to the evidence set aside, and the
Sequence Element then starts a session of its own."""

data = PipelineBuilder({
"add_javascript_builder": False,
"use_setheader_properties": False}) \
.add(SequenceElement()).build().create_flowdata()
data.evidence.add("Query.Session-Id", "abc-123")
data.evidence.add("QUERY.SEQUENCE", "4")
data.process()
self.assertEqual({}, data.evidence.get_unaccepted())
self.assertEqual("abc-123", data.evidence.get("query.session-id"))

def test_the_script_takes_the_session_and_sequence_in_any_case(self):
variables = rendered_variables({
"Query.Session-Id": "abc-123", "QUERY.SEQUENCE": "4"})
self.assertEqual("abc-123", variables["_sessionId"])
# The Sequence Element counts this request as the next one.
self.assertEqual(5, variables["_sequence"])

def test_the_script_sends_a_query_value_whose_key_is_in_any_case(self):
variables = rendered_variables({"Query.Example": "a value"})
self.assertEqual(
{"Example": "a+value"}, json.loads(variables["_parameters"]))


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