diff --git a/python/lib/sift_client/_tests/util/test_metadata.py b/python/lib/sift_client/_tests/util/test_metadata.py new file mode 100644 index 000000000..3bc14a5a0 --- /dev/null +++ b/python/lib/sift_client/_tests/util/test_metadata.py @@ -0,0 +1,54 @@ +from sift.metadata.v1.metadata_pb2 import ( + MetadataKey, + MetadataKeyType, + MetadataValue, +) + +from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict + + +def _string_value(name: str, value: str) -> MetadataValue: + return MetadataValue( + key=MetadataKey(name=name, type=MetadataKeyType.METADATA_KEY_TYPE_STRING), + string_value=value, + ) + + +class TestMetadataProtoToDict: + def test_unwraps_each_value_type(self): + metadata = [ + _string_value("env", "prod"), + MetadataValue( + key=MetadataKey(name="build", type=MetadataKeyType.METADATA_KEY_TYPE_NUMBER), + number_value=1.5, + ), + MetadataValue( + key=MetadataKey(name="armed", type=MetadataKeyType.METADATA_KEY_TYPE_BOOLEAN), + boolean_value=True, + ), + ] + assert metadata_proto_to_dict(metadata) == { + "env": "prod", + "build": 1.5, + "armed": True, + } + + def test_multi_value_key_keeps_first_value(self): + # A key can hold multiple values (multi-value metadata). The API + # returns values in canonical order; the dict view keeps the first. + metadata = [ + _string_value("associated_parts", "ABC"), + _string_value("associated_parts", "XYZ"), + _string_value("env", "prod"), + ] + assert metadata_proto_to_dict(metadata) == { + "associated_parts": "ABC", + "env": "prod", + } + + def test_empty_metadata(self): + assert metadata_proto_to_dict([]) == {} + + def test_round_trip_from_dict(self): + original = {"env": "prod", "build": 1.5, "armed": True} + assert metadata_proto_to_dict(metadata_dict_to_proto(original)) == original diff --git a/python/lib/sift_client/util/metadata.py b/python/lib/sift_client/util/metadata.py index 0b31fecce..bda406a91 100644 --- a/python/lib/sift_client/util/metadata.py +++ b/python/lib/sift_client/util/metadata.py @@ -54,16 +54,21 @@ def metadata_dict_to_proto(_metadata: dict[str, str | float | bool]) -> list[Met def metadata_proto_to_dict(metadata: list[MetadataProto]) -> dict[str, str | float | bool]: """Converts a list of MetadataValue objects into a dictionary. + A key may appear multiple times when it holds multiple values + (multi-value metadata). The dictionary keeps the first value of each + key -- the API returns values in canonical order, so this matches the + value every other single-value context (e.g. backend flattening) uses. + Args: metadata: List of MetadataValue objects. Returns: - Dictionary of metadata key-value pairs. + Dictionary of metadata key-value pairs (first value per key). """ unwrapped_metadata: dict[str, str | float | bool] = {} for md in metadata: if md.key.name in unwrapped_metadata: - raise ValueError(f"Key already exists: {md.key.name}") + continue if md.key.type == MetadataKeyType.METADATA_KEY_TYPE_STRING: unwrapped_metadata[md.key.name] = md.string_value elif md.key.type == MetadataKeyType.METADATA_KEY_TYPE_BOOLEAN: