From 43a3a853964a288a09a4d3aa94697902369eca50 Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:58:46 +0530 Subject: [PATCH 1/6] fix: keep numeric/boolean owner and tag strings instead of crashing Co-Authored-By: Claude --- elementary/utils/schema.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/elementary/utils/schema.py b/elementary/utils/schema.py index 068dd53f6..669d733ce 100644 --- a/elementary/utils/schema.py +++ b/elementary/utils/schema.py @@ -25,6 +25,7 @@ def _load_var_to_list(var: Union[str, list]) -> list: loaded_var = try_load_json(var) if isinstance(loaded_var, dict): loaded_var = [json.dumps(loaded_var)] - if loaded_var is None: + if loaded_var is None or not isinstance(loaded_var, list): + # Not JSON, or a JSON scalar such as 12345 or true. Keep it as a plain string. loaded_var = [var] return loaded_var From 1dce664ca0fc72b1b7db5af40b798b69980551cf Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:59:12 +0530 Subject: [PATCH 2/6] fix: keep numeric/boolean scalar tokens when unpacking owner and tag lists Co-Authored-By: Claude --- elementary/utils/json_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/elementary/utils/json_utils.py b/elementary/utils/json_utils.py index d0e3ee3c1..81529ab5d 100644 --- a/elementary/utils/json_utils.py +++ b/elementary/utils/json_utils.py @@ -33,7 +33,10 @@ def unpack_and_flatten_str_to_list(list_as_str: str) -> List[str]: if isinstance(list_unpacked, list): return list_unpacked - return [] # edge case of a string of an empty dict or IDK + if isinstance(list_unpacked, dict): + return [] # edge case of a string of an empty dict or IDK + # A JSON scalar such as 12345 or true is just a plain token. + return [list_as_str.strip()] def sum_lists(list_of_lists: List[List]) -> List: From 677295e83a522eff8b2caf9f9837f9beaf6d9c2d Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:59:42 +0530 Subject: [PATCH 3/6] test: cover numeric/boolean owner and tag strings Co-Authored-By: Claude --- tests/unit/utils/test_json_utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/unit/utils/test_json_utils.py diff --git a/tests/unit/utils/test_json_utils.py b/tests/unit/utils/test_json_utils.py new file mode 100644 index 000000000..c780dacd6 --- /dev/null +++ b/tests/unit/utils/test_json_utils.py @@ -0,0 +1,29 @@ +import pytest + +from elementary.utils.json_utils import ( + unpack_and_flatten_and_dedup_list_of_strings, + unpack_and_flatten_str_to_list, +) +from elementary.utils.schema import ExtendedBaseModel + + +@pytest.mark.parametrize("token", ["12345", "1.5", "true", "false"]) +def test_unpack_and_flatten_keeps_json_scalar_tokens(token): + assert unpack_and_flatten_str_to_list(token) == [token] + assert unpack_and_flatten_and_dedup_list_of_strings(token) == [token] + + +def test_unpack_and_flatten_still_handles_lists_and_commas(): + assert unpack_and_flatten_str_to_list('["a", "b"]') == ["a", "b"] + assert unpack_and_flatten_str_to_list("a, b") == ["a", "b"] + assert unpack_and_flatten_str_to_list("{}") == [] + + +@pytest.mark.parametrize("token", ["12345", "1.5", "true"]) +def test_load_var_to_list_keeps_json_scalar_tokens(token): + assert ExtendedBaseModel._load_var_to_list(token) == [token] + + +def test_load_var_to_list_still_handles_lists_and_strings(): + assert ExtendedBaseModel._load_var_to_list('["a"]') == ["a"] + assert ExtendedBaseModel._load_var_to_list("prod") == ["prod"] From afdb10d929ff0746d8ac2a477f1f9ec80a91d347 Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:48:45 +0530 Subject: [PATCH 4/6] fix: normalize quoted scalars and stringify alert list tokens --- elementary/utils/json_utils.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/elementary/utils/json_utils.py b/elementary/utils/json_utils.py index 81529ab5d..35cdcd461 100644 --- a/elementary/utils/json_utils.py +++ b/elementary/utils/json_utils.py @@ -16,6 +16,11 @@ def try_load_json(value: Optional[Union[str, dict, list]]): return None +def normalize_scalar_token(raw: str, decoded: Any) -> str: + """Use decoded JSON strings; keep other scalar tokens as written.""" + return (decoded if isinstance(decoded, str) else raw).strip() + + def unpack_and_flatten_str_to_list(list_as_str: str) -> List[str]: """ if given a simple token like "marketing" -> return ["marketing"] @@ -36,7 +41,7 @@ def unpack_and_flatten_str_to_list(list_as_str: str) -> List[str]: if isinstance(list_unpacked, dict): return [] # edge case of a string of an empty dict or IDK # A JSON scalar such as 12345 or true is just a plain token. - return [list_as_str.strip()] + return [normalize_scalar_token(list_as_str, list_unpacked)] def sum_lists(list_of_lists: List[List]) -> List: @@ -47,7 +52,7 @@ def sum_lists(list_of_lists: List[List]) -> List: def unpack_and_flatten_and_dedup_list_of_strings( - list_maybe_jsoned: Optional[Union[List[str], str]] + list_maybe_jsoned: Optional[Union[List[str], str]], ) -> List[str]: if not list_maybe_jsoned: return [] @@ -57,11 +62,16 @@ def unpack_and_flatten_and_dedup_list_of_strings( elif isinstance(list_maybe_jsoned, list): ret = sum_lists( [ - unpack_and_flatten_str_to_list(x) + unpack_and_flatten_str_to_list(x) if isinstance(x, str) else [x] for x in list_maybe_jsoned - if isinstance(x, str) ] ) + # Owners/tags need strings, but the general parser also handles sample rows. + ret = [ + item.strip() if isinstance(item, str) else json.dumps(item) + for item in ret + if isinstance(item, (str, int, float, bool)) + ] return list(set(ret)) From b1c988b71a3d3a24c67b85ba0117d6356466aedb Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:49:04 +0530 Subject: [PATCH 5/6] fix: share scalar normalization with report parser --- elementary/utils/schema.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/elementary/utils/schema.py b/elementary/utils/schema.py index 669d733ce..9e810d378 100644 --- a/elementary/utils/schema.py +++ b/elementary/utils/schema.py @@ -1,7 +1,7 @@ import json from typing import Union -from elementary.utils.json_utils import try_load_json +from elementary.utils.json_utils import normalize_scalar_token, try_load_json from elementary.utils.pydantic_shim import BaseModel @@ -25,7 +25,6 @@ def _load_var_to_list(var: Union[str, list]) -> list: loaded_var = try_load_json(var) if isinstance(loaded_var, dict): loaded_var = [json.dumps(loaded_var)] - if loaded_var is None or not isinstance(loaded_var, list): - # Not JSON, or a JSON scalar such as 12345 or true. Keep it as a plain string. - loaded_var = [var] + if not isinstance(loaded_var, list): + loaded_var = [normalize_scalar_token(var, loaded_var)] return loaded_var From 7511906653bc0a5443886f8ba7a2b963164d2e94 Mon Sep 17 00:00:00 2001 From: Deepanshu Pal <40927968+DeepanshuPal@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:49:26 +0530 Subject: [PATCH 6/6] test: cover reviewer scalar list and caller compatibility cases --- tests/unit/utils/test_json_utils.py | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/unit/utils/test_json_utils.py b/tests/unit/utils/test_json_utils.py index c780dacd6..3b51ce42c 100644 --- a/tests/unit/utils/test_json_utils.py +++ b/tests/unit/utils/test_json_utils.py @@ -27,3 +27,63 @@ def test_load_var_to_list_keeps_json_scalar_tokens(token): def test_load_var_to_list_still_handles_lists_and_strings(): assert ExtendedBaseModel._load_var_to_list('["a"]') == ["a"] assert ExtendedBaseModel._load_var_to_list("prod") == ["prod"] + + +@pytest.mark.parametrize( + "token, expected", + [ + ('"@alice"', ["@alice"]), + ('"prod"', ["prod"]), + ('" prod "', ["prod"]), + (" prod ", ["prod"]), + (" 12345 ", ["12345"]), + (" true ", ["true"]), + ], +) +def test_scalar_tokens_match_in_report_and_alerts(token, expected): + assert ExtendedBaseModel._load_var_to_list(token) == expected + assert unpack_and_flatten_and_dedup_list_of_strings(token) == expected + + +@pytest.mark.parametrize("value", ["[12345]", '[1, "a"]', [1, "a"]]) +def test_alert_numeric_list_items_are_strings(value): + from elementary.utils.strings import prettify_and_dedup_list + + result = unpack_and_flatten_and_dedup_list_of_strings(value) + expected = ["12345"] if value == "[12345]" else ["1", "a"] + assert sorted(result) == expected + assert prettify_and_dedup_list(result) == ", ".join(expected) + + +def test_general_list_parser_preserves_sample_rows(): + rows = [{"a": 1}, {"b": True}] + assert unpack_and_flatten_str_to_list('[{"a": 1}, {"b": true}]') == rows + assert ExtendedBaseModel._load_var_to_list(rows) is rows + + +def test_existing_dict_and_comma_policies_are_preserved(): + assert unpack_and_flatten_and_dedup_list_of_strings('{"a": 1}') == [] + assert ExtendedBaseModel._load_var_to_list('{"a": 1}') == ['{"a": 1}'] + assert sorted(unpack_and_flatten_and_dedup_list_of_strings("a, b")) == ["a", "b"] + assert ExtendedBaseModel._load_var_to_list("a, b") == ["a, b"] + assert ExtendedBaseModel._load_var_to_list(["a", "a", "b"]) == ["a", "a", "b"] + + +@pytest.mark.parametrize("value", ["[12345]", '[1, "a"]', [1, "a"]]) +def test_report_owner_lists_match_alert_strings(value): + from elementary.monitor.fetchers.models.schema import ArtifactSchema + + expected = ["12345"] if value == "[12345]" else ["1", "a"] + assert ArtifactSchema(owners=value).owners == expected + assert sorted(unpack_and_flatten_and_dedup_list_of_strings(value)) == expected + + +def test_dependency_nodes_keep_commas_order_and_duplicates(): + from elementary.monitor.fetchers.models.schema import ExposureSchema + + assert ExposureSchema(depends_on_nodes="a, b").depends_on_nodes == ["a, b"] + assert ExposureSchema(depends_on_nodes=["a", "a", "b"]).depends_on_nodes == [ + "a", + "a", + "b", + ]