syft-flwr 0.1.7__py3-none-any.whl → 0.2.1__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 +1 -1
- syft_flwr/consts.py +2 -0
- syft_flwr/flower_client.py +150 -61
- syft_flwr/flower_server.py +12 -4
- syft_flwr/grid.py +446 -109
- syft_flwr/mounts.py +1 -1
- syft_flwr/run.py +2 -1
- syft_flwr/run_simulation.py +124 -24
- syft_flwr/utils.py +79 -0
- {syft_flwr-0.1.7.dist-info → syft_flwr-0.2.1.dist-info}/METADATA +3 -3
- syft_flwr-0.2.1.dist-info/RECORD +21 -0
- syft_flwr/flwr_compatibility.py +0 -121
- syft_flwr-0.1.7.dist-info/RECORD +0 -21
- {syft_flwr-0.1.7.dist-info → syft_flwr-0.2.1.dist-info}/WHEEL +0 -0
- {syft_flwr-0.1.7.dist-info → syft_flwr-0.2.1.dist-info}/entry_points.txt +0 -0
- {syft_flwr-0.1.7.dist-info → syft_flwr-0.2.1.dist-info}/licenses/LICENSE +0 -0
syft_flwr/__init__.py
CHANGED
syft_flwr/consts.py
ADDED
syft_flwr/flower_client.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import sys
|
|
2
3
|
import traceback
|
|
3
4
|
|
|
@@ -5,88 +6,176 @@ from flwr.client import ClientApp
|
|
|
5
6
|
from flwr.common import Context
|
|
6
7
|
from flwr.common.constant import ErrorCode, MessageType
|
|
7
8
|
from flwr.common.message import Error, Message
|
|
9
|
+
from flwr.common.record import RecordDict
|
|
8
10
|
from loguru import logger
|
|
9
11
|
from syft_event import SyftEvents
|
|
10
12
|
from syft_event.types import Request
|
|
13
|
+
from typing_extensions import Optional, Union
|
|
11
14
|
|
|
12
|
-
from syft_flwr.flwr_compatibility import RecordDict, create_flwr_message
|
|
13
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
|
|
14
17
|
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
message
|
|
18
|
-
) -> bytes:
|
|
19
|
-
# Normal message handling
|
|
20
|
-
logger.info(f"Receive message with metadata: {message.metadata}")
|
|
21
|
-
reply_message: Message = client_app(message=message, context=context)
|
|
22
|
-
res_bytes: bytes = flower_message_to_bytes(reply_message)
|
|
23
|
-
logger.info(f"Reply message size: {len(res_bytes)/2**20} MB")
|
|
24
|
-
return res_bytes
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _create_error_reply(message: Message, error: Error) -> bytes:
|
|
28
|
-
"""Create and return error reply message in bytes."""
|
|
29
|
-
error_reply: Message = create_flwr_message(
|
|
30
|
-
content=RecordDict(),
|
|
31
|
-
reply_to=message,
|
|
32
|
-
message_type=message.metadata.message_type,
|
|
33
|
-
src_node_id=message.metadata.dst_node_id,
|
|
34
|
-
dst_node_id=message.metadata.src_node_id,
|
|
35
|
-
group_id=message.metadata.group_id,
|
|
36
|
-
run_id=message.metadata.run_id,
|
|
37
|
-
error=error,
|
|
38
|
-
)
|
|
39
|
-
error_bytes: bytes = flower_message_to_bytes(error_reply)
|
|
40
|
-
logger.info(f"Error reply message size: {len(error_bytes)/2**20} MB")
|
|
41
|
-
return error_bytes
|
|
19
|
+
class MessageHandler:
|
|
20
|
+
"""Handles message processing for Flower client."""
|
|
42
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
|
|
43
28
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
)
|
|
51
111
|
|
|
52
|
-
@box.on_request("/messages")
|
|
53
|
-
def handle_messages(request: Request) -> None:
|
|
54
112
|
logger.info(
|
|
55
|
-
f"Received request
|
|
113
|
+
f"{encryption_status} Received request from {original_sender}, "
|
|
114
|
+
f"id: {request.id}, size: {len(request.body) / 1024 / 1024:.2f} MB"
|
|
56
115
|
)
|
|
57
|
-
|
|
116
|
+
|
|
117
|
+
# Parse message
|
|
58
118
|
try:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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)
|
|
78
146
|
|
|
79
147
|
except Exception as e:
|
|
80
|
-
|
|
81
|
-
error_message = f"Client: '{client_email}'. Error: {str(e)}. Traceback: {error_traceback}"
|
|
148
|
+
error_message = f"Client: '{self.client_email}'. Error: {str(e)}. Traceback: {traceback.format_exc()}"
|
|
82
149
|
logger.error(error_message)
|
|
83
150
|
|
|
84
151
|
error = Error(
|
|
85
152
|
code=ErrorCode.CLIENT_APP_RAISED_EXCEPTION, reason=error_message
|
|
86
153
|
)
|
|
87
|
-
box._stop_event.set()
|
|
88
|
-
return
|
|
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)
|
|
89
177
|
|
|
178
|
+
# Run
|
|
90
179
|
try:
|
|
91
180
|
box.run_forever()
|
|
92
181
|
except Exception as e:
|
syft_flwr/flower_server.py
CHANGED
|
@@ -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 =
|
|
20
|
-
|
|
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
|
|