syft-flwr 0.2.2__tar.gz → 0.4.1__tar.gz
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.
Potentially problematic release.
This version of syft-flwr might be problematic. Click here for more details.
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/PKG-INFO +10 -6
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/README.md +6 -2
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/pyproject.toml +20 -8
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/__init__.py +1 -1
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/bootstrap.py +1 -1
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/flower_client.py +21 -7
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/grid.py +48 -26
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/run_simulation.py +29 -5
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/templates/main.py.tpl +4 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/utils.py +12 -1
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/.gitignore +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/LICENSE +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/cli.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/config.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/consts.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/flower_server.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/mounts.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/run.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/serde.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/strategy/__init__.py +0 -0
- {syft_flwr-0.2.2 → syft_flwr-0.4.1}/src/syft_flwr/strategy/fedavg.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syft-flwr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: syft_flwr is an open source framework that facilitate federated learning projects using Flower over the SyftBox protocol
|
|
5
5
|
License-File: LICENSE
|
|
6
|
-
Requires-Python:
|
|
6
|
+
Requires-Python: <3.14,>=3.11
|
|
7
7
|
Requires-Dist: flwr-datasets[vision]>=0.5.0
|
|
8
8
|
Requires-Dist: flwr[simulation]==1.21.0
|
|
9
9
|
Requires-Dist: loguru>=0.7.3
|
|
10
10
|
Requires-Dist: safetensors>=0.6.2
|
|
11
|
-
Requires-Dist: syft-rds>=0.
|
|
11
|
+
Requires-Dist: syft-rds>=0.4.2
|
|
12
12
|
Requires-Dist: tomli-w>=1.2.0
|
|
13
|
-
Requires-Dist: tomli>=2.
|
|
13
|
+
Requires-Dist: tomli>=2.3.0
|
|
14
14
|
Requires-Dist: typing-extensions>=4.13.0
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
@@ -18,10 +18,14 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
-

|
|
21
|
+

|
|
22
22
|
|
|
23
23
|
## Example Usages
|
|
24
24
|
Please look at the `notebooks/` folder for example use cases:
|
|
25
25
|
- [FL diabetes prediction](notebooks/fl-diabetes-prediction/README.md) shows how to train a federated model over distributed machines for multiple rounds
|
|
26
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
|
|
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.
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
`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
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
## Example Usages
|
|
8
8
|
Please look at the `notebooks/` folder for example use cases:
|
|
9
9
|
- [FL diabetes prediction](notebooks/fl-diabetes-prediction/README.md) shows how to train a federated model over distributed machines for multiple rounds
|
|
10
10
|
- [Federated analytics](notebooks/federated-analytics-diabetes/README.md) shows how to query statistics from private datasets from distributed machines and then aggregate them
|
|
11
|
-
- [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
|
|
11
|
+
- [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
|
|
12
|
+
|
|
13
|
+
## Development
|
|
14
|
+
### Releasing
|
|
15
|
+
See [RELEASE.md](RELEASE.md) for the complete release process.
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "syft-flwr"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "syft_flwr is an open source framework that facilitate federated learning projects using Flower over the SyftBox protocol"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.11,<3.14"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"syft-rds>=0.
|
|
8
|
+
"syft-rds>=0.4.2",
|
|
9
9
|
"flwr[simulation]==1.21.0",
|
|
10
10
|
"flwr-datasets[vision]>=0.5.0",
|
|
11
11
|
"loguru>=0.7.3",
|
|
12
12
|
"safetensors>=0.6.2",
|
|
13
13
|
"typing-extensions>=4.13.0",
|
|
14
|
-
"tomli>=2.
|
|
14
|
+
"tomli>=2.3.0",
|
|
15
15
|
"tomli-w>=1.2.0",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
# [tool.uv.sources]
|
|
19
|
-
# syft-rds = {
|
|
20
|
-
|
|
19
|
+
# syft-rds = { git = "https://github.com/OpenMined/syft-rds/", branch = "feature/python-runtime-uv-envs" } # for development
|
|
20
|
+
# syft-rds = { path = "../syft-rds", editable = true } # for development
|
|
21
21
|
|
|
22
22
|
[project.scripts]
|
|
23
23
|
syft_flwr = "syft_flwr.cli:main"
|
|
24
24
|
|
|
25
|
-
[
|
|
26
|
-
dev
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
27
|
"ipykernel>=6.30.1",
|
|
28
28
|
"ipywidgets>=8.1.7",
|
|
29
29
|
"pytest>=8.4.1",
|
|
@@ -33,6 +33,7 @@ dev-dependencies = [
|
|
|
33
33
|
"jupyterlab",
|
|
34
34
|
"pytest-xdist>=3.8.0",
|
|
35
35
|
"pytest-asyncio>=1.1.0",
|
|
36
|
+
"commitizen>=4.9.1",
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
[build-system]
|
|
@@ -56,3 +57,14 @@ extend-select = ["I"]
|
|
|
56
57
|
|
|
57
58
|
[tool.ruff.lint.per-file-ignores]
|
|
58
59
|
"**/__init__.py" = ["F401"]
|
|
60
|
+
|
|
61
|
+
[tool.commitizen]
|
|
62
|
+
name = "cz_conventional_commits"
|
|
63
|
+
version_provider = "pep621"
|
|
64
|
+
tag_format = "v$version"
|
|
65
|
+
update_changelog_on_bump = false
|
|
66
|
+
version_files = [
|
|
67
|
+
"pyproject.toml:^version\\s*=\\s*\"(?P<version>.*)\"$",
|
|
68
|
+
"src/syft_flwr/__init__.py:^__version__\\s*=\\s*\"(?P<version>.*)\"$",
|
|
69
|
+
"notebooks/*/pyproject.toml:\"syft-flwr>=(?P<version>[^\"]+)\"",
|
|
70
|
+
]
|
|
@@ -101,5 +101,5 @@ def bootstrap(
|
|
|
101
101
|
__copy_main_py(flwr_project_dir)
|
|
102
102
|
|
|
103
103
|
logger.info(
|
|
104
|
-
f"Successfully bootstrapped syft-flwr project at {flwr_project_dir} with datasites {datasites} and aggregator {aggregator} ✅"
|
|
104
|
+
f"Successfully bootstrapped syft-flwr project at {flwr_project_dir} with datasites {datasites} and aggregator '{aggregator}' ✅"
|
|
105
105
|
)
|
|
@@ -49,7 +49,9 @@ class MessageHandler:
|
|
|
49
49
|
error_reply = create_flwr_message(
|
|
50
50
|
content=RecordDict(),
|
|
51
51
|
reply_to=message,
|
|
52
|
-
message_type=message.metadata.message_type
|
|
52
|
+
message_type=message.metadata.message_type
|
|
53
|
+
if message
|
|
54
|
+
else MessageType.SYSTEM,
|
|
53
55
|
dst_node_id=message.metadata.src_node_id if message else 0,
|
|
54
56
|
group_id=message.metadata.group_id if message else "",
|
|
55
57
|
error=error,
|
|
@@ -127,11 +129,15 @@ class RequestProcessor:
|
|
|
127
129
|
logger.error(
|
|
128
130
|
f"❌ Failed to deserialize message from {original_sender}: {e}"
|
|
129
131
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"Request body preview (first 200 bytes): {str(request.body[:200])}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Can't create error reply without valid message - skip response
|
|
137
|
+
logger.warning(
|
|
138
|
+
"Skipping error reply (cannot create without valid parsed message)"
|
|
133
139
|
)
|
|
134
|
-
return
|
|
140
|
+
return None
|
|
135
141
|
|
|
136
142
|
# Handle message
|
|
137
143
|
try:
|
|
@@ -151,7 +157,6 @@ class RequestProcessor:
|
|
|
151
157
|
error = Error(
|
|
152
158
|
code=ErrorCode.CLIENT_APP_RAISED_EXCEPTION, reason=error_message
|
|
153
159
|
)
|
|
154
|
-
self.box._stop_event.set()
|
|
155
160
|
return self.message_handler.create_error_reply(message, error)
|
|
156
161
|
|
|
157
162
|
|
|
@@ -159,11 +164,20 @@ def syftbox_flwr_client(client_app: ClientApp, context: Context, app_name: str):
|
|
|
159
164
|
"""Run the Flower ClientApp with SyftBox."""
|
|
160
165
|
# Setup
|
|
161
166
|
client, encryption_enabled, syft_flwr_app_name = setup_client(app_name)
|
|
162
|
-
box = SyftEvents(
|
|
167
|
+
box = SyftEvents(
|
|
168
|
+
app_name=syft_flwr_app_name,
|
|
169
|
+
client=client,
|
|
170
|
+
cleanup_expiry="1d", # Keep request/response files for 1 days
|
|
171
|
+
cleanup_interval="1d", # Run cleanup daily
|
|
172
|
+
)
|
|
163
173
|
|
|
164
174
|
logger.info(f"Started SyftBox Flower Client on: {box.client.email}")
|
|
165
175
|
logger.info(f"syft_flwr app name: {syft_flwr_app_name}")
|
|
166
176
|
|
|
177
|
+
# Check if cleanup is running
|
|
178
|
+
if box.is_cleanup_running():
|
|
179
|
+
logger.info("Cleanup service is active")
|
|
180
|
+
|
|
167
181
|
# Create handlers
|
|
168
182
|
message_handler = MessageHandler(client_app, context, encryption_enabled)
|
|
169
183
|
processor = RequestProcessor(message_handler, box, box.client.email)
|
|
@@ -169,16 +169,19 @@ class SyftGrid(Grid):
|
|
|
169
169
|
|
|
170
170
|
return message_ids
|
|
171
171
|
|
|
172
|
-
def pull_messages(self, message_ids: List[str]) -> Dict[str, Message]:
|
|
172
|
+
def pull_messages(self, message_ids: List[str]) -> Tuple[Dict[str, Message], set]:
|
|
173
173
|
"""Pull response messages from clients using future IDs.
|
|
174
174
|
|
|
175
175
|
Args:
|
|
176
176
|
message_ids: List of future IDs from push_messages()
|
|
177
177
|
|
|
178
178
|
Returns:
|
|
179
|
-
|
|
179
|
+
Tuple of:
|
|
180
|
+
- Dict mapping message_id to Flower Message response (includes both successes and client errors)
|
|
181
|
+
- Set of message_ids that are completed (got response, deserialized successfully, or permanently failed)
|
|
180
182
|
"""
|
|
181
183
|
messages = {}
|
|
184
|
+
completed_ids = set()
|
|
182
185
|
|
|
183
186
|
for msg_id in message_ids:
|
|
184
187
|
try:
|
|
@@ -194,9 +197,15 @@ class SyftGrid(Grid):
|
|
|
194
197
|
# Process the response
|
|
195
198
|
message = self._process_response(response, msg_id)
|
|
196
199
|
|
|
200
|
+
# Always delete the future once we get a response (success or error)
|
|
201
|
+
# This prevents retrying failed messages indefinitely
|
|
202
|
+
rpc_db.delete_future(future_id=msg_id, client=self._client)
|
|
203
|
+
|
|
204
|
+
# Mark as completed regardless of success/failure
|
|
205
|
+
completed_ids.add(msg_id)
|
|
206
|
+
|
|
197
207
|
if message:
|
|
198
208
|
messages[msg_id] = message
|
|
199
|
-
rpc_db.delete_future(future_id=msg_id, client=self._client)
|
|
200
209
|
|
|
201
210
|
except Exception as e:
|
|
202
211
|
logger.error(f"❌ Unexpected error pulling message {msg_id}: {e}")
|
|
@@ -205,7 +214,7 @@ class SyftGrid(Grid):
|
|
|
205
214
|
# Log summary
|
|
206
215
|
self._log_pull_summary(messages, message_ids)
|
|
207
216
|
|
|
208
|
-
return messages
|
|
217
|
+
return messages, completed_ids
|
|
209
218
|
|
|
210
219
|
def send_and_receive(
|
|
211
220
|
self,
|
|
@@ -448,9 +457,10 @@ class SyftGrid(Grid):
|
|
|
448
457
|
|
|
449
458
|
while pending_ids and (timeout is None or time.time() < end_time):
|
|
450
459
|
# Pull available messages
|
|
451
|
-
batch = self.pull_messages(pending_ids)
|
|
460
|
+
batch, completed = self.pull_messages(list(pending_ids))
|
|
452
461
|
responses.update(batch)
|
|
453
|
-
|
|
462
|
+
# Remove all completed IDs (both successes and failures)
|
|
463
|
+
pending_ids.difference_update(completed)
|
|
454
464
|
|
|
455
465
|
if pending_ids:
|
|
456
466
|
time.sleep(poll_interval) # Configurable polling interval
|
|
@@ -487,25 +497,25 @@ class SyftGrid(Grid):
|
|
|
487
497
|
)
|
|
488
498
|
return None
|
|
489
499
|
|
|
490
|
-
# Check for errors in message
|
|
500
|
+
# Check for errors in message (but still return it so Flower can handle the failure)
|
|
491
501
|
if message.has_error():
|
|
492
502
|
error = message.error
|
|
493
503
|
logger.error(
|
|
494
504
|
f"❌ Message {msg_id} returned error with code={error.code}, "
|
|
495
|
-
f"reason={error.reason}"
|
|
505
|
+
f"reason={error.reason}. Returning error message to Flower for proper failure handling."
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
# Log successful pull only if no error
|
|
509
|
+
encryption_status = (
|
|
510
|
+
"🔐 ENCRYPTED" if self._encryption_enabled else "📥 PLAINTEXT"
|
|
511
|
+
)
|
|
512
|
+
logger.debug(
|
|
513
|
+
f"{encryption_status} Pulled message from {response.url} "
|
|
514
|
+
f"with metadata: {message.metadata}, "
|
|
515
|
+
f"size: {len(response_body) / 1024 / 1024:.2f} MB"
|
|
496
516
|
)
|
|
497
|
-
return None
|
|
498
|
-
|
|
499
|
-
# Log successful pull
|
|
500
|
-
encryption_status = (
|
|
501
|
-
"🔐 ENCRYPTED" if self._encryption_enabled else "📥 PLAINTEXT"
|
|
502
|
-
)
|
|
503
|
-
logger.debug(
|
|
504
|
-
f"{encryption_status} Pulled message from {response.url} "
|
|
505
|
-
f"with metadata: {message.metadata}, "
|
|
506
|
-
f"size: {len(response_body) / 1024 / 1024:.2f} MB"
|
|
507
|
-
)
|
|
508
517
|
|
|
518
|
+
# Always return the message (even with errors) so Flower's strategy can handle failures
|
|
509
519
|
return message
|
|
510
520
|
|
|
511
521
|
def _try_decrypt_response(self, body: bytes, msg_id: str) -> bytes:
|
|
@@ -545,14 +555,26 @@ class SyftGrid(Grid):
|
|
|
545
555
|
)
|
|
546
556
|
|
|
547
557
|
def _get_timeout(self, timeout: Optional[float]) -> Optional[float]:
|
|
548
|
-
"""Get timeout value from environment or parameter.
|
|
558
|
+
"""Get timeout value from environment or parameter.
|
|
559
|
+
|
|
560
|
+
Priority:
|
|
561
|
+
1. Explicit timeout parameter
|
|
562
|
+
2. SYFT_FLWR_MSG_TIMEOUT environment variable
|
|
563
|
+
3. Default: 120 seconds (to prevent indefinite waiting)
|
|
564
|
+
"""
|
|
565
|
+
# First check explicit parameter
|
|
566
|
+
if timeout is not None:
|
|
567
|
+
logger.debug(f"Message timeout: {timeout}s (from parameter)")
|
|
568
|
+
return timeout
|
|
569
|
+
|
|
570
|
+
# Then check environment variable
|
|
549
571
|
env_timeout = os.environ.get(SYFT_FLWR_MSG_TIMEOUT)
|
|
550
572
|
if env_timeout is not None:
|
|
551
573
|
timeout = float(env_timeout)
|
|
574
|
+
logger.debug(f"Message timeout: {timeout}s (from env var)")
|
|
575
|
+
return timeout
|
|
552
576
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return timeout
|
|
577
|
+
# Default to 120 seconds to prevent indefinite waiting
|
|
578
|
+
default_timeout = 120.0
|
|
579
|
+
logger.debug(f"Message timeout: {default_timeout}s (default)")
|
|
580
|
+
return default_timeout
|
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
|
+
import shutil
|
|
3
4
|
import sys
|
|
4
5
|
import tempfile
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
from typing import TypeAlias
|
|
6
8
|
|
|
7
9
|
from loguru import logger
|
|
8
10
|
from syft_core import Client
|
|
9
11
|
from syft_crypto import did_path, ensure_bootstrap, get_did_document, private_key_path
|
|
12
|
+
from syft_rds import init_session
|
|
10
13
|
from syft_rds.client.rds_client import RDSClient
|
|
11
|
-
from syft_rds.orchestra import SingleRDSStack, remove_rds_stack_dir
|
|
12
14
|
from typing_extensions import Optional, Union
|
|
13
15
|
|
|
14
16
|
from syft_flwr.config import load_flwr_pyproject
|
|
15
17
|
from syft_flwr.consts import SYFT_FLWR_ENCRYPTION_ENABLED
|
|
16
18
|
from syft_flwr.utils import create_temp_client
|
|
17
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
|
+
|
|
18
40
|
|
|
19
41
|
def _setup_mock_rds_clients(
|
|
20
42
|
project_dir: Path, aggregator: str, datasites: list[str]
|
|
@@ -26,16 +48,18 @@ def _setup_mock_rds_clients(
|
|
|
26
48
|
ds_syftbox_client = create_temp_client(
|
|
27
49
|
email=aggregator, workspace_dir=simulated_syftbox_network_dir
|
|
28
50
|
)
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
ds_rds_client = init_session(
|
|
52
|
+
host=aggregator, email=aggregator, syftbox_client=ds_syftbox_client
|
|
53
|
+
)
|
|
31
54
|
|
|
32
55
|
do_rds_clients = []
|
|
33
56
|
for datasite in datasites:
|
|
34
57
|
do_syftbox_client = create_temp_client(
|
|
35
58
|
email=datasite, workspace_dir=simulated_syftbox_network_dir
|
|
36
59
|
)
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
do_rds_client = init_session(
|
|
61
|
+
host=datasite, email=datasite, syftbox_client=do_syftbox_client
|
|
62
|
+
)
|
|
39
63
|
do_rds_clients.append(do_rds_client)
|
|
40
64
|
|
|
41
65
|
return simulated_syftbox_network_dir, do_rds_clients, ds_rds_client
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from syft_core import Client
|
|
@@ -20,8 +21,11 @@ is_server = client.email in config["tool"]["syft_flwr"]["aggregator"]
|
|
|
20
21
|
if is_client:
|
|
21
22
|
# run by each DO
|
|
22
23
|
syftbox_run_flwr_client(flower_project_dir)
|
|
24
|
+
sys.exit(0) # Exit cleanly after client work completes
|
|
23
25
|
elif is_server:
|
|
24
26
|
# run by the DS
|
|
25
27
|
syftbox_run_flwr_server(flower_project_dir)
|
|
28
|
+
sys.exit(0) # Exit cleanly after server work completes
|
|
26
29
|
else:
|
|
27
30
|
raise ValueError(f"{client.email} is not in config.datasites or config.aggregator")
|
|
31
|
+
sys.exit(1) # Exit with error code
|
|
@@ -104,8 +104,19 @@ def create_flwr_message(
|
|
|
104
104
|
return Message(reply_to=reply_to, error=error)
|
|
105
105
|
return Message(content=content, reply_to=reply_to)
|
|
106
106
|
else:
|
|
107
|
+
# Allow standalone error messages when we can't parse the original message
|
|
107
108
|
if error is not None:
|
|
108
|
-
|
|
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
|
+
)
|
|
109
120
|
return Message(
|
|
110
121
|
content=content,
|
|
111
122
|
dst_node_id=dst_node_id,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|