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.

@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syft-flwr
3
- Version: 0.2.2
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: >=3.10
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.2.2
11
+ Requires-Dist: syft-rds>=0.4.2
12
12
  Requires-Dist: tomli-w>=1.2.0
13
- Requires-Dist: tomli>=2.2.1
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
- ![FL Training Process](notebooks/fl-diabetes-prediction/images/fltraining.gif)
21
+ ![FL Training Process](https://github.com/OpenMined/syft-flwr/raw/main/notebooks/fl-diabetes-prediction/images/fltraining.gif)
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
- ![FL Training Process](notebooks/fl-diabetes-prediction/images/fltraining.gif)
5
+ ![FL Training Process](https://github.com/OpenMined/syft-flwr/raw/main/notebooks/fl-diabetes-prediction/images/fltraining.gif)
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.2.2"
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.10"
6
+ requires-python = ">=3.11,<3.14"
7
7
  dependencies = [
8
- "syft-rds>=0.2.2",
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.2.1",
14
+ "tomli>=2.3.0",
15
15
  "tomli-w>=1.2.0",
16
16
  ]
17
17
 
18
18
  # [tool.uv.sources]
19
- # syft-rds = { path = "../syft-data-science" } # for development
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
- [tool.uv]
26
- dev-dependencies = [
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
+ ]
@@ -1,4 +1,4 @@
1
- __version__ = "0.2.2"
1
+ __version__ = "0.4.1"
2
2
 
3
3
  from syft_flwr.bootstrap import bootstrap
4
4
  from syft_flwr.run import run
@@ -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 if message else MessageType.TASK,
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
- error = Error(
131
- code=ErrorCode.CLIENT_APP_RAISED_EXCEPTION,
132
- reason=f"Message deserialization failed: {e}",
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 self.message_handler.create_error_reply(None, error)
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(app_name=syft_flwr_app_name, client=client)
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
- Dict mapping message_id to Flower Message response
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
- pending_ids.difference_update(batch.keys())
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
- if timeout is not None:
554
- logger.debug(f"Message timeout: {timeout}s")
555
- else:
556
- logger.debug("No timeout - will wait indefinitely for replies")
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
- ds_stack = SingleRDSStack(client=ds_syftbox_client)
30
- ds_rds_client = ds_stack.init_session(host=aggregator)
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
- do_stack = SingleRDSStack(client=do_syftbox_client)
38
- do_rds_client = do_stack.init_session(host=datasite)
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
- raise ValueError("Error and reply_to cannot both be 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
+ )
109
120
  return Message(
110
121
  content=content,
111
122
  dst_node_id=dst_node_id,
File without changes
File without changes