diff --git a/panaetius/utilities/__init__.py b/panaetius/utilities/__init__.py new file mode 100644 index 0000000..5dc9cbc --- /dev/null +++ b/panaetius/utilities/__init__.py @@ -0,0 +1,3 @@ +"""General utilities.""" + +from panaetius.utilities import squasher diff --git a/panaetius/utilities/squasher.py b/panaetius/utilities/squasher.py new file mode 100644 index 0000000..b0f81a9 --- /dev/null +++ b/panaetius/utilities/squasher.py @@ -0,0 +1,64 @@ +"""Squash a json object or Python dictionary into a single level dictionary.""" + +from __future__ import annotations + +from copy import deepcopy +import itertools +from typing import Iterator, Tuple + + +class Squash: + """Squash a json object or Python dictionary into a single level dictionary.""" + + def __init__(self, data: dict) -> None: + """ + Create a Squash object to squash data into a single level dictionary. + + Args: + data (dict): [description] + + Example: + squashed_data = Squash(my_data) + + squashed_data.as_dict + """ + self.data = data + + @property + def as_dict(self) -> dict: + """ + Return the squashed data as a dictionary. + + Returns: + dict: The original data squashed as a dict. + """ + return self._squash() + + @staticmethod + def _unpack_dict( + key: str, value: dict | list | str + ) -> Iterator[Tuple[str, dict | list | str]]: + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + temporary_key = f"{key}_{sub_key}" + yield temporary_key, sub_value + elif isinstance(value, list): + for index, sub_value in enumerate(value): + temporary_key = f"{key}_{index}" + yield temporary_key, sub_value + else: + yield key, value + + def _squash(self) -> dict: + result = deepcopy(self.data) + while True: + result = dict( + itertools.chain.from_iterable( + itertools.starmap(self._unpack_dict, result.items()) + ) + ) + if not any( + isinstance(value, dict) for value in result.values() + ) and not any(isinstance(value, list) for value in result.values()): + break + return result diff --git a/tests/test_utilities/__init__.py b/tests/test_utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_utilities/test_squasher.py b/tests/test_utilities/test_squasher.py new file mode 100644 index 0000000..8b5817e --- /dev/null +++ b/tests/test_utilities/test_squasher.py @@ -0,0 +1,119 @@ +import pytest + +from panaetius import utilities + + +def test_squashed_data(squashed_data, squashed_data_result): + # act + squashed_data_pre_squashed = utilities.squasher.Squash(squashed_data).as_dict + + # assert + assert squashed_data_pre_squashed == squashed_data_result + + +@pytest.fixture +def squashed_data(): + return { + "destination_addresses": [ + "Washington, DC, USA", + "Philadelphia, PA, USA", + "Santa Barbara, CA, USA", + "Miami, FL, USA", + "Austin, TX, USA", + "Napa County, CA, USA", + ], + "origin_addresses": ["New York, NY, USA"], + "rows": [ + { + "elements": [ + { + "distance": {"text": "227 mi", "value": 365468}, + "duration": { + "text": "3 hours 54 mins", + "value": 14064, + }, + "status": "OK", + }, + { + "distance": {"text": "94.6 mi", "value": 152193}, + "duration": {"text": "1 hour 44 mins", "value": 6227}, + "status": "OK", + }, + { + "distance": {"text": "2,878 mi", "value": 4632197}, + "duration": { + "text": "1 day 18 hours", + "value": 151772, + }, + "status": "OK", + }, + { + "distance": {"text": "1,286 mi", "value": 2069031}, + "duration": { + "text": "18 hours 43 mins", + "value": 67405, + }, + "status": "OK", + }, + { + "distance": {"text": "1,742 mi", "value": 2802972}, + "duration": {"text": "1 day 2 hours", "value": 93070}, + "status": "OK", + }, + { + "distance": {"text": "2,871 mi", "value": 4620514}, + "duration": { + "text": "1 day 18 hours", + "value": 152913, + }, + "status": "OK", + }, + ] + } + ], + "status": "OK", + } + + +@pytest.fixture +def squashed_data_result(): + return { + "destination_addresses_0": "Washington, DC, USA", + "destination_addresses_1": "Philadelphia, PA, USA", + "destination_addresses_2": "Santa Barbara, CA, USA", + "destination_addresses_3": "Miami, FL, USA", + "destination_addresses_4": "Austin, TX, USA", + "destination_addresses_5": "Napa County, CA, USA", + "origin_addresses_0": "New York, NY, USA", + "rows_0_elements_0_distance_text": "227 mi", + "rows_0_elements_0_distance_value": 365468, + "rows_0_elements_0_duration_text": "3 hours 54 mins", + "rows_0_elements_0_duration_value": 14064, + "rows_0_elements_0_status": "OK", + "rows_0_elements_1_distance_text": "94.6 mi", + "rows_0_elements_1_distance_value": 152193, + "rows_0_elements_1_duration_text": "1 hour 44 mins", + "rows_0_elements_1_duration_value": 6227, + "rows_0_elements_1_status": "OK", + "rows_0_elements_2_distance_text": "2,878 mi", + "rows_0_elements_2_distance_value": 4632197, + "rows_0_elements_2_duration_text": "1 day 18 hours", + "rows_0_elements_2_duration_value": 151772, + "rows_0_elements_2_status": "OK", + "rows_0_elements_3_distance_text": "1,286 mi", + "rows_0_elements_3_distance_value": 2069031, + "rows_0_elements_3_duration_text": "18 hours 43 mins", + "rows_0_elements_3_duration_value": 67405, + "rows_0_elements_3_status": "OK", + "rows_0_elements_4_distance_text": "1,742 mi", + "rows_0_elements_4_distance_value": 2802972, + "rows_0_elements_4_duration_text": "1 day 2 hours", + "rows_0_elements_4_duration_value": 93070, + "rows_0_elements_4_status": "OK", + "rows_0_elements_5_distance_text": "2,871 mi", + "rows_0_elements_5_distance_value": 4620514, + "rows_0_elements_5_duration_text": "1 day 18 hours", + "rows_0_elements_5_duration_value": 152913, + "rows_0_elements_5_status": "OK", + "status": "OK", + }