From 8f155957d268aa3707bf1708ebf95ae65bc0d709 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Sat, 10 May 2025 19:05:32 +0300 Subject: [PATCH 01/19] added more code --- .gitignore | 4 + client.py | 13 +++- main.py | 6 -- pyproject.toml | 7 ++ server.py | 43 ++++++++++- tests/test_server.py | 27 +++++++ uv.lock | 179 ++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 .gitignore delete mode 100644 main.py create mode 100644 tests/test_server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..730ab72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +.venv +.vscode +.pytest_cache \ No newline at end of file diff --git a/client.py b/client.py index 47cbd27..40567c6 100644 --- a/client.py +++ b/client.py @@ -1,9 +1,18 @@ +from typing import List from server import Server +from pydantic import BaseModel + + +class FilePosition(BaseModel): + name: str + id: int + class Client: def __init__(self) -> None: - pass + self._stash = [] + self._position_map: List[FilePosition] = [] def store_data(self, server: Server, id: int, data: str): pass @@ -12,4 +21,4 @@ def retrieve_data(self, server: Server, id: int): pass def delete_data(self, server: Server, id: int, data: str): - pass \ No newline at end of file + pass diff --git a/main.py b/main.py deleted file mode 100644 index 6a523ea..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from oram!") - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 55fb2ea..0e16ff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,11 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "crypto>=1.4.1", + "pydantic>=2.11.4", + "ruff>=0.11.9", +] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", ] diff --git a/server.py b/server.py index d0ef3e6..7be8b1f 100644 --- a/server.py +++ b/server.py @@ -1,9 +1,44 @@ +class Block: + def __init__(self, data: str = "xxxx") -> None: + self._data = data + + +class Bucket: + def __init__(self, num_blocks: int = 4) -> None: + self._blocks = [Block() for _ in range(num_blocks)] + + +class TreeNode[T]: + def __init__(self, value: T) -> None: + self.value: T = value + self.left: TreeNode[T] = None + self.right: TreeNode[T] = None + + class Server: - def __init__(self) -> None: - pass + def __init__(self, tree_hight: int = 4) -> None: + self._tree_hight = tree_hight + self._root = self.initialize_tree(self._tree_hight) + + def initialize_tree(self, depth: int) -> TreeNode[Bucket]: + if depth < 0: + return None + node = TreeNode(Bucket()) + node.left = self.initialize_tree(depth - 1) + node.right = self.initialize_tree(depth - 1) + return node def remap_block(self, position: int): pass - def read_path(self, path): - pass \ No newline at end of file + def read_path(self, id: int): + node = self._root + path = [node] + for level in range(self._tree_hight - 1, -1, -1): + if (id >> level) & 1: + path.append(node.right) + node = node.right + else: + path.append(node.left) + node = node.left + return path diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..53f3d47 --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,27 @@ +import pytest +from server import Server, TreeNode, Bucket + + +def generate_tree_hights(): + for h in range(6): + for id in range(int(2 ** (h - 1))): + yield h, id + + +@pytest.mark.parametrize("tree_hight, id", generate_tree_hights()) +def test_read_path(tree_hight, id): + if id >= 2 ** (tree_hight - 1): + pytest.skip( + f"Skipping id {id} as it is out of range for tree_hight {tree_hight}" + ) + + server = Server(tree_hight=tree_hight) + path = server.read_path(id) + + # Validate the path contains the correct number of nodes + assert len(path) == tree_hight + 1 + + # Validate each node in the path is a TreeNode containing a Bucket + for node in path: + assert isinstance(node, TreeNode) + assert isinstance(node.value, Bucket) diff --git a/uv.lock b/uv.lock index de3d4dd..d31d98c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 2 requires-python = ">=3.12" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -46,6 +55,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "crypto" version = "1.4.1" @@ -68,6 +86,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + [[package]] name = "naked" version = "0.1.32" @@ -87,10 +114,114 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "crypto" }, + { name = "pydantic" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, ] [package.metadata] -requires-dist = [{ name = "crypto", specifier = ">=1.4.1" }] +requires-dist = [ + { name = "crypto", specifier = ">=1.4.1" }, + { name = "pydantic", specifier = ">=2.11.4" }, + { name = "ruff", specifier = ">=0.11.9" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] [[package]] name = "pyyaml" @@ -133,6 +264,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] +[[package]] +name = "ruff" +version = "0.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134, upload-time = "2025-05-09T16:19:41.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453, upload-time = "2025-05-09T16:18:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566, upload-time = "2025-05-09T16:19:01.432Z" }, + { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020, upload-time = "2025-05-09T16:19:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935, upload-time = "2025-05-09T16:19:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971, upload-time = "2025-05-09T16:19:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631, upload-time = "2025-05-09T16:19:12.307Z" }, + { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236, upload-time = "2025-05-09T16:19:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436, upload-time = "2025-05-09T16:19:17.063Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759, upload-time = "2025-05-09T16:19:19.693Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985, upload-time = "2025-05-09T16:19:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775, upload-time = "2025-05-09T16:19:24.401Z" }, + { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957, upload-time = "2025-05-09T16:19:27.08Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307, upload-time = "2025-05-09T16:19:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026, upload-time = "2025-05-09T16:19:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627, upload-time = "2025-05-09T16:19:33.657Z" }, + { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340, upload-time = "2025-05-09T16:19:35.815Z" }, + { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" }, +] + [[package]] name = "shellescape" version = "3.8.1" @@ -142,6 +298,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/f4/0081137fceff5779cd4205c1e96657e41cc2d2d56c940dc8eeb6111780f7/shellescape-3.8.1-py2.py3-none-any.whl", hash = "sha256:f17127e390fa3f9aaa80c69c16ea73615fd9b5318fd8309c1dca6168ae7d85bf", size = 3081, upload-time = "2020-01-25T21:28:21.772Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, +] + [[package]] name = "urllib3" version = "2.4.0" From 7c266092eda3c05c60be0cc1ce1bab79498c4c4d Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 00:15:19 +0300 Subject: [PATCH 02/19] added client implementation of retrieving data. should export mutual code for storing data to function and implement storing data. added get_path and write_path for server. --- client.py | 62 +++++++++++++++---- server.py | 144 +++++++++++++++++++++++++++++++++++++------ tests/test_server.py | 16 ++--- 3 files changed, 183 insertions(+), 39 deletions(-) diff --git a/client.py b/client.py index 40567c6..59f81fe 100644 --- a/client.py +++ b/client.py @@ -1,24 +1,62 @@ +import math +import random from typing import List -from server import Server - -from pydantic import BaseModel - - -class FilePosition(BaseModel): - name: str - id: int +from server import Block, Bucket, Server class Client: - def __init__(self) -> None: - self._stash = [] - self._position_map: List[FilePosition] = [] + def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: + self._num_blocks = num_blocks + self._num_blocks_per_bucket = blocks_per_bucket + self._tree_height = math.log2(num_blocks // blocks_per_bucket + 1) - 1 + self._stash: List[Block] = [] + self._position_map = {} + + def remap_block(self, block_id: int): + new_position = random.randint(0, 2**self._tree_height - 1) + self._position_map[block_id] = new_position def store_data(self, server: Server, id: int, data: str): pass def retrieve_data(self, server: Server, id: int): - pass + leaf_index = self._position_map.get(id) + if leaf_index is None: + return None + self.remap_block(id) + path = server.get_path(leaf_index) + + # add the blocks in the path to the stash + for bucket in path: + for block in bucket._blocks: + if block._id != -1: # not a dummy block + self._stash.append(block) + + # write to path all the blocks in the stash that are mapped to the same leaf + # and remove the from the stash + new_path = [Bucket() for _ in path] + blocks_to_remove = [] + bucket_index = 0 + block_index = 0 + for block in self._stash: + if self._position_map.get(block._id) == leaf_index: + new_path[bucket_index]._blocks[block_index] = block + blocks_to_remove.append(block) + block_index += 1 + if block_index == self._num_blocks_per_bucket: # full bucket + bucket_index += 1 + block_index = 0 + + # remove the blocks from the stash + for block in blocks_to_remove: + self._stash.remove(block) + + server.write_path(new_path) + + # return the desired block + for block in self._stash: + if block._id == id: + return block._data def delete_data(self, server: Server, id: int, data: str): pass diff --git a/server.py b/server.py index 7be8b1f..9b014e5 100644 --- a/server.py +++ b/server.py @@ -1,44 +1,150 @@ +# Z Number of blocks in a bucket +# L Number of levels in the tree +# N Number of blocks in the tree +# N / Z Total number of buckets +# L = log(N / Z + 1) - 1 +# N = Z * (2 ** (L + 1) - 1) +# 2 ** L Number of leaves = (N / Z) / 2 + + +import math +from typing import List +from collections import deque + + class Block: - def __init__(self, data: str = "xxxx") -> None: + def __init__(self, id: int = -1, data: str = "xxxx") -> None: + self._id = id self._data = data + def __str__(self) -> str: + """ + Returns a string representation of the block, showing its ID and data. + """ + return f"({self._id},{self._data})" + class Bucket: def __init__(self, num_blocks: int = 4) -> None: self._blocks = [Block() for _ in range(num_blocks)] + def __str__(self) -> str: + """ + Returns a string representation of the bucket, showing the IDs of the blocks. + """ + return f"[{', '.join(str(block) for block in self._blocks)}]" + class TreeNode[T]: def __init__(self, value: T) -> None: - self.value: T = value - self.left: TreeNode[T] = None - self.right: TreeNode[T] = None + self._value: T = value + self._left: TreeNode[T] = None + self._right: TreeNode[T] = None class Server: - def __init__(self, tree_hight: int = 4) -> None: - self._tree_hight = tree_hight - self._root = self.initialize_tree(self._tree_hight) + def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: + self._num_blocks = num_blocks + self._tree_height = math.log2(num_blocks // blocks_per_bucket + 1) - 1 + self._root = self.initialize_tree(self._tree_height) def initialize_tree(self, depth: int) -> TreeNode[Bucket]: if depth < 0: return None node = TreeNode(Bucket()) - node.left = self.initialize_tree(depth - 1) - node.right = self.initialize_tree(depth - 1) + node._left = self.initialize_tree(depth - 1) + node._right = self.initialize_tree(depth - 1) return node - def remap_block(self, position: int): - pass + def get_path(self, leaf_index: int) -> List[Bucket]: + """ + Retrieve the path from the root of the tree to the specified leaf. - def read_path(self, id: int): + This method run through the tree from the root to the leaf, and collects the + buckets. The path is determined by the binary representation of `leaf_index`, + where each bit indicates whether to move to the left (0) or right (1) child + at each level of the tree. + + Args: + leaf_index (int): The index of the leaf node to retrieve the path for + + Returns: + List[Bucket]: A list of `Bucket` objects representing the values of + the nodes along the path from the root to the specified leaf + """ node = self._root - path = [node] - for level in range(self._tree_hight - 1, -1, -1): - if (id >> level) & 1: - path.append(node.right) - node = node.right + path = [node._value] + for level in range(self._tree_height - 1, -1, -1): + if (leaf_index >> level) & 1: + path.append(node._right._value) + node = node._right else: - path.append(node.left) - node = node.left + path.append(node._left._value) + node = node._left return path + + def write_path(self, path: List[Bucket], leaf_index: int) -> None: + """ + Write the specified path to the tree on the path to the leaf + + This method run through the tree from the root to the leaf, and writes the + provided path of `Bucket` objects to the corresponding nodes in the tree. + + Args: + path (List[Bucket]): The list of `Bucket` objects to write to the tree + leaf_index (int): The index of the leaf to write the path for + """ + node = self._root + self._root._value = path.pop() + for level in range(self._tree_height - 1, -1, -1): + if (leaf_index >> level) & 1: + node._right._value = path.pop() + node = node._right + else: + node._left._value = path.pop() + node = node._left + + def print_tree(self) -> None: + """ + Print the tree in a structured and readable format. + + This method performs a level-order traversal of the tree and prints + each level on a new line, showing the structure of the tree. + """ + if not self._root: + print("Tree is empty.") + return + + queue = deque([(self._root, 0)]) # Queue to hold nodes and their levels + current_level = 0 + level_nodes = [] + + while queue: + node, level = queue.popleft() + + # If we move to a new level, print the collected nodes of the previous level + if level != current_level: + print( + f"Level {current_level}: {' '.join(str(n._value) for n in level_nodes)}" + ) + level_nodes = [] + current_level = level + + level_nodes.append(node) + + # Add child nodes to the queue + if node._left: + queue.append((node._left, level + 1)) + if node._right: + queue.append((node._right, level + 1)) + + # Print the last level + if level_nodes: + print( + f"Level {current_level}: {' '.join(str(n._value) for n in level_nodes)}" + ) + + +if __name__ == "__main__": + server = Server() + server.print_tree() diff --git a/tests/test_server.py b/tests/test_server.py index 53f3d47..d63048d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -2,26 +2,26 @@ from server import Server, TreeNode, Bucket -def generate_tree_hights(): +def generate_tree_heights(): for h in range(6): for id in range(int(2 ** (h - 1))): yield h, id -@pytest.mark.parametrize("tree_hight, id", generate_tree_hights()) -def test_read_path(tree_hight, id): - if id >= 2 ** (tree_hight - 1): +@pytest.mark.parametrize("tree_height, id", generate_tree_heights()) +def test_read_path(tree_height, id): + if id >= 2 ** (tree_height - 1): pytest.skip( - f"Skipping id {id} as it is out of range for tree_hight {tree_hight}" + f"Skipping id {id} as it is out of range for tree_height {tree_height}" ) - server = Server(tree_hight=tree_hight) + server = Server(tree_height=tree_height) path = server.read_path(id) # Validate the path contains the correct number of nodes - assert len(path) == tree_hight + 1 + assert len(path) == tree_height + 1 # Validate each node in the path is a TreeNode containing a Bucket for node in path: assert isinstance(node, TreeNode) - assert isinstance(node.value, Bucket) + assert isinstance(node._value, Bucket) From 35297b864eae299c10542ff97bea6f5c64228f81 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 17:30:36 +0300 Subject: [PATCH 03/19] implemented store_data and exported mutual code to functions --- client.py | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/client.py b/client.py index 59f81fe..8f2727a 100644 --- a/client.py +++ b/client.py @@ -17,7 +17,24 @@ def remap_block(self, block_id: int): self._position_map[block_id] = new_position def store_data(self, server: Server, id: int, data: str): - pass + leaf_index = self._position_map.get(id) + self.remap_block(id) + if not leaf_index: # if new block + leaf_index = self._position_map.get(id) + path = server.get_path(leaf_index) + self._update_stash(path, id) + + # write new data to stash + for block in self._stash: + if block._id == id: + block._data = data + break + else: + # if block not found in stash, add it + self._stash.append(Block(id, data)) + + self._build_new_path(server, leaf_index, len(path)) + server.write_path(path) def retrieve_data(self, server: Server, id: int): leaf_index = self._position_map.get(id) @@ -25,22 +42,34 @@ def retrieve_data(self, server: Server, id: int): return None self.remap_block(id) path = server.get_path(leaf_index) + return_block = self._update_stash(path, id) + path = self._build_new_path(server, leaf_index, len(path)) + server.write_path(path) + return return_block + def delete_data(self, server: Server, id: int, data: str): + pass + + def _update_stash(self, path: List[Bucket], id: int) -> Block: # add the blocks in the path to the stash for bucket in path: for block in bucket._blocks: if block._id != -1: # not a dummy block self._stash.append(block) + if block._id == id: # desired block + return_block = block + return return_block + def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: # write to path all the blocks in the stash that are mapped to the same leaf - # and remove the from the stash - new_path = [Bucket() for _ in path] + # and remove them from the stash + path = [Bucket() for _ in range(path_length)] blocks_to_remove = [] bucket_index = 0 block_index = 0 for block in self._stash: if self._position_map.get(block._id) == leaf_index: - new_path[bucket_index]._blocks[block_index] = block + path[bucket_index]._blocks[block_index] = block blocks_to_remove.append(block) block_index += 1 if block_index == self._num_blocks_per_bucket: # full bucket @@ -51,12 +80,4 @@ def retrieve_data(self, server: Server, id: int): for block in blocks_to_remove: self._stash.remove(block) - server.write_path(new_path) - - # return the desired block - for block in self._stash: - if block._id == id: - return block._data - - def delete_data(self, server: Server, id: int, data: str): - pass + return path From 3b7dca9f463117dd74de076990a7ddebda3ac26b Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 20:23:18 +0300 Subject: [PATCH 04/19] finished first implementation - no encryption and simple retrieval from stash to path --- client.py | 109 ++++++++++++++++++++++++++++++++++++------------------ server.py | 30 +++++++++++---- 2 files changed, 97 insertions(+), 42 deletions(-) diff --git a/client.py b/client.py index 8f2727a..2b8ec88 100644 --- a/client.py +++ b/client.py @@ -1,83 +1,122 @@ import math import random +import logging from typing import List from server import Block, Bucket, Server +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) + class Client: def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: + self._logger = logging.getLogger(__name__) self._num_blocks = num_blocks self._num_blocks_per_bucket = blocks_per_bucket - self._tree_height = math.log2(num_blocks // blocks_per_bucket + 1) - 1 - self._stash: List[Block] = [] + self._tree_height = int(math.log2(num_blocks // blocks_per_bucket + 1)) - 1 + self._stash: dict[int, Block] = {} # Changed from List to dict self._position_map = {} def remap_block(self, block_id: int): - new_position = random.randint(0, 2**self._tree_height - 1) + new_position = random.randint(0, int(2**self._tree_height) - 1) self._position_map[block_id] = new_position + self._logger.debug(f"Block {block_id} remapped to position {new_position}.") def store_data(self, server: Server, id: int, data: str): + self._logger.info(f"Storing data for block {id}.") leaf_index = self._position_map.get(id) self.remap_block(id) if not leaf_index: # if new block leaf_index = self._position_map.get(id) + self._logger.debug(f"Leaf index for block {id}: {leaf_index}.") path = server.get_path(leaf_index) self._update_stash(path, id) # write new data to stash - for block in self._stash: - if block._id == id: - block._data = data - break - else: - # if block not found in stash, add it - self._stash.append(Block(id, data)) + self._stash[id] = Block(id, data) + self._logger.debug(f"Stash updated with block {id}.") - self._build_new_path(server, leaf_index, len(path)) - server.write_path(path) + path = self._build_new_path(leaf_index, len(path)) + server.write_path(path, leaf_index) + self._logger.info(f"Data for block {id} stored successfully.") - def retrieve_data(self, server: Server, id: int): + def retrieve_data(self, server: Server, id: int) -> str: + self._logger.info(f"Retrieving data for block {id}.") leaf_index = self._position_map.get(id) if leaf_index is None: + self._logger.warning(f"Block {id} not found.") return None self.remap_block(id) path = server.get_path(leaf_index) - return_block = self._update_stash(path, id) - path = self._build_new_path(server, leaf_index, len(path)) - server.write_path(path) - return return_block + self._update_stash(path, id) + path = self._build_new_path(leaf_index, len(path)) + server.write_path(path, leaf_index) + self._logger.info(f"Data for block {id} retrieved successfully.") + return self._stash.get(id)._data - def delete_data(self, server: Server, id: int, data: str): - pass + def delete_data(self, server: Server, id: int): + self._logger.info(f"Deleting data for block {id}.") + leaf_index = self._position_map.get(id) + if leaf_index is None: + self._logger.warning(f"Block {id} not found.") + return None + path = server.get_path(leaf_index) + self._update_stash(path, id) + del self._stash[id] + self._logger.debug(f"Block {id} removed from stash.") + path = self._build_new_path(leaf_index, len(path)) + server.write_path(path, leaf_index) + self._logger.info(f"Data for block {id} deleted successfully.") - def _update_stash(self, path: List[Bucket], id: int) -> Block: - # add the blocks in the path to the stash + def _update_stash(self, path: List[Bucket], id: int) -> None: + self._logger.debug(f"Updating stash with path for block {id}.") for bucket in path: for block in bucket._blocks: if block._id != -1: # not a dummy block - self._stash.append(block) - if block._id == id: # desired block - return_block = block - return return_block + self._stash[id] = block + self._logger.debug(f"Stash updated for block {id}.") def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: - # write to path all the blocks in the stash that are mapped to the same leaf - # and remove them from the stash - path = [Bucket() for _ in range(path_length)] - blocks_to_remove = [] + self._logger.debug(f"Building new path for leaf index {leaf_index}.") + path = [Bucket(self._num_blocks_per_bucket) for _ in range(path_length)] bucket_index = 0 block_index = 0 - for block in self._stash: + for block in list(self._stash.values()): if self._position_map.get(block._id) == leaf_index: path[bucket_index]._blocks[block_index] = block - blocks_to_remove.append(block) + del self._stash[block._id] block_index += 1 if block_index == self._num_blocks_per_bucket: # full bucket bucket_index += 1 block_index = 0 - # remove the blocks from the stash - for block in blocks_to_remove: - self._stash.remove(block) - + self._logger.debug(f"New path built for leaf index {leaf_index}.") return path + + def print_stash(self): + """Prints the current contents of the stash.""" + self._logger.info("Printing stash contents:") + if not self._stash: + print("[]") + else: + for block_id, block in self._stash.items(): + print(f"Block ID: {block_id}, Data: {block._data}") + + +if __name__ == "__main__": + server = Server(num_blocks=14, blocks_per_bucket=2) + server.print_tree() + client = Client(num_blocks=14, blocks_per_bucket=2) + client.store_data(server, 1, "abcd") + server.print_tree() + print(client._stash) + client.store_data(server, 2, "efgh") + server.print_tree() + print(client._stash) + print(client.retrieve_data(server, 2)) + server.print_tree() + print(client._stash) + client.delete_data(server, 2) + server.print_tree() + print(client._stash) diff --git a/server.py b/server.py index 9b014e5..598d438 100644 --- a/server.py +++ b/server.py @@ -7,10 +7,15 @@ # 2 ** L Number of leaves = (N / Z) / 2 +import logging import math from typing import List from collections import deque +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) + class Block: def __init__(self, id: int = -1, data: str = "xxxx") -> None: @@ -23,6 +28,12 @@ def __str__(self) -> str: """ return f"({self._id},{self._data})" + def __repr__(self) -> str: + """ + Returns a string representation of the block, showing its ID and data. + """ + return self.__str__() + class Bucket: def __init__(self, num_blocks: int = 4) -> None: @@ -34,6 +45,12 @@ def __str__(self) -> str: """ return f"[{', '.join(str(block) for block in self._blocks)}]" + def __repr__(self) -> str: + """ + Returns a string representation of the block, showing its ID and data. + """ + return self.__str__() + class TreeNode[T]: def __init__(self, value: T) -> None: @@ -44,14 +61,16 @@ def __init__(self, value: T) -> None: class Server: def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: + self._logger = logging.getLogger(__name__) self._num_blocks = num_blocks - self._tree_height = math.log2(num_blocks // blocks_per_bucket + 1) - 1 + self._blocks_per_bucket = blocks_per_bucket + self._tree_height = int(math.log2(num_blocks // blocks_per_bucket + 1)) - 1 self._root = self.initialize_tree(self._tree_height) def initialize_tree(self, depth: int) -> TreeNode[Bucket]: if depth < 0: return None - node = TreeNode(Bucket()) + node = TreeNode(Bucket(self._blocks_per_bucket)) node._left = self.initialize_tree(depth - 1) node._right = self.initialize_tree(depth - 1) return node @@ -72,6 +91,7 @@ def get_path(self, leaf_index: int) -> List[Bucket]: List[Bucket]: A list of `Bucket` objects representing the values of the nodes along the path from the root to the specified leaf """ + self._logger.debug(f"Retrieving path for leaf index {leaf_index}") node = self._root path = [node._value] for level in range(self._tree_height - 1, -1, -1): @@ -94,6 +114,7 @@ def write_path(self, path: List[Bucket], leaf_index: int) -> None: path (List[Bucket]): The list of `Bucket` objects to write to the tree leaf_index (int): The index of the leaf to write the path for """ + self._logger.debug(f"Writing path for leaf index {leaf_index}") node = self._root self._root._value = path.pop() for level in range(self._tree_height - 1, -1, -1): @@ -143,8 +164,3 @@ def print_tree(self) -> None: print( f"Level {current_level}: {' '.join(str(n._value) for n in level_nodes)}" ) - - -if __name__ == "__main__": - server = Server() - server.print_tree() From ecc61231cac5e6205a55de55cc7a5c161e8e6003 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 20:42:57 +0300 Subject: [PATCH 05/19] added pre-commit and ci --- .github/workflows/ci.yaml | 28 +++++++ .gitignore | 2 +- .pre-commit-config.yaml | 16 ++++ pyproject.toml | 2 +- client.py => src/client.py | 0 server.py => src/server.py | 0 uv.lock | 155 ++++++++++++++++++------------------- 7 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .pre-commit-config.yaml rename client.py => src/client.py (100%) rename server.py => src/server.py (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..aeab3ae --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,28 @@ +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install uv + uv sync --frozen + + - name: Run pre-commit + run: uv run pre-commit run --all-files + + - name: Run tests + run: uv run pytest tests/ diff --git a/.gitignore b/.gitignore index 730ab72..74f7105 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ __pycache__ .venv .vscode -.pytest_cache \ No newline at end of file +.pytest_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4deaa8b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.8 + hooks: + - id: ruff + args: + - --fix + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index 0e16ff0..e1ed346 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,11 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "crypto>=1.4.1", - "pydantic>=2.11.4", "ruff>=0.11.9", ] [dependency-groups] dev = [ + "pre-commit>=4.2.0", "pytest>=8.3.5", ] diff --git a/client.py b/src/client.py similarity index 100% rename from client.py rename to src/client.py diff --git a/server.py b/src/server.py similarity index 100% rename from server.py rename to src/server.py diff --git a/uv.lock b/uv.lock index d31d98c..0ce7d02 100644 --- a/uv.lock +++ b/uv.lock @@ -3,21 +3,21 @@ revision = 2 requires-python = ">=3.12" [[package]] -name = "annotated-types" -version = "0.7.0" +name = "certifi" +version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, ] [[package]] -name = "certifi" -version = "2025.4.26" +name = "cfgv" +version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] @@ -77,6 +77,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/bb/0b812dc02e6357606228edfbf5808f5ca0a675a84273578c3a199e841cd8/crypto-1.4.1-py2.py3-none-any.whl", hash = "sha256:985120aa86f71545388199f96a2a0e00f7ccfe5ecd14c56355eb399e1a63d164", size = 18019, upload-time = "2015-05-14T00:41:50.011Z" }, ] +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "identify" +version = "2.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -108,30 +135,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/7b/e55081f0cdd50bf443c0a6bbddb6bf012527c53125ae94aba6583d49461e/Naked-0.1.32-py2.py3-none-any.whl", hash = "sha256:ea3d7eeada6b89bd8464ba0cfaa631867aaa68a3e2d5d6a6800cbe74f8941e5f", size = 587731, upload-time = "2022-11-06T14:20:18.346Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "oram" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "crypto" }, - { name = "pydantic" }, { name = "ruff" }, ] [package.dev-dependencies] dev = [ + { name = "pre-commit" }, { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "crypto", specifier = ">=1.4.1" }, - { name = "pydantic", specifier = ">=2.11.4" }, { name = "ruff", specifier = ">=0.11.9" }, ] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.3.5" }] +dev = [ + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "pytest", specifier = ">=8.3.5" }, +] [[package]] name = "packaging" @@ -143,69 +181,37 @@ wheels = [ ] [[package]] -name = "pluggy" -version = "1.5.0" +name = "platformdirs" +version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] -name = "pydantic" -version = "2.11.4" +name = "pluggy" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] -name = "pydantic-core" -version = "2.33.2" +name = "pre-commit" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] [[package]] @@ -299,31 +305,24 @@ wheels = [ ] [[package]] -name = "typing-extensions" -version = "4.13.2" +name = "urllib3" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] [[package]] -name = "typing-inspection" -version = "0.4.0" +name = "virtualenv" +version = "20.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, -] - -[[package]] -name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] From 3454bfac988b31fc5b043b8c83be481b685a1168 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 22:19:18 +0300 Subject: [PATCH 06/19] added tests --- src/client.py | 8 ++++---- src/server.py | 42 ++++++++++++++++++++++++++++-------------- tests/test_client.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_server.py | 38 ++++++++++++++------------------------ 4 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 tests/test_client.py diff --git a/src/client.py b/src/client.py index 2b8ec88..7237289 100644 --- a/src/client.py +++ b/src/client.py @@ -2,7 +2,7 @@ import random import logging from typing import List -from server import Block, Bucket, Server +from src.server import Block, Bucket, Server logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -38,7 +38,7 @@ def store_data(self, server: Server, id: int, data: str): self._logger.debug(f"Stash updated with block {id}.") path = self._build_new_path(leaf_index, len(path)) - server.write_path(path, leaf_index) + server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} stored successfully.") def retrieve_data(self, server: Server, id: int) -> str: @@ -51,7 +51,7 @@ def retrieve_data(self, server: Server, id: int) -> str: path = server.get_path(leaf_index) self._update_stash(path, id) path = self._build_new_path(leaf_index, len(path)) - server.write_path(path, leaf_index) + server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} retrieved successfully.") return self._stash.get(id)._data @@ -66,7 +66,7 @@ def delete_data(self, server: Server, id: int): del self._stash[id] self._logger.debug(f"Block {id} removed from stash.") path = self._build_new_path(leaf_index, len(path)) - server.write_path(path, leaf_index) + server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} deleted successfully.") def _update_stash(self, path: List[Bucket], id: int) -> None: diff --git a/src/server.py b/src/server.py index 598d438..479aa85 100644 --- a/src/server.py +++ b/src/server.py @@ -28,11 +28,11 @@ def __str__(self) -> str: """ return f"({self._id},{self._data})" - def __repr__(self) -> str: - """ - Returns a string representation of the block, showing its ID and data. - """ - return self.__str__() + # def __repr__(self) -> str: + # """ + # Returns a string representation of the block, showing its ID and data. + # """ + # return self.__str__() class Bucket: @@ -45,11 +45,11 @@ def __str__(self) -> str: """ return f"[{', '.join(str(block) for block in self._blocks)}]" - def __repr__(self) -> str: - """ - Returns a string representation of the block, showing its ID and data. - """ - return self.__str__() + # def __repr__(self) -> str: + # """ + # Returns a string representation of the block, showing its ID and data. + # """ + # return self.__str__() class TreeNode[T]: @@ -79,7 +79,7 @@ def get_path(self, leaf_index: int) -> List[Bucket]: """ Retrieve the path from the root of the tree to the specified leaf. - This method run through the tree from the root to the leaf, and collects the + This method runs through the tree from the root to the leaf and collects the buckets. The path is determined by the binary representation of `leaf_index`, where each bit indicates whether to move to the left (0) or right (1) child at each level of the tree. @@ -89,8 +89,15 @@ def get_path(self, leaf_index: int) -> List[Bucket]: Returns: List[Bucket]: A list of `Bucket` objects representing the values of - the nodes along the path from the root to the specified leaf + the nodes along the path from the root to the specified leaf + + Raises: + ValueError: If the `leaf_index` is out of bounds for the tree height """ + if leaf_index < 0 or leaf_index >= 2**self._tree_height: + raise ValueError( + f"Leaf index {leaf_index} is out of bounds for tree height {self._tree_height}" + ) self._logger.debug(f"Retrieving path for leaf index {leaf_index}") node = self._root path = [node._value] @@ -103,17 +110,24 @@ def get_path(self, leaf_index: int) -> List[Bucket]: node = node._left return path - def write_path(self, path: List[Bucket], leaf_index: int) -> None: + def set_path(self, path: List[Bucket], leaf_index: int) -> None: """ Write the specified path to the tree on the path to the leaf - This method run through the tree from the root to the leaf, and writes the + This method runs through the tree from the root to the leaf, and writes the provided path of `Bucket` objects to the corresponding nodes in the tree. Args: path (List[Bucket]): The list of `Bucket` objects to write to the tree leaf_index (int): The index of the leaf to write the path for + + Raises: + ValueError: If the `leaf_index` is out of bounds for the tree height """ + if leaf_index < 0 or leaf_index >= 2**self._tree_height: + raise ValueError( + f"Leaf index {leaf_index} is out of bounds for tree height {self._tree_height}" + ) self._logger.debug(f"Writing path for leaf index {leaf_index}") node = self._root self._root._value = path.pop() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..7995054 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,38 @@ +from unittest.mock import MagicMock, patch +from src.client import Client +from src.server import Server, Block, Bucket + + +def test_retrieve_data_existing_block(): + # Arrange + block_id = 1 + block_data = "data" + leaf_index = 0 + new_leaf_index = 1 + blocks_per_bucket = 2 + num_blocks = 14 + bucket = Bucket(blocks_per_bucket) + block = Block(block_id, block_data) + bucket._blocks[0] = block + + server = Server(num_blocks=14, blocks_per_bucket=blocks_per_bucket) # L=2 + server.get_path = MagicMock( + return_value=[Bucket(blocks_per_bucket), Bucket(blocks_per_bucket), bucket] + ) + server.set_path = MagicMock() + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._position_map[block_id] = leaf_index + + with patch("random.randint", return_value=new_leaf_index): + # Act + result = client.retrieve_data(server, block_id) + + # Assert + assert result == block_data + assert client._stash[block_id]._data == block._data + assert client._position_map[block_id] == new_leaf_index + server.get_path.assert_called_once_with(leaf_index) + path, index = server.set_path.call_args[0] + for bucket in path: + assert bucket._blocks[0]._id == -1 + assert index == leaf_index diff --git a/tests/test_server.py b/tests/test_server.py index d63048d..4e0cd8d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,27 +1,17 @@ +from src.server import Server, Bucket import pytest -from server import Server, TreeNode, Bucket -def generate_tree_heights(): - for h in range(6): - for id in range(int(2 ** (h - 1))): - yield h, id - - -@pytest.mark.parametrize("tree_height, id", generate_tree_heights()) -def test_read_path(tree_height, id): - if id >= 2 ** (tree_height - 1): - pytest.skip( - f"Skipping id {id} as it is out of range for tree_height {tree_height}" - ) - - server = Server(tree_height=tree_height) - path = server.read_path(id) - - # Validate the path contains the correct number of nodes - assert len(path) == tree_height + 1 - - # Validate each node in the path is a TreeNode containing a Bucket - for node in path: - assert isinstance(node, TreeNode) - assert isinstance(node._value, Bucket) +@pytest.mark.parametrize("leaf_index", [0, 1, 2, 3]) +@pytest.mark.parametrize("depth", [0, 1, 2, 3, 4]) +@pytest.mark.parametrize("blocks_per_bucket", [3, 4, 5]) +def test_get_path(depth, blocks_per_bucket, leaf_index): + if leaf_index >= 2 ** (depth - 1): + pytest.skip(f"Skipping test for leaf_index {leaf_index} at depth {depth}.") + num_blocks = int(2 ** (depth + 1) - 1) * blocks_per_bucket + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + path = server.get_path(leaf_index) + assert len(path) == depth + 1, f"Path length mismatch for leaf_index {leaf_index}" + assert all(isinstance(bucket, Bucket) for bucket in path), ( + "Path contains non-Bucket elements" + ) From 8c5c2ed6e4a6d6befa6be9694f4da05ec5d9f398 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 22:22:25 +0300 Subject: [PATCH 07/19] . --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aeab3ae..e1f842d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,4 +25,4 @@ jobs: run: uv run pre-commit run --all-files - name: Run tests - run: uv run pytest tests/ + run: uv run pytest From 59d334a67b8fcc4cbb730cbcf4e7d57b54446374 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 15 May 2025 22:23:37 +0300 Subject: [PATCH 08/19] Added dunder init --- src/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/__init__.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 From c64b3745728aed03012120d7ffd9f2625a8f3c95 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Fri, 16 May 2025 02:15:17 +0300 Subject: [PATCH 09/19] starting to move server to use strings and client to use basemodel --- .gitignore | 1 + pyproject.toml | 1 + src/client.py | 28 ++++++++------ src/server.py | 43 ++++++++++----------- tests/test_client.py | 19 ++++++++-- uv.lock | 89 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 144 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 74f7105..532ecf9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__ .venv .vscode .pytest_cache +.ruff_cache diff --git a/pyproject.toml b/pyproject.toml index e1ed346..e7ad8a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "crypto>=1.4.1", + "pydantic>=2.11.4", "ruff>=0.11.9", ] diff --git a/src/client.py b/src/client.py index 7237289..3e88562 100644 --- a/src/client.py +++ b/src/client.py @@ -2,13 +2,20 @@ import random import logging from typing import List -from src.server import Block, Bucket, Server + +from pydantic import BaseModel +from src.server import Bucket, Server logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) +class Block(BaseModel): + id: int = -1 + data: str = "xxxx" + + class Client: def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: self._logger = logging.getLogger(__name__) @@ -34,7 +41,7 @@ def store_data(self, server: Server, id: int, data: str): self._update_stash(path, id) # write new data to stash - self._stash[id] = Block(id, data) + self._stash[id] = Block(id=id, data=data) self._logger.debug(f"Stash updated with block {id}.") path = self._build_new_path(leaf_index, len(path)) @@ -53,7 +60,7 @@ def retrieve_data(self, server: Server, id: int) -> str: path = self._build_new_path(leaf_index, len(path)) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} retrieved successfully.") - return self._stash.get(id)._data + return self._stash.get(id).data def delete_data(self, server: Server, id: int): self._logger.info(f"Deleting data for block {id}.") @@ -72,10 +79,9 @@ def delete_data(self, server: Server, id: int): def _update_stash(self, path: List[Bucket], id: int) -> None: self._logger.debug(f"Updating stash with path for block {id}.") for bucket in path: - for block in bucket._blocks: - if block._id != -1: # not a dummy block - self._stash[id] = block - self._logger.debug(f"Stash updated for block {id}.") + for block in bucket.blocks: + if block.id != -1: # not a dummy block + self._stash[block.id] = block def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: self._logger.debug(f"Building new path for leaf index {leaf_index}.") @@ -83,15 +89,13 @@ def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: bucket_index = 0 block_index = 0 for block in list(self._stash.values()): - if self._position_map.get(block._id) == leaf_index: - path[bucket_index]._blocks[block_index] = block - del self._stash[block._id] + if self._position_map.get(block.id) == leaf_index: + path[bucket_index].blocks[block_index] = block + del self._stash[block.id] block_index += 1 if block_index == self._num_blocks_per_bucket: # full bucket bucket_index += 1 block_index = 0 - - self._logger.debug(f"New path built for leaf index {leaf_index}.") return path def print_stash(self): diff --git a/src/server.py b/src/server.py index 479aa85..2785ded 100644 --- a/src/server.py +++ b/src/server.py @@ -12,38 +12,39 @@ from typing import List from collections import deque +from pydantic import BaseModel + + logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -class Block: - def __init__(self, id: int = -1, data: str = "xxxx") -> None: - self._id = id - self._data = data +# def __str__(self) -> str: +# """ +# Returns a string representation of the block, showing its ID and data. +# """ +# return f"({self._id},{self._data})" - def __str__(self) -> str: - """ - Returns a string representation of the block, showing its ID and data. - """ - return f"({self._id},{self._data})" +# def __repr__(self) -> str: +# """ +# Returns a string representation of the block, showing its ID and data. +# """ +# return self.__str__() - # def __repr__(self) -> str: - # """ - # Returns a string representation of the block, showing its ID and data. - # """ - # return self.__str__() +# server should use strings and not blocks +class Bucket(BaseModel): + blocks: List[str] -class Bucket: def __init__(self, num_blocks: int = 4) -> None: - self._blocks = [Block() for _ in range(num_blocks)] + super().__init__(blocks=["xxxx" for _ in range(num_blocks)]) - def __str__(self) -> str: - """ - Returns a string representation of the bucket, showing the IDs of the blocks. - """ - return f"[{', '.join(str(block) for block in self._blocks)}]" + # def __str__(self) -> str: + # """ + # Returns a string representation of the bucket, showing the IDs of the blocks. + # """ + # return f"[{', '.join(str(block) for block in self._blocks)}]" # def __repr__(self) -> str: # """ diff --git a/tests/test_client.py b/tests/test_client.py index 7995054..2eb9e83 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,8 +12,8 @@ def test_retrieve_data_existing_block(): blocks_per_bucket = 2 num_blocks = 14 bucket = Bucket(blocks_per_bucket) - block = Block(block_id, block_data) - bucket._blocks[0] = block + block = Block(id=block_id, data=block_data) + bucket.blocks[0] = block server = Server(num_blocks=14, blocks_per_bucket=blocks_per_bucket) # L=2 server.get_path = MagicMock( @@ -29,10 +29,21 @@ def test_retrieve_data_existing_block(): # Assert assert result == block_data - assert client._stash[block_id]._data == block._data + assert client._stash[block_id].data == block.data assert client._position_map[block_id] == new_leaf_index server.get_path.assert_called_once_with(leaf_index) path, index = server.set_path.call_args[0] for bucket in path: - assert bucket._blocks[0]._id == -1 + assert bucket.blocks[0].id == -1 assert index == leaf_index + + +def test_new_data_to_same_path_leaves_stash_empty(): + server = Server(num_blocks=6, blocks_per_bucket=2) + client = Client(num_blocks=6, blocks_per_bucket=2) + + with patch("random.randint", return_value=1): + client.store_data(server, 1, "abcd") + client.store_data(server, 2, "efgh") + + assert not client._stash diff --git a/uv.lock b/uv.lock index 0ce7d02..3e5d47c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 2 requires-python = ">=3.12" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -150,6 +159,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "crypto" }, + { name = "pydantic" }, { name = "ruff" }, ] @@ -162,6 +172,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "crypto", specifier = ">=1.4.1" }, + { name = "pydantic", specifier = ">=2.11.4" }, { name = "ruff", specifier = ">=0.11.9" }, ] @@ -214,6 +225,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] +[[package]] +name = "pydantic" +version = "2.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + [[package]] name = "pytest" version = "8.3.5" @@ -304,6 +372,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/f4/0081137fceff5779cd4205c1e96657e41cc2d2d56c940dc8eeb6111780f7/shellescape-3.8.1-py2.py3-none-any.whl", hash = "sha256:f17127e390fa3f9aaa80c69c16ea73615fd9b5318fd8309c1dca6168ae7d85bf", size = 3081, upload-time = "2020-01-25T21:28:21.772Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, +] + [[package]] name = "urllib3" version = "2.4.0" From ecd678760adf5513b6fd8b5633ed80974c87e33d Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Sat, 17 May 2025 22:36:26 +0300 Subject: [PATCH 10/19] server using strings for blocks --- src/client.py | 12 ++++++++++-- src/server.py | 2 +- tests/test_client.py | 7 ++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/client.py b/src/client.py index 3e88562..892a8be 100644 --- a/src/client.py +++ b/src/client.py @@ -38,6 +38,7 @@ def store_data(self, server: Server, id: int, data: str): leaf_index = self._position_map.get(id) self._logger.debug(f"Leaf index for block {id}: {leaf_index}.") path = server.get_path(leaf_index) + self._parse_path(path) self._update_stash(path, id) # write new data to stash @@ -56,19 +57,21 @@ def retrieve_data(self, server: Server, id: int) -> str: return None self.remap_block(id) path = server.get_path(leaf_index) + self._parse_path(path) self._update_stash(path, id) path = self._build_new_path(leaf_index, len(path)) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} retrieved successfully.") return self._stash.get(id).data - def delete_data(self, server: Server, id: int): + def delete_data(self, server: Server, id: int) -> None: self._logger.info(f"Deleting data for block {id}.") leaf_index = self._position_map.get(id) if leaf_index is None: self._logger.warning(f"Block {id} not found.") return None path = server.get_path(leaf_index) + self._parse_path(path) self._update_stash(path, id) del self._stash[id] self._logger.debug(f"Block {id} removed from stash.") @@ -90,7 +93,7 @@ def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: block_index = 0 for block in list(self._stash.values()): if self._position_map.get(block.id) == leaf_index: - path[bucket_index].blocks[block_index] = block + path[bucket_index].blocks[block_index] = block.model_dump_json() del self._stash[block.id] block_index += 1 if block_index == self._num_blocks_per_bucket: # full bucket @@ -98,6 +101,11 @@ def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: block_index = 0 return path + def _parse_path(self, path: List[Bucket]) -> None: + for bucket in path: + for i, block in enumerate(bucket.blocks): + bucket.blocks[i] = Block.model_validate_json(block) + def print_stash(self): """Prints the current contents of the stash.""" self._logger.info("Printing stash contents:") diff --git a/src/server.py b/src/server.py index 2785ded..10ddc1d 100644 --- a/src/server.py +++ b/src/server.py @@ -38,7 +38,7 @@ class Bucket(BaseModel): blocks: List[str] def __init__(self, num_blocks: int = 4) -> None: - super().__init__(blocks=["xxxx" for _ in range(num_blocks)]) + super().__init__(blocks=['{"id":-1,"data":"xxxx"}' for _ in range(num_blocks)]) # def __str__(self) -> str: # """ diff --git a/tests/test_client.py b/tests/test_client.py index 2eb9e83..67785bd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock, patch -from src.client import Client -from src.server import Server, Block, Bucket +from src.client import Block, Client +from src.server import Server, Bucket def test_retrieve_data_existing_block(): @@ -13,7 +13,7 @@ def test_retrieve_data_existing_block(): num_blocks = 14 bucket = Bucket(blocks_per_bucket) block = Block(id=block_id, data=block_data) - bucket.blocks[0] = block + bucket.blocks[0] = block.model_dump_json() server = Server(num_blocks=14, blocks_per_bucket=blocks_per_bucket) # L=2 server.get_path = MagicMock( @@ -33,6 +33,7 @@ def test_retrieve_data_existing_block(): assert client._position_map[block_id] == new_leaf_index server.get_path.assert_called_once_with(leaf_index) path, index = server.set_path.call_args[0] + client._parse_path(path) for bucket in path: assert bucket.blocks[0].id == -1 assert index == leaf_index From 3319b6c64f6ce3953ee3cc309340573d17d189f8 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Sun, 18 May 2025 10:02:37 +0300 Subject: [PATCH 11/19] started to add encryption and authentication --- pyproject.toml | 2 +- src/client.py | 28 ++++++-- src/server.py | 4 +- uv.lock | 169 ++++++++++++++++++++----------------------------- 4 files changed, 92 insertions(+), 111 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7ad8a2..0214502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "crypto>=1.4.1", + "cryptography>=45.0.2", "pydantic>=2.11.4", "ruff>=0.11.9", ] diff --git a/src/client.py b/src/client.py index 892a8be..4fe196f 100644 --- a/src/client.py +++ b/src/client.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from src.server import Bucket, Server +from cryptography.fernet import Fernet logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -24,6 +25,8 @@ def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: self._tree_height = int(math.log2(num_blocks // blocks_per_bucket + 1)) - 1 self._stash: dict[int, Block] = {} # Changed from List to dict self._position_map = {} + self._key = Fernet.generate_key() + self._cipher = Fernet(self._key) def remap_block(self, block_id: int): new_position = random.randint(0, int(2**self._tree_height) - 1) @@ -38,7 +41,7 @@ def store_data(self, server: Server, id: int, data: str): leaf_index = self._position_map.get(id) self._logger.debug(f"Leaf index for block {id}: {leaf_index}.") path = server.get_path(leaf_index) - self._parse_path(path) + self._decrypt_and_parse_path(path) self._update_stash(path, id) # write new data to stash @@ -57,7 +60,7 @@ def retrieve_data(self, server: Server, id: int) -> str: return None self.remap_block(id) path = server.get_path(leaf_index) - self._parse_path(path) + self._decrypt_and_parse_path(path) self._update_stash(path, id) path = self._build_new_path(leaf_index, len(path)) server.set_path(path, leaf_index) @@ -71,7 +74,7 @@ def delete_data(self, server: Server, id: int) -> None: self._logger.warning(f"Block {id} not found.") return None path = server.get_path(leaf_index) - self._parse_path(path) + self._decrypt_and_parse_path(path) self._update_stash(path, id) del self._stash[id] self._logger.debug(f"Block {id} removed from stash.") @@ -93,7 +96,9 @@ def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: block_index = 0 for block in list(self._stash.values()): if self._position_map.get(block.id) == leaf_index: - path[bucket_index].blocks[block_index] = block.model_dump_json() + path[bucket_index].blocks[block_index] = self._cipher.encrypt( + block.model_dump_json().encode() + ) del self._stash[block.id] block_index += 1 if block_index == self._num_blocks_per_bucket: # full bucket @@ -101,10 +106,19 @@ def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: block_index = 0 return path - def _parse_path(self, path: List[Bucket]) -> None: + def _decrypt_and_parse_path(self, path: List[Bucket]) -> None: for bucket in path: for i, block in enumerate(bucket.blocks): - bucket.blocks[i] = Block.model_validate_json(block) + bucket.blocks[i] = Block.model_validate_json( + self._cipher.decrypt(block).decode() + ) + + def _unparse_and_encrypt_path(self, path: List[Bucket]) -> None: + for bucket in path: + for i, block in enumerate(bucket.blocks): + bucket.blocks[i] = self._cipher.encrypt( + block.model_dump_json().encode() + ) def print_stash(self): """Prints the current contents of the stash.""" @@ -113,7 +127,7 @@ def print_stash(self): print("[]") else: for block_id, block in self._stash.items(): - print(f"Block ID: {block_id}, Data: {block._data}") + print(f"Block ID: {block_id}, Data: {block.data}") if __name__ == "__main__": diff --git a/src/server.py b/src/server.py index 10ddc1d..304854d 100644 --- a/src/server.py +++ b/src/server.py @@ -14,6 +14,8 @@ from pydantic import BaseModel +from src.client import Block + logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -35,7 +37,7 @@ # server should use strings and not blocks class Bucket(BaseModel): - blocks: List[str] + blocks: List[str | Block] def __init__(self, num_blocks: int = 4) -> None: super().__init__(blocks=['{"id":-1,"data":"xxxx"}' for _ in range(num_blocks)]) diff --git a/uv.lock b/uv.lock index 3e5d47c..f56fec4 100644 --- a/uv.lock +++ b/uv.lock @@ -12,12 +12,36 @@ wheels = [ ] [[package]] -name = "certifi" -version = "2025.4.26" +name = "cffi" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] [[package]] @@ -29,41 +53,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -74,16 +63,38 @@ wheels = [ ] [[package]] -name = "crypto" -version = "1.4.1" +name = "cryptography" +version = "45.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "naked" }, - { name = "shellescape" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/0d/34dce1487b3158a0ccd0e5982cba0259c798c24c0de4cc25ec265b37cd98/crypto-1.4.1.tar.gz", hash = "sha256:8f2ee9756a0265c18845ac097ae447c75cfbde158abe1361b7491619f866a9bd", size = 13278, upload-time = "2015-05-14T00:41:44.952Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/47/92a8914716f2405f33f1814b97353e3cfa223cd94a77104075d42de3099e/cryptography-45.0.2.tar.gz", hash = "sha256:d784d57b958ffd07e9e226d17272f9af0c41572557604ca7554214def32c26bf", size = 743865, upload-time = "2025-05-18T02:46:34.986Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/bb/0b812dc02e6357606228edfbf5808f5ca0a675a84273578c3a199e841cd8/crypto-1.4.1-py2.py3-none-any.whl", hash = "sha256:985120aa86f71545388199f96a2a0e00f7ccfe5ecd14c56355eb399e1a63d164", size = 18019, upload-time = "2015-05-14T00:41:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2f/46b9e715157643ad16f039ec3c3c47d174da6f825bf5034b1c5f692ab9e2/cryptography-45.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:61a8b1bbddd9332917485b2453d1de49f142e6334ce1d97b7916d5a85d179c84", size = 7043448, upload-time = "2025-05-18T02:45:12.495Z" }, + { url = "https://files.pythonhosted.org/packages/90/52/49e6c86278e1b5ec226e96b62322538ccc466306517bf9aad8854116a088/cryptography-45.0.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cc31c66411e14dd70e2f384a9204a859dc25b05e1f303df0f5326691061b839", size = 4201098, upload-time = "2025-05-18T02:45:15.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3a/201272539ac5b66b4cb1af89021e423fc0bfacb73498950280c51695fb78/cryptography-45.0.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:463096533acd5097f8751115bc600b0b64620c4aafcac10c6d0041e6e68f88fe", size = 4429839, upload-time = "2025-05-18T02:45:17.614Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/fa1a84832b8f8f3917875cb15324bba98def5a70175a889df7d21a45dc75/cryptography-45.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdafb86eb673c3211accffbffdb3cdffa3aaafacd14819e0898d23696d18e4d3", size = 4205154, upload-time = "2025-05-18T02:45:19.874Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c5/5225d5230d538ab461725711cf5220560a813d1eb68bafcfb00131b8f631/cryptography-45.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05c2385b1f5c89a17df19900cfb1345115a77168f5ed44bdf6fd3de1ce5cc65b", size = 3897145, upload-time = "2025-05-18T02:45:22.209Z" }, + { url = "https://files.pythonhosted.org/packages/fe/24/f19aae32526cc55ae17d473bc4588b1234af2979483d99cbfc57e55ffea6/cryptography-45.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e9e4bdcd70216b08801e267c0b563316b787f957a46e215249921f99288456f9", size = 4462192, upload-time = "2025-05-18T02:45:24.773Z" }, + { url = "https://files.pythonhosted.org/packages/19/18/4a69ac95b0b3f03355970baa6c3f9502bbfc54e7df81fdb179654a00f48e/cryptography-45.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b2de529027579e43b6dc1f805f467b102fb7d13c1e54c334f1403ee2b37d0059", size = 4208093, upload-time = "2025-05-18T02:45:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/2dea55ccc9558b8fa14f67156250b6ee231e31765601524e4757d0b5db6b/cryptography-45.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10d68763892a7b19c22508ab57799c4423c7c8cd61d7eee4c5a6a55a46511949", size = 4461819, upload-time = "2025-05-18T02:45:29.39Z" }, + { url = "https://files.pythonhosted.org/packages/37/f1/1b220fcd5ef4b1f0ff3e59e733b61597505e47f945606cc877adab2c1a17/cryptography-45.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2a90ce2f0f5b695e4785ac07c19a58244092f3c85d57db6d8eb1a2b26d2aad6", size = 4329202, upload-time = "2025-05-18T02:45:31.925Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e0/51d1dc4f96f819a56db70f0b4039b4185055bbb8616135884c3c3acc4c6d/cryptography-45.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:59c0c8f043dd376bbd9d4f636223836aed50431af4c5a467ed9bf61520294627", size = 4570412, upload-time = "2025-05-18T02:45:34.348Z" }, + { url = "https://files.pythonhosted.org/packages/dc/44/88efb40a3600d15277a77cdc69eeeab45a98532078d2a36cffd9325d3b3f/cryptography-45.0.2-cp311-abi3-win32.whl", hash = "sha256:80303ee6a02ef38c4253160446cbeb5c400c07e01d4ddbd4ff722a89b736d95a", size = 2933584, upload-time = "2025-05-18T02:45:36.198Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/bc9f82ba08760442cc8346d1b4e7b769b86d197193c45b42b3595d231e84/cryptography-45.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7429936146063bd1b2cfc54f0e04016b90ee9b1c908a7bed0800049cbace70eb", size = 3408537, upload-time = "2025-05-18T02:45:38.184Z" }, + { url = "https://files.pythonhosted.org/packages/59/bc/1b6acb1dca366f9c0b3880888ecd7fcfb68023930d57df854847c6da1d10/cryptography-45.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:e86c8d54cd19a13e9081898b3c24351683fd39d726ecf8e774aaa9d8d96f5f3a", size = 7025581, upload-time = "2025-05-18T02:45:40.632Z" }, + { url = "https://files.pythonhosted.org/packages/31/a3/a3e4a298d3db4a04085728f5ae6c8cda157e49c5bb784886d463b9fbff70/cryptography-45.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e328357b6bbf79928363dbf13f4635b7aac0306afb7e5ad24d21d0c5761c3253", size = 4189148, upload-time = "2025-05-18T02:45:42.538Z" }, + { url = "https://files.pythonhosted.org/packages/53/90/100dfadd4663b389cb56972541ec1103490a19ebad0132af284114ba0868/cryptography-45.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49af56491473231159c98c2c26f1a8f3799a60e5cf0e872d00745b858ddac9d2", size = 4424113, upload-time = "2025-05-18T02:45:44.316Z" }, + { url = "https://files.pythonhosted.org/packages/0d/40/e2b9177dbed6f3fcbbf1942e1acea2fd15b17007204b79d675540dd053af/cryptography-45.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f169469d04a23282de9d0be349499cb6683b6ff1b68901210faacac9b0c24b7d", size = 4189696, upload-time = "2025-05-18T02:45:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/ec29c79f481e1767c2ff916424ba36f3cf7774de93bbd60428a3c52d1357/cryptography-45.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9cfd1399064b13043082c660ddd97a0358e41c8b0dc7b77c1243e013d305c344", size = 3881498, upload-time = "2025-05-18T02:45:48.884Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4a/72937090e5637a232b2f73801c9361cd08404a2d4e620ca4ec58c7ea4b70/cryptography-45.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f8084b7ca3ce1b8d38bdfe33c48116edf9a08b4d056ef4a96dceaa36d8d965", size = 4451678, upload-time = "2025-05-18T02:45:50.706Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fa/1377fced81fd67a4a27514248261bb0d45c3c1e02169411fe231583088c8/cryptography-45.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2cb03a944a1a412724d15a7c051d50e63a868031f26b6a312f2016965b661942", size = 4192296, upload-time = "2025-05-18T02:45:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/b6fe837c83a08b9df81e63299d75fc5b3c6d82cf24b3e1e0e331050e9e5c/cryptography-45.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a9727a21957d3327cf6b7eb5ffc9e4b663909a25fea158e3fcbc49d4cdd7881b", size = 4451749, upload-time = "2025-05-18T02:45:55.025Z" }, + { url = "https://files.pythonhosted.org/packages/af/d8/5a655675cc635c7190bfc8cffb84bcdc44fc62ce945ad1d844adaa884252/cryptography-45.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddb8d01aa900b741d6b7cc585a97aff787175f160ab975e21f880e89d810781a", size = 4317601, upload-time = "2025-05-18T02:45:56.911Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d4/75d2375a20d80aa262a8adee77bf56950e9292929e394b9fae2481803f11/cryptography-45.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c0c000c1a09f069632d8a9eb3b610ac029fcc682f1d69b758e625d6ee713f4ed", size = 4560535, upload-time = "2025-05-18T02:45:59.33Z" }, + { url = "https://files.pythonhosted.org/packages/aa/18/c3a94474987ebcfb88692036b2ec44880d243fefa73794bdcbf748679a6e/cryptography-45.0.2-cp37-abi3-win32.whl", hash = "sha256:08281de408e7eb71ba3cd5098709a356bfdf65eebd7ee7633c3610f0aa80d79b", size = 2922045, upload-time = "2025-05-18T02:46:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/63/63/fb28b30c144182fd44ce93d13ab859791adbf923e43bdfb610024bfecda1/cryptography-45.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:48caa55c528617fa6db1a9c3bf2e37ccb31b73e098ac2b71408d1f2db551dde4", size = 3393321, upload-time = "2025-05-18T02:46:03.441Z" }, ] [[package]] @@ -113,15 +124,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, ] -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - [[package]] name = "iniconfig" version = "2.1.0" @@ -131,19 +133,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] -[[package]] -name = "naked" -version = "0.1.32" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/a5/7a4f84bb6a7b0d6ba84fda48119e29afd339a1bc4f9589dab1fe064d6eb6/Naked-0.1.32.tar.gz", hash = "sha256:f81015107e3aefdc801d7144fbae214bdf3cb179c0020a1dc6d5acb3659d5d5c", size = 552681, upload-time = "2022-11-06T14:20:20.577Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/7b/e55081f0cdd50bf443c0a6bbddb6bf012527c53125ae94aba6583d49461e/Naked-0.1.32-py2.py3-none-any.whl", hash = "sha256:ea3d7eeada6b89bd8464ba0cfaa631867aaa68a3e2d5d6a6800cbe74f8941e5f", size = 587731, upload-time = "2022-11-06T14:20:18.346Z" }, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -158,7 +147,7 @@ name = "oram" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "crypto" }, + { name = "cryptography" }, { name = "pydantic" }, { name = "ruff" }, ] @@ -171,7 +160,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "crypto", specifier = ">=1.4.1" }, + { name = "cryptography", specifier = ">=45.0.2" }, { name = "pydantic", specifier = ">=2.11.4" }, { name = "ruff", specifier = ">=0.11.9" }, ] @@ -225,6 +214,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + [[package]] name = "pydantic" version = "2.11.4" @@ -323,21 +321,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, -] - [[package]] name = "ruff" version = "0.11.9" @@ -363,15 +346,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" }, ] -[[package]] -name = "shellescape" -version = "3.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/40/13b9e84bf04774365830cbed1bd95a989d5324a99d207bcb1619a6c517f2/shellescape-3.8.1.tar.gz", hash = "sha256:40b310b30479be771bf3ab28bd8d40753778488bd46ea0969ba0b35038c3ec26", size = 5246, upload-time = "2020-01-25T21:28:23.228Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/f4/0081137fceff5779cd4205c1e96657e41cc2d2d56c940dc8eeb6111780f7/shellescape-3.8.1-py2.py3-none-any.whl", hash = "sha256:f17127e390fa3f9aaa80c69c16ea73615fd9b5318fd8309c1dca6168ae7d85bf", size = 3081, upload-time = "2020-01-25T21:28:21.772Z" }, -] - [[package]] name = "typing-extensions" version = "4.13.2" @@ -393,15 +367,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, ] -[[package]] -name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, -] - [[package]] name = "virtualenv" version = "20.31.2" From 6a5f2a7f0971daa3934943e8f36f55a725c5034b Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Sun, 18 May 2025 23:24:39 +0300 Subject: [PATCH 12/19] added E2E encryption and authentication, added tests --- src/client.py | 89 ++++++++++++++++++++--------- src/server.py | 116 ++++++++----------------------------- tests/test_client.py | 133 +++++++++++++++++++++++++++++++++++-------- tests/test_server.py | 17 ------ 4 files changed, 194 insertions(+), 161 deletions(-) delete mode 100644 tests/test_server.py diff --git a/src/client.py b/src/client.py index 4fe196f..2291f12 100644 --- a/src/client.py +++ b/src/client.py @@ -1,11 +1,12 @@ +import logging import math import random -import logging -from typing import List +from typing import List, Tuple -from pydantic import BaseModel -from src.server import Bucket, Server from cryptography.fernet import Fernet +from pydantic import BaseModel + +from src.server import Server logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -17,6 +18,16 @@ class Block(BaseModel): data: str = "xxxx" +class Bucket(BaseModel): + blocks: List[Block] + + def __init__(self, num_blocks: int = 4, blocks: List[Block] = None, **data) -> None: + if blocks: + super().__init__(blocks=blocks, **data) + else: + super().__init__(blocks=[Block() for _ in range(num_blocks)], **data) + + class Client: def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: self._logger = logging.getLogger(__name__) @@ -41,14 +52,15 @@ def store_data(self, server: Server, id: int, data: str): leaf_index = self._position_map.get(id) self._logger.debug(f"Leaf index for block {id}: {leaf_index}.") path = server.get_path(leaf_index) - self._decrypt_and_parse_path(path) + path = self._decrypt_and_parse_path(path) self._update_stash(path, id) # write new data to stash self._stash[id] = Block(id=id, data=data) self._logger.debug(f"Stash updated with block {id}.") - path = self._build_new_path(leaf_index, len(path)) + path, _ = self._build_new_path(leaf_index, len(path), id) + path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} stored successfully.") @@ -60,12 +72,13 @@ def retrieve_data(self, server: Server, id: int) -> str: return None self.remap_block(id) path = server.get_path(leaf_index) - self._decrypt_and_parse_path(path) + path = self._decrypt_and_parse_path(path) self._update_stash(path, id) - path = self._build_new_path(leaf_index, len(path)) + path, block = self._build_new_path(leaf_index, len(path), id) + path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} retrieved successfully.") - return self._stash.get(id).data + return block.data def delete_data(self, server: Server, id: int) -> None: self._logger.info(f"Deleting data for block {id}.") @@ -74,11 +87,13 @@ def delete_data(self, server: Server, id: int) -> None: self._logger.warning(f"Block {id} not found.") return None path = server.get_path(leaf_index) - self._decrypt_and_parse_path(path) + path = self._decrypt_and_parse_path(path) self._update_stash(path, id) del self._stash[id] + del self._position_map[id] self._logger.debug(f"Block {id} removed from stash.") - path = self._build_new_path(leaf_index, len(path)) + path, _ = self._build_new_path(leaf_index, len(path), id) + path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} deleted successfully.") @@ -89,36 +104,53 @@ def _update_stash(self, path: List[Bucket], id: int) -> None: if block.id != -1: # not a dummy block self._stash[block.id] = block - def _build_new_path(self, leaf_index: int, path_length: int) -> List[Bucket]: + def _build_new_path( + self, leaf_index: int, path_length: int, id: int + ) -> Tuple[List[Bucket], Block]: self._logger.debug(f"Building new path for leaf index {leaf_index}.") path = [Bucket(self._num_blocks_per_bucket) for _ in range(path_length)] bucket_index = 0 block_index = 0 + block_to_return = None for block in list(self._stash.values()): + if block.id == id: + block_to_return = block if self._position_map.get(block.id) == leaf_index: - path[bucket_index].blocks[block_index] = self._cipher.encrypt( - block.model_dump_json().encode() - ) + path[bucket_index].blocks[block_index] = block del self._stash[block.id] block_index += 1 if block_index == self._num_blocks_per_bucket: # full bucket bucket_index += 1 block_index = 0 - return path + return path, block_to_return - def _decrypt_and_parse_path(self, path: List[Bucket]) -> None: + def _decrypt_and_parse_path(self, path: List[List[bytes]]) -> List[Bucket]: + new_path = [] for bucket in path: - for i, block in enumerate(bucket.blocks): - bucket.blocks[i] = Block.model_validate_json( - self._cipher.decrypt(block).decode() - ) - - def _unparse_and_encrypt_path(self, path: List[Bucket]) -> None: + blocks = [ + Block.model_validate_json(self._cipher.decrypt(data).decode()) + for data in bucket + ] + new_path.append(Bucket(blocks=blocks)) + return new_path + + def _unparse_and_encrypt_path(self, path: List[Bucket]) -> List[List[bytes]]: + server_path = [] for bucket in path: - for i, block in enumerate(bucket.blocks): - bucket.blocks[i] = self._cipher.encrypt( - block.model_dump_json().encode() - ) + bucket_data = [ + self._cipher.encrypt(block.model_dump_json().encode()) + for block in bucket.blocks + ] + server_path.append(bucket_data) + return server_path + + def _initialize_server_tree(self, server: Server) -> None: + dummy_elements = [ + Bucket(self._num_blocks_per_bucket) + for _ in range(self._num_blocks // self._num_blocks_per_bucket) + ] + dummy_elements = self._unparse_and_encrypt_path(dummy_elements) + server.initialize_tree(dummy_elements) def print_stash(self): """Prints the current contents of the stash.""" @@ -132,8 +164,9 @@ def print_stash(self): if __name__ == "__main__": server = Server(num_blocks=14, blocks_per_bucket=2) - server.print_tree() client = Client(num_blocks=14, blocks_per_bucket=2) + client._initialize_server_tree(server) + server.print_tree() client.store_data(server, 1, "abcd") server.print_tree() print(client._stash) diff --git a/src/server.py b/src/server.py index 304854d..891e851 100644 --- a/src/server.py +++ b/src/server.py @@ -10,49 +10,16 @@ import logging import math from typing import List -from collections import deque from pydantic import BaseModel -from src.client import Block - - logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -# def __str__(self) -> str: -# """ -# Returns a string representation of the block, showing its ID and data. -# """ -# return f"({self._id},{self._data})" - -# def __repr__(self) -> str: -# """ -# Returns a string representation of the block, showing its ID and data. -# """ -# return self.__str__() - - -# server should use strings and not blocks class Bucket(BaseModel): - blocks: List[str | Block] - - def __init__(self, num_blocks: int = 4) -> None: - super().__init__(blocks=['{"id":-1,"data":"xxxx"}' for _ in range(num_blocks)]) - - # def __str__(self) -> str: - # """ - # Returns a string representation of the bucket, showing the IDs of the blocks. - # """ - # return f"[{', '.join(str(block) for block in self._blocks)}]" - - # def __repr__(self) -> str: - # """ - # Returns a string representation of the block, showing its ID and data. - # """ - # return self.__str__() + blocks: List[bytes] class TreeNode[T]: @@ -68,17 +35,20 @@ def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: self._num_blocks = num_blocks self._blocks_per_bucket = blocks_per_bucket self._tree_height = int(math.log2(num_blocks // blocks_per_bucket + 1)) - 1 - self._root = self.initialize_tree(self._tree_height) + self._root: TreeNode[Bucket] = None + + def initialize_tree(self, elements: List[List[bytes]]) -> TreeNode[Bucket]: + def recursive_init(depth: int, i: int) -> TreeNode[Bucket]: + if depth < 0: + return None + node = TreeNode(Bucket(blocks=elements[i])) + node._left = recursive_init(depth - 1, i + 1) + node._right = recursive_init(depth - 1, i + 2) + return node - def initialize_tree(self, depth: int) -> TreeNode[Bucket]: - if depth < 0: - return None - node = TreeNode(Bucket(self._blocks_per_bucket)) - node._left = self.initialize_tree(depth - 1) - node._right = self.initialize_tree(depth - 1) - return node + self._root = recursive_init(self._tree_height, 0) - def get_path(self, leaf_index: int) -> List[Bucket]: + def get_path(self, leaf_index: int) -> List[List[bytes]]: """ Retrieve the path from the root of the tree to the specified leaf. @@ -91,8 +61,8 @@ def get_path(self, leaf_index: int) -> List[Bucket]: leaf_index (int): The index of the leaf node to retrieve the path for Returns: - List[Bucket]: A list of `Bucket` objects representing the values of - the nodes along the path from the root to the specified leaf + List[List[str]]: A list of buckets represented as list of bytes, representing + the values of the nodes along the path from the root to the specified leaf Raises: ValueError: If the `leaf_index` is out of bounds for the tree height @@ -103,17 +73,17 @@ def get_path(self, leaf_index: int) -> List[Bucket]: ) self._logger.debug(f"Retrieving path for leaf index {leaf_index}") node = self._root - path = [node._value] + path = [node._value.blocks] for level in range(self._tree_height - 1, -1, -1): if (leaf_index >> level) & 1: - path.append(node._right._value) + path.append(node._right._value.blocks) node = node._right else: - path.append(node._left._value) + path.append(node._left._value.blocks) node = node._left return path - def set_path(self, path: List[Bucket], leaf_index: int) -> None: + def set_path(self, path: List[List[bytes]], leaf_index: int) -> None: """ Write the specified path to the tree on the path to the leaf @@ -121,7 +91,7 @@ def set_path(self, path: List[Bucket], leaf_index: int) -> None: provided path of `Bucket` objects to the corresponding nodes in the tree. Args: - path (List[Bucket]): The list of `Bucket` objects to write to the tree + path (List[List[bytes]]): The list of buckets to write to the tree leaf_index (int): The index of the leaf to write the path for Raises: @@ -133,51 +103,11 @@ def set_path(self, path: List[Bucket], leaf_index: int) -> None: ) self._logger.debug(f"Writing path for leaf index {leaf_index}") node = self._root - self._root._value = path.pop() + self._root._value.blocks = path.pop() for level in range(self._tree_height - 1, -1, -1): if (leaf_index >> level) & 1: - node._right._value = path.pop() + node._right._value.blocks = path.pop() node = node._right else: - node._left._value = path.pop() + node._left._value.blocks = path.pop() node = node._left - - def print_tree(self) -> None: - """ - Print the tree in a structured and readable format. - - This method performs a level-order traversal of the tree and prints - each level on a new line, showing the structure of the tree. - """ - if not self._root: - print("Tree is empty.") - return - - queue = deque([(self._root, 0)]) # Queue to hold nodes and their levels - current_level = 0 - level_nodes = [] - - while queue: - node, level = queue.popleft() - - # If we move to a new level, print the collected nodes of the previous level - if level != current_level: - print( - f"Level {current_level}: {' '.join(str(n._value) for n in level_nodes)}" - ) - level_nodes = [] - current_level = level - - level_nodes.append(node) - - # Add child nodes to the queue - if node._left: - queue.append((node._left, level + 1)) - if node._right: - queue.append((node._right, level + 1)) - - # Print the last level - if level_nodes: - print( - f"Level {current_level}: {' '.join(str(n._value) for n in level_nodes)}" - ) diff --git a/tests/test_client.py b/tests/test_client.py index 67785bd..1e089bb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,9 +1,10 @@ -from unittest.mock import MagicMock, patch -from src.client import Block, Client -from src.server import Server, Bucket +from unittest.mock import patch +from src.client import Block, Bucket, Client +from src.server import Server -def test_retrieve_data_existing_block(): + +def test_retrieve_after_store_new_leaf_id(): # Arrange block_id = 1 block_data = "data" @@ -11,40 +12,126 @@ def test_retrieve_data_existing_block(): new_leaf_index = 1 blocks_per_bucket = 2 num_blocks = 14 - bucket = Bucket(blocks_per_bucket) - block = Block(id=block_id, data=block_data) - bucket.blocks[0] = block.model_dump_json() - - server = Server(num_blocks=14, blocks_per_bucket=blocks_per_bucket) # L=2 - server.get_path = MagicMock( - return_value=[Bucket(blocks_per_bucket), Bucket(blocks_per_bucket), bucket] - ) - server.set_path = MagicMock() + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) - client._position_map[block_id] = leaf_index + client._initialize_server_tree(server) + # Act + with patch("random.randint", return_value=leaf_index): + client.store_data(server, block_id, block_data) with patch("random.randint", return_value=new_leaf_index): - # Act result = client.retrieve_data(server, block_id) # Assert assert result == block_data - assert client._stash[block_id].data == block.data - assert client._position_map[block_id] == new_leaf_index - server.get_path.assert_called_once_with(leaf_index) - path, index = server.set_path.call_args[0] - client._parse_path(path) - for bucket in path: - assert bucket.blocks[0].id == -1 - assert index == leaf_index + assert block_id in client._stash # Stash should be empty after retrieval + assert block_id in client._position_map + + +def test_retrieve_after_store_same_leaf_id(): + # Arrange + block_id = 1 + block_data = "data" + leaf_index = 0 + blocks_per_bucket = 2 + num_blocks = 14 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._initialize_server_tree(server) + + # Act + with patch("random.randint", return_value=leaf_index): + client.store_data(server, block_id, block_data) + result = client.retrieve_data(server, block_id) + + # Assert + assert result == block_data + assert not client._stash # Stash should be empty after retrieval + assert block_id in client._position_map + + +def test_encryption(): + # Arrange + client = Client(num_blocks=6, blocks_per_bucket=2) + path = [ + Bucket(blocks=[Block(id=1, data="abcd"), Block()]), + Bucket(2), + ] + + # Act + encrypted_path = client._unparse_and_encrypt_path(path) + decrypted_path = client._decrypt_and_parse_path(encrypted_path) + + # Assert + assert decrypted_path[0].blocks[0].id == 1 + assert decrypted_path[0].blocks[0].data == "abcd" def test_new_data_to_same_path_leaves_stash_empty(): + # Arrange server = Server(num_blocks=6, blocks_per_bucket=2) client = Client(num_blocks=6, blocks_per_bucket=2) with patch("random.randint", return_value=1): + # Act client.store_data(server, 1, "abcd") client.store_data(server, 2, "efgh") + # Assert assert not client._stash + + +def test_delete(): + # Arrange + block_id = 1 + block_data = "data" + blocks_per_bucket = 2 + num_blocks = 14 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._initialize_server_tree(server) + + # Act + client.store_data(server, block_id, block_data) + client.delete_data(server, block_id) + + # Assert + assert not client._stash # Stash should be empty after retrieval + assert not client._position_map + + +def test_retrieve_not_found(): + # Arrange + block_id = 1 + blocks_per_bucket = 2 + num_blocks = 14 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._initialize_server_tree(server) + + # Act + result = client.retrieve_data(server, block_id) + + # Assert + assert result is None # Should return None if block is not found + + +def test_flow(): + # Arrange + block_id = 1 + block_data = "data" + blocks_per_bucket = 2 + num_blocks = 14 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._initialize_server_tree(server) + + # Act & Assert + client.store_data(server, block_id, block_data) + assert client.retrieve_data(server, block_id) == block_data + + client.store_data(server, 2, block_data) + client.delete_data(server, block_id) + assert not client.retrieve_data(server, block_id) + assert client.retrieve_data(server, 2) == block_data + assert client.retrieve_data(server, 2) == block_data diff --git a/tests/test_server.py b/tests/test_server.py deleted file mode 100644 index 4e0cd8d..0000000 --- a/tests/test_server.py +++ /dev/null @@ -1,17 +0,0 @@ -from src.server import Server, Bucket -import pytest - - -@pytest.mark.parametrize("leaf_index", [0, 1, 2, 3]) -@pytest.mark.parametrize("depth", [0, 1, 2, 3, 4]) -@pytest.mark.parametrize("blocks_per_bucket", [3, 4, 5]) -def test_get_path(depth, blocks_per_bucket, leaf_index): - if leaf_index >= 2 ** (depth - 1): - pytest.skip(f"Skipping test for leaf_index {leaf_index} at depth {depth}.") - num_blocks = int(2 ** (depth + 1) - 1) * blocks_per_bucket - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) - path = server.get_path(leaf_index) - assert len(path) == depth + 1, f"Path length mismatch for leaf_index {leaf_index}" - assert all(isinstance(bucket, Bucket) for bucket in path), ( - "Path contains non-Bucket elements" - ) From f2c26cdfb87284bd671bc0d0122a0a77afe0272b Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Mon, 19 May 2025 21:56:09 +0300 Subject: [PATCH 13/19] did smart retrieval from stash to new path, added tests --- src/client.py | 55 ++++++++++++++++--------- tests/test_client.py | 98 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 113 insertions(+), 40 deletions(-) diff --git a/src/client.py b/src/client.py index 2291f12..b207844 100644 --- a/src/client.py +++ b/src/client.py @@ -1,7 +1,7 @@ import logging import math import random -from typing import List, Tuple +from typing import List from cryptography.fernet import Fernet from pydantic import BaseModel @@ -59,7 +59,7 @@ def store_data(self, server: Server, id: int, data: str): self._stash[id] = Block(id=id, data=data) self._logger.debug(f"Stash updated with block {id}.") - path, _ = self._build_new_path(leaf_index, len(path), id) + path = self._build_new_path(leaf_index, len(path), id) path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} stored successfully.") @@ -74,7 +74,8 @@ def retrieve_data(self, server: Server, id: int) -> str: path = server.get_path(leaf_index) path = self._decrypt_and_parse_path(path) self._update_stash(path, id) - path, block = self._build_new_path(leaf_index, len(path), id) + block = self._stash.get(id) + path = self._build_new_path(leaf_index, len(path), id) path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} retrieved successfully.") @@ -92,7 +93,7 @@ def delete_data(self, server: Server, id: int) -> None: del self._stash[id] del self._position_map[id] self._logger.debug(f"Block {id} removed from stash.") - path, _ = self._build_new_path(leaf_index, len(path), id) + path = self._build_new_path(leaf_index, len(path), id) path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} deleted successfully.") @@ -106,23 +107,39 @@ def _update_stash(self, path: List[Bucket], id: int) -> None: def _build_new_path( self, leaf_index: int, path_length: int, id: int - ) -> Tuple[List[Bucket], Block]: + ) -> List[Bucket]: self._logger.debug(f"Building new path for leaf index {leaf_index}.") path = [Bucket(self._num_blocks_per_bucket) for _ in range(path_length)] - bucket_index = 0 - block_index = 0 - block_to_return = None - for block in list(self._stash.values()): - if block.id == id: - block_to_return = block - if self._position_map.get(block.id) == leaf_index: - path[bucket_index].blocks[block_index] = block - del self._stash[block.id] - block_index += 1 - if block_index == self._num_blocks_per_bucket: # full bucket - bucket_index += 1 - block_index = 0 - return path, block_to_return + + # Iterate over the tree levels from leaf to root + for depth in range(self._tree_height, -1, -1): + reachable_leaves = self._calculate_reachable_leaves(leaf_index, depth) + i = 0 + j = 0 + blocks_ids_in_stash = list(self._stash.keys()) + while i < self._num_blocks_per_bucket and j < len(blocks_ids_in_stash): + block_id = blocks_ids_in_stash[j] + if self._position_map.get(block_id) in reachable_leaves: + # If the block in the stash is reachable, add it to the path + path[self._tree_height - depth].blocks[i] = self._stash[block_id] + del self._stash[block_id] + i += 1 + j += 1 + + return path + + def _calculate_reachable_leaves(self, leaf_index: int, depth: int) -> List[int]: + binary = format(leaf_index, f"0{self._tree_height}b") + # Get first depth bits (path so far) + path_bits = binary[:depth] + # Compute base index: decimal of path_bits * 2^(L-depth) + base = ( + int(path_bits, 2) * (1 << (self._tree_height - depth)) if path_bits else 0 + ) + # Number of reachable leaves: 2^(L-depth) + num_leaves = 1 << (self._tree_height - depth) + # List of reachable leaves + return list(range(base, base + num_leaves)) def _decrypt_and_parse_path(self, path: List[List[bytes]]) -> List[Bucket]: new_path = [] diff --git a/tests/test_client.py b/tests/test_client.py index 1e089bb..08faccb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,15 +4,37 @@ from src.server import Server +def test_store_data(): + # Arrange + block_id = 1 + block_data = "data" + leaf_index = 0 + blocks_per_bucket = 2 + num_blocks = 14 # L=2 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._initialize_server_tree(server) + + # Act + with patch("random.randint", return_value=leaf_index): + client.store_data(server, block_id, block_data) + + # Assert + assert not client._stash # block should be in root + path = client._decrypt_and_parse_path(server.get_path(leaf_index)) + assert path[-1].blocks[0].data == block_data + assert client._position_map.get(block_id) == leaf_index + + def test_retrieve_after_store_new_leaf_id(): # Arrange block_id = 1 block_data = "data" leaf_index = 0 - new_leaf_index = 1 + new_leaf_index = 3 blocks_per_bucket = 2 - num_blocks = 14 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + num_blocks = 14 # L=2 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) client._initialize_server_tree(server) @@ -24,8 +46,13 @@ def test_retrieve_after_store_new_leaf_id(): # Assert assert result == block_data - assert block_id in client._stash # Stash should be empty after retrieval - assert block_id in client._position_map + assert not client._stash # block should be in root + print(client._decrypt_and_parse_path(server.get_path(leaf_index))) + assert ( + client._decrypt_and_parse_path([server._root._value.blocks])[0].blocks[0].data + == block_data + ) + assert client._position_map.get(block_id) == new_leaf_index def test_retrieve_after_store_same_leaf_id(): @@ -34,8 +61,8 @@ def test_retrieve_after_store_same_leaf_id(): block_data = "data" leaf_index = 0 blocks_per_bucket = 2 - num_blocks = 14 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + num_blocks = 14 # L=2 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) client._initialize_server_tree(server) @@ -67,20 +94,6 @@ def test_encryption(): assert decrypted_path[0].blocks[0].data == "abcd" -def test_new_data_to_same_path_leaves_stash_empty(): - # Arrange - server = Server(num_blocks=6, blocks_per_bucket=2) - client = Client(num_blocks=6, blocks_per_bucket=2) - - with patch("random.randint", return_value=1): - # Act - client.store_data(server, 1, "abcd") - client.store_data(server, 2, "efgh") - - # Assert - assert not client._stash - - def test_delete(): # Arrange block_id = 1 @@ -135,3 +148,46 @@ def test_flow(): assert not client.retrieve_data(server, block_id) assert client.retrieve_data(server, 2) == block_data assert client.retrieve_data(server, 2) == block_data + + +def test_smart_stash_retrieval(): + # Arrange + block_id = 1 + block_data = "data" + blocks_per_bucket = 2 + num_blocks = 14 + server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client._initialize_server_tree(server) + + # Act + with patch("random.randint", return_value=0): + client.store_data(server, block_id, block_data) + with patch("random.randint", return_value=1): + result = client.retrieve_data(server, block_id) + + # Assert + assert result == block_data + assert not client._stash + # During the path building in the client side, we take elements from the stash + # even though the block is now mapped to leaf 1, and we build path for lead 0, + # there are nodes in the path that are mutual for both paths to leaves 0 and 1. + # Therefor, when we reach to a node where the path to any of the leaves in the stash + # goes through, we can take the block from the stash and add it to the path, + # and not just the block that is mapped to the same leaf we build the path for. + # For the leftest leaf and the rightest leaf, the mutual node is the root, + # which is in any path + # This test should pass. + + +def test_reachable_leaves(): + # Arrange + blocks_per_bucket = 2 + num_blocks = 30 # L = 3 + client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + + leaf_index = 5 + reachable_leaves = [[0, 1, 2, 3, 4, 5, 6, 7], [4, 5, 6, 7], [4, 5], [5]] + # Act & Assert + for i in range(4): + assert reachable_leaves[i] == client._calculate_reachable_leaves(leaf_index, i) From 30fd9c2b099f5c019b7d7882b0046cae659135ec Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Mon, 19 May 2025 22:10:09 +0300 Subject: [PATCH 14/19] some refactors --- src/client.py | 54 ++++++++++++++++++++++++++------------------------- src/server.py | 8 ++++---- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/client.py b/src/client.py index b207844..b5d69a1 100644 --- a/src/client.py +++ b/src/client.py @@ -59,7 +59,7 @@ def store_data(self, server: Server, id: int, data: str): self._stash[id] = Block(id=id, data=data) self._logger.debug(f"Stash updated with block {id}.") - path = self._build_new_path(leaf_index, len(path), id) + path = self._build_new_path(leaf_index) path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} stored successfully.") @@ -75,7 +75,7 @@ def retrieve_data(self, server: Server, id: int) -> str: path = self._decrypt_and_parse_path(path) self._update_stash(path, id) block = self._stash.get(id) - path = self._build_new_path(leaf_index, len(path), id) + path = self._build_new_path(leaf_index) path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} retrieved successfully.") @@ -93,7 +93,7 @@ def delete_data(self, server: Server, id: int) -> None: del self._stash[id] del self._position_map[id] self._logger.debug(f"Block {id} removed from stash.") - path = self._build_new_path(leaf_index, len(path), id) + path = self._build_new_path(leaf_index) path = self._unparse_and_encrypt_path(path) server.set_path(path, leaf_index) self._logger.info(f"Data for block {id} deleted successfully.") @@ -105,39 +105,41 @@ def _update_stash(self, path: List[Bucket], id: int) -> None: if block.id != -1: # not a dummy block self._stash[block.id] = block - def _build_new_path( - self, leaf_index: int, path_length: int, id: int - ) -> List[Bucket]: + def _build_new_path(self, leaf_index: int) -> List[Bucket]: self._logger.debug(f"Building new path for leaf index {leaf_index}.") - path = [Bucket(self._num_blocks_per_bucket) for _ in range(path_length)] + path = [ + Bucket(self._num_blocks_per_bucket) for _ in range(self._tree_height + 1) + ] # Iterate over the tree levels from leaf to root - for depth in range(self._tree_height, -1, -1): - reachable_leaves = self._calculate_reachable_leaves(leaf_index, depth) - i = 0 - j = 0 - blocks_ids_in_stash = list(self._stash.keys()) - while i < self._num_blocks_per_bucket and j < len(blocks_ids_in_stash): - block_id = blocks_ids_in_stash[j] + for level in range(self._tree_height, -1, -1): + reachable_leaves = self._calculate_reachable_leaves(leaf_index, level) + bucket_index = self._tree_height - level + num_written_blocks = 0 + block_ids = list( + self._stash.keys() + ) # to avoid modifying dict during iteration + for block_id in block_ids: + if num_written_blocks >= self._num_blocks_per_bucket: + break if self._position_map.get(block_id) in reachable_leaves: - # If the block in the stash is reachable, add it to the path - path[self._tree_height - depth].blocks[i] = self._stash[block_id] + path[bucket_index].blocks[num_written_blocks] = self._stash[ + block_id + ] del self._stash[block_id] - i += 1 - j += 1 - + num_written_blocks += 1 return path - def _calculate_reachable_leaves(self, leaf_index: int, depth: int) -> List[int]: + def _calculate_reachable_leaves(self, leaf_index: int, level: int) -> List[int]: binary = format(leaf_index, f"0{self._tree_height}b") - # Get first depth bits (path so far) - path_bits = binary[:depth] - # Compute base index: decimal of path_bits * 2^(L-depth) + # Get first level bits (path so far) + path_bits = binary[:level] + # Compute base index: decimal of path_bits * 2^(L-level) base = ( - int(path_bits, 2) * (1 << (self._tree_height - depth)) if path_bits else 0 + int(path_bits, 2) * (1 << (self._tree_height - level)) if path_bits else 0 ) - # Number of reachable leaves: 2^(L-depth) - num_leaves = 1 << (self._tree_height - depth) + # Number of reachable leaves: 2^(L-level) + num_leaves = 1 << (self._tree_height - level) # List of reachable leaves return list(range(base, base + num_leaves)) diff --git a/src/server.py b/src/server.py index 891e851..8d4758e 100644 --- a/src/server.py +++ b/src/server.py @@ -38,12 +38,12 @@ def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: self._root: TreeNode[Bucket] = None def initialize_tree(self, elements: List[List[bytes]]) -> TreeNode[Bucket]: - def recursive_init(depth: int, i: int) -> TreeNode[Bucket]: - if depth < 0: + def recursive_init(level: int, i: int) -> TreeNode[Bucket]: + if level < 0: return None node = TreeNode(Bucket(blocks=elements[i])) - node._left = recursive_init(depth - 1, i + 1) - node._right = recursive_init(depth - 1, i + 2) + node._left = recursive_init(level - 1, i + 1) + node._right = recursive_init(level - 1, i + 2) return node self._root = recursive_init(self._tree_height, 0) From 88ae7f38ce98b042a5245898acc58f53eee6da82 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Wed, 21 May 2025 22:54:58 +0300 Subject: [PATCH 15/19] . --- .gitignore | 1 + src/client.py | 6 +++--- tests/{test_client.py => tests.py} | 0 3 files changed, 4 insertions(+), 3 deletions(-) rename tests/{test_client.py => tests.py} (100%) diff --git a/.gitignore b/.gitignore index 532ecf9..68b686d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ .vscode .pytest_cache .ruff_cache +.python-version diff --git a/src/client.py b/src/client.py index b5d69a1..e6e1b86 100644 --- a/src/client.py +++ b/src/client.py @@ -39,7 +39,7 @@ def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: self._key = Fernet.generate_key() self._cipher = Fernet(self._key) - def remap_block(self, block_id: int): + def _remap_block(self, block_id: int): new_position = random.randint(0, int(2**self._tree_height) - 1) self._position_map[block_id] = new_position self._logger.debug(f"Block {block_id} remapped to position {new_position}.") @@ -47,7 +47,7 @@ def remap_block(self, block_id: int): def store_data(self, server: Server, id: int, data: str): self._logger.info(f"Storing data for block {id}.") leaf_index = self._position_map.get(id) - self.remap_block(id) + self._remap_block(id) if not leaf_index: # if new block leaf_index = self._position_map.get(id) self._logger.debug(f"Leaf index for block {id}: {leaf_index}.") @@ -70,7 +70,7 @@ def retrieve_data(self, server: Server, id: int) -> str: if leaf_index is None: self._logger.warning(f"Block {id} not found.") return None - self.remap_block(id) + self._remap_block(id) path = server.get_path(leaf_index) path = self._decrypt_and_parse_path(path) self._update_stash(path, id) diff --git a/tests/test_client.py b/tests/tests.py similarity index 100% rename from tests/test_client.py rename to tests/tests.py From 0efe9419ae9490593b6292afba3b14c1096d0290 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Wed, 21 May 2025 23:34:39 +0300 Subject: [PATCH 16/19] changed n to total number of blocks client wants --- src/client.py | 25 ++-------------- src/server.py | 15 ++-------- tests/{tests.py => test_code.py} | 50 ++++++++++++-------------------- 3 files changed, 23 insertions(+), 67 deletions(-) rename tests/{tests.py => test_code.py} (74%) diff --git a/src/client.py b/src/client.py index e6e1b86..7a449ef 100644 --- a/src/client.py +++ b/src/client.py @@ -29,11 +29,11 @@ def __init__(self, num_blocks: int = 4, blocks: List[Block] = None, **data) -> N class Client: - def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: + def __init__(self, num_blocks: int = 100, blocks_per_bucket: int = 4) -> None: self._logger = logging.getLogger(__name__) self._num_blocks = num_blocks self._num_blocks_per_bucket = blocks_per_bucket - self._tree_height = int(math.log2(num_blocks // blocks_per_bucket + 1)) - 1 + self._tree_height = round(math.log2(num_blocks)) self._stash: dict[int, Block] = {} # Changed from List to dict self._position_map = {} self._key = Fernet.generate_key() @@ -166,7 +166,7 @@ def _unparse_and_encrypt_path(self, path: List[Bucket]) -> List[List[bytes]]: def _initialize_server_tree(self, server: Server) -> None: dummy_elements = [ Bucket(self._num_blocks_per_bucket) - for _ in range(self._num_blocks // self._num_blocks_per_bucket) + for _ in range(int(2 ** (self._tree_height + 1) - 1)) ] dummy_elements = self._unparse_and_encrypt_path(dummy_elements) server.initialize_tree(dummy_elements) @@ -179,22 +179,3 @@ def print_stash(self): else: for block_id, block in self._stash.items(): print(f"Block ID: {block_id}, Data: {block.data}") - - -if __name__ == "__main__": - server = Server(num_blocks=14, blocks_per_bucket=2) - client = Client(num_blocks=14, blocks_per_bucket=2) - client._initialize_server_tree(server) - server.print_tree() - client.store_data(server, 1, "abcd") - server.print_tree() - print(client._stash) - client.store_data(server, 2, "efgh") - server.print_tree() - print(client._stash) - print(client.retrieve_data(server, 2)) - server.print_tree() - print(client._stash) - client.delete_data(server, 2) - server.print_tree() - print(client._stash) diff --git a/src/server.py b/src/server.py index 8d4758e..d28339c 100644 --- a/src/server.py +++ b/src/server.py @@ -1,12 +1,3 @@ -# Z Number of blocks in a bucket -# L Number of levels in the tree -# N Number of blocks in the tree -# N / Z Total number of buckets -# L = log(N / Z + 1) - 1 -# N = Z * (2 ** (L + 1) - 1) -# 2 ** L Number of leaves = (N / Z) / 2 - - import logging import math from typing import List @@ -30,11 +21,9 @@ def __init__(self, value: T) -> None: class Server: - def __init__(self, num_blocks: int = 124, blocks_per_bucket: int = 4) -> None: + def __init__(self, num_blocks: int = 100) -> None: self._logger = logging.getLogger(__name__) - self._num_blocks = num_blocks - self._blocks_per_bucket = blocks_per_bucket - self._tree_height = int(math.log2(num_blocks // blocks_per_bucket + 1)) - 1 + self._tree_height = round(math.log2(num_blocks)) self._root: TreeNode[Bucket] = None def initialize_tree(self, elements: List[List[bytes]]) -> TreeNode[Bucket]: diff --git a/tests/tests.py b/tests/test_code.py similarity index 74% rename from tests/tests.py rename to tests/test_code.py index 08faccb..08c217c 100644 --- a/tests/tests.py +++ b/tests/test_code.py @@ -9,10 +9,8 @@ def test_store_data(): block_id = 1 block_data = "data" leaf_index = 0 - blocks_per_bucket = 2 - num_blocks = 14 # L=2 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() client._initialize_server_tree(server) # Act @@ -31,11 +29,9 @@ def test_retrieve_after_store_new_leaf_id(): block_id = 1 block_data = "data" leaf_index = 0 - new_leaf_index = 3 - blocks_per_bucket = 2 - num_blocks = 14 # L=2 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() + new_leaf_index = 2**client._tree_height - 1 client._initialize_server_tree(server) # Act @@ -60,10 +56,8 @@ def test_retrieve_after_store_same_leaf_id(): block_id = 1 block_data = "data" leaf_index = 0 - blocks_per_bucket = 2 - num_blocks = 14 # L=2 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() client._initialize_server_tree(server) # Act @@ -83,6 +77,8 @@ def test_encryption(): path = [ Bucket(blocks=[Block(id=1, data="abcd"), Block()]), Bucket(2), + Bucket(2), + Bucket(2), ] # Act @@ -98,10 +94,8 @@ def test_delete(): # Arrange block_id = 1 block_data = "data" - blocks_per_bucket = 2 - num_blocks = 14 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() client._initialize_server_tree(server) # Act @@ -116,10 +110,8 @@ def test_delete(): def test_retrieve_not_found(): # Arrange block_id = 1 - blocks_per_bucket = 2 - num_blocks = 14 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() client._initialize_server_tree(server) # Act @@ -133,10 +125,8 @@ def test_flow(): # Arrange block_id = 1 block_data = "data" - blocks_per_bucket = 2 - num_blocks = 14 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() client._initialize_server_tree(server) # Act & Assert @@ -154,10 +144,8 @@ def test_smart_stash_retrieval(): # Arrange block_id = 1 block_data = "data" - blocks_per_bucket = 2 - num_blocks = 14 - server = Server(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) # L=2 - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + server = Server() + client = Client() client._initialize_server_tree(server) # Act @@ -182,9 +170,7 @@ def test_smart_stash_retrieval(): def test_reachable_leaves(): # Arrange - blocks_per_bucket = 2 - num_blocks = 30 # L = 3 - client = Client(num_blocks=num_blocks, blocks_per_bucket=blocks_per_bucket) + client = Client(num_blocks=8) leaf_index = 5 reachable_leaves = [[0, 1, 2, 3, 4, 5, 6, 7], [4, 5, 6, 7], [4, 5], [5]] From 6b76de956b9998b7bec4a7f412f84e2261361bcd Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Thu, 22 May 2025 01:01:15 +0300 Subject: [PATCH 17/19] removed code duplication, added benchmarks file and graphs --- LT vs TP 2.png | Bin 0 -> 26574 bytes TP vs N 1.png | Bin 0 -> 23765 bytes TP vs N 2.png | Bin 0 -> 23720 bytes pyproject.toml | 1 + src/benchmark.py | 59 ++++++++++ src/client.py | 47 ++++---- src/server.py | 2 +- tests/test_code.py | 94 +++++++--------- uv.lock | 274 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 395 insertions(+), 82 deletions(-) create mode 100644 LT vs TP 2.png create mode 100644 TP vs N 1.png create mode 100644 TP vs N 2.png create mode 100644 src/benchmark.py diff --git a/LT vs TP 2.png b/LT vs TP 2.png new file mode 100644 index 0000000000000000000000000000000000000000..d594fa835b13d114fc25a25ad4da831da9cbc0ae GIT binary patch literal 26574 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 literal 0 HcmV?d00001 diff --git a/TP vs N 2.png b/TP vs N 2.png new file mode 100644 index 0000000000000000000000000000000000000000..3c77e745a4398d4f2401c42827077d71902b1120 GIT binary patch literal 23720 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 DAAh%k literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 0214502..cf9c0bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "cryptography>=45.0.2", + "matplotlib>=3.10.3", "pydantic>=2.11.4", "ruff>=0.11.9", ] diff --git a/src/benchmark.py b/src/benchmark.py new file mode 100644 index 0000000..6cfbb8f --- /dev/null +++ b/src/benchmark.py @@ -0,0 +1,59 @@ +import time + +import matplotlib.pyplot as plt + +from src.client import Client +from src.server import Server + + +def benchmark_throughput_vs_db_size(db_sizes): + throughputs = [] + latencies = [] + for num_blocks in db_sizes: + server = Server(num_blocks=num_blocks) + client = Client(num_blocks=num_blocks) + client._initialize_server_tree(server) + + repeats = 1000 // num_blocks + + start = time.time() + for _ in range(repeats): + for i in range(num_blocks): + client.store_data(server, i, f"data_{i}") + client.retrieve_data(server, i) + + for i in range(num_blocks): + client.retrieve_data(server, i) + client.delete_data(server, i) + end = time.time() + + delta = end - start + total_requests = repeats * 4 * num_blocks + throughput = total_requests / delta + latency = delta / total_requests + throughputs.append(throughput) + latencies.append(latency) + print(f"{delta=}") + print(f"N={num_blocks}: {throughput:.2f} req/sec") + + return throughputs, latencies + + +if __name__ == "__main__": + db_sizes = [10, 50, 100, 200, 500, 1000] + throughputs, latencies = benchmark_throughput_vs_db_size(db_sizes) + plt.figure() + plt.plot(db_sizes, throughputs, marker="o") + plt.xlabel("N (DB size)") + plt.ylabel("Throughput (requests/sec)") + plt.title("Throughput vs. DB Size") + plt.grid(True) + plt.show() + + plt.figure() + plt.plot(throughputs, [latency * 1000 for latency in latencies], marker="o") + plt.xlabel("Throughput (requests/sec)") + plt.ylabel("Latency (ms/request)") + plt.title("Latency vs. Throughput") + plt.grid(True) + plt.show() diff --git a/src/client.py b/src/client.py index 7a449ef..450880e 100644 --- a/src/client.py +++ b/src/client.py @@ -9,7 +9,7 @@ from src.server import Server logging.basicConfig( - level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -51,17 +51,13 @@ def store_data(self, server: Server, id: int, data: str): if not leaf_index: # if new block leaf_index = self._position_map.get(id) self._logger.debug(f"Leaf index for block {id}: {leaf_index}.") - path = server.get_path(leaf_index) - path = self._decrypt_and_parse_path(path) - self._update_stash(path, id) + self._fetch_decrypt_and_update_stash(leaf_index, server) # write new data to stash self._stash[id] = Block(id=id, data=data) self._logger.debug(f"Stash updated with block {id}.") - path = self._build_new_path(leaf_index) - path = self._unparse_and_encrypt_path(path) - server.set_path(path, leaf_index) + self._build_encrypt_and_set_path(leaf_index, server) self._logger.info(f"Data for block {id} stored successfully.") def retrieve_data(self, server: Server, id: int) -> str: @@ -71,31 +67,25 @@ def retrieve_data(self, server: Server, id: int) -> str: self._logger.warning(f"Block {id} not found.") return None self._remap_block(id) - path = server.get_path(leaf_index) - path = self._decrypt_and_parse_path(path) - self._update_stash(path, id) + self._fetch_decrypt_and_update_stash(leaf_index, server) + block = self._stash.get(id) - path = self._build_new_path(leaf_index) - path = self._unparse_and_encrypt_path(path) - server.set_path(path, leaf_index) + + self._build_encrypt_and_set_path(leaf_index, server) self._logger.info(f"Data for block {id} retrieved successfully.") return block.data - def delete_data(self, server: Server, id: int) -> None: + def delete_data(self, server: Server, id: int, data=None) -> None: self._logger.info(f"Deleting data for block {id}.") leaf_index = self._position_map.get(id) if leaf_index is None: self._logger.warning(f"Block {id} not found.") return None - path = server.get_path(leaf_index) - path = self._decrypt_and_parse_path(path) - self._update_stash(path, id) + self._fetch_decrypt_and_update_stash(leaf_index, server) del self._stash[id] del self._position_map[id] self._logger.debug(f"Block {id} removed from stash.") - path = self._build_new_path(leaf_index) - path = self._unparse_and_encrypt_path(path) - server.set_path(path, leaf_index) + self._build_encrypt_and_set_path(leaf_index, server) self._logger.info(f"Data for block {id} deleted successfully.") def _update_stash(self, path: List[Bucket], id: int) -> None: @@ -171,11 +161,12 @@ def _initialize_server_tree(self, server: Server) -> None: dummy_elements = self._unparse_and_encrypt_path(dummy_elements) server.initialize_tree(dummy_elements) - def print_stash(self): - """Prints the current contents of the stash.""" - self._logger.info("Printing stash contents:") - if not self._stash: - print("[]") - else: - for block_id, block in self._stash.items(): - print(f"Block ID: {block_id}, Data: {block.data}") + def _fetch_decrypt_and_update_stash(self, leaf_index: int, server: Server) -> None: + path = server.get_path(leaf_index) + path = self._decrypt_and_parse_path(path) + self._update_stash(path, id) + + def _build_encrypt_and_set_path(self, leaf_index: int, server: Server) -> None: + path = self._build_new_path(leaf_index) + path = self._unparse_and_encrypt_path(path) + server.set_path(path, leaf_index) diff --git a/src/server.py b/src/server.py index d28339c..65c7a0a 100644 --- a/src/server.py +++ b/src/server.py @@ -5,7 +5,7 @@ from pydantic import BaseModel logging.basicConfig( - level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) diff --git a/tests/test_code.py b/tests/test_code.py index 08c217c..bb62f87 100644 --- a/tests/test_code.py +++ b/tests/test_code.py @@ -1,17 +1,36 @@ from unittest.mock import patch +import pytest + from src.client import Block, Bucket, Client from src.server import Server -def test_store_data(): +@pytest.fixture +def server() -> Server: + return Server() + + +@pytest.fixture +def client(server: Server) -> Client: + c = Client() + c._initialize_server_tree(server) + return c + + +@pytest.fixture +def block_id() -> int: + return 1 + + +@pytest.fixture +def block_data() -> str: + return "data" + + +def test_store_data(client: Client, server: Server, block_id: int, block_data: str): # Arrange - block_id = 1 - block_data = "data" leaf_index = 0 - server = Server() - client = Client() - client._initialize_server_tree(server) # Act with patch("random.randint", return_value=leaf_index): @@ -24,15 +43,12 @@ def test_store_data(): assert client._position_map.get(block_id) == leaf_index -def test_retrieve_after_store_new_leaf_id(): +def test_retrieve_after_store_new_leaf_id( + client: Client, server: Server, block_id: int, block_data: str +): # Arrange - block_id = 1 - block_data = "data" leaf_index = 0 - server = Server() - client = Client() new_leaf_index = 2**client._tree_height - 1 - client._initialize_server_tree(server) # Act with patch("random.randint", return_value=leaf_index): @@ -51,14 +67,11 @@ def test_retrieve_after_store_new_leaf_id(): assert client._position_map.get(block_id) == new_leaf_index -def test_retrieve_after_store_same_leaf_id(): +def test_retrieve_after_store_same_leaf_id( + client: Client, server: Server, block_id: int, block_data: str +): # Arrange - block_id = 1 - block_data = "data" leaf_index = 0 - server = Server() - client = Client() - client._initialize_server_tree(server) # Act with patch("random.randint", return_value=leaf_index): @@ -71,11 +84,11 @@ def test_retrieve_after_store_same_leaf_id(): assert block_id in client._position_map -def test_encryption(): +def test_encryption(block_id: int, block_data: str): # Arrange client = Client(num_blocks=6, blocks_per_bucket=2) path = [ - Bucket(blocks=[Block(id=1, data="abcd"), Block()]), + Bucket(blocks=[Block(id=block_id, data=block_data), Block()]), Bucket(2), Bucket(2), Bucket(2), @@ -86,18 +99,11 @@ def test_encryption(): decrypted_path = client._decrypt_and_parse_path(encrypted_path) # Assert - assert decrypted_path[0].blocks[0].id == 1 - assert decrypted_path[0].blocks[0].data == "abcd" + assert decrypted_path[0].blocks[0].id == block_id + assert decrypted_path[0].blocks[0].data == block_data -def test_delete(): - # Arrange - block_id = 1 - block_data = "data" - server = Server() - client = Client() - client._initialize_server_tree(server) - +def test_delete(client: Client, server: Server, block_id: int, block_data: str): # Act client.store_data(server, block_id, block_data) client.delete_data(server, block_id) @@ -107,13 +113,7 @@ def test_delete(): assert not client._position_map -def test_retrieve_not_found(): - # Arrange - block_id = 1 - server = Server() - client = Client() - client._initialize_server_tree(server) - +def test_retrieve_not_found(client: Client, server: Server, block_id: int): # Act result = client.retrieve_data(server, block_id) @@ -121,14 +121,7 @@ def test_retrieve_not_found(): assert result is None # Should return None if block is not found -def test_flow(): - # Arrange - block_id = 1 - block_data = "data" - server = Server() - client = Client() - client._initialize_server_tree(server) - +def test_flow(client: Client, server: Server, block_id: int, block_data: str): # Act & Assert client.store_data(server, block_id, block_data) assert client.retrieve_data(server, block_id) == block_data @@ -140,14 +133,9 @@ def test_flow(): assert client.retrieve_data(server, 2) == block_data -def test_smart_stash_retrieval(): - # Arrange - block_id = 1 - block_data = "data" - server = Server() - client = Client() - client._initialize_server_tree(server) - +def test_smart_stash_retrieval( + client: Client, server: Server, block_id: int, block_data: str +): # Act with patch("random.randint", return_value=0): client.store_data(server, block_id, block_data) diff --git a/uv.lock b/uv.lock index f56fec4..62dce06 100644 --- a/uv.lock +++ b/uv.lock @@ -62,6 +62,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, +] + [[package]] name = "cryptography" version = "45.0.2" @@ -97,6 +138,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/63/fb28b30c144182fd44ce93d13ab859791adbf923e43bdfb610024bfecda1/cryptography-45.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:48caa55c528617fa6db1a9c3bf2e37ccb31b73e098ac2b71408d1f2db551dde4", size = 3393321, upload-time = "2025-05-18T02:46:03.441Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "distlib" version = "0.3.9" @@ -115,6 +165,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] +[[package]] +name = "fonttools" +version = "4.58.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/4d037663e2a1fe30fddb655d755d76e18624be44ad467c07412c2319ab97/fonttools-4.58.0.tar.gz", hash = "sha256:27423d0606a2c7b336913254bf0b1193ebd471d5f725d665e875c5e88a011a43", size = 3514522, upload-time = "2025-05-10T17:36:35.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/4e/1c6b35ec7c04d739df4cf5aace4b7ec284d6af2533a65de21972e2f237d9/fonttools-4.58.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aa8316798f982c751d71f0025b372151ea36405733b62d0d94d5e7b8dd674fa6", size = 2737502, upload-time = "2025-05-10T17:35:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/fc/72/c6fcafa3c9ed2b69991ae25a1ba7a3fec8bf74928a96e8229c37faa8eda2/fonttools-4.58.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6db489511e867633b859b11aefe1b7c0d90281c5bdb903413edbb2ba77b97f1", size = 2307214, upload-time = "2025-05-10T17:35:38.939Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/1015cedc9878da6d8d1758049749eef857b693e5828d477287a959c8650f/fonttools-4.58.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:107bdb2dacb1f627db3c4b77fb16d065a10fe88978d02b4fc327b9ecf8a62060", size = 4811136, upload-time = "2025-05-10T17:35:41.491Z" }, + { url = "https://files.pythonhosted.org/packages/32/b9/6a1bc1af6ec17eead5d32e87075e22d0dab001eace0b5a1542d38c6a9483/fonttools-4.58.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba7212068ab20f1128a0475f169068ba8e5b6e35a39ba1980b9f53f6ac9720ac", size = 4876598, upload-time = "2025-05-10T17:35:43.986Z" }, + { url = "https://files.pythonhosted.org/packages/d8/46/b14584c7ea65ad1609fb9632251016cda8a2cd66b15606753b9f888d3677/fonttools-4.58.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f95ea3b6a3b9962da3c82db73f46d6a6845a6c3f3f968f5293b3ac1864e771c2", size = 4872256, upload-time = "2025-05-10T17:35:46.617Z" }, + { url = "https://files.pythonhosted.org/packages/05/78/b2105a7812ca4ef9bf180cd741c82f4522316c652ce2a56f788e2eb54b62/fonttools-4.58.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:874f1225cc4ccfeac32009887f722d7f8b107ca5e867dcee067597eef9d4c80b", size = 5028710, upload-time = "2025-05-10T17:35:49.227Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a9/a38c85ffd30d1f2c7a5460c8abfd1aa66e00c198df3ff0b08117f5c6fcd9/fonttools-4.58.0-cp312-cp312-win32.whl", hash = "sha256:5f3cde64ec99c43260e2e6c4fa70dfb0a5e2c1c1d27a4f4fe4618c16f6c9ff71", size = 2173593, upload-time = "2025-05-10T17:35:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/66/48/29752962a74b7ed95da976b5a968bba1fe611a4a7e50b9fefa345e6e7025/fonttools-4.58.0-cp312-cp312-win_amd64.whl", hash = "sha256:2aee08e2818de45067109a207cbd1b3072939f77751ef05904d506111df5d824", size = 2223230, upload-time = "2025-05-10T17:35:53.653Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d7/d77cae11c445916d767cace93ba8283b3f360197d95d7470b90a9e984e10/fonttools-4.58.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4809790f2371d8a08e59e1ce2b734c954cf09742e75642d7f4c46cfdac488fdd", size = 2728320, upload-time = "2025-05-10T17:35:56.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/48/7d8b3c519ef4b48081d40310262224a38785e39a8610ccb92a229a6f085d/fonttools-4.58.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b00f240280f204ce4546b05ff3515bf8ff47a9cae914c718490025ea2bb9b324", size = 2302570, upload-time = "2025-05-10T17:35:58.794Z" }, + { url = "https://files.pythonhosted.org/packages/2c/48/156b83eb8fb7261056e448bfda1b495b90e761b28ec23cee10e3e19f1967/fonttools-4.58.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a62015ad463e1925544e9159dd6eefe33ebfb80938d5ab15d8b1c4b354ff47b", size = 4790066, upload-time = "2025-05-10T17:36:01.174Z" }, + { url = "https://files.pythonhosted.org/packages/60/49/aaecb1b3cea2b9b9c7cea6240d6bc8090feb5489a6fbf93cb68003be979b/fonttools-4.58.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ceef6f6ab58061a811967e3e32e630747fcb823dcc33a9a2c80e2d0d17cb292", size = 4861076, upload-time = "2025-05-10T17:36:03.663Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c8/97cbb41bee81ea9daf6109e0f3f70a274a3c69418e5ac6b0193f5dacf506/fonttools-4.58.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7be21ac52370b515cdbdd0f400803fd29432a4fa4ddb4244ac8b322e54f36c0", size = 4858394, upload-time = "2025-05-10T17:36:06.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/23/c2c231457361f869a7d7374a557208e303b469d48a4a697c0fb249733ea1/fonttools-4.58.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:85836be4c3c4aacf6fcb7a6f263896d0e9ce431da9fa6fe9213d70f221f131c9", size = 5002160, upload-time = "2025-05-10T17:36:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/c2262f941a43b810c5c192db94b5d1ce8eda91bec2757f7e2416398f4072/fonttools-4.58.0-cp313-cp313-win32.whl", hash = "sha256:2b32b7130277bd742cb8c4379a6a303963597d22adea77a940343f3eadbcaa4c", size = 2171919, upload-time = "2025-05-10T17:36:10.644Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/e4aa7bb4ce510ad57a808d321df1bbed1eeb6e1dfb20aaee1a5d9c076849/fonttools-4.58.0-cp313-cp313-win_amd64.whl", hash = "sha256:75e68ee2ec9aaa173cf5e33f243da1d51d653d5e25090f2722bc644a78db0f1a", size = 2222972, upload-time = "2025-05-10T17:36:12.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1f/4417c26e26a1feab85a27e927f7a73d8aabc84544be8ba108ce4aa90eb1e/fonttools-4.58.0-py3-none-any.whl", hash = "sha256:c96c36880be2268be409df7b08c5b5dacac1827083461a6bc2cb07b8cbcec1d7", size = 1111440, upload-time = "2025-05-10T17:36:33.607Z" }, +] + [[package]] name = "identify" version = "2.6.10" @@ -133,6 +208,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -142,12 +305,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, +] + [[package]] name = "oram" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "cryptography" }, + { name = "matplotlib" }, { name = "pydantic" }, { name = "ruff" }, ] @@ -161,6 +363,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cryptography", specifier = ">=45.0.2" }, + { name = "matplotlib", specifier = ">=3.10.3" }, { name = "pydantic", specifier = ">=2.11.4" }, { name = "ruff", specifier = ">=0.11.9" }, ] @@ -180,6 +383,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -280,6 +524,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, ] +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + [[package]] name = "pytest" version = "8.3.5" @@ -295,6 +548,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -346,6 +611,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "typing-extensions" version = "4.13.2" From 103382314aeba318a6eed7ef7ab2c1ec76be80c5 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Sun, 25 May 2025 19:25:48 +0300 Subject: [PATCH 18/19] added some docs --- src/client.py | 41 +++++++++++++++++++++++++++++++++++------ src/server.py | 4 ++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/client.py b/src/client.py index 450880e..1e378ff 100644 --- a/src/client.py +++ b/src/client.py @@ -9,7 +9,7 @@ from src.server import Server logging.basicConfig( - level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -82,9 +82,12 @@ def delete_data(self, server: Server, id: int, data=None) -> None: self._logger.warning(f"Block {id} not found.") return None self._fetch_decrypt_and_update_stash(leaf_index, server) + + # remove block from stash and position map del self._stash[id] del self._position_map[id] self._logger.debug(f"Block {id} removed from stash.") + self._build_encrypt_and_set_path(leaf_index, server) self._logger.info(f"Data for block {id} deleted successfully.") @@ -96,12 +99,38 @@ def _update_stash(self, path: List[Bucket], id: int) -> None: self._stash[block.id] = block def _build_new_path(self, leaf_index: int) -> List[Bucket]: + """ + Constructs a new path from the leaf node up to the root, filling each bucket along + the path with blocks from the stash that are reachable from the current node in the path. + For example: + 0 + / \ + 1 2 + / \ / \ + 3 4 5 6 + When we build the path for leaf index 3, we will first fill the bucket of node 3 with + all the blocks that are mapped to node 3 because it is a leaf. + Then, we got to the next node in the path -> 1. + We will node 1 bucket with all the blocks that are mapped to leaves that are reachable + from node 1, which are 3 and 4. + Finally, we will fill the bucket of the root node with all the blocks that are left in + the stash, because every leaf is reachable from the root. + + Args: + leaf_index (int): The index of the leaf node for which the path is being built + Returns: + List[Bucket]: A list of Bucket objects representing the path from the leaf to + the root, with each bucket filled with as many appropriate blocks from the + stash as possible + Side Effects: + Removes blocks from the stash that are placed into the path buckets. + """ self._logger.debug(f"Building new path for leaf index {leaf_index}.") path = [ Bucket(self._num_blocks_per_bucket) for _ in range(self._tree_height + 1) ] - # Iterate over the tree levels from leaf to root + # iterate over the tree levels from leaf to root for level in range(self._tree_height, -1, -1): reachable_leaves = self._calculate_reachable_leaves(leaf_index, level) bucket_index = self._tree_height - level @@ -122,15 +151,15 @@ def _build_new_path(self, leaf_index: int) -> List[Bucket]: def _calculate_reachable_leaves(self, leaf_index: int, level: int) -> List[int]: binary = format(leaf_index, f"0{self._tree_height}b") - # Get first level bits (path so far) + # get first level bits (path so far) path_bits = binary[:level] - # Compute base index: decimal of path_bits * 2^(L-level) + # compute base index: decimal of path_bits * 2^(L-level) base = ( int(path_bits, 2) * (1 << (self._tree_height - level)) if path_bits else 0 ) - # Number of reachable leaves: 2^(L-level) + # number of reachable leaves: 2^(L-level) num_leaves = 1 << (self._tree_height - level) - # List of reachable leaves + # list of reachable leaves return list(range(base, base + num_leaves)) def _decrypt_and_parse_path(self, path: List[List[bytes]]) -> List[Bucket]: diff --git a/src/server.py b/src/server.py index 65c7a0a..100f8d1 100644 --- a/src/server.py +++ b/src/server.py @@ -5,7 +5,7 @@ from pydantic import BaseModel logging.basicConfig( - level=logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -50,7 +50,7 @@ def get_path(self, leaf_index: int) -> List[List[bytes]]: leaf_index (int): The index of the leaf node to retrieve the path for Returns: - List[List[str]]: A list of buckets represented as list of bytes, representing + List[List[bytes]]: A list of buckets represented as list of bytes, representing the values of the nodes along the path from the root to the specified leaf Raises: From 272b96b4ebf943090e6fafe7d771af213e7c1d77 Mon Sep 17 00:00:00 2001 From: Yonatan Cohen Date: Sun, 25 May 2025 19:42:58 +0300 Subject: [PATCH 19/19] added pythonpath to envs --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e1f842d..de8649a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,9 @@ jobs: with: python-version: '3.12' + - name: Set PYTHONPATH + run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV + - name: Install dependencies run: | python -m pip install --upgrade pip