From 2d3398a86ad85b572c54395ef351e096bf3703d9 Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Sun, 16 Jun 2024 23:15:37 +0200 Subject: [PATCH 01/17] feat(framework:skip) Add Flower File Storage interface and disk based implementation --- src/py/flwr/server/superlink/ffs/__init__.py | 24 ++++ src/py/flwr/server/superlink/ffs/disk_ffs.py | 119 ++++++++++++++++ src/py/flwr/server/superlink/ffs/ffs.py | 78 +++++++++++ src/py/flwr/server/superlink/ffs/ffs_test.py | 139 +++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 src/py/flwr/server/superlink/ffs/__init__.py create mode 100644 src/py/flwr/server/superlink/ffs/disk_ffs.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs_test.py diff --git a/src/py/flwr/server/superlink/ffs/__init__.py b/src/py/flwr/server/superlink/ffs/__init__.py new file mode 100644 index 00000000000..4b2ee7a2e2a --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower File Storage for large objects.""" + + +from .disk_ffs import DiskFfs as DiskFfs +from .ffs import Ffs as Ffs + +__all__ = [ + "Ffs", + "DiskFfs", +] diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py new file mode 100644 index 00000000000..833654bf64c --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -0,0 +1,119 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Disk based Flower File Storage.""" + +import hashlib +import json +import os +from typing import Dict, List, Tuple + +from flwr.server.superlink.ffs.ffs import Ffs + + +def write_dict_to_file(data_dict, file_path): + """Write a Dict to a file in JSON format.""" + with open(file_path, "w") as file: + json.dump(data_dict, file) + + +def read_dict_from_file(file_path): + """Read a Dict from a JSON file.""" + with open(file_path) as file: + data_dict = json.load(file) + return data_dict + + +class DiskFfs(Ffs): # pylint: disable=R0904 + """Disk based Flower File Storage interface for large objects.""" + + def __init__(self, base_dir: str) -> None: + """Create a new DiskFfs instance. + + Parameters + ---------- + base_dir : string + The base directory to store the objects. + """ + self.base_dir = base_dir + + def put(self, content: bytes, meta: Dict[str, str]) -> str: + """Store bytes and metadata. Return sha256hex hash of data as str. + + Parameters + ---------- + content : bytes + The content to be stored. + meta : Dict[str, str] + The metadata to be stored. + + Returns + ------- + sha256hex : string + The sha256hex hash of the content. + """ + content_hash = hashlib.sha256(content).hexdigest() + + with open(os.path.join(self.base_dir, content_hash), "wb") as file: + file.write(content) + + write_dict_to_file(meta, os.path.join(self.base_dir, f"{content_hash}.META")) + + return content_hash + + def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + """Return tuple containing the object and it's meta fields. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be retrieved. + + Returns + ------- + Tuple[bytes, Dict[str, str]] + A tuple containing the object and it's metadata. + """ + with open(os.path.join(self.base_dir, key), "rb") as file: + content = file.read() + + meta = read_dict_from_file(os.path.join(self.base_dir, f"{key}.META")) + + return content, meta + + def delete(self, key: str) -> None: + """Delete object with hash. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be deleted. + """ + os.remove(os.path.join(self.base_dir, key)) + os.remove(os.path.join(self.base_dir, f"{key}.META")) + + def list(self) -> List[str]: + """List all keys. + + Can be used to list all keys in the storage and e.g. to clean up + the storage with the delete method. + + Returns + ------- + List[str] + A list of all keys. + """ + return [ + item for item in os.listdir(self.base_dir) if not item.endswith(".META") + ] diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py new file mode 100644 index 00000000000..d114cee0f0c --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -0,0 +1,78 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Abstract base class for Flower File Storage interface.""" + + +import abc +from typing import Dict, List, Tuple + + +class Ffs(abc.ABC): # pylint: disable=R0904 + """Abstract Flower File Storage interface for large objects.""" + + @abc.abstractmethod + def put(self, content: bytes, meta: Dict[str, str]) -> str: + """Store bytes and metadata. Return sha256hex hash of data as str. + + Parameters + ---------- + content : bytes + The content to be stored. + meta : Dict[str, str] + The metadata to be stored. + + Returns + ------- + sha256hex : string + The sha256hex hash of the content. + """ + + @abc.abstractmethod + def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + """Return tuple containing the object and it's meta fields. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be retrieved. + + Returns + ------- + Tuple[bytes, Dict[str, str]] + A tuple containing the object and it's metadata. + """ + + @abc.abstractmethod + def delete(self, key: str) -> None: + """Delete object with hash. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be deleted. + """ + + @abc.abstractmethod + def list(self) -> List[str]: + """List all keys. + + Can be used to list all keys in the storage and e.g. to clean up + the storage with the delete method. + + Returns + ------- + List[str] + A list of all keys. + """ diff --git a/src/py/flwr/server/superlink/ffs/ffs_test.py b/src/py/flwr/server/superlink/ffs/ffs_test.py new file mode 100644 index 00000000000..fef765c7ac9 --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs_test.py @@ -0,0 +1,139 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests all Ffs implemenations have to conform to.""" +# pylint: disable=invalid-name, disable=R0904 + +import hashlib +import json +import os +import tempfile +import unittest +from abc import abstractmethod + +from flwr.server.superlink.ffs import DiskFfs, Ffs + + +class FfsTest(unittest.TestCase): + """Test all ffs implementations.""" + + # This is to True in each child class + __test__ = False + + @abstractmethod + def ffs_factory(self) -> Ffs: + """Provide Ffs implementation to test.""" + raise NotImplementedError() + + def test_put(self) -> None: + """Test if object can be stored.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content = b"content" + hash_expected = hashlib.sha256(content).hexdigest() + + # Execute + hash_actual = ffs.put(b"content", {"meta": "data"}) + + # Assert + assert isinstance(hash_actual, str) + assert len(hash_actual) == 64 + assert hash_actual == hash_expected + + # Check if file was created + assert [hash_expected, f"{hash_expected}.META"] == os.listdir(self.tmp_dir.name) + + def test_get(self) -> None: + """Test if object can be retrieved.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected = {} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + ) as file: + json.dump(meta_expected, file) + + # Execute + content_actual, meta_actual = ffs.get(hash_expected) + + # Assert + assert content_actual == content_expected + assert meta_actual == meta_expected + + def test_delete(self) -> None: + """Test if object can be deleted.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected = {} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + ) as file: + json.dump(meta_expected, file) + + # Execute + ffs.delete(hash_expected) + + # Assert + assert [] == os.listdir(self.tmp_dir.name) + + def test_list(self) -> None: + """Test if object hashes can be listed.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected = {} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + ) as file: + json.dump(meta_expected, file) + + # Execute + hashes = ffs.list() + + # Assert + assert [hash_expected] == hashes + + +class DiskFfsTest(FfsTest, unittest.TestCase): + """Test DiskFfs implementation.""" + + __test__ = True + + def ffs_factory(self) -> DiskFfs: + """Return SqliteState with file-based database.""" + # pylint: disable-next=consider-using-with,attribute-defined-outside-init + self.tmp_dir = tempfile.TemporaryDirectory() + ffs = DiskFfs(self.tmp_dir.name) + return ffs + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 8f4d7c26f1b841d433f35cf7a030dc597f10660b Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Sun, 16 Jun 2024 23:15:37 +0200 Subject: [PATCH 02/17] feat(framework:skip) Add Flower File Storage interface and disk based implementation --- src/py/flwr/server/superlink/ffs/__init__.py | 24 ++++ src/py/flwr/server/superlink/ffs/disk_ffs.py | 119 ++++++++++++++++ src/py/flwr/server/superlink/ffs/ffs.py | 78 +++++++++++ src/py/flwr/server/superlink/ffs/ffs_test.py | 139 +++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 src/py/flwr/server/superlink/ffs/__init__.py create mode 100644 src/py/flwr/server/superlink/ffs/disk_ffs.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs_test.py diff --git a/src/py/flwr/server/superlink/ffs/__init__.py b/src/py/flwr/server/superlink/ffs/__init__.py new file mode 100644 index 00000000000..4b2ee7a2e2a --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower File Storage for large objects.""" + + +from .disk_ffs import DiskFfs as DiskFfs +from .ffs import Ffs as Ffs + +__all__ = [ + "Ffs", + "DiskFfs", +] diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py new file mode 100644 index 00000000000..46e102639d4 --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -0,0 +1,119 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Disk based Flower File Storage.""" + +import hashlib +import json +import os +from typing import Dict, List, Tuple + +from flwr.server.superlink.ffs.ffs import Ffs + + +def write_dict_to_file(data_dict: Dict[str, str], file_path: str) -> None: + """Write a Dict to a file in JSON format.""" + with open(file_path, "w") as file: + json.dump(data_dict, file) + + +def read_dict_from_file(file_path: str) -> Dict[str, str]: + """Read a Dict from a JSON file.""" + with open(file_path) as file: + data_dict = json.load(file) + return data_dict + + +class DiskFfs(Ffs): # pylint: disable=R0904 + """Disk based Flower File Storage interface for large objects.""" + + def __init__(self, base_dir: str) -> None: + """Create a new DiskFfs instance. + + Parameters + ---------- + base_dir : string + The base directory to store the objects. + """ + self.base_dir = base_dir + + def put(self, content: bytes, meta: Dict[str, str]) -> str: + """Store bytes and metadata. Return sha256hex hash of data as str. + + Parameters + ---------- + content : bytes + The content to be stored. + meta : Dict[str, str] + The metadata to be stored. + + Returns + ------- + sha256hex : string + The sha256hex hash of the content. + """ + content_hash = hashlib.sha256(content).hexdigest() + + with open(os.path.join(self.base_dir, content_hash), "wb") as file: + file.write(content) + + write_dict_to_file(meta, os.path.join(self.base_dir, f"{content_hash}.META")) + + return content_hash + + def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + """Return tuple containing the object and it's meta fields. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be retrieved. + + Returns + ------- + Tuple[bytes, Dict[str, str]] + A tuple containing the object and it's metadata. + """ + with open(os.path.join(self.base_dir, key), "rb") as file: + content = file.read() + + meta = read_dict_from_file(os.path.join(self.base_dir, f"{key}.META")) + + return content, meta + + def delete(self, key: str) -> None: + """Delete object with hash. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be deleted. + """ + os.remove(os.path.join(self.base_dir, key)) + os.remove(os.path.join(self.base_dir, f"{key}.META")) + + def list(self) -> List[str]: + """List all keys. + + Can be used to list all keys in the storage and e.g. to clean up + the storage with the delete method. + + Returns + ------- + List[str] + A list of all keys. + """ + return [ + item for item in os.listdir(self.base_dir) if not item.endswith(".META") + ] diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py new file mode 100644 index 00000000000..d114cee0f0c --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -0,0 +1,78 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Abstract base class for Flower File Storage interface.""" + + +import abc +from typing import Dict, List, Tuple + + +class Ffs(abc.ABC): # pylint: disable=R0904 + """Abstract Flower File Storage interface for large objects.""" + + @abc.abstractmethod + def put(self, content: bytes, meta: Dict[str, str]) -> str: + """Store bytes and metadata. Return sha256hex hash of data as str. + + Parameters + ---------- + content : bytes + The content to be stored. + meta : Dict[str, str] + The metadata to be stored. + + Returns + ------- + sha256hex : string + The sha256hex hash of the content. + """ + + @abc.abstractmethod + def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + """Return tuple containing the object and it's meta fields. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be retrieved. + + Returns + ------- + Tuple[bytes, Dict[str, str]] + A tuple containing the object and it's metadata. + """ + + @abc.abstractmethod + def delete(self, key: str) -> None: + """Delete object with hash. + + Parameters + ---------- + hash : string + The sha256hex hash of the object to be deleted. + """ + + @abc.abstractmethod + def list(self) -> List[str]: + """List all keys. + + Can be used to list all keys in the storage and e.g. to clean up + the storage with the delete method. + + Returns + ------- + List[str] + A list of all keys. + """ diff --git a/src/py/flwr/server/superlink/ffs/ffs_test.py b/src/py/flwr/server/superlink/ffs/ffs_test.py new file mode 100644 index 00000000000..fef765c7ac9 --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs_test.py @@ -0,0 +1,139 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests all Ffs implemenations have to conform to.""" +# pylint: disable=invalid-name, disable=R0904 + +import hashlib +import json +import os +import tempfile +import unittest +from abc import abstractmethod + +from flwr.server.superlink.ffs import DiskFfs, Ffs + + +class FfsTest(unittest.TestCase): + """Test all ffs implementations.""" + + # This is to True in each child class + __test__ = False + + @abstractmethod + def ffs_factory(self) -> Ffs: + """Provide Ffs implementation to test.""" + raise NotImplementedError() + + def test_put(self) -> None: + """Test if object can be stored.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content = b"content" + hash_expected = hashlib.sha256(content).hexdigest() + + # Execute + hash_actual = ffs.put(b"content", {"meta": "data"}) + + # Assert + assert isinstance(hash_actual, str) + assert len(hash_actual) == 64 + assert hash_actual == hash_expected + + # Check if file was created + assert [hash_expected, f"{hash_expected}.META"] == os.listdir(self.tmp_dir.name) + + def test_get(self) -> None: + """Test if object can be retrieved.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected = {} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + ) as file: + json.dump(meta_expected, file) + + # Execute + content_actual, meta_actual = ffs.get(hash_expected) + + # Assert + assert content_actual == content_expected + assert meta_actual == meta_expected + + def test_delete(self) -> None: + """Test if object can be deleted.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected = {} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + ) as file: + json.dump(meta_expected, file) + + # Execute + ffs.delete(hash_expected) + + # Assert + assert [] == os.listdir(self.tmp_dir.name) + + def test_list(self) -> None: + """Test if object hashes can be listed.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected = {} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + ) as file: + json.dump(meta_expected, file) + + # Execute + hashes = ffs.list() + + # Assert + assert [hash_expected] == hashes + + +class DiskFfsTest(FfsTest, unittest.TestCase): + """Test DiskFfs implementation.""" + + __test__ = True + + def ffs_factory(self) -> DiskFfs: + """Return SqliteState with file-based database.""" + # pylint: disable-next=consider-using-with,attribute-defined-outside-init + self.tmp_dir = tempfile.TemporaryDirectory() + ffs = DiskFfs(self.tmp_dir.name) + return ffs + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 5f579465e5395f769797f4df7d55de2697a5887a Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Sun, 16 Jun 2024 23:53:21 +0200 Subject: [PATCH 03/17] Fix type errors --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 6 +++--- src/py/flwr/server/superlink/ffs/ffs_test.py | 21 ++++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 46e102639d4..aab5ba3d424 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -24,14 +24,14 @@ def write_dict_to_file(data_dict: Dict[str, str], file_path: str) -> None: """Write a Dict to a file in JSON format.""" - with open(file_path, "w") as file: + with open(file_path, "w", encoding="utf-8") as file: json.dump(data_dict, file) def read_dict_from_file(file_path: str) -> Dict[str, str]: """Read a Dict from a JSON file.""" - with open(file_path) as file: - data_dict = json.load(file) + with open(file_path, encoding="utf-8") as file: + data_dict: Dict[str, str] = json.load(file) return data_dict diff --git a/src/py/flwr/server/superlink/ffs/ffs_test.py b/src/py/flwr/server/superlink/ffs/ffs_test.py index fef765c7ac9..3baacbe0489 100644 --- a/src/py/flwr/server/superlink/ffs/ffs_test.py +++ b/src/py/flwr/server/superlink/ffs/ffs_test.py @@ -21,6 +21,7 @@ import tempfile import unittest from abc import abstractmethod +from typing import Dict from flwr.server.superlink.ffs import DiskFfs, Ffs @@ -31,6 +32,8 @@ class FfsTest(unittest.TestCase): # This is to True in each child class __test__ = False + tmp_dir: tempfile.TemporaryDirectory # type: ignore + @abstractmethod def ffs_factory(self) -> Ffs: """Provide Ffs implementation to test.""" @@ -60,13 +63,15 @@ def test_get(self) -> None: ffs: Ffs = self.ffs_factory() content_expected = b"content" hash_expected = hashlib.sha256(content_expected).hexdigest() - meta_expected = {} + meta_expected: Dict[str, str] = {"meta_key": "meta_value"} with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: file.write(content_expected) with open( - os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), + "w", + encoding="utf-8", ) as file: json.dump(meta_expected, file) @@ -83,13 +88,15 @@ def test_delete(self) -> None: ffs: Ffs = self.ffs_factory() content_expected = b"content" hash_expected = hashlib.sha256(content_expected).hexdigest() - meta_expected = {} + meta_expected: Dict[str, str] = {"meta_key": "meta_value"} with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: file.write(content_expected) with open( - os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), + "w", + encoding="utf-8", ) as file: json.dump(meta_expected, file) @@ -105,13 +112,15 @@ def test_list(self) -> None: ffs: Ffs = self.ffs_factory() content_expected = b"content" hash_expected = hashlib.sha256(content_expected).hexdigest() - meta_expected = {} + meta_expected: Dict[str, str] = {"meta_key": "meta_value"} with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: file.write(content_expected) with open( - os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), "w" + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), + "w", + encoding="utf-8", ) as file: json.dump(meta_expected, file) From adeeb3ceff6a2589d1e108bee6da5a1213f8764d Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Mon, 17 Jun 2024 00:05:42 +0200 Subject: [PATCH 04/17] Fix --- src/py/flwr/server/superlink/ffs/ffs_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/ffs_test.py b/src/py/flwr/server/superlink/ffs/ffs_test.py index 3baacbe0489..3b25ac7b206 100644 --- a/src/py/flwr/server/superlink/ffs/ffs_test.py +++ b/src/py/flwr/server/superlink/ffs/ffs_test.py @@ -55,7 +55,9 @@ def test_put(self) -> None: assert hash_actual == hash_expected # Check if file was created - assert [hash_expected, f"{hash_expected}.META"] == os.listdir(self.tmp_dir.name) + assert {hash_expected, f"{hash_expected}.META"} == set( + os.listdir(self.tmp_dir.name) + ) def test_get(self) -> None: """Test if object can be retrieved.""" @@ -104,7 +106,7 @@ def test_delete(self) -> None: ffs.delete(hash_expected) # Assert - assert [] == os.listdir(self.tmp_dir.name) + assert set() == set(os.listdir(self.tmp_dir.name)) def test_list(self) -> None: """Test if object hashes can be listed.""" @@ -128,7 +130,7 @@ def test_list(self) -> None: hashes = ffs.list() # Assert - assert [hash_expected] == hashes + assert {hash_expected} == set(hashes) class DiskFfsTest(FfsTest, unittest.TestCase): From ab76f278a9a652b64c73f5bb4829f104784692d6 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 19 Jul 2024 11:48:38 +0200 Subject: [PATCH 05/17] Fix docstring --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index aab5ba3d424..6b1d8151c0c 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -49,7 +49,7 @@ def __init__(self, base_dir: str) -> None: self.base_dir = base_dir def put(self, content: bytes, meta: Dict[str, str]) -> str: - """Store bytes and metadata. Return sha256hex hash of data as str. + """Store bytes and metadata and returns sha256hex hash of data as str. Parameters ---------- From d565c87822a1f62e62d58cfb27bf03687fd411b9 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 19 Jul 2024 12:18:35 +0200 Subject: [PATCH 06/17] Sort _all_ --- src/py/flwr/server/superlink/ffs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/__init__.py b/src/py/flwr/server/superlink/ffs/__init__.py index 4b2ee7a2e2a..0273d2a630e 100644 --- a/src/py/flwr/server/superlink/ffs/__init__.py +++ b/src/py/flwr/server/superlink/ffs/__init__.py @@ -19,6 +19,6 @@ from .ffs import Ffs as Ffs __all__ = [ - "Ffs", "DiskFfs", + "Ffs", ] From 4345891eeed1361c7c799f56e691a6b638fa4520 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 19 Jul 2024 17:15:56 +0200 Subject: [PATCH 07/17] Use Pathlib instead of os.path --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 35 +++++--------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 6b1d8151c0c..b8893f14fd8 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -16,25 +16,12 @@ import hashlib import json -import os +from pathlib import Path from typing import Dict, List, Tuple from flwr.server.superlink.ffs.ffs import Ffs -def write_dict_to_file(data_dict: Dict[str, str], file_path: str) -> None: - """Write a Dict to a file in JSON format.""" - with open(file_path, "w", encoding="utf-8") as file: - json.dump(data_dict, file) - - -def read_dict_from_file(file_path: str) -> Dict[str, str]: - """Read a Dict from a JSON file.""" - with open(file_path, encoding="utf-8") as file: - data_dict: Dict[str, str] = json.load(file) - return data_dict - - class DiskFfs(Ffs): # pylint: disable=R0904 """Disk based Flower File Storage interface for large objects.""" @@ -46,7 +33,7 @@ def __init__(self, base_dir: str) -> None: base_dir : string The base directory to store the objects. """ - self.base_dir = base_dir + self.base_dir = Path(base_dir) def put(self, content: bytes, meta: Dict[str, str]) -> str: """Store bytes and metadata and returns sha256hex hash of data as str. @@ -65,10 +52,8 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: """ content_hash = hashlib.sha256(content).hexdigest() - with open(os.path.join(self.base_dir, content_hash), "wb") as file: - file.write(content) - - write_dict_to_file(meta, os.path.join(self.base_dir, f"{content_hash}.META")) + (self.base_dir / content_hash).write_bytes(content) + (self.base_dir / f"{content_hash}.META").write_text(json.dumps(meta)) return content_hash @@ -85,10 +70,8 @@ def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: Tuple[bytes, Dict[str, str]] A tuple containing the object and it's metadata. """ - with open(os.path.join(self.base_dir, key), "rb") as file: - content = file.read() - - meta = read_dict_from_file(os.path.join(self.base_dir, f"{key}.META")) + content = (self.base_dir / key).read_bytes() + meta = json.loads((self.base_dir / f"{key}.META").read_text()) return content, meta @@ -100,8 +83,8 @@ def delete(self, key: str) -> None: hash : string The sha256hex hash of the object to be deleted. """ - os.remove(os.path.join(self.base_dir, key)) - os.remove(os.path.join(self.base_dir, f"{key}.META")) + (self.base_dir / key).unlink() + (self.base_dir / f"{key}.META").unlink() def list(self) -> List[str]: """List all keys. @@ -115,5 +98,5 @@ def list(self) -> List[str]: A list of all keys. """ return [ - item for item in os.listdir(self.base_dir) if not item.endswith(".META") + item.name for item in self.base_dir.iterdir() if not item.suffix == ".META" ] From d570b262a6a92477e20219c9d3718adf079dc231 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Tue, 30 Jul 2024 20:54:56 +0200 Subject: [PATCH 08/17] Add makedir --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 1 + src/py/flwr/server/superlink/ffs/ffs.py | 2 +- .../flwr/server/superlink/ffs/ffs_factory.py | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/py/flwr/server/superlink/ffs/ffs_factory.py diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index b8893f14fd8..d113499e702 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -54,6 +54,7 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: (self.base_dir / content_hash).write_bytes(content) (self.base_dir / f"{content_hash}.META").write_text(json.dumps(meta)) + os.makedirs(self.base_dir, exist_ok=True) return content_hash diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py index d114cee0f0c..eab1cb5bf22 100644 --- a/src/py/flwr/server/superlink/ffs/ffs.py +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -24,7 +24,7 @@ class Ffs(abc.ABC): # pylint: disable=R0904 @abc.abstractmethod def put(self, content: bytes, meta: Dict[str, str]) -> str: - """Store bytes and metadata. Return sha256hex hash of data as str. + """Store bytes and metadata and return sha256hex hash of data as str. Parameters ---------- diff --git a/src/py/flwr/server/superlink/ffs/ffs_factory.py b/src/py/flwr/server/superlink/ffs/ffs_factory.py new file mode 100644 index 00000000000..fed078a6c5c --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs_factory.py @@ -0,0 +1,45 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Factory class that creates Ffs instances.""" + + +from logging import DEBUG +from typing import Optional + +from flwr.common.logger import log + +from .disk_ffs import DiskFfs +from .ffs import Ffs + + +class FfsFactory: + """Factory class that creates Ffs instances. + + Parameters + ---------- + base_dir : str + The base directory to store the objects. + """ + + def __init__(self, base_dir: str) -> None: + self.base_dir = base_dir + self.ffs_instance: Optional[Ffs] = None + + def ffs(self) -> Ffs: + """Return a Ffs instance and create it, if necessary.""" + # SqliteState + ffs = DiskFfs(self.base_dir) + log(DEBUG, "Using Disk Flower File System") + return ffs From 8c213f07de69a623a4c1fe48eb05c142fb434c61 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Tue, 30 Jul 2024 20:55:28 +0200 Subject: [PATCH 09/17] Fix makedir --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index d113499e702..0bbb798d271 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -52,9 +52,9 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: """ content_hash = hashlib.sha256(content).hexdigest() + self.base_dir.mkdir(exist_ok=True, parents=True) (self.base_dir / content_hash).write_bytes(content) (self.base_dir / f"{content_hash}.META").write_text(json.dumps(meta)) - os.makedirs(self.base_dir, exist_ok=True) return content_hash From 8264956793acacbd0f045e0fe9a3512ad03ed5f1 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Sun, 11 Aug 2024 21:49:51 +0200 Subject: [PATCH 10/17] Fix ffs --- src/py/flwr/server/superlink/ffs/ffs.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py index eab1cb5bf22..37df62167ae 100644 --- a/src/py/flwr/server/superlink/ffs/ffs.py +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -35,23 +35,23 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: Returns ------- - sha256hex : string - The sha256hex hash of the content. + str + The key (sha256hex hash) of the content. """ @abc.abstractmethod def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: - """Return tuple containing the object and it's meta fields. + """Return tuple containing the object content and metadata. Parameters ---------- - hash : string - The sha256hex hash of the object to be retrieved. + key : str + The key (sha256hex hash) of the object to be retrieved. Returns ------- Tuple[bytes, Dict[str, str]] - A tuple containing the object and it's metadata. + A tuple containing the object content and metadata. """ @abc.abstractmethod @@ -60,19 +60,20 @@ def delete(self, key: str) -> None: Parameters ---------- - hash : string - The sha256hex hash of the object to be deleted. + key : str + The key (sha256hex hash) of the object to be deleted. """ @abc.abstractmethod def list(self) -> List[str]: - """List all keys. + """List keys of all stored objects. - Can be used to list all keys in the storage and e.g. to clean up - the storage with the delete method. + Return all available keys in this `Ffs` instance. + This can be combined with, for example, + the `delete` method to delete objects. Returns ------- List[str] - A list of all keys. + A list of all available keys. """ From fa8ab39e6037ac53fbffa79d8a0602a72f70a5ae Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Sun, 11 Aug 2024 21:52:23 +0200 Subject: [PATCH 11/17] Improve docstrings --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 0bbb798d271..85103dce91d 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -30,7 +30,7 @@ def __init__(self, base_dir: str) -> None: Parameters ---------- - base_dir : string + base_dir : str The base directory to store the objects. """ self.base_dir = Path(base_dir) @@ -47,8 +47,8 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: Returns ------- - sha256hex : string - The sha256hex hash of the content. + str + The key (sha256hex hash) of the content. """ content_hash = hashlib.sha256(content).hexdigest() @@ -59,17 +59,17 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: return content_hash def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: - """Return tuple containing the object and it's meta fields. + """Return tuple containing the object content and metadata. Parameters ---------- - hash : string + key : str The sha256hex hash of the object to be retrieved. Returns ------- Tuple[bytes, Dict[str, str]] - A tuple containing the object and it's metadata. + A tuple containing the object content and metadata. """ content = (self.base_dir / key).read_bytes() meta = json.loads((self.base_dir / f"{key}.META").read_text()) @@ -81,7 +81,7 @@ def delete(self, key: str) -> None: Parameters ---------- - hash : string + key : str The sha256hex hash of the object to be deleted. """ (self.base_dir / key).unlink() @@ -96,7 +96,7 @@ def list(self) -> List[str]: Returns ------- List[str] - A list of all keys. + A list of all available keys. """ return [ item.name for item in self.base_dir.iterdir() if not item.suffix == ".META" From 04e89dca065f5afb44222c8fe38d01ba3332336c Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 12 Aug 2024 22:26:01 +0200 Subject: [PATCH 12/17] Update src/py/flwr/server/superlink/ffs/disk_ffs.py --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 85103dce91d..908994bf634 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -23,7 +23,7 @@ class DiskFfs(Ffs): # pylint: disable=R0904 - """Disk based Flower File Storage interface for large objects.""" + """Disk-based Flower File Storage interface for large objects.""" def __init__(self, base_dir: str) -> None: """Create a new DiskFfs instance. From 24ddd6dd04a64206d936619192bd5afe9681369a Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Mon, 12 Aug 2024 22:26:01 +0200 Subject: [PATCH 13/17] Remove ffs factory --- .../flwr/server/superlink/ffs/ffs_factory.py | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 src/py/flwr/server/superlink/ffs/ffs_factory.py diff --git a/src/py/flwr/server/superlink/ffs/ffs_factory.py b/src/py/flwr/server/superlink/ffs/ffs_factory.py deleted file mode 100644 index fed078a6c5c..00000000000 --- a/src/py/flwr/server/superlink/ffs/ffs_factory.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2024 Flower Labs GmbH. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================== -"""Factory class that creates Ffs instances.""" - - -from logging import DEBUG -from typing import Optional - -from flwr.common.logger import log - -from .disk_ffs import DiskFfs -from .ffs import Ffs - - -class FfsFactory: - """Factory class that creates Ffs instances. - - Parameters - ---------- - base_dir : str - The base directory to store the objects. - """ - - def __init__(self, base_dir: str) -> None: - self.base_dir = base_dir - self.ffs_instance: Optional[Ffs] = None - - def ffs(self) -> Ffs: - """Return a Ffs instance and create it, if necessary.""" - # SqliteState - ffs = DiskFfs(self.base_dir) - log(DEBUG, "Using Disk Flower File System") - return ffs From 35c2bd1f0e1d8c4a1ddd0621cebccd8675010711 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 12 Aug 2024 22:37:47 +0200 Subject: [PATCH 14/17] Update src/py/flwr/server/superlink/ffs/disk_ffs.py --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 908994bf634..9dac1737638 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -47,7 +47,7 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: Returns ------- - str + key : str The key (sha256hex hash) of the content. """ content_hash = hashlib.sha256(content).hexdigest() From 8ef14fb1ad4a35f2a3e95e19f473fe425aaff4c8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 12 Aug 2024 22:39:06 +0200 Subject: [PATCH 15/17] Update src/py/flwr/server/superlink/ffs/disk_ffs.py --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 9dac1737638..b0a78afad15 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -36,7 +36,7 @@ def __init__(self, base_dir: str) -> None: self.base_dir = Path(base_dir) def put(self, content: bytes, meta: Dict[str, str]) -> str: - """Store bytes and metadata and returns sha256hex hash of data as str. + """Store bytes and metadata and return key (hash of content). Parameters ---------- From 5fe921cc183810e7f3d5d0c1924263a0a908c9fc Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 12 Aug 2024 22:43:53 +0200 Subject: [PATCH 16/17] Update src/py/flwr/server/superlink/ffs/ffs.py --- src/py/flwr/server/superlink/ffs/ffs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py index 37df62167ae..622988141c9 100644 --- a/src/py/flwr/server/superlink/ffs/ffs.py +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -35,7 +35,7 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: Returns ------- - str + key : str The key (sha256hex hash) of the content. """ From 61854645ff9d1fbfca3085adf549b516544dca5a Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 12 Aug 2024 22:44:49 +0200 Subject: [PATCH 17/17] Update src/py/flwr/server/superlink/ffs/disk_ffs.py --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index b0a78afad15..5331af50046 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -90,8 +90,9 @@ def delete(self, key: str) -> None: def list(self) -> List[str]: """List all keys. - Can be used to list all keys in the storage and e.g. to clean up - the storage with the delete method. + Return all available keys in this `Ffs` instance. + This can be combined with, for example, + the `delete` method to delete objects. Returns -------