flwr-nightly 1.19.0.dev20250520__py3-none-any.whl → 1.19.0.dev20250522__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.
Files changed (31) hide show
  1. flwr/client/__init__.py +2 -2
  2. flwr/client/grpc_adapter_client/connection.py +4 -4
  3. flwr/client/grpc_rere_client/connection.py +4 -4
  4. flwr/client/rest_client/connection.py +4 -4
  5. flwr/client/start_client_internal.py +495 -0
  6. flwr/client/supernode/app.py +1 -9
  7. flwr/common/inflatable.py +23 -0
  8. flwr/common/inflatable_grpc_utils.py +2 -0
  9. flwr/common/message.py +82 -1
  10. flwr/common/serde.py +8 -56
  11. flwr/common/serde_utils.py +50 -0
  12. flwr/{client → compat/client}/app.py +13 -11
  13. flwr/server/app.py +12 -1
  14. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +6 -1
  15. flwr/server/superlink/linkstate/sqlite_linkstate.py +2 -6
  16. flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
  17. flwr/server/superlink/serverappio/serverappio_servicer.py +6 -1
  18. flwr/supercore/object_store/__init__.py +23 -0
  19. flwr/supercore/object_store/in_memory_object_store.py +65 -0
  20. flwr/supercore/object_store/object_store.py +86 -0
  21. flwr/supercore/object_store/object_store_factory.py +44 -0
  22. flwr/{client → supernode}/nodestate/in_memory_nodestate.py +1 -1
  23. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/METADATA +1 -1
  24. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/RECORD +31 -26
  25. /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
  26. /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
  27. /flwr/{client → supernode}/nodestate/__init__.py +0 -0
  28. /flwr/{client → supernode}/nodestate/nodestate.py +0 -0
  29. /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
  30. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/WHEEL +0 -0
  31. {flwr_nightly-1.19.0.dev20250520.dist-info → flwr_nightly-1.19.0.dev20250522.dist-info}/entry_points.txt +0 -0
flwr/client/__init__.py CHANGED
@@ -15,8 +15,8 @@
15
15
  """Flower client."""
16
16
 
17
17
 
18
- from .app import start_client as start_client
19
- from .app import start_numpy_client as start_numpy_client
18
+ from ..compat.client.app import start_client as start_client # Deprecated
19
+ from ..compat.client.app import start_numpy_client as start_numpy_client # Deprecated
20
20
  from .client import Client as Client
21
21
  from .client_app import ClientApp as ClientApp
22
22
  from .numpy_client import NumPyClient as NumPyClient
@@ -45,10 +45,10 @@ def grpc_adapter( # pylint: disable=R0913,too-many-positional-arguments
45
45
  tuple[
46
46
  Callable[[], Optional[Message]],
47
47
  Callable[[Message], None],
48
- Optional[Callable[[], Optional[int]]],
49
- Optional[Callable[[], None]],
50
- Optional[Callable[[int], Run]],
51
- Optional[Callable[[str, int], Fab]],
48
+ Callable[[], Optional[int]],
49
+ Callable[[], None],
50
+ Callable[[int], Run],
51
+ Callable[[str, int], Fab],
52
52
  ]
53
53
  ]:
54
54
  """Primitives for request/response-based interaction with a server via GrpcAdapter.
@@ -74,10 +74,10 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
74
74
  tuple[
75
75
  Callable[[], Optional[Message]],
76
76
  Callable[[Message], None],
77
- Optional[Callable[[], Optional[int]]],
78
- Optional[Callable[[], None]],
79
- Optional[Callable[[int], Run]],
80
- Optional[Callable[[str, int], Fab]],
77
+ Callable[[], Optional[int]],
78
+ Callable[[], None],
79
+ Callable[[int], Run],
80
+ Callable[[str, int], Fab],
81
81
  ]
82
82
  ]:
83
83
  """Primitives for request/response-based interaction with a server.
@@ -87,10 +87,10 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
87
87
  tuple[
88
88
  Callable[[], Optional[Message]],
89
89
  Callable[[Message], None],
90
- Optional[Callable[[], Optional[int]]],
91
- Optional[Callable[[], None]],
92
- Optional[Callable[[int], Run]],
93
- Optional[Callable[[str, int], Fab]],
90
+ Callable[[], Optional[int]],
91
+ Callable[[], None],
92
+ Callable[[int], Run],
93
+ Callable[[str, int], Fab],
94
94
  ]
95
95
  ]:
96
96
  """Primitives for request/response-based interaction with a server.
@@ -0,0 +1,495 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Main loop for Flower SuperNode."""
16
+
17
+
18
+ import multiprocessing
19
+ import os
20
+ import sys
21
+ import threading
22
+ import time
23
+ from contextlib import AbstractContextManager
24
+ from logging import ERROR, INFO, WARN
25
+ from os import urandom
26
+ from pathlib import Path
27
+ from typing import Callable, Optional, Union
28
+
29
+ import grpc
30
+ from cryptography.hazmat.primitives.asymmetric import ec
31
+ from grpc import RpcError
32
+
33
+ from flwr.app.error import Error
34
+ from flwr.cli.config_utils import get_fab_metadata
35
+ from flwr.client.clientapp.app import flwr_clientapp
36
+ from flwr.client.clientapp.clientappio_servicer import (
37
+ ClientAppInputs,
38
+ ClientAppIoServicer,
39
+ )
40
+ from flwr.client.grpc_adapter_client.connection import grpc_adapter
41
+ from flwr.client.grpc_rere_client.connection import grpc_request_response
42
+ from flwr.client.message_handler.message_handler import handle_control_message
43
+ from flwr.client.run_info_store import DeprecatedRunInfoStore
44
+ from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Message
45
+ from flwr.common.address import parse_address
46
+ from flwr.common.constant import (
47
+ CLIENT_OCTET,
48
+ CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
49
+ ISOLATION_MODE_SUBPROCESS,
50
+ MAX_RETRY_DELAY,
51
+ RUN_ID_NUM_BYTES,
52
+ SERVER_OCTET,
53
+ TRANSPORT_TYPE_GRPC_ADAPTER,
54
+ TRANSPORT_TYPE_GRPC_RERE,
55
+ TRANSPORT_TYPE_REST,
56
+ TRANSPORT_TYPES,
57
+ ErrorCode,
58
+ )
59
+ from flwr.common.exit import ExitCode, flwr_exit
60
+ from flwr.common.grpc import generic_create_grpc_server
61
+ from flwr.common.logger import log
62
+ from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
63
+ from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
64
+ from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
65
+ from flwr.supernode.nodestate import NodeStateFactory
66
+
67
+
68
+ # pylint: disable=import-outside-toplevel
69
+ # pylint: disable=too-many-branches
70
+ # pylint: disable=too-many-locals
71
+ # pylint: disable=too-many-statements
72
+ # pylint: disable=too-many-arguments
73
+ def start_client_internal(
74
+ *,
75
+ server_address: str,
76
+ node_config: UserConfig,
77
+ grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
78
+ root_certificates: Optional[Union[bytes, str]] = None,
79
+ insecure: Optional[bool] = None,
80
+ transport: str,
81
+ authentication_keys: Optional[
82
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
83
+ ] = None,
84
+ max_retries: Optional[int] = None,
85
+ max_wait_time: Optional[float] = None,
86
+ flwr_path: Optional[Path] = None,
87
+ isolation: str = ISOLATION_MODE_SUBPROCESS,
88
+ clientappio_api_address: str = CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
89
+ ) -> None:
90
+ """Start a Flower client node which connects to a Flower server.
91
+
92
+ Parameters
93
+ ----------
94
+ server_address : str
95
+ The IPv4 or IPv6 address of the server. If the Flower
96
+ server runs on the same machine on port 8080, then `server_address`
97
+ would be `"[::]:8080"`.
98
+ node_config: UserConfig
99
+ The configuration of the node.
100
+ grpc_max_message_length : int (default: 536_870_912, this equals 512MB)
101
+ The maximum length of gRPC messages that can be exchanged with the
102
+ Flower server. The default should be sufficient for most models.
103
+ Users who train very large models might need to increase this
104
+ value. Note that the Flower server needs to be started with the
105
+ same value (see `flwr.server.start_server`), otherwise it will not
106
+ know about the increased limit and block larger messages.
107
+ root_certificates : Optional[Union[bytes, str]] (default: None)
108
+ The PEM-encoded root certificates as a byte string or a path string.
109
+ If provided, a secure connection using the certificates will be
110
+ established to an SSL-enabled Flower server.
111
+ insecure : Optional[bool] (default: None)
112
+ Starts an insecure gRPC connection when True. Enables HTTPS connection
113
+ when False, using system certificates if `root_certificates` is None.
114
+ transport : str
115
+ Configure the transport layer. Allowed values:
116
+ - 'grpc-rere': gRPC, request-response
117
+ - 'grpc-adapter': gRPC via 3rd party adapter (experimental)
118
+ - 'rest': HTTP (experimental)
119
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
120
+ Tuple containing the elliptic curve private key and public key for
121
+ authentication from the cryptography library.
122
+ Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
123
+ Used to establish an authenticated connection with the server.
124
+ max_retries: Optional[int] (default: None)
125
+ The maximum number of times the client will try to connect to the
126
+ server before giving up in case of a connection error. If set to None,
127
+ there is no limit to the number of tries.
128
+ max_wait_time: Optional[float] (default: None)
129
+ The maximum duration before the client stops trying to
130
+ connect to the server in case of connection error.
131
+ If set to None, there is no limit to the total time.
132
+ flwr_path: Optional[Path] (default: None)
133
+ The fully resolved path containing installed Flower Apps.
134
+ isolation : str (default: ISOLATION_MODE_SUBPROCESS)
135
+ Isolation mode for `ClientApp`. Possible values are `subprocess` and
136
+ `process`. If `subprocess`, the `ClientApp` runs in a subprocess started
137
+ by the SueprNode and communicates using gRPC at the address
138
+ `clientappio_api_address`. If `process`, the `ClientApp` runs in a separate
139
+ isolated process and communicates using gRPC at the address
140
+ `clientappio_api_address`.
141
+ clientappio_api_address : str
142
+ (default: `CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS`)
143
+ The SuperNode gRPC server address.
144
+ """
145
+ if insecure is None:
146
+ insecure = root_certificates is None
147
+
148
+ _clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc(
149
+ address=clientappio_api_address,
150
+ certificates=None,
151
+ )
152
+
153
+ # Initialize connection context manager
154
+ connection, address, connection_error_type = _init_connection(
155
+ transport, server_address
156
+ )
157
+
158
+ def _on_sucess(retry_state: RetryState) -> None:
159
+ if retry_state.tries > 1:
160
+ log(
161
+ INFO,
162
+ "Connection successful after %.2f seconds and %s tries.",
163
+ retry_state.elapsed_time,
164
+ retry_state.tries,
165
+ )
166
+
167
+ def _on_backoff(retry_state: RetryState) -> None:
168
+ if retry_state.tries == 1:
169
+ log(WARN, "Connection attempt failed, retrying...")
170
+ else:
171
+ log(
172
+ WARN,
173
+ "Connection attempt failed, retrying in %.2f seconds",
174
+ retry_state.actual_wait,
175
+ )
176
+
177
+ retry_invoker = RetryInvoker(
178
+ wait_gen_factory=lambda: exponential(max_delay=MAX_RETRY_DELAY),
179
+ recoverable_exceptions=connection_error_type,
180
+ max_tries=max_retries + 1 if max_retries is not None else None,
181
+ max_time=max_wait_time,
182
+ on_giveup=lambda retry_state: (
183
+ log(
184
+ WARN,
185
+ "Giving up reconnection after %.2f seconds and %s tries.",
186
+ retry_state.elapsed_time,
187
+ retry_state.tries,
188
+ )
189
+ if retry_state.tries > 1
190
+ else None
191
+ ),
192
+ on_success=_on_sucess,
193
+ on_backoff=_on_backoff,
194
+ )
195
+
196
+ # DeprecatedRunInfoStore gets initialized when the first connection is established
197
+ run_info_store: Optional[DeprecatedRunInfoStore] = None
198
+ state_factory = NodeStateFactory()
199
+ state = state_factory.state()
200
+ mp_spawn_context = multiprocessing.get_context("spawn")
201
+
202
+ runs: dict[int, Run] = {}
203
+
204
+ while True:
205
+ sleep_duration: int = 0
206
+ with connection(
207
+ address,
208
+ insecure,
209
+ retry_invoker,
210
+ grpc_max_message_length,
211
+ root_certificates,
212
+ authentication_keys,
213
+ ) as conn:
214
+ receive, send, create_node, delete_node, get_run, get_fab = conn
215
+
216
+ # Register node when connecting the first time
217
+ if run_info_store is None:
218
+ # Call create_node fn to register node
219
+ # and store node_id in state
220
+ if (node_id := create_node()) is None:
221
+ raise ValueError("Failed to register SuperNode with the SuperLink")
222
+ state.set_node_id(node_id)
223
+ run_info_store = DeprecatedRunInfoStore(
224
+ node_id=state.get_node_id(),
225
+ node_config=node_config,
226
+ )
227
+
228
+ # pylint: disable=too-many-nested-blocks
229
+ while True:
230
+ try:
231
+ # Receive
232
+ message = receive()
233
+ if message is None:
234
+ time.sleep(3) # Wait for 3s before asking again
235
+ continue
236
+
237
+ log(INFO, "")
238
+ if len(message.metadata.group_id) > 0:
239
+ log(
240
+ INFO,
241
+ "[RUN %s, ROUND %s]",
242
+ message.metadata.run_id,
243
+ message.metadata.group_id,
244
+ )
245
+ log(
246
+ INFO,
247
+ "Received: %s message %s",
248
+ message.metadata.message_type,
249
+ message.metadata.message_id,
250
+ )
251
+
252
+ # Handle control message
253
+ out_message, sleep_duration = handle_control_message(message)
254
+ if out_message:
255
+ send(out_message)
256
+ break
257
+
258
+ # Get run info
259
+ run_id = message.metadata.run_id
260
+ if run_id not in runs:
261
+ runs[run_id] = get_run(run_id)
262
+
263
+ run: Run = runs[run_id]
264
+ if get_fab is not None and run.fab_hash:
265
+ fab = get_fab(run.fab_hash, run_id)
266
+ fab_id, fab_version = get_fab_metadata(fab.content)
267
+ else:
268
+ fab = None
269
+ fab_id, fab_version = run.fab_id, run.fab_version
270
+
271
+ run.fab_id, run.fab_version = fab_id, fab_version
272
+
273
+ # Register context for this run
274
+ run_info_store.register_context(
275
+ run_id=run_id,
276
+ run=run,
277
+ flwr_path=flwr_path,
278
+ fab=fab,
279
+ )
280
+
281
+ # Retrieve context for this run
282
+ context = run_info_store.retrieve_context(run_id=run_id)
283
+ # Create an error reply message that will never be used to prevent
284
+ # the used-before-assignment linting error
285
+ reply_message = Message(
286
+ Error(code=ErrorCode.UNKNOWN, reason="Unknown"),
287
+ reply_to=message,
288
+ )
289
+
290
+ # Handle app loading and task message
291
+ try:
292
+ # Two isolation modes:
293
+ # 1. `subprocess`: SuperNode is starting the ClientApp
294
+ # process as a subprocess.
295
+ # 2. `process`: ClientApp process gets started separately
296
+ # (via `flwr-clientapp`), for example, in a separate
297
+ # Docker container.
298
+
299
+ # Generate SuperNode token
300
+ token = int.from_bytes(urandom(RUN_ID_NUM_BYTES), "little")
301
+
302
+ # Mode 1: SuperNode starts ClientApp as subprocess
303
+ start_subprocess = isolation == ISOLATION_MODE_SUBPROCESS
304
+
305
+ # Share Message and Context with servicer
306
+ clientappio_servicer.set_inputs(
307
+ clientapp_input=ClientAppInputs(
308
+ message=message,
309
+ context=context,
310
+ run=run,
311
+ fab=fab,
312
+ token=token,
313
+ ),
314
+ token_returned=start_subprocess,
315
+ )
316
+
317
+ if start_subprocess:
318
+ _octet, _colon, _port = clientappio_api_address.rpartition(
319
+ ":"
320
+ )
321
+ io_address = (
322
+ f"{CLIENT_OCTET}:{_port}"
323
+ if _octet == SERVER_OCTET
324
+ else clientappio_api_address
325
+ )
326
+ # Start ClientApp subprocess
327
+ command = [
328
+ "flwr-clientapp",
329
+ "--clientappio-api-address",
330
+ io_address,
331
+ "--token",
332
+ str(token),
333
+ ]
334
+ command.append("--insecure")
335
+
336
+ proc = mp_spawn_context.Process(
337
+ target=_run_flwr_clientapp,
338
+ args=(command, os.getpid()),
339
+ daemon=True,
340
+ )
341
+ proc.start()
342
+ proc.join()
343
+ else:
344
+ # Wait for output to become available
345
+ while not clientappio_servicer.has_outputs():
346
+ time.sleep(0.1)
347
+
348
+ outputs = clientappio_servicer.get_outputs()
349
+ reply_message, context = outputs.message, outputs.context
350
+ except Exception as ex: # pylint: disable=broad-exception-caught
351
+
352
+ # Don't update/change DeprecatedRunInfoStore
353
+
354
+ e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
355
+ # Ex fmt: "<class 'ZeroDivisionError'>:<'division by zero'>"
356
+ reason = str(type(ex)) + ":<'" + str(ex) + "'>"
357
+ exc_entity = "ClientApp"
358
+
359
+ log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
360
+
361
+ # Create error message
362
+ reply_message = Message(
363
+ Error(code=e_code, reason=reason),
364
+ reply_to=message,
365
+ )
366
+ else:
367
+ # No exception, update node state
368
+ run_info_store.update_context(
369
+ run_id=run_id,
370
+ context=context,
371
+ )
372
+
373
+ # Send
374
+ send(reply_message)
375
+ log(INFO, "Sent reply")
376
+
377
+ except RunNotRunningException:
378
+ log(INFO, "")
379
+ log(
380
+ INFO,
381
+ "SuperNode aborted sending the reply message. "
382
+ "Run ID %s is not in `RUNNING` status.",
383
+ run_id,
384
+ )
385
+ log(INFO, "")
386
+ # pylint: enable=too-many-nested-blocks
387
+
388
+ # Unregister node
389
+ if delete_node is not None:
390
+ delete_node() # pylint: disable=not-callable
391
+
392
+ if sleep_duration == 0:
393
+ log(INFO, "Disconnect and shut down")
394
+ break
395
+
396
+ # Sleep and reconnect afterwards
397
+ log(
398
+ INFO,
399
+ "Disconnect, then re-establish connection after %s second(s)",
400
+ sleep_duration,
401
+ )
402
+ time.sleep(sleep_duration)
403
+
404
+
405
+ def _init_connection(transport: str, server_address: str) -> tuple[
406
+ Callable[
407
+ [
408
+ str,
409
+ bool,
410
+ RetryInvoker,
411
+ int,
412
+ Union[bytes, str, None],
413
+ Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
414
+ ],
415
+ AbstractContextManager[
416
+ tuple[
417
+ Callable[[], Optional[Message]],
418
+ Callable[[Message], None],
419
+ Callable[[], Optional[int]],
420
+ Callable[[], None],
421
+ Callable[[int], Run],
422
+ Callable[[str, int], Fab],
423
+ ]
424
+ ],
425
+ ],
426
+ str,
427
+ type[Exception],
428
+ ]:
429
+ # Parse IP address
430
+ parsed_address = parse_address(server_address)
431
+ if not parsed_address:
432
+ flwr_exit(
433
+ ExitCode.COMMON_ADDRESS_INVALID,
434
+ f"SuperLink address ({server_address}) cannot be parsed.",
435
+ )
436
+ host, port, is_v6 = parsed_address
437
+ address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
438
+
439
+ # Use either gRPC bidirectional streaming or REST request/response
440
+ if transport == TRANSPORT_TYPE_REST:
441
+ try:
442
+ from requests.exceptions import ConnectionError as RequestsConnectionError
443
+
444
+ from flwr.client.rest_client.connection import http_request_response
445
+ except ModuleNotFoundError:
446
+ flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
447
+ if server_address[:4] != "http":
448
+ flwr_exit(ExitCode.SUPERNODE_REST_ADDRESS_INVALID)
449
+ connection, error_type = http_request_response, RequestsConnectionError
450
+ elif transport == TRANSPORT_TYPE_GRPC_RERE:
451
+ connection, error_type = grpc_request_response, RpcError
452
+ elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
453
+ connection, error_type = grpc_adapter, RpcError
454
+ else:
455
+ raise ValueError(
456
+ f"Unknown transport type: {transport} (possible: {TRANSPORT_TYPES})"
457
+ )
458
+
459
+ return connection, address, error_type
460
+
461
+
462
+ def _run_flwr_clientapp(args: list[str], main_pid: int) -> None:
463
+ # Monitor the main process in case of SIGKILL
464
+ def main_process_monitor() -> None:
465
+ while True:
466
+ time.sleep(1)
467
+ if os.getppid() != main_pid:
468
+ os.kill(os.getpid(), 9)
469
+
470
+ threading.Thread(target=main_process_monitor, daemon=True).start()
471
+
472
+ # Run the command
473
+ sys.argv = args
474
+ flwr_clientapp()
475
+
476
+
477
+ def run_clientappio_api_grpc(
478
+ address: str,
479
+ certificates: Optional[tuple[bytes, bytes, bytes]],
480
+ ) -> tuple[grpc.Server, ClientAppIoServicer]:
481
+ """Run ClientAppIo API gRPC server."""
482
+ clientappio_servicer: grpc.Server = ClientAppIoServicer()
483
+ clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server
484
+ clientappio_grpc_server = generic_create_grpc_server(
485
+ servicer_and_add_fn=(
486
+ clientappio_servicer,
487
+ clientappio_add_servicer_to_server_fn,
488
+ ),
489
+ server_address=address,
490
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
491
+ certificates=certificates,
492
+ )
493
+ log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address)
494
+ clientappio_grpc_server.start()
495
+ return clientappio_grpc_server, clientappio_servicer
@@ -43,8 +43,7 @@ from flwr.common.exit import ExitCode, flwr_exit
43
43
  from flwr.common.exit_handlers import register_exit_handlers
44
44
  from flwr.common.logger import log
45
45
 
46
- from ..app import start_client_internal
47
- from ..clientapp.utils import get_load_client_app_fn
46
+ from ..start_client_internal import start_client_internal
48
47
 
49
48
 
50
49
  def run_supernode() -> None:
@@ -64,12 +63,6 @@ def run_supernode() -> None:
64
63
  )
65
64
 
66
65
  root_certificates = try_obtain_root_certificates(args, args.superlink)
67
- load_fn = get_load_client_app_fn(
68
- default_app_ref="",
69
- app_path=None,
70
- flwr_dir=args.flwr_dir,
71
- multi_app=True,
72
- )
73
66
  authentication_keys = _try_setup_client_authentication(args)
74
67
 
75
68
  log(DEBUG, "Isolation mode: %s", args.isolation)
@@ -82,7 +75,6 @@ def run_supernode() -> None:
82
75
 
83
76
  start_client_internal(
84
77
  server_address=args.superlink,
85
- load_client_app_fn=load_fn,
86
78
  transport=args.transport,
87
79
  root_certificates=root_certificates,
88
80
  insecure=args.insecure,
flwr/common/inflatable.py CHANGED
@@ -112,6 +112,29 @@ def _get_object_body(object_content: bytes) -> bytes:
112
112
  return object_content.split(HEAD_BODY_DIVIDER, 1)[1]
113
113
 
114
114
 
115
+ def is_valid_sha256_hash(object_id: str) -> bool:
116
+ """Check if the given string is a valid SHA-256 hash.
117
+
118
+ Parameters
119
+ ----------
120
+ object_id : str
121
+ The string to check.
122
+
123
+ Returns
124
+ -------
125
+ bool
126
+ ``True`` if the string is a valid SHA-256 hash, ``False`` otherwise.
127
+ """
128
+ if len(object_id) != 64:
129
+ return False
130
+ try:
131
+ # If base 16 int conversion succeeds, it's a valid hexadecimal str
132
+ int(object_id, 16)
133
+ return True
134
+ except ValueError:
135
+ return False
136
+
137
+
115
138
  def get_object_type_from_object_content(object_content: bytes) -> str:
116
139
  """Return object type from bytes."""
117
140
  return get_object_head_values_from_object_content(object_content)[0]
@@ -31,6 +31,7 @@ from .inflatable import (
31
31
  get_object_head_values_from_object_content,
32
32
  get_object_id,
33
33
  )
34
+ from .message import Message
34
35
  from .record import Array, ArrayRecord, ConfigRecord, MetricRecord, RecordDict
35
36
 
36
37
  # Helper registry that maps names of classes to their type
@@ -38,6 +39,7 @@ inflatable_class_registry: dict[str, type[InflatableObject]] = {
38
39
  Array.__qualname__: Array,
39
40
  ArrayRecord.__qualname__: ArrayRecord,
40
41
  ConfigRecord.__qualname__: ConfigRecord,
42
+ Message.__qualname__: Message,
41
43
  MetricRecord.__qualname__: MetricRecord,
42
44
  RecordDict.__qualname__: RecordDict,
43
45
  }