syft-flwr 0.2.0__py3-none-any.whl → 0.2.2__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.

Potentially problematic release.


This version of syft-flwr might be problematic. Click here for more details.

syft_flwr/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.2.0"
1
+ __version__ = "0.2.2"
2
2
 
3
3
  from syft_flwr.bootstrap import bootstrap
4
4
  from syft_flwr.run import run
@@ -6,14 +6,14 @@ from flwr.client import ClientApp
6
6
  from flwr.common import Context
7
7
  from flwr.common.constant import ErrorCode, MessageType
8
8
  from flwr.common.message import Error, Message
9
+ from flwr.common.record import RecordDict
9
10
  from loguru import logger
10
11
  from syft_event import SyftEvents
11
12
  from syft_event.types import Request
12
13
  from typing_extensions import Optional, Union
13
14
 
14
- from syft_flwr.flwr_compatibility import RecordDict, create_flwr_message
15
15
  from syft_flwr.serde import bytes_to_flower_message, flower_message_to_bytes
16
- from syft_flwr.utils import setup_client
16
+ from syft_flwr.utils import create_flwr_message, setup_client
17
17
 
18
18
 
19
19
  class MessageHandler:
@@ -50,10 +50,8 @@ class MessageHandler:
50
50
  content=RecordDict(),
51
51
  reply_to=message,
52
52
  message_type=message.metadata.message_type if message else MessageType.TASK,
53
- src_node_id=message.metadata.dst_node_id if message else 0,
54
53
  dst_node_id=message.metadata.src_node_id if message else 0,
55
54
  group_id=message.metadata.group_id if message else "",
56
- run_id=message.metadata.run_id if message else 0,
57
55
  error=error,
58
56
  )
59
57
  error_bytes = flower_message_to_bytes(error_reply)
syft_flwr/grid.py CHANGED
@@ -1,12 +1,15 @@
1
1
  import base64
2
2
  import os
3
+ import random
3
4
  import time
4
5
 
5
6
  from flwr.common import ConfigRecord
6
7
  from flwr.common.constant import MessageType
7
8
  from flwr.common.message import Message
9
+ from flwr.common.record import RecordDict
8
10
  from flwr.common.typing import Run
9
11
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
12
+ from flwr.server.grid import Grid
10
13
  from loguru import logger
11
14
  from syft_core import Client
12
15
  from syft_crypto import EncryptedPayload, decrypt_message
@@ -14,20 +17,15 @@ from syft_rpc import SyftResponse, rpc, rpc_db
14
17
  from typing_extensions import Dict, Iterable, List, Optional, Tuple, cast
15
18
 
16
19
  from syft_flwr.consts import SYFT_FLWR_ENCRYPTION_ENABLED
17
- from syft_flwr.flwr_compatibility import (
18
- Grid,
19
- RecordDict,
20
- check_reply_to_field,
21
- create_flwr_message,
22
- )
23
20
  from syft_flwr.serde import bytes_to_flower_message, flower_message_to_bytes
24
- from syft_flwr.utils import str_to_int
21
+ from syft_flwr.utils import check_reply_to_field, create_flwr_message, str_to_int
25
22
 
26
23
  # this is what superlink super node do
27
24
  AGGREGATOR_NODE_ID = 1
28
25
 
29
26
  # env vars
30
27
  SYFT_FLWR_MSG_TIMEOUT = "SYFT_FLWR_MSG_TIMEOUT"
28
+ SYFT_FLWR_POLL_INTERVAL = "SYFT_FLWR_POLL_INTERVAL"
31
29
 
32
30
 
33
31
  class SyftGrid(Grid):
@@ -127,8 +125,6 @@ class SyftGrid(Grid):
127
125
  dst_node_id=dst_node_id,
128
126
  group_id=group_id,
129
127
  ttl=ttl,
130
- run_id=cast(Run, self._run).run_id,
131
- src_node_id=self.node.node_id,
132
128
  )
133
129
 
134
130
  def get_node_ids(self) -> list[int]:
@@ -318,27 +314,91 @@ class SyftGrid(Grid):
318
314
 
319
315
  return dest_datasite, url, msg_bytes
320
316
 
317
+ def _retry_with_backoff(
318
+ self,
319
+ func,
320
+ max_retries: int = 3,
321
+ initial_delay: float = 0.1,
322
+ context: str = "",
323
+ check_error=None,
324
+ ):
325
+ """Generic retry logic with exponential backoff and jitter.
326
+
327
+ Args:
328
+ func: Function to retry
329
+ max_retries: Maximum number of retry attempts
330
+ initial_delay: Initial delay in seconds
331
+ context: Context string for logging
332
+ check_error: Optional function to check if error is retryable
333
+
334
+ Returns:
335
+ Result of func if successful
336
+
337
+ Raises:
338
+ Last exception if all retries fail
339
+ """
340
+ for attempt in range(max_retries):
341
+ try:
342
+ return func()
343
+ except Exception as e:
344
+ is_retryable = check_error(e) if check_error else True
345
+ if is_retryable and attempt < max_retries - 1:
346
+ jitter = random.uniform(0, 0.05)
347
+ delay = initial_delay * (2**attempt) + jitter
348
+ logger.debug(
349
+ f"{context} failed (attempt {attempt + 1}/{max_retries}): {e}. "
350
+ f"Retrying in {delay:.3f}s"
351
+ )
352
+ time.sleep(delay)
353
+ else:
354
+ raise
355
+
356
+ def _save_future_with_retry(self, future, dest_datasite: str) -> bool:
357
+ """Save future to database with retry logic for database locks.
358
+
359
+ Returns:
360
+ True if saved successfully, False if failed after retries
361
+ """
362
+ try:
363
+ self._retry_with_backoff(
364
+ func=lambda: rpc_db.save_future(
365
+ future=future, namespace=self.app_name, client=self._client
366
+ ),
367
+ context=f"Database save for {dest_datasite}",
368
+ check_error=lambda e: "database is locked" in str(e).lower(),
369
+ )
370
+ return True
371
+ except Exception as e:
372
+ logger.warning(
373
+ f"⚠️ Failed to save future to database for {dest_datasite}: {e}. "
374
+ f"Message sent but future not persisted."
375
+ )
376
+ return False
377
+
321
378
  def _send_encrypted_message(
322
379
  self, url: str, msg_bytes: bytes, dest_datasite: str, msg: Message
323
380
  ) -> Optional[str]:
324
381
  """Send an encrypted message and return future ID if successful."""
325
382
  try:
383
+ # Send encrypted message
326
384
  future = rpc.send(
327
385
  url=url,
328
386
  body=base64.b64encode(msg_bytes).decode("utf-8"),
329
387
  client=self._client,
330
388
  encrypt=True,
331
389
  )
390
+
332
391
  logger.debug(
333
392
  f"🔐 Pushed ENCRYPTED message to {dest_datasite} at {url} "
334
393
  f"with metadata {msg.metadata}; size {len(msg_bytes) / 1024 / 1024:.2f} MB"
335
394
  )
336
- rpc_db.save_future(
337
- future=future, namespace=self.app_name, client=self._client
338
- )
395
+
396
+ # Save future to database (non-critical - log warning if fails)
397
+ self._save_future_with_retry(future, dest_datasite)
339
398
  return future.id
340
399
 
341
400
  except (KeyError, ValueError) as e:
401
+ # Encryption setup errors - don't retry or fallback
342
402
  error_type = (
343
403
  "Encryption key" if isinstance(e, KeyError) else "Encryption parameter"
344
404
  )
@@ -349,6 +409,7 @@ class SyftGrid(Grid):
349
409
  return None
350
410
 
351
411
  except Exception as e:
412
+ # Other errors - fallback to unencrypted
352
413
  logger.warning(
353
414
  f"⚠️ Encryption failed for {dest_datasite}: {e}. "
354
415
  f"Falling back to unencrypted transmission"
@@ -382,6 +443,9 @@ class SyftGrid(Grid):
382
443
  responses = {}
383
444
  pending_ids = msg_ids.copy()
384
445
 
446
+ # Get polling interval from environment or use default
447
+ poll_interval = float(os.environ.get(SYFT_FLWR_POLL_INTERVAL, "3"))
448
+
385
449
  while pending_ids and (timeout is None or time.time() < end_time):
386
450
  # Pull available messages
387
451
  batch = self.pull_messages(pending_ids)
@@ -389,7 +453,7 @@ class SyftGrid(Grid):
389
453
  pending_ids.difference_update(batch.keys())
390
454
 
391
455
  if pending_ids:
392
- time.sleep(3) # Polling interval
456
+ time.sleep(poll_interval) # Configurable polling interval
393
457
 
394
458
  # Log any missing responses
395
459
  if pending_ids:
syft_flwr/run.py CHANGED
@@ -5,11 +5,12 @@ from uuid import uuid4
5
5
  from flwr.client.client_app import LoadClientAppError
6
6
  from flwr.common import Context
7
7
  from flwr.common.object_ref import load_app
8
+ from flwr.common.record import RecordDict
8
9
  from flwr.server.server_app import LoadServerAppError
10
+
9
11
  from syft_flwr.config import load_flwr_pyproject
10
12
  from syft_flwr.flower_client import syftbox_flwr_client
11
13
  from syft_flwr.flower_server import syftbox_flwr_server
12
- from syft_flwr.flwr_compatibility import RecordDict
13
14
  from syft_flwr.run_simulation import run
14
15
 
15
16
  __all__ = ["syftbox_run_flwr_client", "syftbox_run_flwr_server", "run"]
syft_flwr/utils.py CHANGED
@@ -3,10 +3,13 @@ import re
3
3
  import zlib
4
4
  from pathlib import Path
5
5
 
6
+ from flwr.common import Metadata
7
+ from flwr.common.message import Error, Message
8
+ from flwr.common.record import RecordDict
6
9
  from loguru import logger
7
10
  from syft_core import Client, SyftClientConfig
8
11
  from syft_crypto.x3dh_bootstrap import ensure_bootstrap
9
- from typing_extensions import Tuple
12
+ from typing_extensions import Optional, Tuple
10
13
 
11
14
  from syft_flwr.consts import SYFT_FLWR_ENCRYPTION_ENABLED
12
15
 
@@ -79,3 +82,34 @@ def setup_client(app_name: str) -> Tuple[Client, bool, str]:
79
82
  )
80
83
 
81
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
+ if error is not None:
108
+ raise ValueError("Error and reply_to cannot both be None")
109
+ return Message(
110
+ content=content,
111
+ dst_node_id=dst_node_id,
112
+ message_type=message_type,
113
+ ttl=ttl,
114
+ group_id=group_id,
115
+ )
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syft-flwr
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
6
  Requires-Python: >=3.10
7
7
  Requires-Dist: flwr-datasets[vision]>=0.5.0
8
- Requires-Dist: flwr[simulation]>=1.20.0
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.1
11
+ Requires-Dist: syft-rds>=0.2.2
12
12
  Requires-Dist: tomli-w>=1.2.0
13
13
  Requires-Dist: tomli>=2.2.1
14
14
  Requires-Dist: typing-extensions>=4.13.0
@@ -23,4 +23,5 @@ Description-Content-Type: text/markdown
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
- - [Federated analytics](notebooks/federated-analytics-diabetes/README.md) shows how to query statistics from private datasets from distributed machines and then aggregate them
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
@@ -1,22 +1,21 @@
1
- syft_flwr/__init__.py,sha256=ayTCluE-u-ttvrPNq2REmFOfBOd6Zmr_QBF9oh2BpJc,426
1
+ syft_flwr/__init__.py,sha256=-LMe4eMC2ISKkP20MQg42hRwYTd4FPyG0Og6wGufJyU,426
2
2
  syft_flwr/bootstrap.py,sha256=-T6SRh_p6u6uWpbTPZ6-URsAfMQAI2jakpjZAh0UUlw,3690
3
3
  syft_flwr/cli.py,sha256=imctwdQMxQeGQZaiKSX1Mo2nU_-RmA-cGB3H4huuUeA,3274
4
4
  syft_flwr/config.py,sha256=4hwkovGtFOLNULjJwoGYcA0uT4y3vZSrxndXqYXquMY,821
5
5
  syft_flwr/consts.py,sha256=u3QK-Wp8D2Va7iZcp5z4ormVm_FAUDeK4u-w81UL_eY,107
6
- syft_flwr/flower_client.py,sha256=-HsPp2Uw0RlILthezQHzZdwyVAzklbh_VHnl_nANgx8,7210
6
+ syft_flwr/flower_client.py,sha256=dBM8QPJSyQmeoG41w2m9nXM5451VmqUviQz117z4FM4,7066
7
7
  syft_flwr/flower_server.py,sha256=ZNDUR1U79M0BaG7n39TGUkVHV2NYi-LDsN8FqKJFfFQ,1508
8
- syft_flwr/flwr_compatibility.py,sha256=vURf9rfsZ1uPm04szw6RpGYxtlG3BE4tW3YijptiGyk,3197
9
- syft_flwr/grid.py,sha256=zjhkKHIYxRoCmda75Bw1L1Qra7b5DXhMTpY7L3Ujy_4,17799
8
+ syft_flwr/grid.py,sha256=5rbv6ncLKK2S1PAifkavaOG36gih-QexPJHh8nnwLws,20309
10
9
  syft_flwr/mounts.py,sha256=hp0TKVot16SaPYO10Y_mSJGei7aiNteJfK4U4vynWmU,2330
11
- syft_flwr/run.py,sha256=OPW9bVt366DT-U-SxMpMLNXASwTZjp7XNNXfDP767f4,2153
10
+ syft_flwr/run.py,sha256=oy_ovAVmO84teKisLYYrY8a5ZmWH8tpWVDwXKB-O0rU,2144
12
11
  syft_flwr/run_simulation.py,sha256=frHytbsxYLjiCM4r4m1NVQOc1j98hm4sQQoBLeagJi8,11539
13
12
  syft_flwr/serde.py,sha256=5fCI-cRUOh5wE7cXQd4J6jex1grRGnyD1Jx-VlEDOXM,495
14
- syft_flwr/utils.py,sha256=SC-lnCydP9t2_FNlUZEUFDcb6wtIE9v0soiW8nH7G0w,2594
13
+ syft_flwr/utils.py,sha256=KYwijACpHOR7pkvezNBqbCE48y3o4G9OUtnvdm1NkaU,3672
15
14
  syft_flwr/strategy/__init__.py,sha256=mpUmExjjFkqU8gg41XsOBKfO3aqCBe7XPJSU-_P7smU,97
16
15
  syft_flwr/strategy/fedavg.py,sha256=N8jULUkjvuaBIEVINowyQln8W8yFhkO-J8k0-iPcGMA,1562
17
16
  syft_flwr/templates/main.py.tpl,sha256=p0uK97jvLGk3LJdy1_HF1R5BQgIjaTGkYnr-csfh39M,791
18
- syft_flwr-0.2.0.dist-info/METADATA,sha256=R6CudwzWKXVdL-wKi-iJd_Fm7yEGc9eNXgiTSu5sPsM,1254
19
- syft_flwr-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- syft_flwr-0.2.0.dist-info/entry_points.txt,sha256=o7oT0dCoHn-3WyIwdDw1lBh2q-GvhB_8s0hWeJU4myc,49
21
- syft_flwr-0.2.0.dist-info/licenses/LICENSE,sha256=0msOUar8uPZTqkAOTBp4rCzd7Jl9eRhfKiNufwrsg7k,11361
22
- syft_flwr-0.2.0.dist-info/RECORD,,
17
+ syft_flwr-0.2.2.dist-info/METADATA,sha256=d5xYKf8IcgwA6eXcQBlOuAAdTl59UFeGYXrwIheDjqQ,1468
18
+ syft_flwr-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ syft_flwr-0.2.2.dist-info/entry_points.txt,sha256=o7oT0dCoHn-3WyIwdDw1lBh2q-GvhB_8s0hWeJU4myc,49
20
+ syft_flwr-0.2.2.dist-info/licenses/LICENSE,sha256=0msOUar8uPZTqkAOTBp4rCzd7Jl9eRhfKiNufwrsg7k,11361
21
+ syft_flwr-0.2.2.dist-info/RECORD,,
@@ -1,121 +0,0 @@
1
- import flwr
2
- from flwr.common import Metadata
3
- from flwr.common.message import Error, Message
4
- from packaging.version import Version
5
- from typing_extensions import Optional
6
-
7
-
8
- def flwr_later_than_1_17():
9
- return Version(flwr.__version__) >= Version("1.17.0")
10
-
11
-
12
- # Version-dependent imports
13
- if flwr_later_than_1_17():
14
- from flwr.common.record import RecordDict
15
- from flwr.server.grid import Grid
16
- else:
17
- from flwr.common.record import RecordSet as RecordDict
18
- from flwr.server.driver import Driver as Grid
19
-
20
-
21
- __all__ = ["Grid", "RecordDict"]
22
-
23
-
24
- def check_reply_to_field(metadata: Metadata) -> bool:
25
- """Check if reply_to field is empty based on Flower version."""
26
- if flwr_later_than_1_17():
27
- return metadata.reply_to_message_id == ""
28
- else:
29
- return metadata.reply_to_message == ""
30
-
31
-
32
- def create_flwr_message(
33
- content: RecordDict,
34
- message_type: str,
35
- src_node_id: int,
36
- dst_node_id: int,
37
- group_id: str,
38
- run_id: int,
39
- ttl: Optional[float] = None,
40
- error: Optional[Error] = None,
41
- reply_to: Optional[Message] = None,
42
- ) -> Message:
43
- """Create a Flower message with version-compatible parameters."""
44
- if flwr_later_than_1_17():
45
- return _create_message_v1_17_plus(
46
- content,
47
- message_type,
48
- dst_node_id,
49
- group_id,
50
- ttl,
51
- error,
52
- reply_to,
53
- )
54
- else:
55
- return _create_message_pre_v1_17(
56
- content,
57
- message_type,
58
- src_node_id,
59
- dst_node_id,
60
- group_id,
61
- run_id,
62
- ttl,
63
- error,
64
- )
65
-
66
-
67
- def _create_message_v1_17_plus(
68
- content: RecordDict,
69
- message_type: str,
70
- dst_node_id: int,
71
- group_id: str,
72
- ttl: Optional[float],
73
- error: Optional[Error],
74
- reply_to: Optional[Message],
75
- ) -> Message:
76
- """Create message for Flower version 1.17+."""
77
- if reply_to is not None:
78
- if error is not None:
79
- return Message(reply_to=reply_to, error=error)
80
- return Message(content=content, reply_to=reply_to)
81
- else:
82
- if error is not None:
83
- raise ValueError("Error and reply_to cannot both be None")
84
- return Message(
85
- content=content,
86
- dst_node_id=dst_node_id,
87
- message_type=message_type,
88
- ttl=ttl,
89
- group_id=group_id,
90
- )
91
-
92
-
93
- def _create_message_pre_v1_17(
94
- content: RecordDict,
95
- message_type: str,
96
- src_node_id: int,
97
- dst_node_id: int,
98
- group_id: str,
99
- run_id: int,
100
- ttl: Optional[float],
101
- error: Optional[Error],
102
- ) -> Message:
103
- """Create message for Flower versions before 1.17."""
104
- from flwr.common import DEFAULT_TTL
105
-
106
- ttl_ = DEFAULT_TTL if ttl is None else ttl
107
- metadata = Metadata(
108
- run_id=run_id,
109
- message_id="", # Will be set when saving to file
110
- src_node_id=src_node_id,
111
- dst_node_id=dst_node_id,
112
- reply_to_message="",
113
- group_id=group_id,
114
- ttl=ttl_,
115
- message_type=message_type,
116
- )
117
-
118
- if error is not None:
119
- return Message(metadata=metadata, error=error)
120
- else:
121
- return Message(metadata=metadata, content=content)