syft-flwr 0.1.7__tar.gz → 0.2.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.

Files changed (25) hide show
  1. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/.gitignore +4 -1
  2. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/PKG-INFO +3 -3
  3. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/pyproject.toml +6 -4
  4. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/__init__.py +1 -1
  5. syft_flwr-0.2.1/src/syft_flwr/consts.py +2 -0
  6. syft_flwr-0.2.1/src/syft_flwr/flower_client.py +185 -0
  7. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/flower_server.py +12 -4
  8. syft_flwr-0.2.1/src/syft_flwr/grid.py +558 -0
  9. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/mounts.py +1 -1
  10. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/run.py +2 -1
  11. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/run_simulation.py +124 -24
  12. syft_flwr-0.2.1/src/syft_flwr/utils.py +115 -0
  13. syft_flwr-0.1.7/src/syft_flwr/flower_client.py +0 -96
  14. syft_flwr-0.1.7/src/syft_flwr/flwr_compatibility.py +0 -121
  15. syft_flwr-0.1.7/src/syft_flwr/grid.py +0 -221
  16. syft_flwr-0.1.7/src/syft_flwr/utils.py +0 -36
  17. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/LICENSE +0 -0
  18. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/README.md +0 -0
  19. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/bootstrap.py +0 -0
  20. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/cli.py +0 -0
  21. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/config.py +0 -0
  22. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/serde.py +0 -0
  23. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/strategy/__init__.py +0 -0
  24. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/strategy/fedavg.py +0 -0
  25. {syft_flwr-0.1.7 → syft_flwr-0.2.1}/src/syft_flwr/templates/main.py.tpl +0 -0
@@ -185,4 +185,7 @@ checkpoints/
185
185
  **/dataset/
186
186
 
187
187
  # Ruff cache
188
- .ruff_cache/
188
+ .ruff_cache/
189
+
190
+ # AI stuff
191
+ .claude/
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syft-flwr
3
- Version: 0.1.7
3
+ Version: 0.2.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.9.2
6
+ Requires-Python: >=3.10
7
7
  Requires-Dist: flwr-datasets[vision]>=0.5.0
8
8
  Requires-Dist: flwr[simulation]>=1.20.0
9
9
  Requires-Dist: loguru>=0.7.3
10
10
  Requires-Dist: safetensors>=0.6.2
11
- Requires-Dist: syft-rds==0.1.5
11
+ Requires-Dist: syft-rds>=0.2.1
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
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "syft-flwr"
3
- version = "0.1.7"
3
+ version = "0.2.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.9.2"
6
+ requires-python = ">=3.10"
7
7
  dependencies = [
8
+ "syft-rds>=0.2.1",
8
9
  "flwr[simulation]>=1.20.0",
9
10
  "flwr-datasets[vision]>=0.5.0",
10
11
  "loguru>=0.7.3",
@@ -12,7 +13,6 @@ dependencies = [
12
13
  "typing-extensions>=4.13.0",
13
14
  "tomli>=2.2.1",
14
15
  "tomli-w>=1.2.0",
15
- "syft-rds==0.1.5",
16
16
  ]
17
17
 
18
18
  [project.scripts]
@@ -26,7 +26,9 @@ dev-dependencies = [
26
26
  "pre-commit>=4.3.0",
27
27
  "torch>=2.0.0",
28
28
  "imblearn>=0.0",
29
- "jupyterlab"
29
+ "jupyterlab",
30
+ "pytest-xdist>=3.8.0",
31
+ "pytest-asyncio>=1.1.0",
30
32
  ]
31
33
 
32
34
  [build-system]
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.7"
1
+ __version__ = "0.2.1"
2
2
 
3
3
  from syft_flwr.bootstrap import bootstrap
4
4
  from syft_flwr.run import run
@@ -0,0 +1,2 @@
1
+ # Environment variable to control encryption
2
+ SYFT_FLWR_ENCRYPTION_ENABLED = "SYFT_FLWR_ENCRYPTION_ENABLED"
@@ -0,0 +1,185 @@
1
+ import base64
2
+ import sys
3
+ import traceback
4
+
5
+ from flwr.client import ClientApp
6
+ from flwr.common import Context
7
+ from flwr.common.constant import ErrorCode, MessageType
8
+ from flwr.common.message import Error, Message
9
+ from flwr.common.record import RecordDict
10
+ from loguru import logger
11
+ from syft_event import SyftEvents
12
+ from syft_event.types import Request
13
+ from typing_extensions import Optional, Union
14
+
15
+ from syft_flwr.serde import bytes_to_flower_message, flower_message_to_bytes
16
+ from syft_flwr.utils import create_flwr_message, setup_client
17
+
18
+
19
+ class MessageHandler:
20
+ """Handles message processing for Flower client."""
21
+
22
+ def __init__(
23
+ self, client_app: ClientApp, context: Context, encryption_enabled: bool
24
+ ):
25
+ self.client_app = client_app
26
+ self.context = context
27
+ self.encryption_enabled = encryption_enabled
28
+
29
+ def prepare_reply(self, data: bytes) -> Union[str, bytes]:
30
+ """Prepare reply data based on encryption setting."""
31
+ if self.encryption_enabled:
32
+ logger.info(f"🔒 Preparing ENCRYPTED reply, size: {len(data)/2**20:.2f} MB")
33
+ return base64.b64encode(data).decode("utf-8")
34
+ else:
35
+ logger.info(f"📤 Preparing PLAINTEXT reply, size: {len(data)/2**20:.2f} MB")
36
+ return data
37
+
38
+ def process_message(self, message: Message) -> Union[str, bytes]:
39
+ """Process normal Flower message and return reply."""
40
+ logger.info(f"Processing message with metadata: {message.metadata}")
41
+ reply_message = self.client_app(message=message, context=self.context)
42
+ reply_bytes = flower_message_to_bytes(reply_message)
43
+ return self.prepare_reply(reply_bytes)
44
+
45
+ def create_error_reply(
46
+ self, message: Optional[Message], error: Error
47
+ ) -> Union[str, bytes]:
48
+ """Create error reply message."""
49
+ error_reply = create_flwr_message(
50
+ content=RecordDict(),
51
+ reply_to=message,
52
+ message_type=message.metadata.message_type if message else MessageType.TASK,
53
+ dst_node_id=message.metadata.src_node_id if message else 0,
54
+ group_id=message.metadata.group_id if message else "",
55
+ error=error,
56
+ )
57
+ error_bytes = flower_message_to_bytes(error_reply)
58
+ logger.info(f"Error reply size: {len(error_bytes)/2**20:.2f} MB")
59
+ return self.prepare_reply(error_bytes)
60
+
61
+
62
+ class RequestProcessor:
63
+ """Processes incoming requests and handles encryption/decryption."""
64
+
65
+ def __init__(
66
+ self, message_handler: MessageHandler, box: SyftEvents, client_email: str
67
+ ):
68
+ self.message_handler = message_handler
69
+ self.box = box
70
+ self.client_email = client_email
71
+
72
+ def decode_request_body(self, request_body: Union[bytes, str]) -> bytes:
73
+ """Decode request body, handling base64 if encrypted."""
74
+ if not self.message_handler.encryption_enabled:
75
+ return request_body
76
+
77
+ try:
78
+ # Convert to string if bytes
79
+ if isinstance(request_body, bytes):
80
+ request_body_str = request_body.decode("utf-8")
81
+ else:
82
+ request_body_str = request_body
83
+ # Decode base64
84
+ decoded = base64.b64decode(request_body_str)
85
+ logger.debug("🔓 Decoded base64 message")
86
+ return decoded
87
+ except Exception:
88
+ # Not base64 or decoding failed, use as-is
89
+ return request_body
90
+
91
+ def is_stop_signal(self, message: Message) -> bool:
92
+ """Check if message is a stop signal."""
93
+ if message.metadata.message_type != MessageType.SYSTEM:
94
+ return False
95
+
96
+ # Check for stop action in config
97
+ if "config" in message.content and "action" in message.content["config"]:
98
+ return message.content["config"]["action"] == "stop"
99
+
100
+ # Alternative stop signal format
101
+ return message.metadata.group_id == "final"
102
+
103
+ def process(self, request: Request) -> Optional[Union[str, bytes]]:
104
+ """Process incoming request and return response."""
105
+ original_sender = request.headers.get("X-Syft-Original-Sender", "unknown")
106
+ encryption_status = (
107
+ "🔐 ENCRYPTED"
108
+ if self.message_handler.encryption_enabled
109
+ else "📥 PLAINTEXT"
110
+ )
111
+
112
+ logger.info(
113
+ f"{encryption_status} Received request from {original_sender}, "
114
+ f"id: {request.id}, size: {len(request.body) / 1024 / 1024:.2f} MB"
115
+ )
116
+
117
+ # Parse message
118
+ try:
119
+ request_body = self.decode_request_body(request.body)
120
+ message = bytes_to_flower_message(request_body)
121
+
122
+ if self.message_handler.encryption_enabled:
123
+ logger.debug(
124
+ f"🔓 Successfully decrypted message from {original_sender}"
125
+ )
126
+ except Exception as e:
127
+ logger.error(
128
+ f"❌ Failed to deserialize message from {original_sender}: {e}"
129
+ )
130
+ error = Error(
131
+ code=ErrorCode.CLIENT_APP_RAISED_EXCEPTION,
132
+ reason=f"Message deserialization failed: {e}",
133
+ )
134
+ return self.message_handler.create_error_reply(None, error)
135
+
136
+ # Handle message
137
+ try:
138
+ # Check for stop signal
139
+ if self.is_stop_signal(message):
140
+ logger.info("Received stop signal")
141
+ self.box._stop_event.set()
142
+ return None
143
+
144
+ # Process normal message
145
+ return self.message_handler.process_message(message)
146
+
147
+ except Exception as e:
148
+ error_message = f"Client: '{self.client_email}'. Error: {str(e)}. Traceback: {traceback.format_exc()}"
149
+ logger.error(error_message)
150
+
151
+ error = Error(
152
+ code=ErrorCode.CLIENT_APP_RAISED_EXCEPTION, reason=error_message
153
+ )
154
+ self.box._stop_event.set()
155
+ return self.message_handler.create_error_reply(message, error)
156
+
157
+
158
+ def syftbox_flwr_client(client_app: ClientApp, context: Context, app_name: str):
159
+ """Run the Flower ClientApp with SyftBox."""
160
+ # Setup
161
+ client, encryption_enabled, syft_flwr_app_name = setup_client(app_name)
162
+ box = SyftEvents(app_name=syft_flwr_app_name, client=client)
163
+
164
+ logger.info(f"Started SyftBox Flower Client on: {box.client.email}")
165
+ logger.info(f"syft_flwr app name: {syft_flwr_app_name}")
166
+
167
+ # Create handlers
168
+ message_handler = MessageHandler(client_app, context, encryption_enabled)
169
+ processor = RequestProcessor(message_handler, box, box.client.email)
170
+
171
+ # Register message handler
172
+ @box.on_request(
173
+ "/messages", auto_decrypt=encryption_enabled, encrypt_reply=encryption_enabled
174
+ )
175
+ def handle_messages(request: Request) -> Optional[Union[str, bytes]]:
176
+ return processor.process(request)
177
+
178
+ # Run
179
+ try:
180
+ box.run_forever()
181
+ except Exception as e:
182
+ logger.error(
183
+ f"Fatal error in syftbox_flwr_client: {str(e)}\n{traceback.format_exc()}"
184
+ )
185
+ sys.exit(1)
@@ -1,12 +1,13 @@
1
1
  import traceback
2
2
  from random import randint
3
3
 
4
- from loguru import logger
5
-
6
4
  from flwr.common import Context
7
5
  from flwr.server import ServerApp
8
6
  from flwr.server.run_serverapp import run as run_server
7
+ from loguru import logger
8
+
9
9
  from syft_flwr.grid import SyftGrid
10
+ from syft_flwr.utils import setup_client
10
11
 
11
12
 
12
13
  def syftbox_flwr_server(
@@ -16,10 +17,17 @@ def syftbox_flwr_server(
16
17
  app_name: str,
17
18
  ) -> Context:
18
19
  """Run the Flower ServerApp with SyftBox."""
19
- syft_flwr_app_name = f"flwr/{app_name}"
20
- syft_grid = SyftGrid(app_name=syft_flwr_app_name, datasites=datasites)
20
+ client, _, syft_flwr_app_name = setup_client(app_name)
21
+
22
+ # Construct the SyftGrid
23
+ syft_grid = SyftGrid(
24
+ app_name=syft_flwr_app_name, datasites=datasites, client=client
25
+ )
26
+
27
+ # Set the run id (random for now)
21
28
  run_id = randint(0, 1000)
22
29
  syft_grid.set_run(run_id)
30
+
23
31
  logger.info(f"Started SyftBox Flower Server on: {syft_grid._client.email}")
24
32
  logger.info(f"syft_flwr app name: {syft_flwr_app_name}")
25
33