From 0f076194ce47f599dd204eb1eae71a109b381c2b Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Mon, 8 Jan 2024 08:05:36 -0500 Subject: [PATCH 1/3] Initial commit --- anta/tools/get_item.py | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 anta/tools/get_item.py diff --git a/anta/tools/get_item.py b/anta/tools/get_item.py new file mode 100644 index 000000000..db5695b6b --- /dev/null +++ b/anta/tools/get_item.py @@ -0,0 +1,83 @@ +# Copyright (c) 2023-2024 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. + +"""Get one dictionary from a list of dictionaries by matching the given key and value.""" +from __future__ import annotations + +from typing import Any, Optional + + +# pylint: disable=too-many-arguments +def get_item( + list_of_dicts: list[dict[Any, Any]], + key: Any, + value: Any, + default: Optional[Any] = None, + required: bool = False, + case_sensitive: bool = False, + var_name: Optional[str] = None, + custom_error_msg: Optional[str] = None, +) -> Any: + """Get one dictionary from a list of dictionaries by matching the given key and value. + + Returns the supplied default value or None if there is no match and "required" is False. + + Will return the first matching item if there are multiple matching items. + + Parameters + ---------- + list_of_dicts : list(dict) + List of Dictionaries to get list item from + key : any + Dictionary Key to match on + value : any + Value that must match + default : any + Default value returned if the key and value is not found + required : bool + Fail if there is no match + case_sensitive : bool + If the search value is a string, the comparison will ignore case by default + var_name : str + String used for raising exception with the full variable name + custom_error_msg : str + Custom error message to raise when required is True and the value is not found + + Returns + ------- + any + Dict or default value + + Raises + ------ + ValueError + If the key and value is not found and "required" == True + """ + if var_name is None: + var_name = key + + if (not isinstance(list_of_dicts, list)) or list_of_dicts == [] or value is None or key is None: + if required is True: + raise ValueError(custom_error_msg or var_name) + return default + + for list_item in list_of_dicts: + if not isinstance(list_item, dict): + # List item is not a dict as required. Skip this item + continue + + item_value = list_item.get(key) + + # Perform case-insensitive comparison if value and item_value are strings and case_sensitive is False + if not case_sensitive and isinstance(value, str) and isinstance(item_value, str): + if item_value.casefold() == value.casefold(): + return list_item + elif item_value == value: + # Match. Return this item + return list_item + + # No Match + if required is True: + raise ValueError(custom_error_msg or var_name) + return default From e2843cf78f1d11b17e9177cc6198d9a21cfcf529 Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Tue, 9 Jan 2024 08:45:43 -0500 Subject: [PATCH 2/3] Added unit test --- tests/units/tools/test_get_item.py | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/units/tools/test_get_item.py diff --git a/tests/units/tools/test_get_item.py b/tests/units/tools/test_get_item.py new file mode 100644 index 000000000..20c798a06 --- /dev/null +++ b/tests/units/tools/test_get_item.py @@ -0,0 +1,70 @@ +# Copyright (c) 2023-2024 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the LICENSE file. + +"""Tests for `anta.tools.get_item`.""" +from __future__ import annotations + +from contextlib import nullcontext as does_not_raise +from typing import Any + +import pytest + +from anta.tools.get_item import get_item + +DUMMY_DATA = [ + { + "id": 1, + "name": "Alice", + "age": 30, + "email": "alice@example.com", + }, + { + "id": 2, + "name": "Bob", + "age": 35, + "email": "bob@example.com", + }, + { + "id": 3, + "name": "Charlie", + "age": 40, + "email": "charlie@example.com", + }, +] + + +@pytest.mark.parametrize( + "list_of_dicts, key, value, default, required, case_sensitive, var_name, custom_error_msg, expected_result, expected_raise", + [ + pytest.param([], "name", "Bob", None, False, False, None, None, None, does_not_raise(), id="empty list"), + pytest.param(DUMMY_DATA, "name", "Jack", None, False, False, None, None, None, does_not_raise(), id="missing item"), + pytest.param(DUMMY_DATA, "name", "Alice", None, False, False, None, None, DUMMY_DATA[0], does_not_raise(), id="found item"), + pytest.param(DUMMY_DATA, "name", "Jack", "default_value", False, False, None, None, "default_value", does_not_raise(), id="default value"), + pytest.param(DUMMY_DATA, "name", "Jack", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="required"), + pytest.param(DUMMY_DATA, "name", "bob", None, False, True, None, None, None, does_not_raise(), id="case sensitive"), + pytest.param(DUMMY_DATA, "name", "charlie", None, False, False, None, None, DUMMY_DATA[2], does_not_raise(), id="case insensitive"), + pytest.param( + DUMMY_DATA, "name", "Jack", None, True, False, "custom_var_name", None, None, pytest.raises(ValueError, match="custom_var_name"), id="custom var_name" + ), + pytest.param( + DUMMY_DATA, "name", "Jack", None, True, False, None, "custom_error_msg", None, pytest.raises(ValueError, match="custom_error_msg"), id="custom error msg" + ), + ], +) +def test_get_item( + list_of_dicts: list[dict[Any, Any]], + key: Any, + value: Any, + default: Any | None, + required: bool, + case_sensitive: bool, + var_name: str | None, + custom_error_msg: str | None, + expected_result: str, + expected_raise: Any, +) -> None: + """Test get_item.""" + # pylint: disable=too-many-arguments + with expected_raise: + assert get_item(list_of_dicts, key, value, default, required, case_sensitive, var_name, custom_error_msg) == expected_result From 98edb255a475b4da7d149c7eacd3ec93e0fe1682 Mon Sep 17 00:00:00 2001 From: Carl Baillargeon Date: Tue, 9 Jan 2024 09:18:48 -0500 Subject: [PATCH 3/3] 100% coverage --- tests/units/tools/test_get_item.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/units/tools/test_get_item.py b/tests/units/tools/test_get_item.py index 20c798a06..7d75e9c2a 100644 --- a/tests/units/tools/test_get_item.py +++ b/tests/units/tools/test_get_item.py @@ -13,6 +13,7 @@ from anta.tools.get_item import get_item DUMMY_DATA = [ + ("id", 0), { "id": 1, "name": "Alice", @@ -38,12 +39,13 @@ "list_of_dicts, key, value, default, required, case_sensitive, var_name, custom_error_msg, expected_result, expected_raise", [ pytest.param([], "name", "Bob", None, False, False, None, None, None, does_not_raise(), id="empty list"), + pytest.param([], "name", "Bob", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="empty list and required"), pytest.param(DUMMY_DATA, "name", "Jack", None, False, False, None, None, None, does_not_raise(), id="missing item"), - pytest.param(DUMMY_DATA, "name", "Alice", None, False, False, None, None, DUMMY_DATA[0], does_not_raise(), id="found item"), + pytest.param(DUMMY_DATA, "name", "Alice", None, False, False, None, None, DUMMY_DATA[1], does_not_raise(), id="found item"), pytest.param(DUMMY_DATA, "name", "Jack", "default_value", False, False, None, None, "default_value", does_not_raise(), id="default value"), pytest.param(DUMMY_DATA, "name", "Jack", None, True, False, None, None, None, pytest.raises(ValueError, match="name"), id="required"), - pytest.param(DUMMY_DATA, "name", "bob", None, False, True, None, None, None, does_not_raise(), id="case sensitive"), - pytest.param(DUMMY_DATA, "name", "charlie", None, False, False, None, None, DUMMY_DATA[2], does_not_raise(), id="case insensitive"), + pytest.param(DUMMY_DATA, "name", "Bob", None, False, True, None, None, DUMMY_DATA[2], does_not_raise(), id="case sensitive"), + pytest.param(DUMMY_DATA, "name", "charlie", None, False, False, None, None, DUMMY_DATA[3], does_not_raise(), id="case insensitive"), pytest.param( DUMMY_DATA, "name", "Jack", None, True, False, "custom_var_name", None, None, pytest.raises(ValueError, match="custom_var_name"), id="custom var_name" ),