syft-flwr 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
syft_flwr/run.py ADDED
@@ -0,0 +1,63 @@
1
+ import warnings
2
+ from pathlib import Path
3
+ from uuid import uuid4
4
+
5
+ from flwr.clientapp.client_app import LoadClientAppError
6
+ from flwr.common import Context
7
+ from flwr.common.object_ref import load_app
8
+ from flwr.common.record import RecordDict
9
+ from flwr.server.server_app import LoadServerAppError
10
+
11
+ from syft_flwr.config import load_flwr_pyproject
12
+ from syft_flwr.flower_client import syftbox_flwr_client
13
+ from syft_flwr.flower_server import syftbox_flwr_server
14
+ from syft_flwr.run_simulation import run
15
+
16
+ __all__ = ["syftbox_run_flwr_client", "syftbox_run_flwr_server", "run"]
17
+
18
+
19
+ # Suppress Pydantic deprecation warnings
20
+ warnings.filterwarnings("ignore", category=DeprecationWarning, module="pydantic")
21
+
22
+
23
+ def syftbox_run_flwr_client(flower_project_dir: Path) -> None:
24
+ pyproject_conf = load_flwr_pyproject(flower_project_dir)
25
+ client_ref = pyproject_conf["tool"]["flwr"]["app"]["components"]["clientapp"]
26
+ app_name = pyproject_conf["tool"]["syft_flwr"]["app_name"]
27
+
28
+ context = Context(
29
+ run_id=uuid4().int,
30
+ node_id=uuid4().int,
31
+ node_config=pyproject_conf["tool"]["flwr"]["app"]["config"],
32
+ state=RecordDict(),
33
+ run_config=pyproject_conf["tool"]["flwr"]["app"]["config"],
34
+ )
35
+ client_app = load_app(
36
+ client_ref,
37
+ LoadClientAppError,
38
+ flower_project_dir,
39
+ )
40
+
41
+ syftbox_flwr_client(client_app, context, app_name)
42
+
43
+
44
+ def syftbox_run_flwr_server(flower_project_dir: Path) -> None:
45
+ pyproject_conf = load_flwr_pyproject(flower_project_dir)
46
+ datasites = pyproject_conf["tool"]["syft_flwr"]["datasites"]
47
+ server_ref = pyproject_conf["tool"]["flwr"]["app"]["components"]["serverapp"]
48
+ app_name = pyproject_conf["tool"]["syft_flwr"]["app_name"]
49
+
50
+ context = Context(
51
+ run_id=uuid4().int,
52
+ node_id=uuid4().int,
53
+ node_config=pyproject_conf["tool"]["flwr"]["app"]["config"],
54
+ state=RecordDict(),
55
+ run_config=pyproject_conf["tool"]["flwr"]["app"]["config"],
56
+ )
57
+ server_app = load_app(
58
+ server_ref,
59
+ LoadServerAppError,
60
+ flower_project_dir,
61
+ )
62
+
63
+ syftbox_flwr_server(server_app, context, datasites, app_name)
@@ -0,0 +1,328 @@
1
+ import asyncio
2
+ import os
3
+ import shutil
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import TypeAlias
8
+
9
+ from loguru import logger
10
+ from syft_core import Client
11
+ from syft_crypto import did_path, ensure_bootstrap, get_did_document, private_key_path
12
+ from syft_rds import init_session
13
+ from syft_rds.client.rds_client import RDSClient
14
+ from typing_extensions import Optional, Union
15
+
16
+ from syft_flwr.config import load_flwr_pyproject
17
+ from syft_flwr.consts import SYFT_FLWR_ENCRYPTION_ENABLED
18
+ from syft_flwr.utils import create_temp_client
19
+
20
+ PathLike: TypeAlias = Union[str, os.PathLike, Path]
21
+
22
+
23
+ def remove_rds_stack_dir(
24
+ key: str = "shared_client_dir", root_dir: Optional[PathLike] = None
25
+ ) -> None:
26
+ root_path = (
27
+ Path(root_dir).resolve() / key if root_dir else Path(tempfile.gettempdir(), key)
28
+ )
29
+
30
+ if not root_path.exists():
31
+ logger.warning(f"⚠️ Skipping removal, as path {root_path} does not exist")
32
+ return None
33
+
34
+ try:
35
+ shutil.rmtree(root_path)
36
+ logger.info(f"✅ Successfully removed directory {root_path}")
37
+ except Exception as e:
38
+ logger.error(f"❌ Failed to remove directory {root_path}: {e}")
39
+
40
+
41
+ def _setup_mock_rds_clients(
42
+ project_dir: Path, aggregator: str, datasites: list[str]
43
+ ) -> tuple[Path, list[RDSClient], RDSClient]:
44
+ """Setup mock RDS clients for the given project directory"""
45
+ simulated_syftbox_network_dir = Path(tempfile.gettempdir(), project_dir.name)
46
+ remove_rds_stack_dir(root_dir=simulated_syftbox_network_dir)
47
+
48
+ ds_syftbox_client = create_temp_client(
49
+ email=aggregator, workspace_dir=simulated_syftbox_network_dir
50
+ )
51
+ ds_rds_client = init_session(
52
+ host=aggregator, email=aggregator, syftbox_client=ds_syftbox_client
53
+ )
54
+
55
+ do_rds_clients = []
56
+ for datasite in datasites:
57
+ do_syftbox_client = create_temp_client(
58
+ email=datasite, workspace_dir=simulated_syftbox_network_dir
59
+ )
60
+ do_rds_client = init_session(
61
+ host=datasite, email=datasite, syftbox_client=do_syftbox_client
62
+ )
63
+ do_rds_clients.append(do_rds_client)
64
+
65
+ return simulated_syftbox_network_dir, do_rds_clients, ds_rds_client
66
+
67
+
68
+ def _bootstrap_encryption_keys(
69
+ do_clients: list[RDSClient], ds_client: RDSClient
70
+ ) -> None:
71
+ """Bootstrap the encryption keys for all clients if encryption is enabled."""
72
+ # Check if encryption is enabled
73
+ encryption_enabled = (
74
+ os.environ.get(SYFT_FLWR_ENCRYPTION_ENABLED, "true").lower() != "false"
75
+ )
76
+
77
+ if not encryption_enabled:
78
+ logger.warning("⚠️ Encryption disabled - skipping key bootstrap")
79
+ return
80
+
81
+ logger.info("🔐 Bootstrapping encryption keys for all participants...")
82
+
83
+ all_syftbox_clients: list[Client] = []
84
+
85
+ # Bootstrap server
86
+ try:
87
+ server_client: Client = ds_client._syftbox_client
88
+ ensure_bootstrap(server_client)
89
+ server_client_did_path = did_path(server_client, server_client.email)
90
+ server_client_private_key_path = private_key_path(server_client)
91
+ logger.debug(
92
+ f"✅ Server {ds_client.email} bootstrapped with private encryption keys at {server_client_private_key_path} and did path at {server_client_did_path}"
93
+ )
94
+ all_syftbox_clients.append(server_client)
95
+ except Exception as e:
96
+ logger.error(f"❌ Failed to bootstrap server {ds_client.email}: {e}")
97
+ raise
98
+
99
+ # Bootstrap each client
100
+ for do_client in do_clients:
101
+ try:
102
+ client: Client = do_client._syftbox_client
103
+ ensure_bootstrap(client)
104
+ client_did_path = did_path(client, client.email)
105
+ client_did_doc = get_did_document(client, client.email)
106
+ client_private_key_path = private_key_path(client)
107
+ logger.debug(
108
+ f"✅ Client {do_client.email} bootstrapped with private encryption keys at {client_private_key_path} and did path at {client_did_path} with content: {client_did_doc}"
109
+ )
110
+ all_syftbox_clients.append(client)
111
+ except Exception as e:
112
+ logger.error(f"❌ Failed to bootstrap client {do_client.email}: {e}")
113
+ raise
114
+
115
+ # Verify all DID documents are accessible
116
+ for checking_client in all_syftbox_clients:
117
+ for target_client in all_syftbox_clients:
118
+ if checking_client.email != target_client.email:
119
+ # Verify that checking_client can see target_client's DID document
120
+ did_file_path = did_path(checking_client, target_client.email)
121
+ if did_file_path.exists():
122
+ logger.debug(
123
+ f"✅ {checking_client.email} can see {target_client.email}'s DID at {did_file_path}"
124
+ )
125
+ else:
126
+ logger.warning(
127
+ f"⚠️ {checking_client.email} cannot find {target_client.email}'s DID at {did_file_path}"
128
+ )
129
+
130
+ logger.info("🔐 All participants bootstrapped for E2E encryption ✅✅✅")
131
+
132
+
133
+ async def _run_main_py(
134
+ main_py_path: Path,
135
+ config_path: Path,
136
+ client_email: str,
137
+ log_dir: Path,
138
+ dataset_path: Optional[Union[str, Path]] = None,
139
+ ) -> int:
140
+ """Run the `main.py` file for a given client"""
141
+ log_file_path = log_dir / f"{client_email}.log"
142
+
143
+ # setting up env variables
144
+ env = os.environ.copy()
145
+ env["SYFTBOX_CLIENT_CONFIG_PATH"] = str(config_path)
146
+ env["DATA_DIR"] = str(dataset_path)
147
+
148
+ # running the main.py file asynchronously in a subprocess
149
+ try:
150
+ with open(log_file_path, "w") as f:
151
+ process = await asyncio.create_subprocess_exec(
152
+ sys.executable, # Use the current Python executable
153
+ str(main_py_path),
154
+ "-s",
155
+ stdout=f,
156
+ stderr=f,
157
+ env=env,
158
+ )
159
+ return_code = await process.wait()
160
+ logger.debug(
161
+ f"`{client_email}` returns code {return_code} for running `{main_py_path}`"
162
+ )
163
+ return return_code
164
+ except Exception as e:
165
+ logger.error(f"Error running `{main_py_path}` for `{client_email}`: {e}")
166
+ return 1
167
+
168
+
169
+ async def _run_simulated_flwr_project(
170
+ project_dir: Path,
171
+ do_clients: list[RDSClient],
172
+ ds_client: RDSClient,
173
+ mock_dataset_paths: list[Union[str, Path]],
174
+ ) -> bool:
175
+ """Run all clients and server concurrently"""
176
+ run_success = True
177
+
178
+ log_dir = project_dir / "simulation_logs"
179
+ log_dir.mkdir(parents=True, exist_ok=True)
180
+ logger.info(f"📝 Log directory: {log_dir}")
181
+
182
+ main_py_path = project_dir / "main.py"
183
+
184
+ logger.info(
185
+ f"Running DS client '{ds_client.email}' with config path {ds_client._syftbox_client.config_path}"
186
+ )
187
+ ds_task: asyncio.Task = asyncio.create_task(
188
+ _run_main_py(
189
+ main_py_path,
190
+ ds_client._syftbox_client.config_path,
191
+ ds_client.email,
192
+ log_dir,
193
+ )
194
+ )
195
+
196
+ client_tasks: list[asyncio.Task] = []
197
+ for client, mock_dataset_path in zip(do_clients, mock_dataset_paths):
198
+ # check if the client has a mock dataset path
199
+ logger.info(
200
+ f"Running DO client '{client.email}' with config path {client._syftbox_client.config_path} on mock dataset {mock_dataset_path}"
201
+ )
202
+ client_tasks.append(
203
+ asyncio.create_task(
204
+ _run_main_py(
205
+ main_py_path,
206
+ client._syftbox_client.config_path,
207
+ client.email,
208
+ log_dir,
209
+ mock_dataset_path,
210
+ )
211
+ )
212
+ )
213
+
214
+ ds_return_code = await ds_task
215
+ if ds_return_code != 0:
216
+ run_success = False
217
+
218
+ # log out ds client logs
219
+ with open(log_dir / f"{ds_client.email}.log", "r") as log_file:
220
+ log_content = log_file.read().strip()
221
+ logger.info(f"DS client '{ds_client.email}' logs:\n{log_content}")
222
+
223
+ # cancel all client tasks if DS client returns
224
+ logger.debug("Cancelling DO client tasks as DS client returned")
225
+ for task in client_tasks:
226
+ if not task.done():
227
+ task.cancel()
228
+
229
+ await asyncio.gather(*client_tasks, return_exceptions=True)
230
+
231
+ return run_success
232
+
233
+
234
+ def validate_bootstraped_project(project_dir: Path) -> None:
235
+ """Validate a bootstraped `syft_flwr` project directory"""
236
+ if not project_dir.exists():
237
+ raise FileNotFoundError(f"Project directory {project_dir} does not exist")
238
+
239
+ if not project_dir.is_dir():
240
+ raise NotADirectoryError(f"Project directory {project_dir} is not a directory")
241
+
242
+ if not (project_dir / "main.py").exists():
243
+ raise FileNotFoundError(f"main.py not found at {project_dir}")
244
+
245
+ if not (project_dir / "pyproject.toml").exists():
246
+ raise FileNotFoundError(f"pyproject.toml not found at {project_dir}")
247
+
248
+
249
+ def _validate_mock_dataset_paths(mock_dataset_paths: list[str]) -> list[Path]:
250
+ """Validate the mock dataset paths"""
251
+ resolved_paths = []
252
+ for path in mock_dataset_paths:
253
+ path = Path(path).expanduser().resolve()
254
+ if not path.exists():
255
+ raise ValueError(f"Mock dataset path {path} does not exist")
256
+ resolved_paths.append(path)
257
+ return resolved_paths
258
+
259
+
260
+ def run(
261
+ project_dir: Union[str, Path], mock_dataset_paths: list[Union[str, Path]]
262
+ ) -> Union[bool, asyncio.Task]:
263
+ """Run a syft_flwr project in simulation mode over mock data.
264
+
265
+ Returns:
266
+ bool: True if simulation succeeded, False otherwise (synchronous execution)
267
+ asyncio.Task: Task handle if running in async environment (e.g., Jupyter)
268
+ """
269
+
270
+ project_dir = Path(project_dir).expanduser().resolve()
271
+ validate_bootstraped_project(project_dir)
272
+ mock_dataset_paths = _validate_mock_dataset_paths(mock_dataset_paths)
273
+
274
+ # Skip module validation during testing to avoid parallel test issues
275
+ skip_module_check = (
276
+ os.environ.get("SYFT_FLWR_SKIP_MODULE_CHECK", "false").lower() == "true"
277
+ )
278
+ pyproject_conf = load_flwr_pyproject(
279
+ project_dir, check_module=not skip_module_check
280
+ )
281
+ datasites = pyproject_conf["tool"]["syft_flwr"]["datasites"]
282
+ aggregator = pyproject_conf["tool"]["syft_flwr"]["aggregator"]
283
+
284
+ simulated_syftbox_network_dir, do_clients, ds_client = _setup_mock_rds_clients(
285
+ project_dir, aggregator, datasites
286
+ )
287
+
288
+ _bootstrap_encryption_keys(do_clients, ds_client)
289
+
290
+ simulation_success = False # Track success status
291
+
292
+ async def main():
293
+ nonlocal simulation_success
294
+ try:
295
+ run_success = await _run_simulated_flwr_project(
296
+ project_dir, do_clients, ds_client, mock_dataset_paths
297
+ )
298
+ simulation_success = run_success
299
+ if run_success:
300
+ logger.success("Simulation completed successfully ✅")
301
+ else:
302
+ logger.error("Simulation failed ❌")
303
+ except Exception as e:
304
+ logger.error(f"Simulation failed ❌: {e}")
305
+ simulation_success = False
306
+ finally:
307
+ # Clean up the RDS stack
308
+ remove_rds_stack_dir(simulated_syftbox_network_dir)
309
+ logger.debug(f"Removed RDS stack: {simulated_syftbox_network_dir}")
310
+ # Also remove the .syftbox folder that saves the config files and private keys
311
+ remove_rds_stack_dir(simulated_syftbox_network_dir.parent / ".syftbox")
312
+
313
+ return simulation_success
314
+
315
+ try:
316
+ loop = asyncio.get_running_loop()
317
+ logger.debug(f"Running in an environment with an existing event loop {loop}")
318
+ # We are in an environment with an existing event loop (like Jupyter)
319
+ task = asyncio.create_task(main())
320
+ return task # Return the task so callers can await it
321
+ except RuntimeError:
322
+ logger.debug("No existing event loop, creating and running one")
323
+ # No existing event loop, create and run one (for scripts)
324
+ loop = asyncio.new_event_loop()
325
+ asyncio.set_event_loop(loop)
326
+ loop.run_until_complete(main())
327
+ loop.close()
328
+ return simulation_success # Return success status for synchronous execution
syft_flwr/serde.py ADDED
@@ -0,0 +1,15 @@
1
+ from flwr.common.message import Message
2
+ from flwr.common.serde import message_from_proto, message_to_proto
3
+ from flwr.proto.message_pb2 import Message as ProtoMessage
4
+
5
+
6
+ def bytes_to_flower_message(data: bytes) -> Message:
7
+ message_pb = ProtoMessage()
8
+ message_pb.ParseFromString(data)
9
+ message = message_from_proto(message_pb)
10
+ return message
11
+
12
+
13
+ def flower_message_to_bytes(message: Message) -> bytes:
14
+ msg_proto = message_to_proto(message)
15
+ return msg_proto.SerializeToString()
@@ -0,0 +1,3 @@
1
+ from syft_flwr.strategy.fedavg import FedAvgWithModelSaving
2
+
3
+ __all__ = ["FedAvgWithModelSaving"]
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+
3
+ from loguru import logger
4
+ from safetensors.numpy import save_file
5
+
6
+ from flwr.common import parameters_to_ndarrays
7
+ from flwr.server.strategy import FedAvg
8
+
9
+
10
+ class FedAvgWithModelSaving(FedAvg):
11
+ """This is a custom strategy that behaves exactly like
12
+ FedAvg with the difference of storing of the state of
13
+ the global model to disk after each round.
14
+ Ref: https://discuss.flower.ai/t/how-do-i-save-the-global-model-after-training/71/2
15
+ """
16
+
17
+ def __init__(self, save_path: str, *args, **kwargs):
18
+ self.save_path = Path(save_path)
19
+ self.save_path.mkdir(exist_ok=True, parents=True)
20
+ super().__init__(*args, **kwargs)
21
+
22
+ def _save_global_model(self, server_round: int, parameters):
23
+ """A new method to save the parameters to disk."""
24
+ ndarrays = parameters_to_ndarrays(parameters)
25
+ tensor_dict = {f"layer_{i}": array for i, array in enumerate(ndarrays)}
26
+ filename = self.save_path / f"parameters_round_{server_round}.safetensors"
27
+ if not self.save_path.exists():
28
+ logger.error(
29
+ f"Save directory {self.save_path} does NOT exist! Maybe it's deleted or moved."
30
+ )
31
+ else:
32
+ save_file(tensor_dict, str(filename))
33
+ logger.info(f"Checkpoint saved to: {filename}")
34
+
35
+ def evaluate(self, server_round: int, parameters):
36
+ """Evaluate model parameters using an evaluation function."""
37
+ self._save_global_model(server_round, parameters)
38
+ return super().evaluate(server_round, parameters)
@@ -0,0 +1,31 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from syft_core import Client
6
+
7
+ from syft_flwr.config import load_flwr_pyproject
8
+ from syft_flwr.run import syftbox_run_flwr_client, syftbox_run_flwr_server
9
+
10
+ DATA_DIR = os.getenv("DATA_DIR")
11
+ OUTPUT_DIR = os.getenv("OUTPUT_DIR")
12
+
13
+
14
+ flower_project_dir = Path(__file__).parent.absolute()
15
+ client = Client.load()
16
+ config = load_flwr_pyproject(flower_project_dir)
17
+
18
+ is_client = client.email in config["tool"]["syft_flwr"]["datasites"]
19
+ is_server = client.email in config["tool"]["syft_flwr"]["aggregator"]
20
+
21
+ if is_client:
22
+ # run by each DO
23
+ syftbox_run_flwr_client(flower_project_dir)
24
+ sys.exit(0) # Exit cleanly after client work completes
25
+ elif is_server:
26
+ # run by the DS
27
+ syftbox_run_flwr_server(flower_project_dir)
28
+ sys.exit(0) # Exit cleanly after server work completes
29
+ else:
30
+ raise ValueError(f"{client.email} is not in config.datasites or config.aggregator")
31
+ sys.exit(1) # Exit with error code
syft_flwr/utils.py ADDED
@@ -0,0 +1,126 @@
1
+ import os
2
+ import re
3
+ import zlib
4
+ from pathlib import Path
5
+
6
+ from flwr.common import Metadata
7
+ from flwr.common.message import Error, Message
8
+ from flwr.common.record import RecordDict
9
+ from loguru import logger
10
+ from syft_core import Client, SyftClientConfig
11
+ from syft_crypto.x3dh_bootstrap import ensure_bootstrap
12
+ from typing_extensions import Optional, Tuple
13
+
14
+ from syft_flwr.consts import SYFT_FLWR_ENCRYPTION_ENABLED
15
+
16
+ EMAIL_REGEX = r"^[^@]+@[^@]+\.[^@]+$"
17
+
18
+
19
+ def is_valid_datasite(datasite: str) -> bool:
20
+ return re.match(EMAIL_REGEX, datasite)
21
+
22
+
23
+ def str_to_int(input_string: str) -> int:
24
+ """Convert a string to an int32"""
25
+ return zlib.crc32(input_string.encode())
26
+
27
+
28
+ def get_syftbox_dataset_path() -> Path:
29
+ """Get the path to the syftbox dataset from the environment variable"""
30
+ data_dir = Path(os.getenv("DATA_DIR", ".data/"))
31
+ if not data_dir.exists():
32
+ raise FileNotFoundError(
33
+ f"Path {data_dir} does not exist (must be a valid file or directory)"
34
+ )
35
+ return data_dir
36
+
37
+
38
+ def run_syft_flwr() -> bool:
39
+ """Util function to check if we are running with syft_flwr or plain flwr
40
+ Currently only checks the `DATA_DIR` environment variable.
41
+ """
42
+ try:
43
+ get_syftbox_dataset_path()
44
+ return True
45
+ except FileNotFoundError:
46
+ return False
47
+
48
+
49
+ def create_temp_client(email: str, workspace_dir: Path) -> Client:
50
+ """Create a temporary Client instance for testing"""
51
+ workspace_hash = hash(str(workspace_dir)) % 10000
52
+ server_port = 8080 + workspace_hash
53
+ client_port = 8082 + workspace_hash
54
+ config: SyftClientConfig = SyftClientConfig(
55
+ email=email,
56
+ data_dir=workspace_dir,
57
+ server_url=f"http://localhost:{server_port}",
58
+ client_url=f"http://localhost:{client_port}",
59
+ path=workspace_dir / ".syftbox" / f"{email.split('@')[0]}_config.json",
60
+ ).save()
61
+ logger.debug(f"Created temp client {email} with config {config}")
62
+ return Client(config)
63
+
64
+
65
+ def setup_client(app_name: str) -> Tuple[Client, bool, str]:
66
+ """Setup SyftBox client and encryption."""
67
+ client = Client.load()
68
+
69
+ # Check encryption setting
70
+ encryption_enabled = (
71
+ os.environ.get(SYFT_FLWR_ENCRYPTION_ENABLED, "true").lower() != "false"
72
+ )
73
+
74
+ # Bootstrap encryption if needed
75
+ if encryption_enabled:
76
+ client = ensure_bootstrap(client)
77
+ logger.info("🔐 End-to-end encryption is ENABLED for FL messages")
78
+ else:
79
+ logger.warning("⚠️ Encryption disabled - skipping client key bootstrap")
80
+ logger.warning(
81
+ "⚠️ End-to-end encryption is DISABLED for FL messages (development mode / insecure)"
82
+ )
83
+
84
+ return client, encryption_enabled, f"flwr/{app_name}"
85
+
86
+
87
+ def check_reply_to_field(metadata: Metadata) -> bool:
88
+ """Check if reply_to field is empty (Flower 1.17+ format)."""
89
+ return metadata.reply_to_message_id == ""
90
+
91
+
92
+ def create_flwr_message(
93
+ content: RecordDict,
94
+ message_type: str,
95
+ dst_node_id: int,
96
+ group_id: str,
97
+ ttl: Optional[float] = None,
98
+ error: Optional[Error] = None,
99
+ reply_to: Optional[Message] = None,
100
+ ) -> Message:
101
+ """Create a Flower message (requires Flower >= 1.17)."""
102
+ if reply_to is not None:
103
+ if error is not None:
104
+ return Message(reply_to=reply_to, error=error)
105
+ return Message(content=content, reply_to=reply_to)
106
+ else:
107
+ # Allow standalone error messages when we can't parse the original message
108
+ if error is not None:
109
+ logger.warning(
110
+ "Creating error message without reply_to (failed to parse request)"
111
+ )
112
+ return Message(
113
+ content=RecordDict(),
114
+ dst_node_id=dst_node_id,
115
+ message_type=message_type,
116
+ ttl=ttl,
117
+ group_id=group_id,
118
+ error=error,
119
+ )
120
+ return Message(
121
+ content=content,
122
+ dst_node_id=dst_node_id,
123
+ message_type=message_type,
124
+ ttl=ttl,
125
+ group_id=group_id,
126
+ )
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: syft-flwr
3
+ Version: 0.4.3
4
+ Summary: syft_flwr is an open source framework that facilitate federated learning projects using Flower over the SyftBox protocol
5
+ License-File: LICENSE
6
+ Requires-Python: <3.14,>=3.11
7
+ Requires-Dist: flwr-datasets[vision]>=0.5.0
8
+ Requires-Dist: flwr[simulation]==1.23.0
9
+ Requires-Dist: loguru>=0.7.3
10
+ Requires-Dist: safetensors>=0.6.2
11
+ Requires-Dist: syft-rds>=0.5.0
12
+ Requires-Dist: tomli-w>=1.2.0
13
+ Requires-Dist: tomli>=2.3.0
14
+ Requires-Dist: typing-extensions>=4.13.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # syft_flwr
18
+
19
+ `syft_flwr` is an open source framework that facilitate federated learning (FL) projects using [Flower](https://github.com/adap/flower) over the [SyftBox](https://github.com/OpenMined/syftbox) protocol
20
+
21
+ ![FL Training Process](https://github.com/OpenMined/syft-flwr/raw/main/notebooks/fl-diabetes-prediction/images/fltraining.gif)
22
+
23
+ ## Example Usages
24
+ Please look at the `notebooks/` folder for example use cases:
25
+ - [FL diabetes prediction](notebooks/fl-diabetes-prediction/README.md) shows how to train a federated model over distributed machines for multiple rounds
26
+ - [Federated analytics](notebooks/federated-analytics-diabetes/README.md) shows how to query statistics from private datasets from distributed machines and then aggregate them
27
+ - [FedRAG (Federated RAG)](notebooks/fedrag/README.md) demonstrates privacy-preserving question answering using Retrieval Augmented Generation across distributed document sources with remote data science workflow
28
+
29
+ ## Development
30
+ ### Releasing
31
+ See [RELEASE.md](RELEASE.md) for the complete release process.
@@ -0,0 +1,21 @@
1
+ syft_flwr/__init__.py,sha256=qv6zlv2IIqGKbasMfg9lxF1seQFruuG0WPBMQ6KaVS0,426
2
+ syft_flwr/bootstrap.py,sha256=mtEWo5z7HZwf73KvxYZOdgXFBzTsNMxRaugByRuYJyU,3578
3
+ syft_flwr/cli.py,sha256=imctwdQMxQeGQZaiKSX1Mo2nU_-RmA-cGB3H4huuUeA,3274
4
+ syft_flwr/config.py,sha256=4hwkovGtFOLNULjJwoGYcA0uT4y3vZSrxndXqYXquMY,821
5
+ syft_flwr/consts.py,sha256=u3QK-Wp8D2Va7iZcp5z4ormVm_FAUDeK4u-w81UL_eY,107
6
+ syft_flwr/flower_client.py,sha256=F6YA_abPjOlXdu0DNw9H6w1pVrXtkxMfpsAXLXBk8DU,7433
7
+ syft_flwr/flower_server.py,sha256=ZNDUR1U79M0BaG7n39TGUkVHV2NYi-LDsN8FqKJFfFQ,1508
8
+ syft_flwr/grid.py,sha256=WCEjrHJNZQBSnhIOKfU-gTAm7p_DIyBxNtTx-rdjmVE,21646
9
+ syft_flwr/mounts.py,sha256=hp0TKVot16SaPYO10Y_mSJGei7aiNteJfK4U4vynWmU,2330
10
+ syft_flwr/run.py,sha256=9KkRvT99yGX4beAMR2oKG0__b9gXdsE2Y2XKbAtELec,2147
11
+ syft_flwr/run_simulation.py,sha256=KPokzKvS6XWselHE0vQ3ZsYjxhieuAFTdZBisOogIkw,12182
12
+ syft_flwr/serde.py,sha256=5fCI-cRUOh5wE7cXQd4J6jex1grRGnyD1Jx-VlEDOXM,495
13
+ syft_flwr/utils.py,sha256=7DNNPAFoy0GDxB3qV6Yt6uP0tGxb-ms6nt_QlcE6T7k,4063
14
+ syft_flwr/strategy/__init__.py,sha256=mpUmExjjFkqU8gg41XsOBKfO3aqCBe7XPJSU-_P7smU,97
15
+ syft_flwr/strategy/fedavg.py,sha256=N8jULUkjvuaBIEVINowyQln8W8yFhkO-J8k0-iPcGMA,1562
16
+ syft_flwr/templates/main.py.tpl,sha256=u6IbtNGv6YyEx60DxVM1FLSeMAFjXL-KSD1lNGWSifg,962
17
+ syft_flwr-0.4.3.dist-info/METADATA,sha256=7NgLuczwcuf8McinTZ4GvS1YB3EsbJ_NaMR4JU96IZw,1615
18
+ syft_flwr-0.4.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ syft_flwr-0.4.3.dist-info/entry_points.txt,sha256=o7oT0dCoHn-3WyIwdDw1lBh2q-GvhB_8s0hWeJU4myc,49
20
+ syft_flwr-0.4.3.dist-info/licenses/LICENSE,sha256=0msOUar8uPZTqkAOTBp4rCzd7Jl9eRhfKiNufwrsg7k,11361
21
+ syft_flwr-0.4.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ syft_flwr = syft_flwr.cli:main