From 7624ffead15ff1b5667cff8e1db3dee433ea7eed Mon Sep 17 00:00:00 2001 From: moshecarmeli Date: Mon, 27 Jul 2020 13:14:26 -0700 Subject: [PATCH 1/7] Submission for backend skills test - Moshe Carmeli --- compressor.py | 100 +++++++++++++++++++++++++++++++++++++++++ tests.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 compressor.py create mode 100644 tests.py diff --git a/compressor.py b/compressor.py new file mode 100644 index 0000000..ece9747 --- /dev/null +++ b/compressor.py @@ -0,0 +1,100 @@ +from collections import abc + + +class ObjectCompressor: + """ + This is a class containing compress and decompress functions as described on https://github.com/Ontraport/Backend-Test + """ + def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = None) -> dict: + """ + This function compresses a multi-dimensional container of any size (tested with nested Python dictionaries and nested custom classes) + and returns a compressed version of the original container as a Python dictionary + """ + if(dictionary is None): + dictionary = {} + # If this is a Collection, it will always be Sized, but we want to handle Mappings slightly different, since they can contain nested objects + if(isinstance(object_to_compress, abc.Collection)): + # If this is a Mapping (e.g. a dictionary), iterate through keys and compress recursively + if(isinstance(object_to_compress, abc.Mapping)): + if(len(object_to_compress) > 0): + for attribute in object_to_compress.keys(): + value = object_to_compress.get(attribute) + self.helper_function(path_so_far + "/" + str(attribute), dictionary, value) + else: + if(path_so_far): # only add this object if it wasn't top level + self.helper_function(path_so_far, dictionary, {}, True) + # If this is a Sized Collection (e.g. a List), iterate through elements and compress recursively + elif(isinstance(object_to_compress, abc.Sized)): + if(len(object_to_compress) > 0): + for i in range(len(object_to_compress)): + value = object_to_compress[i] + self.helper_function(path_so_far + "/" + str(i), dictionary, value) + else: + if(path_so_far): # only add this object if it wasn't top level + self.helper_function(path_so_far, dictionary, [], True) + # If this is a custom object, iterate through non-private attributes and compress + else: + filtered_attributes = list(filter(lambda x: not x.startswith('__'), dir(object_to_compress))) + if(len(filtered_attributes) > 0): + for attribute in filtered_attributes: + value = getattr(object_to_compress, str(attribute)) + self.helper_function(path_so_far + "/" + str(attribute), dictionary, value) + else: + if(path_so_far): # only add this object if it wasn't top level + self.helper_function(path_so_far, dictionary, {}, True) + return dictionary + + def helper_function(self, path_so_far: str, dictionary: dict, value, force_insert: bool = False) -> dict: + """ + This helper function was introduced to minimize duplicate code in the compress(...) method + """ + if(path_so_far.startswith("/")): + path_so_far = path_so_far[1:] + if (not isinstance(value, (float, int, str, bool, type(None))) and force_insert is False): + return self.compress(value, path_so_far, dictionary) + else: + dictionary[path_so_far] = value + return dictionary + + def decompress(self, compressedObject: dict) -> dict: + """ + This function expands a compressed container into a dictionary representation of its original form + NOTE: If a custom object was flattened, decompress will still return a dictionary representation + """ + if(isinstance(compressedObject, abc.Collection)): + if(isinstance(compressedObject, abc.Mapping)): + resultObject = {} + keys = compressedObject.keys() + # For each key in the compressed object + for attribute in compressedObject.keys(): + pathList = attribute.split("/") + if(pathList[0].isnumeric() and isinstance(resultObject, abc.Mapping)): + resultObject = [] + pointer = resultObject + # For each token in the path + for i in range(len(pathList)): + token = pathList[i] + if(isinstance(pointer, abc.MutableSequence)): # We are in a list + if(token.isnumeric()): + if(int(token) < len(pointer)): # Already exists in this list + pointer = pointer[int(token)] + else: + if(i < len(pathList) - 1 and pathList[i+1].isnumeric()): + pointer.append([]) + elif(i == len(pathList) - 1): + pointer.append(compressedObject[attribute]) + else: + pointer.append({}) + pointer = pointer[-1] + else: # We are in a Map + if(i < len(pathList) - 1): + if(pathList[i+1].isnumeric()): # There is an intermediate array + if(token not in pointer): + pointer[token] = [] + else: + if(token not in pointer): + pointer[token] = {} + pointer = pointer[token] + else: + pointer[token] = compressedObject[attribute] + return resultObject diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..c51d50c --- /dev/null +++ b/tests.py @@ -0,0 +1,122 @@ +import unittest +from compressor import ObjectCompressor + + +class CompressionTests(unittest.TestCase): + def setUp(self): + self.object_compressor = ObjectCompressor() + self.example_dictionary = {'one': {'two': 3, 'four': [5, 6, 7]}, 'eight': {'nine': {'ten': 11}}} + self.example_dictionary_compressed = {'one/two': 3, 'one/four/0': 5, 'one/four/1': 6, 'one/four/2': 7, 'eight/nine/ten': 11} + + self.custom_object = MyCustomObject() + self.custom_object.one = MyCustomObject() + self.custom_object.one.two = 3 + self.custom_object.one.four = [5, 6, 7] + self.custom_object.eight = MyCustomObject() + self.custom_object.eight.nine = MyCustomObject() + self.custom_object.eight.nine.ten = 11 + + def test_example_case_compression(self): + compressed_version = self.object_compressor.compress(self.example_dictionary) + self.assertEqual(self.example_dictionary_compressed, compressed_version) + + def test_example_caseDecompression(self): + decompressed_version = self.object_compressor.decompress(self.example_dictionary_compressed) + self.assertEqual(self.example_dictionary, decompressed_version) + + def test_example_case_compression_and_decompression(self): + compressed_version = self.object_compressor.compress(self.example_dictionary) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(self.example_dictionary, decompressed_version) + + def test_middle_array_compression(self): + dictionary_with_middle_array = {'one': {'two': 3, 'four': [{'a': 2.3, 'b': None}, 'c']}, 'eight': {'nine': {'ten': 11}}} + empty_dictionary_compressed = {'one/two': 3, 'one/four/0/a': 2.3, 'one/four/0/b': None, 'one/four/1': 'c', 'eight/nine/ten': 11} + compressed_version = self.object_compressor.compress(dictionary_with_middle_array) + self.assertEqual(empty_dictionary_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(dictionary_with_middle_array, decompressed_version) + + def test_top_array_compression(self): + dictionary_with_middle_array = [{'two': 3}, {'four': [{'a': 2.3, 'b': None}, 'c']}, {'eight': {'nine': {'ten': 11}}}] + empty_dictionary_compressed = {'0/two': 3, '1/four/0/a': 2.3, '1/four/0/b': None, '1/four/1': 'c', '2/eight/nine/ten': 11} + compressed_version = self.object_compressor.compress(dictionary_with_middle_array) + self.assertEqual(empty_dictionary_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(dictionary_with_middle_array, decompressed_version) + + def test_empty_array_compression(self): + dictionary_with_empty_array = {'one': {'two': 3, 'four': []}, 'eight': {'nine': {'ten': 11}}} + empty_dictionary_compressed = {'one/two': 3, 'one/four': [], 'eight/nine/ten': 11} + compressed_version = self.object_compressor.compress(dictionary_with_empty_array) + self.assertEqual(empty_dictionary_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(dictionary_with_empty_array, decompressed_version) + + def test_empty_object_compression(self): + dictionary_with_empty_container = {'one': {'two': 3, 'four': [5, 6, 7]}, 'eight': {'nine': {}}} + empty_dictionary_compressed = {'one/two': 3, 'one/four/0': 5, 'one/four/1': 6, 'one/four/2': 7, 'eight/nine': {}} + compressed_version = self.object_compressor.compress(dictionary_with_empty_container) + self.assertEqual(empty_dictionary_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(dictionary_with_empty_container, decompressed_version) + + def test_string_and_bool_and_float_compression(self): + object_to_test = {'a': {'d': True}, 'b': 'myString', 'c': 21.89} + decompressed_object_to_test = {'a/d': True, 'b': 'myString', 'c': 21.89} + compressed_version = self.object_compressor.compress(object_to_test) + self.assertEqual(decompressed_object_to_test, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(object_to_test, decompressed_version) + + def test_empty_top_level_object_compression(self): + compressed_version = self.object_compressor.compress({}) + self.assertEqual({}, compressed_version) + + def test_single_value_compression(self): + compressed_version = self.object_compressor.compress({'a': 1}) + self.assertEqual({'a': 1}, compressed_version) + + def test_single_value_empty_object_compression(self): + compressed_version = self.object_compressor.compress({'a': {}}) + self.assertEqual({'a': {}}, compressed_version) + + def test_single_value_empty_array_compression(self): + compressed_version = self.object_compressor.compress({'a': []}) + self.assertEqual({'a': []}, compressed_version) + + def test_empty_top_level_array_compression(self): + compressed_version = self.object_compressor.compress([]) + self.assertEqual({}, compressed_version) + + def test_single_array_compression(self): + compressed_version = self.object_compressor.compress({'a': [0, 1, 2]}) + self.assertEqual({'a/0': 0, 'a/1': 1, 'a/2': 2}, compressed_version) + + # These are the same tasts, but using a custom Python object instead of dictionary as a container + def test_custom_object_compression(self): + compressed_version = self.object_compressor.compress(self.custom_object) + self.assertEqual(self.example_dictionary_compressed, compressed_version) + + def test_empty_array_custom_object_compression(self): + empty_dictionary_compressed = {'one/two': 3, 'one/four': [], 'eight/nine/ten': 11} + self.custom_object.one.four = [] + compressed_version = self.object_compressor.compress(self.custom_object) + self.assertEqual(empty_dictionary_compressed, compressed_version) + self.custom_object.one.four = [5, 6, 7] + + def test_empty_object_custom_object_compression(self): + self.custom_object.eight.nine = MyCustomObject() + empty_object_compressed = {'one/two': 3, 'one/four/0': 5, 'one/four/1': 6, 'one/four/2': 7, 'eight/nine': {}} + compressed_version = self.object_compressor.compress(self.custom_object) + self.assertEqual(empty_object_compressed, compressed_version) + self.custom_object.eight.nine.ten = 11 + + +class MyCustomObject: + def __init__(self): + return + + +if __name__ == '__main__': + unittest.main() From 4f73e121cc366cfd44bfd97c16b3e9f484b2cd2b Mon Sep 17 00:00:00 2001 From: "moshecmeli@gmail.com" Date: Mon, 27 Jul 2020 13:33:44 -0700 Subject: [PATCH 2/7] Updated helper function name and a couple of variables in unit tests --- __pycache__/compressor.cpython-37.pyc | Bin 0 -> 3084 bytes compressor.py | 14 +++++++------- tests.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 __pycache__/compressor.cpython-37.pyc diff --git a/__pycache__/compressor.cpython-37.pyc b/__pycache__/compressor.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0c2e195c372e2300942b8d55f6eaceb0b247546 GIT binary patch literal 3084 zcmaJ@&u<&Y6`t8&T+x)QDux`lK(kHS#6%OZD0-Tj8I!8T$t{mc@qfAwKo0YBst^d*yYoZCQ zUs>IFX^ECtf9`bGgbk0I<>sTKFJx>U)90e60ADzzweEK(> zvBDPo*jKHDO>M&pZul40%u@ec$T)BHTjLab#)Wn4sKM#S1wVJ^Y>$0@cMAQvS2%M% zbqjXhC~!x&nehu}#&2Wh!kycko!ooOPA&>dUDP&(oiO3d9nP}e!pj=GT)gJxZ>OJ_ zX5F4~czSp06}<57uxoz2D}Mg`m}R$K_tRhcG3JJ8RX5JeI_+g<&G@td4~=?XFSyz} z-7gyOwKH|h2F{_{#mu_q@ygC#@RLpW+%7DP?_S?sL6~VwzO1fkg_7mHuYe&5Hp6jh zk`F{Ol$i!7qI3x%se-%*?3@KUnaJR-!My_}&wz~}tL4wm%mA=iU3x+@0&I)!c&tr6 ztbr+Yd$k%eslid@ZH--l3-YTqoIiB2-C zCX1!AFurU)$kP=Rj6b7om|`PDU)Ce~5~?K6k8^)#5F_!J9m=9KKbx zwzyxn?Cq*&gTHNge3MV^{Lh1Y*mho9WviE~ScW)7sf=pCS4>-=LjSacii8d!fJQ1b zsO2z5|VVH?ZX#xbt(K>fVu&MduGIKlyC?NtG1gBYwiLJf3d6?~T!)&bH4 zC5TD7f@DR|1DTExPpb%g1{^0~)?AEZ;NIlH5S%@vI1}Shn#7Tjh(jS^RfWRx`0noP z?6iZSMBIs$3q?D6ML^$w*!C4AIkkzZbbDzYnbIa#w@Kj~<#}2<<_xQO8;Z88DBoS4 zoeHS=4pjvI%NtZkn5=`ZiXrASacu$LuzWrVzIH9wu-LZB&E*E9sIBX3A$(I=!?(%} zf5c8M^8xtn50@E%TF4hT?sI33yzzpc{<@a;*bDyo&yg3LbC<#!?CAIr7B zkY6aLJn7STYwBU_wrPNEU9d8^9^;*9qi~A`AmC$!hY`pR@cGTcr+FYvOtbK3#L>tA z$4vbdR+R>0yzEJ_m`~VmGW1^1Y?PT6>$oVq^Ba9O=g2_sU$G@G9`yoL&Yjw#N$sSP z3r|2h^-j!{QM97$st;&>SzvX?4M?JoT@?N<%eF3c$wEgxi#aQwwdILH&iv8i8q6-h z>`nf@LRtmp6^iBQ2vmNJ!xs+_pdF~o!Pcwna0b#R+LL&p5SbCpCq`e=Ji>Oka<%{H z@Td2JUmyboi#$>@Q$VrPOF=acqmm8t2@;92KC+ibZnxdR36Bau_Ok6m$V*8 z8&9Y9S{-d|=(;VO1XAxJ8faqm7U_L@PkjQS?lwj_j!-J~9h$>6Ii6qFq7o0T)w}K| zu+qDzn7aYqbhvMAfmh#zRWvBt*-_1rH8hq z=rOn7<`3J=(htKd8p<#%n_)NvlcglL!ti7qrHh_M7>Ya&!v*^g6e`|P?_r*bQq&Jf zy5yQlqLiU*ROC(*yZN@mZ@stOs 0): for attribute in object_to_compress.keys(): value = object_to_compress.get(attribute) - self.helper_function(path_so_far + "/" + str(attribute), dictionary, value) + self.save_or_recurse(path_so_far + "/" + str(attribute), dictionary, value) else: if(path_so_far): # only add this object if it wasn't top level - self.helper_function(path_so_far, dictionary, {}, True) + self.save_or_recurse(path_so_far, dictionary, {}, True) # If this is a Sized Collection (e.g. a List), iterate through elements and compress recursively elif(isinstance(object_to_compress, abc.Sized)): if(len(object_to_compress) > 0): for i in range(len(object_to_compress)): value = object_to_compress[i] - self.helper_function(path_so_far + "/" + str(i), dictionary, value) + self.save_or_recurse(path_so_far + "/" + str(i), dictionary, value) else: if(path_so_far): # only add this object if it wasn't top level - self.helper_function(path_so_far, dictionary, [], True) + self.save_or_recurse(path_so_far, dictionary, [], True) # If this is a custom object, iterate through non-private attributes and compress else: filtered_attributes = list(filter(lambda x: not x.startswith('__'), dir(object_to_compress))) if(len(filtered_attributes) > 0): for attribute in filtered_attributes: value = getattr(object_to_compress, str(attribute)) - self.helper_function(path_so_far + "/" + str(attribute), dictionary, value) + self.save_or_recurse(path_so_far + "/" + str(attribute), dictionary, value) else: if(path_so_far): # only add this object if it wasn't top level - self.helper_function(path_so_far, dictionary, {}, True) + self.save_or_recurse(path_so_far, dictionary, {}, True) return dictionary - def helper_function(self, path_so_far: str, dictionary: dict, value, force_insert: bool = False) -> dict: + def save_or_recurse(self, path_so_far: str, dictionary: dict, value, force_insert: bool = False) -> dict: """ This helper function was introduced to minimize duplicate code in the compress(...) method """ diff --git a/tests.py b/tests.py index c51d50c..1b0b382 100644 --- a/tests.py +++ b/tests.py @@ -31,19 +31,19 @@ def test_example_case_compression_and_decompression(self): def test_middle_array_compression(self): dictionary_with_middle_array = {'one': {'two': 3, 'four': [{'a': 2.3, 'b': None}, 'c']}, 'eight': {'nine': {'ten': 11}}} - empty_dictionary_compressed = {'one/two': 3, 'one/four/0/a': 2.3, 'one/four/0/b': None, 'one/four/1': 'c', 'eight/nine/ten': 11} + middle_array_compressed = {'one/two': 3, 'one/four/0/a': 2.3, 'one/four/0/b': None, 'one/four/1': 'c', 'eight/nine/ten': 11} compressed_version = self.object_compressor.compress(dictionary_with_middle_array) - self.assertEqual(empty_dictionary_compressed, compressed_version) + self.assertEqual(middle_array_compressed, compressed_version) decompressed_version = self.object_compressor.decompress(compressed_version) self.assertEqual(dictionary_with_middle_array, decompressed_version) def test_top_array_compression(self): - dictionary_with_middle_array = [{'two': 3}, {'four': [{'a': 2.3, 'b': None}, 'c']}, {'eight': {'nine': {'ten': 11}}}] - empty_dictionary_compressed = {'0/two': 3, '1/four/0/a': 2.3, '1/four/0/b': None, '1/four/1': 'c', '2/eight/nine/ten': 11} - compressed_version = self.object_compressor.compress(dictionary_with_middle_array) - self.assertEqual(empty_dictionary_compressed, compressed_version) + array_object = [{'two': 3}, {'four': [{'a': 2.3, 'b': None}, 'c']}, {'eight': {'nine': {'ten': 11}}}] + top_array_compressed = {'0/two': 3, '1/four/0/a': 2.3, '1/four/0/b': None, '1/four/1': 'c', '2/eight/nine/ten': 11} + compressed_version = self.object_compressor.compress(array_object) + self.assertEqual(top_array_compressed, compressed_version) decompressed_version = self.object_compressor.decompress(compressed_version) - self.assertEqual(dictionary_with_middle_array, decompressed_version) + self.assertEqual(array_object, decompressed_version) def test_empty_array_compression(self): dictionary_with_empty_array = {'one': {'two': 3, 'four': []}, 'eight': {'nine': {'ten': 11}}} @@ -93,7 +93,7 @@ def test_single_array_compression(self): compressed_version = self.object_compressor.compress({'a': [0, 1, 2]}) self.assertEqual({'a/0': 0, 'a/1': 1, 'a/2': 2}, compressed_version) - # These are the same tasts, but using a custom Python object instead of dictionary as a container + # These are the tests using a custom Python object instead of dictionary/List as a container def test_custom_object_compression(self): compressed_version = self.object_compressor.compress(self.custom_object) self.assertEqual(self.example_dictionary_compressed, compressed_version) From bc8eaf3a14a01b764b8b8f34a170cebc40ac8bcf Mon Sep 17 00:00:00 2001 From: "moshecmeli@gmail.com" Date: Mon, 27 Jul 2020 13:53:19 -0700 Subject: [PATCH 3/7] Added some comments and fixed type hints --- compressor.py | 6 ++++-- tests.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/compressor.py b/compressor.py index d90ae5b..0d2337d 100644 --- a/compressor.py +++ b/compressor.py @@ -1,11 +1,13 @@ from collections import abc +from typing import Union class ObjectCompressor: """ + Note: Tested with Python v 3.7.3 This is a class containing compress and decompress functions as described on https://github.com/Ontraport/Backend-Test """ - def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = None) -> dict: + def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = None) -> Union[dict, list]: """ This function compresses a multi-dimensional container of any size (tested with nested Python dictionaries and nested custom classes) and returns a compressed version of the original container as a Python dictionary @@ -56,7 +58,7 @@ def save_or_recurse(self, path_so_far: str, dictionary: dict, value, force_inser dictionary[path_so_far] = value return dictionary - def decompress(self, compressedObject: dict) -> dict: + def decompress(self, compressedObject: dict) -> Union[dict, list]: """ This function expands a compressed container into a dictionary representation of its original form NOTE: If a custom object was flattened, decompress will still return a dictionary representation diff --git a/tests.py b/tests.py index 1b0b382..73d7be9 100644 --- a/tests.py +++ b/tests.py @@ -3,6 +3,9 @@ class CompressionTests(unittest.TestCase): + """ + This is a class unit tests for compress/decompress functions + """ def setUp(self): self.object_compressor = ObjectCompressor() self.example_dictionary = {'one': {'two': 3, 'four': [5, 6, 7]}, 'eight': {'nine': {'ten': 11}}} From 7a992e943de2978715ff31548b59e6bb95c76c97 Mon Sep 17 00:00:00 2001 From: "moshecmeli@gmail.com" Date: Mon, 27 Jul 2020 13:55:13 -0700 Subject: [PATCH 4/7] Removing pycache --- __pycache__/compressor.cpython-37.pyc | Bin 3084 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __pycache__/compressor.cpython-37.pyc diff --git a/__pycache__/compressor.cpython-37.pyc b/__pycache__/compressor.cpython-37.pyc deleted file mode 100644 index f0c2e195c372e2300942b8d55f6eaceb0b247546..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3084 zcmaJ@&u<&Y6`t8&T+x)QDux`lK(kHS#6%OZD0-Tj8I!8T$t{mc@qfAwKo0YBst^d*yYoZCQ zUs>IFX^ECtf9`bGgbk0I<>sTKFJx>U)90e60ADzzweEK(> zvBDPo*jKHDO>M&pZul40%u@ec$T)BHTjLab#)Wn4sKM#S1wVJ^Y>$0@cMAQvS2%M% zbqjXhC~!x&nehu}#&2Wh!kycko!ooOPA&>dUDP&(oiO3d9nP}e!pj=GT)gJxZ>OJ_ zX5F4~czSp06}<57uxoz2D}Mg`m}R$K_tRhcG3JJ8RX5JeI_+g<&G@td4~=?XFSyz} z-7gyOwKH|h2F{_{#mu_q@ygC#@RLpW+%7DP?_S?sL6~VwzO1fkg_7mHuYe&5Hp6jh zk`F{Ol$i!7qI3x%se-%*?3@KUnaJR-!My_}&wz~}tL4wm%mA=iU3x+@0&I)!c&tr6 ztbr+Yd$k%eslid@ZH--l3-YTqoIiB2-C zCX1!AFurU)$kP=Rj6b7om|`PDU)Ce~5~?K6k8^)#5F_!J9m=9KKbx zwzyxn?Cq*&gTHNge3MV^{Lh1Y*mho9WviE~ScW)7sf=pCS4>-=LjSacii8d!fJQ1b zsO2z5|VVH?ZX#xbt(K>fVu&MduGIKlyC?NtG1gBYwiLJf3d6?~T!)&bH4 zC5TD7f@DR|1DTExPpb%g1{^0~)?AEZ;NIlH5S%@vI1}Shn#7Tjh(jS^RfWRx`0noP z?6iZSMBIs$3q?D6ML^$w*!C4AIkkzZbbDzYnbIa#w@Kj~<#}2<<_xQO8;Z88DBoS4 zoeHS=4pjvI%NtZkn5=`ZiXrASacu$LuzWrVzIH9wu-LZB&E*E9sIBX3A$(I=!?(%} zf5c8M^8xtn50@E%TF4hT?sI33yzzpc{<@a;*bDyo&yg3LbC<#!?CAIr7B zkY6aLJn7STYwBU_wrPNEU9d8^9^;*9qi~A`AmC$!hY`pR@cGTcr+FYvOtbK3#L>tA z$4vbdR+R>0yzEJ_m`~VmGW1^1Y?PT6>$oVq^Ba9O=g2_sU$G@G9`yoL&Yjw#N$sSP z3r|2h^-j!{QM97$st;&>SzvX?4M?JoT@?N<%eF3c$wEgxi#aQwwdILH&iv8i8q6-h z>`nf@LRtmp6^iBQ2vmNJ!xs+_pdF~o!Pcwna0b#R+LL&p5SbCpCq`e=Ji>Oka<%{H z@Td2JUmyboi#$>@Q$VrPOF=acqmm8t2@;92KC+ibZnxdR36Bau_Ok6m$V*8 z8&9Y9S{-d|=(;VO1XAxJ8faqm7U_L@PkjQS?lwj_j!-J~9h$>6Ii6qFq7o0T)w}K| zu+qDzn7aYqbhvMAfmh#zRWvBt*-_1rH8hq z=rOn7<`3J=(htKd8p<#%n_)NvlcglL!ti7qrHh_M7>Ya&!v*^g6e`|P?_r*bQq&Jf zy5yQlqLiU*ROC(*yZN@mZ@stOs Date: Mon, 27 Jul 2020 14:11:59 -0700 Subject: [PATCH 5/7] Improved some unit tests and added support for top level empty array --- compressor.py | 21 +++++++++++++-------- tests.py | 30 +++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/compressor.py b/compressor.py index 0d2337d..ec95536 100644 --- a/compressor.py +++ b/compressor.py @@ -7,10 +7,11 @@ class ObjectCompressor: Note: Tested with Python v 3.7.3 This is a class containing compress and decompress functions as described on https://github.com/Ontraport/Backend-Test """ - def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = None) -> Union[dict, list]: + def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = None) -> dict: """ This function compresses a multi-dimensional container of any size (tested with nested Python dictionaries and nested custom classes) and returns a compressed version of the original container as a Python dictionary + (note that a top level empty Array becomes an emptyDictionary) """ if(dictionary is None): dictionary = {} @@ -34,6 +35,8 @@ def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = else: if(path_so_far): # only add this object if it wasn't top level self.save_or_recurse(path_so_far, dictionary, [], True) + else: + return [] # If this is a custom object, iterate through non-private attributes and compress else: filtered_attributes = list(filter(lambda x: not x.startswith('__'), dir(object_to_compress))) @@ -58,17 +61,17 @@ def save_or_recurse(self, path_so_far: str, dictionary: dict, value, force_inser dictionary[path_so_far] = value return dictionary - def decompress(self, compressedObject: dict) -> Union[dict, list]: + def decompress(self, compressed_object: dict) -> Union[dict, list]: """ This function expands a compressed container into a dictionary representation of its original form NOTE: If a custom object was flattened, decompress will still return a dictionary representation """ - if(isinstance(compressedObject, abc.Collection)): - if(isinstance(compressedObject, abc.Mapping)): + if(isinstance(compressed_object, abc.Collection)): + if(isinstance(compressed_object, abc.Mapping)): resultObject = {} - keys = compressedObject.keys() + keys = compressed_object.keys() # For each key in the compressed object - for attribute in compressedObject.keys(): + for attribute in compressed_object.keys(): pathList = attribute.split("/") if(pathList[0].isnumeric() and isinstance(resultObject, abc.Mapping)): resultObject = [] @@ -84,7 +87,7 @@ def decompress(self, compressedObject: dict) -> Union[dict, list]: if(i < len(pathList) - 1 and pathList[i+1].isnumeric()): pointer.append([]) elif(i == len(pathList) - 1): - pointer.append(compressedObject[attribute]) + pointer.append(compressed_object[attribute]) else: pointer.append({}) pointer = pointer[-1] @@ -98,5 +101,7 @@ def decompress(self, compressedObject: dict) -> Union[dict, list]: pointer[token] = {} pointer = pointer[token] else: - pointer[token] = compressedObject[attribute] + pointer[token] = compressed_object[attribute] return resultObject + elif(isinstance(compressed_object, abc.Sized) and len(compressed_object) == 0): + return compressed_object diff --git a/tests.py b/tests.py index 73d7be9..c82d302 100644 --- a/tests.py +++ b/tests.py @@ -77,42 +77,62 @@ def test_empty_top_level_object_compression(self): self.assertEqual({}, compressed_version) def test_single_value_compression(self): - compressed_version = self.object_compressor.compress({'a': 1}) + target_object = {'a': 1} + compressed_version = self.object_compressor.compress(target_object) self.assertEqual({'a': 1}, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(target_object, decompressed_version) def test_single_value_empty_object_compression(self): - compressed_version = self.object_compressor.compress({'a': {}}) + target_object = {'a': {}} + compressed_version = self.object_compressor.compress(target_object) self.assertEqual({'a': {}}, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(target_object, decompressed_version) def test_single_value_empty_array_compression(self): - compressed_version = self.object_compressor.compress({'a': []}) + target_object = {'a': []} + compressed_version = self.object_compressor.compress(target_object) self.assertEqual({'a': []}, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(target_object, decompressed_version) def test_empty_top_level_array_compression(self): + target_object = [] compressed_version = self.object_compressor.compress([]) - self.assertEqual({}, compressed_version) + self.assertEqual([], compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(target_object, decompressed_version) def test_single_array_compression(self): compressed_version = self.object_compressor.compress({'a': [0, 1, 2]}) self.assertEqual({'a/0': 0, 'a/1': 1, 'a/2': 2}, compressed_version) - # These are the tests using a custom Python object instead of dictionary/List as a container + # These are the example case tests using a custom Python object instead of dictionary/List as a container def test_custom_object_compression(self): compressed_version = self.object_compressor.compress(self.custom_object) self.assertEqual(self.example_dictionary_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(self.example_dictionary, decompressed_version) def test_empty_array_custom_object_compression(self): + dictionary_with_empty_array = {'one': {'two': 3, 'four': []}, 'eight': {'nine': {'ten': 11}}} empty_dictionary_compressed = {'one/two': 3, 'one/four': [], 'eight/nine/ten': 11} self.custom_object.one.four = [] compressed_version = self.object_compressor.compress(self.custom_object) self.assertEqual(empty_dictionary_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(dictionary_with_empty_array, decompressed_version) self.custom_object.one.four = [5, 6, 7] def test_empty_object_custom_object_compression(self): self.custom_object.eight.nine = MyCustomObject() + dictionary_with_empty_container = {'one': {'two': 3, 'four': [5, 6, 7]}, 'eight': {'nine': {}}} empty_object_compressed = {'one/two': 3, 'one/four/0': 5, 'one/four/1': 6, 'one/four/2': 7, 'eight/nine': {}} compressed_version = self.object_compressor.compress(self.custom_object) self.assertEqual(empty_object_compressed, compressed_version) + decompressed_version = self.object_compressor.decompress(compressed_version) + self.assertEqual(dictionary_with_empty_container, decompressed_version) self.custom_object.eight.nine.ten = 11 From f2da944a8c1978336f9bbe27a42a1b7e282ebb7e Mon Sep 17 00:00:00 2001 From: "moshecmeli@gmail.com" Date: Mon, 27 Jul 2020 14:13:18 -0700 Subject: [PATCH 6/7] Added a comment about running tests --- tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests.py b/tests.py index c82d302..80d87e2 100644 --- a/tests.py +++ b/tests.py @@ -5,6 +5,7 @@ class CompressionTests(unittest.TestCase): """ This is a class unit tests for compress/decompress functions + They can be run with ' tests.py' """ def setUp(self): self.object_compressor = ObjectCompressor() From db54dc5e848e8fd71160ed1e56ec3a02c6a53a56 Mon Sep 17 00:00:00 2001 From: moshecarmeli Date: Tue, 4 Aug 2020 08:44:16 -0700 Subject: [PATCH 7/7] Update compressor.py Fixed comment about empty dictionary for top level empty array --- compressor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compressor.py b/compressor.py index ec95536..b31389d 100644 --- a/compressor.py +++ b/compressor.py @@ -11,7 +11,7 @@ def compress(self, object_to_compress, path_so_far: str = "", dictionary: dict = """ This function compresses a multi-dimensional container of any size (tested with nested Python dictionaries and nested custom classes) and returns a compressed version of the original container as a Python dictionary - (note that a top level empty Array becomes an emptyDictionary) + (note that a top level empty Array becomes an empty Array) """ if(dictionary is None): dictionary = {}