flwr-nightly 1.19.0.dev20250520__py3-none-any.whl → 1.19.0.dev20250521__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.
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
@@ -0,0 +1,608 @@
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, cast
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.cli.install import install_from_fab
36
+ from flwr.client.client import Client
37
+ from flwr.client.client_app import ClientApp, LoadClientAppError
38
+ from flwr.client.clientapp.app import flwr_clientapp
39
+ from flwr.client.clientapp.clientappio_servicer import (
40
+ ClientAppInputs,
41
+ ClientAppIoServicer,
42
+ )
43
+ from flwr.client.grpc_adapter_client.connection import grpc_adapter
44
+ from flwr.client.grpc_client.connection import grpc_connection
45
+ from flwr.client.grpc_rere_client.connection import grpc_request_response
46
+ from flwr.client.message_handler.message_handler import handle_control_message
47
+ from flwr.client.run_info_store import DeprecatedRunInfoStore
48
+ from flwr.client.typing import ClientFnExt
49
+ from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, Message
50
+ from flwr.common.address import parse_address
51
+ from flwr.common.constant import (
52
+ CLIENT_OCTET,
53
+ CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
54
+ ISOLATION_MODE_PROCESS,
55
+ ISOLATION_MODE_SUBPROCESS,
56
+ MAX_RETRY_DELAY,
57
+ RUN_ID_NUM_BYTES,
58
+ SERVER_OCTET,
59
+ TRANSPORT_TYPE_GRPC_ADAPTER,
60
+ TRANSPORT_TYPE_GRPC_BIDI,
61
+ TRANSPORT_TYPE_GRPC_RERE,
62
+ TRANSPORT_TYPE_REST,
63
+ TRANSPORT_TYPES,
64
+ ErrorCode,
65
+ )
66
+ from flwr.common.exit import ExitCode, flwr_exit
67
+ from flwr.common.grpc import generic_create_grpc_server
68
+ from flwr.common.logger import log
69
+ from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
70
+ from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
71
+ from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
72
+ from flwr.supernode.nodestate import NodeStateFactory
73
+
74
+
75
+ def _check_actionable_client(
76
+ client: Optional[Client], client_fn: Optional[ClientFnExt]
77
+ ) -> None:
78
+ if client_fn is None and client is None:
79
+ raise ValueError(
80
+ "Both `client_fn` and `client` are `None`, but one is required"
81
+ )
82
+
83
+ if client_fn is not None and client is not None:
84
+ raise ValueError(
85
+ "Both `client_fn` and `client` are provided, but only one is allowed"
86
+ )
87
+
88
+
89
+ # pylint: disable=import-outside-toplevel
90
+ # pylint: disable=too-many-branches
91
+ # pylint: disable=too-many-locals
92
+ # pylint: disable=too-many-statements
93
+ # pylint: disable=too-many-arguments
94
+ def start_client_internal(
95
+ *,
96
+ server_address: str,
97
+ node_config: UserConfig,
98
+ load_client_app_fn: Optional[Callable[[str, str, str], ClientApp]] = None,
99
+ client_fn: Optional[ClientFnExt] = None,
100
+ client: Optional[Client] = None,
101
+ grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
102
+ root_certificates: Optional[Union[bytes, str]] = None,
103
+ insecure: Optional[bool] = None,
104
+ transport: Optional[str] = None,
105
+ authentication_keys: Optional[
106
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
107
+ ] = None,
108
+ max_retries: Optional[int] = None,
109
+ max_wait_time: Optional[float] = None,
110
+ flwr_path: Optional[Path] = None,
111
+ isolation: Optional[str] = None,
112
+ clientappio_api_address: Optional[str] = CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
113
+ ) -> None:
114
+ """Start a Flower client node which connects to a Flower server.
115
+
116
+ Parameters
117
+ ----------
118
+ server_address : str
119
+ The IPv4 or IPv6 address of the server. If the Flower
120
+ server runs on the same machine on port 8080, then `server_address`
121
+ would be `"[::]:8080"`.
122
+ node_config: UserConfig
123
+ The configuration of the node.
124
+ load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
125
+ A function that can be used to load a `ClientApp` instance.
126
+ client_fn : Optional[ClientFnExt]
127
+ A callable that instantiates a Client. (default: None)
128
+ client : Optional[flwr.client.Client]
129
+ An implementation of the abstract base
130
+ class `flwr.client.Client` (default: None)
131
+ grpc_max_message_length : int (default: 536_870_912, this equals 512MB)
132
+ The maximum length of gRPC messages that can be exchanged with the
133
+ Flower server. The default should be sufficient for most models.
134
+ Users who train very large models might need to increase this
135
+ value. Note that the Flower server needs to be started with the
136
+ same value (see `flwr.server.start_server`), otherwise it will not
137
+ know about the increased limit and block larger messages.
138
+ root_certificates : Optional[Union[bytes, str]] (default: None)
139
+ The PEM-encoded root certificates as a byte string or a path string.
140
+ If provided, a secure connection using the certificates will be
141
+ established to an SSL-enabled Flower server.
142
+ insecure : Optional[bool] (default: None)
143
+ Starts an insecure gRPC connection when True. Enables HTTPS connection
144
+ when False, using system certificates if `root_certificates` is None.
145
+ transport : Optional[str] (default: None)
146
+ Configure the transport layer. Allowed values:
147
+ - 'grpc-bidi': gRPC, bidirectional streaming
148
+ - 'grpc-rere': gRPC, request-response (experimental)
149
+ - 'rest': HTTP (experimental)
150
+ authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
151
+ Tuple containing the elliptic curve private key and public key for
152
+ authentication from the cryptography library.
153
+ Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
154
+ Used to establish an authenticated connection with the server.
155
+ max_retries: Optional[int] (default: None)
156
+ The maximum number of times the client will try to connect to the
157
+ server before giving up in case of a connection error. If set to None,
158
+ there is no limit to the number of tries.
159
+ max_wait_time: Optional[float] (default: None)
160
+ The maximum duration before the client stops trying to
161
+ connect to the server in case of connection error.
162
+ If set to None, there is no limit to the total time.
163
+ flwr_path: Optional[Path] (default: None)
164
+ The fully resolved path containing installed Flower Apps.
165
+ isolation : Optional[str] (default: None)
166
+ Isolation mode for `ClientApp`. Possible values are `subprocess` and
167
+ `process`. Defaults to `None`, which runs the `ClientApp` in the same process
168
+ as the SuperNode. If `subprocess`, the `ClientApp` runs in a subprocess started
169
+ by the SueprNode and communicates using gRPC at the address
170
+ `clientappio_api_address`. If `process`, the `ClientApp` runs in a separate
171
+ isolated process and communicates using gRPC at the address
172
+ `clientappio_api_address`.
173
+ clientappio_api_address : Optional[str]
174
+ (default: `CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS`)
175
+ The SuperNode gRPC server address.
176
+ """
177
+ if insecure is None:
178
+ insecure = root_certificates is None
179
+
180
+ if load_client_app_fn is None:
181
+ _check_actionable_client(client, client_fn)
182
+
183
+ if client_fn is None:
184
+ # Wrap `Client` instance in `client_fn`
185
+ def single_client_factory(
186
+ context: Context, # pylint: disable=unused-argument
187
+ ) -> Client:
188
+ if client is None: # Added this to keep mypy happy
189
+ raise ValueError(
190
+ "Both `client_fn` and `client` are `None`, but one is required"
191
+ )
192
+ return client # Always return the same instance
193
+
194
+ client_fn = single_client_factory
195
+
196
+ def _load_client_app(_1: str, _2: str, _3: str) -> ClientApp:
197
+ return ClientApp(client_fn=client_fn)
198
+
199
+ load_client_app_fn = _load_client_app
200
+
201
+ if isolation:
202
+ if clientappio_api_address is None:
203
+ raise ValueError(
204
+ f"`clientappio_api_address` required when `isolation` is "
205
+ f"{ISOLATION_MODE_SUBPROCESS} or {ISOLATION_MODE_PROCESS}",
206
+ )
207
+ _clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc(
208
+ address=clientappio_api_address,
209
+ certificates=None,
210
+ )
211
+ clientappio_api_address = cast(str, clientappio_api_address)
212
+
213
+ # At this point, only `load_client_app_fn` should be used
214
+ # Both `client` and `client_fn` must not be used directly
215
+
216
+ # Initialize connection context manager
217
+ connection, address, connection_error_type = _init_connection(
218
+ transport, server_address
219
+ )
220
+
221
+ def _on_sucess(retry_state: RetryState) -> None:
222
+ if retry_state.tries > 1:
223
+ log(
224
+ INFO,
225
+ "Connection successful after %.2f seconds and %s tries.",
226
+ retry_state.elapsed_time,
227
+ retry_state.tries,
228
+ )
229
+
230
+ def _on_backoff(retry_state: RetryState) -> None:
231
+ if retry_state.tries == 1:
232
+ log(WARN, "Connection attempt failed, retrying...")
233
+ else:
234
+ log(
235
+ WARN,
236
+ "Connection attempt failed, retrying in %.2f seconds",
237
+ retry_state.actual_wait,
238
+ )
239
+
240
+ retry_invoker = RetryInvoker(
241
+ wait_gen_factory=lambda: exponential(max_delay=MAX_RETRY_DELAY),
242
+ recoverable_exceptions=connection_error_type,
243
+ max_tries=max_retries + 1 if max_retries is not None else None,
244
+ max_time=max_wait_time,
245
+ on_giveup=lambda retry_state: (
246
+ log(
247
+ WARN,
248
+ "Giving up reconnection after %.2f seconds and %s tries.",
249
+ retry_state.elapsed_time,
250
+ retry_state.tries,
251
+ )
252
+ if retry_state.tries > 1
253
+ else None
254
+ ),
255
+ on_success=_on_sucess,
256
+ on_backoff=_on_backoff,
257
+ )
258
+
259
+ # DeprecatedRunInfoStore gets initialized when the first connection is established
260
+ run_info_store: Optional[DeprecatedRunInfoStore] = None
261
+ state_factory = NodeStateFactory()
262
+ state = state_factory.state()
263
+ mp_spawn_context = multiprocessing.get_context("spawn")
264
+
265
+ runs: dict[int, Run] = {}
266
+
267
+ while True:
268
+ sleep_duration: int = 0
269
+ with connection(
270
+ address,
271
+ insecure,
272
+ retry_invoker,
273
+ grpc_max_message_length,
274
+ root_certificates,
275
+ authentication_keys,
276
+ ) as conn:
277
+ receive, send, create_node, delete_node, get_run, get_fab = conn
278
+
279
+ # Register node when connecting the first time
280
+ if run_info_store is None:
281
+ if create_node is None:
282
+ if transport not in ["grpc-bidi", None]:
283
+ raise NotImplementedError(
284
+ "All transports except `grpc-bidi` require "
285
+ "an implementation for `create_node()`.'"
286
+ )
287
+ # gRPC-bidi doesn't have the concept of node_id,
288
+ # so we set it to -1
289
+ run_info_store = DeprecatedRunInfoStore(
290
+ node_id=-1,
291
+ node_config={},
292
+ )
293
+ else:
294
+ # Call create_node fn to register node
295
+ # and store node_id in state
296
+ if (node_id := create_node()) is None:
297
+ raise ValueError(
298
+ "Failed to register SuperNode with the SuperLink"
299
+ )
300
+ state.set_node_id(node_id)
301
+ run_info_store = DeprecatedRunInfoStore(
302
+ node_id=state.get_node_id(),
303
+ node_config=node_config,
304
+ )
305
+
306
+ # pylint: disable=too-many-nested-blocks
307
+ while True:
308
+ try:
309
+ # Receive
310
+ message = receive()
311
+ if message is None:
312
+ time.sleep(3) # Wait for 3s before asking again
313
+ continue
314
+
315
+ log(INFO, "")
316
+ if len(message.metadata.group_id) > 0:
317
+ log(
318
+ INFO,
319
+ "[RUN %s, ROUND %s]",
320
+ message.metadata.run_id,
321
+ message.metadata.group_id,
322
+ )
323
+ log(
324
+ INFO,
325
+ "Received: %s message %s",
326
+ message.metadata.message_type,
327
+ message.metadata.message_id,
328
+ )
329
+
330
+ # Handle control message
331
+ out_message, sleep_duration = handle_control_message(message)
332
+ if out_message:
333
+ send(out_message)
334
+ break
335
+
336
+ # Get run info
337
+ run_id = message.metadata.run_id
338
+ if run_id not in runs:
339
+ if get_run is not None:
340
+ runs[run_id] = get_run(run_id)
341
+ # If get_run is None, i.e., in grpc-bidi mode
342
+ else:
343
+ runs[run_id] = Run.create_empty(run_id=run_id)
344
+
345
+ run: Run = runs[run_id]
346
+ if get_fab is not None and run.fab_hash:
347
+ fab = get_fab(run.fab_hash, run_id)
348
+ if not isolation:
349
+ # If `ClientApp` runs in the same process, install the FAB
350
+ install_from_fab(fab.content, flwr_path, True)
351
+ fab_id, fab_version = get_fab_metadata(fab.content)
352
+ else:
353
+ fab = None
354
+ fab_id, fab_version = run.fab_id, run.fab_version
355
+
356
+ run.fab_id, run.fab_version = fab_id, fab_version
357
+
358
+ # Register context for this run
359
+ run_info_store.register_context(
360
+ run_id=run_id,
361
+ run=run,
362
+ flwr_path=flwr_path,
363
+ fab=fab,
364
+ )
365
+
366
+ # Retrieve context for this run
367
+ context = run_info_store.retrieve_context(run_id=run_id)
368
+ # Create an error reply message that will never be used to prevent
369
+ # the used-before-assignment linting error
370
+ reply_message = Message(
371
+ Error(code=ErrorCode.UNKNOWN, reason="Unknown"),
372
+ reply_to=message,
373
+ )
374
+
375
+ # Handle app loading and task message
376
+ try:
377
+ if isolation:
378
+ # Two isolation modes:
379
+ # 1. `subprocess`: SuperNode is starting the ClientApp
380
+ # process as a subprocess.
381
+ # 2. `process`: ClientApp process gets started separately
382
+ # (via `flwr-clientapp`), for example, in a separate
383
+ # Docker container.
384
+
385
+ # Generate SuperNode token
386
+ token = int.from_bytes(urandom(RUN_ID_NUM_BYTES), "little")
387
+
388
+ # Mode 1: SuperNode starts ClientApp as subprocess
389
+ start_subprocess = isolation == ISOLATION_MODE_SUBPROCESS
390
+
391
+ # Share Message and Context with servicer
392
+ clientappio_servicer.set_inputs(
393
+ clientapp_input=ClientAppInputs(
394
+ message=message,
395
+ context=context,
396
+ run=run,
397
+ fab=fab,
398
+ token=token,
399
+ ),
400
+ token_returned=start_subprocess,
401
+ )
402
+
403
+ if start_subprocess:
404
+ _octet, _colon, _port = (
405
+ clientappio_api_address.rpartition(":")
406
+ )
407
+ io_address = (
408
+ f"{CLIENT_OCTET}:{_port}"
409
+ if _octet == SERVER_OCTET
410
+ else clientappio_api_address
411
+ )
412
+ # Start ClientApp subprocess
413
+ command = [
414
+ "flwr-clientapp",
415
+ "--clientappio-api-address",
416
+ io_address,
417
+ "--token",
418
+ str(token),
419
+ ]
420
+ command.append("--insecure")
421
+
422
+ proc = mp_spawn_context.Process(
423
+ target=_run_flwr_clientapp,
424
+ args=(command, os.getpid()),
425
+ daemon=True,
426
+ )
427
+ proc.start()
428
+ proc.join()
429
+ else:
430
+ # Wait for output to become available
431
+ while not clientappio_servicer.has_outputs():
432
+ time.sleep(0.1)
433
+
434
+ outputs = clientappio_servicer.get_outputs()
435
+ reply_message, context = outputs.message, outputs.context
436
+ else:
437
+ # Load ClientApp instance
438
+ client_app: ClientApp = load_client_app_fn(
439
+ fab_id, fab_version, run.fab_hash
440
+ )
441
+
442
+ # Execute ClientApp
443
+ reply_message = client_app(message=message, context=context)
444
+ except Exception as ex: # pylint: disable=broad-exception-caught
445
+
446
+ # Legacy grpc-bidi
447
+ if transport in ["grpc-bidi", None]:
448
+ log(ERROR, "Client raised an exception.", exc_info=ex)
449
+ # Raise exception, crash process
450
+ raise ex
451
+
452
+ # Don't update/change DeprecatedRunInfoStore
453
+
454
+ e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
455
+ # Ex fmt: "<class 'ZeroDivisionError'>:<'division by zero'>"
456
+ reason = str(type(ex)) + ":<'" + str(ex) + "'>"
457
+ exc_entity = "ClientApp"
458
+ if isinstance(ex, LoadClientAppError):
459
+ reason = (
460
+ "An exception was raised when attempting to load "
461
+ "`ClientApp`"
462
+ )
463
+ e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
464
+ exc_entity = "SuperNode"
465
+
466
+ log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
467
+
468
+ # Create error message
469
+ reply_message = Message(
470
+ Error(code=e_code, reason=reason),
471
+ reply_to=message,
472
+ )
473
+ else:
474
+ # No exception, update node state
475
+ run_info_store.update_context(
476
+ run_id=run_id,
477
+ context=context,
478
+ )
479
+
480
+ # Send
481
+ send(reply_message)
482
+ log(INFO, "Sent reply")
483
+
484
+ except RunNotRunningException:
485
+ log(INFO, "")
486
+ log(
487
+ INFO,
488
+ "SuperNode aborted sending the reply message. "
489
+ "Run ID %s is not in `RUNNING` status.",
490
+ run_id,
491
+ )
492
+ log(INFO, "")
493
+ # pylint: enable=too-many-nested-blocks
494
+
495
+ # Unregister node
496
+ if delete_node is not None:
497
+ delete_node() # pylint: disable=not-callable
498
+
499
+ if sleep_duration == 0:
500
+ log(INFO, "Disconnect and shut down")
501
+ break
502
+
503
+ # Sleep and reconnect afterwards
504
+ log(
505
+ INFO,
506
+ "Disconnect, then re-establish connection after %s second(s)",
507
+ sleep_duration,
508
+ )
509
+ time.sleep(sleep_duration)
510
+
511
+
512
+ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
513
+ Callable[
514
+ [
515
+ str,
516
+ bool,
517
+ RetryInvoker,
518
+ int,
519
+ Union[bytes, str, None],
520
+ Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
521
+ ],
522
+ AbstractContextManager[
523
+ tuple[
524
+ Callable[[], Optional[Message]],
525
+ Callable[[Message], None],
526
+ Optional[Callable[[], Optional[int]]],
527
+ Optional[Callable[[], None]],
528
+ Optional[Callable[[int], Run]],
529
+ Optional[Callable[[str, int], Fab]],
530
+ ]
531
+ ],
532
+ ],
533
+ str,
534
+ type[Exception],
535
+ ]:
536
+ # Parse IP address
537
+ parsed_address = parse_address(server_address)
538
+ if not parsed_address:
539
+ flwr_exit(
540
+ ExitCode.COMMON_ADDRESS_INVALID,
541
+ f"SuperLink address ({server_address}) cannot be parsed.",
542
+ )
543
+ host, port, is_v6 = parsed_address
544
+ address = f"[{host}]:{port}" if is_v6 else f"{host}:{port}"
545
+
546
+ # Set the default transport layer
547
+ if transport is None:
548
+ transport = TRANSPORT_TYPE_GRPC_BIDI
549
+
550
+ # Use either gRPC bidirectional streaming or REST request/response
551
+ if transport == TRANSPORT_TYPE_REST:
552
+ try:
553
+ from requests.exceptions import ConnectionError as RequestsConnectionError
554
+
555
+ from flwr.client.rest_client.connection import http_request_response
556
+ except ModuleNotFoundError:
557
+ flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
558
+ if server_address[:4] != "http":
559
+ flwr_exit(ExitCode.SUPERNODE_REST_ADDRESS_INVALID)
560
+ connection, error_type = http_request_response, RequestsConnectionError
561
+ elif transport == TRANSPORT_TYPE_GRPC_RERE:
562
+ connection, error_type = grpc_request_response, RpcError
563
+ elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
564
+ connection, error_type = grpc_adapter, RpcError
565
+ elif transport == TRANSPORT_TYPE_GRPC_BIDI:
566
+ connection, error_type = grpc_connection, RpcError
567
+ else:
568
+ raise ValueError(
569
+ f"Unknown transport type: {transport} (possible: {TRANSPORT_TYPES})"
570
+ )
571
+
572
+ return connection, address, error_type
573
+
574
+
575
+ def _run_flwr_clientapp(args: list[str], main_pid: int) -> None:
576
+ # Monitor the main process in case of SIGKILL
577
+ def main_process_monitor() -> None:
578
+ while True:
579
+ time.sleep(1)
580
+ if os.getppid() != main_pid:
581
+ os.kill(os.getpid(), 9)
582
+
583
+ threading.Thread(target=main_process_monitor, daemon=True).start()
584
+
585
+ # Run the command
586
+ sys.argv = args
587
+ flwr_clientapp()
588
+
589
+
590
+ def run_clientappio_api_grpc(
591
+ address: str,
592
+ certificates: Optional[tuple[bytes, bytes, bytes]],
593
+ ) -> tuple[grpc.Server, ClientAppIoServicer]:
594
+ """Run ClientAppIo API gRPC server."""
595
+ clientappio_servicer: grpc.Server = ClientAppIoServicer()
596
+ clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server
597
+ clientappio_grpc_server = generic_create_grpc_server(
598
+ servicer_and_add_fn=(
599
+ clientappio_servicer,
600
+ clientappio_add_servicer_to_server_fn,
601
+ ),
602
+ server_address=address,
603
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
604
+ certificates=certificates,
605
+ )
606
+ log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address)
607
+ clientappio_grpc_server.start()
608
+ return clientappio_grpc_server, clientappio_servicer
@@ -43,8 +43,8 @@ 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
46
  from ..clientapp.utils import get_load_client_app_fn
47
+ from ..start_client_internal import start_client_internal
48
48
 
49
49
 
50
50
  def run_supernode() -> None:
flwr/common/message.py CHANGED
@@ -22,12 +22,25 @@ from typing import Any, cast, overload
22
22
 
23
23
  from flwr.common.date import now
24
24
  from flwr.common.logger import warn_deprecated_feature
25
+ from flwr.proto.message_pb2 import Message as ProtoMessage # pylint: disable=E0611
25
26
 
26
27
  from ..app.error import Error
27
28
  from ..app.metadata import Metadata
28
29
  from .constant import MESSAGE_TTL_TOLERANCE
30
+ from .inflatable import (
31
+ InflatableObject,
32
+ add_header_to_object_body,
33
+ get_object_body,
34
+ get_object_children_ids_from_object_content,
35
+ )
29
36
  from .logger import log
30
37
  from .record import RecordDict
38
+ from .serde_utils import (
39
+ error_from_proto,
40
+ error_to_proto,
41
+ metadata_from_proto,
42
+ metadata_to_proto,
43
+ )
31
44
 
32
45
  DEFAULT_TTL = 43200 # This is 12 hours
33
46
  MESSAGE_INIT_ERROR_MESSAGE = (
@@ -58,7 +71,7 @@ class MessageInitializationError(TypeError):
58
71
  super().__init__(message or MESSAGE_INIT_ERROR_MESSAGE)
59
72
 
60
73
 
61
- class Message:
74
+ class Message(InflatableObject):
62
75
  """Represents a message exchanged between ClientApp and ServerApp.
63
76
 
64
77
  This class encapsulates the payload and metadata necessary for communication
@@ -331,6 +344,74 @@ class Message:
331
344
  )
332
345
  return f"{self.__class__.__qualname__}({view})"
333
346
 
347
+ @property
348
+ def children(self) -> dict[str, InflatableObject] | None:
349
+ """Return a dictionary of a single RecordDict with its Object IDs as key."""
350
+ return {self.content.object_id: self.content} if self.has_content() else None
351
+
352
+ def deflate(self) -> bytes:
353
+ """Deflate message."""
354
+ # Store message metadata and error in object body
355
+ obj_body = ProtoMessage(
356
+ metadata=metadata_to_proto(self.metadata),
357
+ content=None,
358
+ error=error_to_proto(self.error) if self.has_error() else None,
359
+ ).SerializeToString(deterministic=True)
360
+
361
+ return add_header_to_object_body(object_body=obj_body, obj=self)
362
+
363
+ @classmethod
364
+ def inflate(
365
+ cls, object_content: bytes, children: dict[str, InflatableObject] | None = None
366
+ ) -> Message:
367
+ """Inflate an Message from bytes.
368
+
369
+ Parameters
370
+ ----------
371
+ object_content : bytes
372
+ The deflated object content of the Message.
373
+ children : Optional[dict[str, InflatableObject]] (default: None)
374
+ Dictionary of children InflatableObjects mapped to their Object IDs.
375
+ These children enable the full inflation of the Message.
376
+
377
+ Returns
378
+ -------
379
+ Message
380
+ The inflated Message.
381
+ """
382
+ if children is None:
383
+ children = {}
384
+
385
+ # Get the children id from the deflated message
386
+ children_ids = get_object_children_ids_from_object_content(object_content)
387
+
388
+ # If the message had content, only one children is possible
389
+ # If the message carried an error, the returned listed should be empty
390
+ if children_ids != list(children.keys()):
391
+ raise ValueError(
392
+ f"Mismatch in children object IDs: expected {children_ids}, but "
393
+ f"received {list(children.keys())}. The provided children must exactly "
394
+ "match the IDs specified in the object head."
395
+ )
396
+
397
+ # Inflate content
398
+ obj_body = get_object_body(object_content, cls)
399
+ proto_message = ProtoMessage.FromString(obj_body)
400
+
401
+ # Prepare content if error wasn't set in protobuf message
402
+ if proto_message.HasField("error"):
403
+ content = None
404
+ error = error_from_proto(proto_message.error)
405
+ else:
406
+ content = cast(RecordDict, children[children_ids[0]])
407
+ error = None
408
+ # Return message
409
+ return make_message(
410
+ metadata=metadata_from_proto(proto_message.metadata),
411
+ content=content,
412
+ error=error,
413
+ )
414
+
334
415
 
335
416
  def make_message(
336
417
  metadata: Metadata, content: RecordDict | None = None, error: Error | None = None
flwr/common/serde.py CHANGED
@@ -20,11 +20,9 @@ from typing import Any, cast
20
20
 
21
21
  # pylint: disable=E0611
22
22
  from flwr.proto.clientappio_pb2 import ClientAppOutputCode, ClientAppOutputStatus
23
- from flwr.proto.error_pb2 import Error as ProtoError
24
23
  from flwr.proto.fab_pb2 import Fab as ProtoFab
25
24
  from flwr.proto.message_pb2 import Context as ProtoContext
26
25
  from flwr.proto.message_pb2 import Message as ProtoMessage
27
- from flwr.proto.message_pb2 import Metadata as ProtoMetadata
28
26
  from flwr.proto.recorddict_pb2 import Array as ProtoArray
29
27
  from flwr.proto.recorddict_pb2 import ArrayRecord as ProtoArrayRecord
30
28
  from flwr.proto.recorddict_pb2 import ConfigRecord as ProtoConfigRecord
@@ -44,9 +42,6 @@ from flwr.proto.transport_pb2 import (
44
42
  Status,
45
43
  )
46
44
 
47
- from ..app.error import Error
48
- from ..app.metadata import Metadata
49
-
50
45
  # pylint: enable=E0611
51
46
  from . import (
52
47
  Array,
@@ -59,7 +54,14 @@ from . import (
59
54
  )
60
55
  from .constant import INT64_MAX_VALUE
61
56
  from .message import Message, make_message
62
- from .serde_utils import record_value_dict_from_proto, record_value_dict_to_proto
57
+ from .serde_utils import (
58
+ error_from_proto,
59
+ error_to_proto,
60
+ metadata_from_proto,
61
+ metadata_to_proto,
62
+ record_value_dict_from_proto,
63
+ record_value_dict_to_proto,
64
+ )
63
65
 
64
66
  # === Parameters message ===
65
67
 
@@ -449,21 +451,6 @@ def config_record_from_proto(record_proto: ProtoConfigRecord) -> ConfigRecord:
449
451
  )
450
452
 
451
453
 
452
- # === Error message ===
453
-
454
-
455
- def error_to_proto(error: Error) -> ProtoError:
456
- """Serialize Error to ProtoBuf."""
457
- reason = error.reason if error.reason else ""
458
- return ProtoError(code=error.code, reason=reason)
459
-
460
-
461
- def error_from_proto(error_proto: ProtoError) -> Error:
462
- """Deserialize Error from ProtoBuf."""
463
- reason = error_proto.reason if len(error_proto.reason) > 0 else None
464
- return Error(code=error_proto.code, reason=reason)
465
-
466
-
467
454
  # === RecordDict message ===
468
455
 
469
456
 
@@ -552,41 +539,6 @@ def user_config_value_from_proto(scalar_msg: Scalar) -> typing.UserConfigValue:
552
539
  return cast(typing.UserConfigValue, scalar)
553
540
 
554
541
 
555
- # === Metadata messages ===
556
-
557
-
558
- def metadata_to_proto(metadata: Metadata) -> ProtoMetadata:
559
- """Serialize `Metadata` to ProtoBuf."""
560
- proto = ProtoMetadata( # pylint: disable=E1101
561
- run_id=metadata.run_id,
562
- message_id=metadata.message_id,
563
- src_node_id=metadata.src_node_id,
564
- dst_node_id=metadata.dst_node_id,
565
- reply_to_message_id=metadata.reply_to_message_id,
566
- group_id=metadata.group_id,
567
- ttl=metadata.ttl,
568
- message_type=metadata.message_type,
569
- created_at=metadata.created_at,
570
- )
571
- return proto
572
-
573
-
574
- def metadata_from_proto(metadata_proto: ProtoMetadata) -> Metadata:
575
- """Deserialize `Metadata` from ProtoBuf."""
576
- metadata = Metadata(
577
- run_id=metadata_proto.run_id,
578
- message_id=metadata_proto.message_id,
579
- src_node_id=metadata_proto.src_node_id,
580
- dst_node_id=metadata_proto.dst_node_id,
581
- reply_to_message_id=metadata_proto.reply_to_message_id,
582
- group_id=metadata_proto.group_id,
583
- created_at=metadata_proto.created_at,
584
- ttl=metadata_proto.ttl,
585
- message_type=metadata_proto.message_type,
586
- )
587
- return metadata
588
-
589
-
590
542
  # === Message messages ===
591
543
 
592
544
 
@@ -20,6 +20,8 @@ from typing import Any, TypeVar, cast
20
20
  from google.protobuf.message import Message as GrpcMessage
21
21
 
22
22
  # pylint: disable=E0611
23
+ from flwr.proto.error_pb2 import Error as ProtoError
24
+ from flwr.proto.message_pb2 import Metadata as ProtoMetadata
23
25
  from flwr.proto.recorddict_pb2 import (
24
26
  BoolList,
25
27
  BytesList,
@@ -29,9 +31,13 @@ from flwr.proto.recorddict_pb2 import (
29
31
  UintList,
30
32
  )
31
33
 
34
+ from ..app.error import Error
35
+ from ..app.metadata import Metadata
32
36
  from .constant import INT64_MAX_VALUE
33
37
  from .record.typeddict import TypedDict
34
38
 
39
+ # pylint: enable=E0611
40
+
35
41
  _type_to_field: dict[type, str] = {
36
42
  float: "double",
37
43
  int: "sint64",
@@ -121,3 +127,47 @@ def record_value_dict_from_proto(
121
127
  ) -> dict[str, Any]:
122
128
  """Deserialize the record value dict from ProtoBuf."""
123
129
  return {k: _record_value_from_proto(v) for k, v in value_dict_proto.items()}
130
+
131
+
132
+ def error_to_proto(error: Error) -> ProtoError:
133
+ """Serialize Error to ProtoBuf."""
134
+ reason = error.reason if error.reason else ""
135
+ return ProtoError(code=error.code, reason=reason)
136
+
137
+
138
+ def error_from_proto(error_proto: ProtoError) -> Error:
139
+ """Deserialize Error from ProtoBuf."""
140
+ reason = error_proto.reason if len(error_proto.reason) > 0 else None
141
+ return Error(code=error_proto.code, reason=reason)
142
+
143
+
144
+ def metadata_to_proto(metadata: Metadata) -> ProtoMetadata:
145
+ """Serialize `Metadata` to ProtoBuf."""
146
+ proto = ProtoMetadata( # pylint: disable=E1101
147
+ run_id=metadata.run_id,
148
+ message_id=metadata.message_id,
149
+ src_node_id=metadata.src_node_id,
150
+ dst_node_id=metadata.dst_node_id,
151
+ reply_to_message_id=metadata.reply_to_message_id,
152
+ group_id=metadata.group_id,
153
+ ttl=metadata.ttl,
154
+ message_type=metadata.message_type,
155
+ created_at=metadata.created_at,
156
+ )
157
+ return proto
158
+
159
+
160
+ def metadata_from_proto(metadata_proto: ProtoMetadata) -> Metadata:
161
+ """Deserialize `Metadata` from ProtoBuf."""
162
+ metadata = Metadata(
163
+ run_id=metadata_proto.run_id,
164
+ message_id=metadata_proto.message_id,
165
+ src_node_id=metadata_proto.src_node_id,
166
+ dst_node_id=metadata_proto.dst_node_id,
167
+ reply_to_message_id=metadata_proto.reply_to_message_id,
168
+ group_id=metadata_proto.group_id,
169
+ created_at=metadata_proto.created_at,
170
+ ttl=metadata_proto.ttl,
171
+ message_type=metadata_proto.message_type,
172
+ )
173
+ return metadata
@@ -36,7 +36,16 @@ from flwr.cli.install import install_from_fab
36
36
  from flwr.client.client import Client
37
37
  from flwr.client.client_app import ClientApp, LoadClientAppError
38
38
  from flwr.client.clientapp.app import flwr_clientapp
39
- from flwr.client.nodestate.nodestate_factory import NodeStateFactory
39
+ from flwr.client.clientapp.clientappio_servicer import (
40
+ ClientAppInputs,
41
+ ClientAppIoServicer,
42
+ )
43
+ from flwr.client.grpc_adapter_client.connection import grpc_adapter
44
+ from flwr.client.grpc_client.connection import grpc_connection
45
+ from flwr.client.grpc_rere_client.connection import grpc_request_response
46
+ from flwr.client.message_handler.message_handler import handle_control_message
47
+ from flwr.client.numpy_client import NumPyClient
48
+ from flwr.client.run_info_store import DeprecatedRunInfoStore
40
49
  from flwr.client.typing import ClientFnExt
41
50
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
42
51
  from flwr.common.address import parse_address
@@ -61,14 +70,7 @@ from flwr.common.logger import log, warn_deprecated_feature
61
70
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
62
71
  from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
63
72
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
64
-
65
- from .clientapp.clientappio_servicer import ClientAppInputs, ClientAppIoServicer
66
- from .grpc_adapter_client.connection import grpc_adapter
67
- from .grpc_client.connection import grpc_connection
68
- from .grpc_rere_client.connection import grpc_request_response
69
- from .message_handler.message_handler import handle_control_message
70
- from .numpy_client import NumPyClient
71
- from .run_info_store import DeprecatedRunInfoStore
73
+ from flwr.supernode.nodestate import NodeStateFactory
72
74
 
73
75
 
74
76
  def _check_actionable_client(
@@ -781,7 +783,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> tuple[
781
783
  try:
782
784
  from requests.exceptions import ConnectionError as RequestsConnectionError
783
785
 
784
- from .rest_client.connection import http_request_response
786
+ from flwr.client.rest_client.connection import http_request_response
785
787
  except ModuleNotFoundError:
786
788
  flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
787
789
  if server_address[:4] != "http":
@@ -40,12 +40,8 @@ from flwr.common.constant import (
40
40
  )
41
41
  from flwr.common.message import make_message
42
42
  from flwr.common.record import ConfigRecord
43
- from flwr.common.serde import (
44
- error_from_proto,
45
- error_to_proto,
46
- recorddict_from_proto,
47
- recorddict_to_proto,
48
- )
43
+ from flwr.common.serde import recorddict_from_proto, recorddict_to_proto
44
+ from flwr.common.serde_utils import error_from_proto, error_to_proto
49
45
  from flwr.common.typing import Run, RunStatus, UserConfig
50
46
 
51
47
  # pylint: disable=E0611
@@ -17,7 +17,7 @@
17
17
 
18
18
  from typing import Optional
19
19
 
20
- from flwr.client.nodestate.nodestate import NodeState
20
+ from .nodestate import NodeState
21
21
 
22
22
 
23
23
  class InMemoryNodeState(NodeState):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.19.0.dev20250520
3
+ Version: 1.19.0.dev20250521
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  License: Apache-2.0
6
6
  Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
@@ -74,8 +74,7 @@ flwr/cli/run/__init__.py,sha256=RPyB7KbYTFl6YRiilCch6oezxrLQrl1kijV7BMGkLbA,790
74
74
  flwr/cli/run/run.py,sha256=mbyf46Tm3qrL8NW02JyDjs6BI49m9UMzXsGK8-Af1r4,8232
75
75
  flwr/cli/stop.py,sha256=iLbh1dq8XMdcIlh0Lh8ufG6h0VvrP1kyp_mGO-kimt0,4976
76
76
  flwr/cli/utils.py,sha256=FjRYfzTw75qh5YHmrg9XzBA6o73T6xWt9WQYIxq-iHY,11207
77
- flwr/client/__init__.py,sha256=FslaZOoCGPIzlK-NhL7bFMVVnmFDOh_PhW4AfGzno68,1192
78
- flwr/client/app.py,sha256=wTZHxy8FS_lcIP3uXc6IzsDpbeDg0arWJGckCy3Xcn4,34277
77
+ flwr/client/__init__.py,sha256=boIhKaK6I977zrILmoTutNx94x5jB0e6F1gnAjaRJnI,1250
79
78
  flwr/client/client.py,sha256=3HAchxvknKG9jYbB7swNyDj-e5vUWDuMKoLvbT7jCVM,7895
80
79
  flwr/client/client_app.py,sha256=zVhi-l3chAb06ozFsKwix3hU_RpOLjST13Ha50AVIPE,16918
81
80
  flwr/client/clientapp/__init__.py,sha256=nPMoWEB1FhwexuW-vKdhwFkFr_4MW-2YMZExP9vfTGg,800
@@ -101,16 +100,13 @@ flwr/client/mod/secure_aggregation/__init__.py,sha256=k8HYXvqu3pd_V3eZ0_5wwH52o-
101
100
  flwr/client/mod/secure_aggregation/secagg_mod.py,sha256=y54DvpM2-DWUiEYqgwZ0DssC1VVRCJEfGgST7O3OcwM,1095
102
101
  flwr/client/mod/secure_aggregation/secaggplus_mod.py,sha256=aKqjZCrikF73y3E-7h40u-s0H6-hmyd4Ah1LHnrLrIg,19661
103
102
  flwr/client/mod/utils.py,sha256=FUgD2TfcWqSeF6jUKZ4i6Ke56U4Nrv85AeVb93s6R9g,1201
104
- flwr/client/nodestate/__init__.py,sha256=CyLLObbmmVgfRO88UCM0VMait1dL57mUauUDfuSHsbU,976
105
- flwr/client/nodestate/in_memory_nodestate.py,sha256=CTwkxhCSg8ExaGFILFTuylF0RHrAIAgxMFLQfmaFm1I,1291
106
- flwr/client/nodestate/nodestate.py,sha256=-LAjZOnS7VyHC05ll3b31cYDjwAt6l4WmYt7duVLRKk,1024
107
- flwr/client/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
108
103
  flwr/client/numpy_client.py,sha256=Qq6ghsIAop2slKqAfgiI5NiHJ4LIxGmrik3Ror4_XVc,9581
109
104
  flwr/client/rest_client/__init__.py,sha256=MBiuK62hj439m9rtwSwI184Hth6Tt5GbmpNMyl3zkZY,735
110
105
  flwr/client/rest_client/connection.py,sha256=Xlf1eEMXq17VVVELPGPT1pqJKw8l0iq4Jnvz13v95C8,12806
111
106
  flwr/client/run_info_store.py,sha256=MaJ3UQ-07hWtK67wnWu0zR29jrk0fsfgJX506dvEOfE,4042
107
+ flwr/client/start_client_internal.py,sha256=OQBOUlXmb5fSCErD6bdYjl2R4vwhv30_fIyedKU0YG8,25266
112
108
  flwr/client/supernode/__init__.py,sha256=i3gFbV5ie_FGyRMpzOvqtZAi0Z0ChIEJ7I2Kr0ym0PM,793
113
- flwr/client/supernode/app.py,sha256=lURLjP8jiOWhlX3-uh-7t_l1o_JEUz_FmkuNY91xmUQ,8975
109
+ flwr/client/supernode/app.py,sha256=pGHzFlidF4Y74zhFNTqCsB1Hl6x-bq4R2L1ktEZgXXI,8993
114
110
  flwr/client/typing.py,sha256=Jw3rawDzI_-ZDcRmEQcs5gZModY7oeQlEeltYsdOhlU,1048
115
111
  flwr/clientapp/__init__.py,sha256=zGW4z49Ojzoi1hDiRC7kyhLjijUilc6fqHhtM_ATRVA,719
116
112
  flwr/common/__init__.py,sha256=5GCLVk399Az_rTJHNticRlL0Sl_oPw_j5_LuFKfX7-M,4171
@@ -136,7 +132,7 @@ flwr/common/heartbeat.py,sha256=SyEpNDnmJ0lni0cWO67rcoJVKasCLmkNHm3dKLeNrLU,5749
136
132
  flwr/common/inflatable.py,sha256=ZKW4L2GMAxInUlbNK_zDZs7uW4-CuQui9TnWVglpDic,5279
137
133
  flwr/common/inflatable_grpc_utils.py,sha256=StkhGH8x9zR-p5MH52HdLG9MLzKv_rT8sPdbR9ZzNyE,3368
138
134
  flwr/common/logger.py,sha256=JbRf6E2vQxXzpDBq1T8IDUJo_usu3gjWEBPQ6uKcmdg,13049
139
- flwr/common/message.py,sha256=4QPeMFP_BWPltgi0-0ARROhtL2k1b4BFflrgWCn3uJ0,16020
135
+ flwr/common/message.py,sha256=dfct6ZGizK2zSj2JLiQTRbOfDNu79KzwUplpQaxFg40,18997
140
136
  flwr/common/object_ref.py,sha256=p3SfTeqo3Aj16SkB-vsnNn01zswOPdGNBitcbRnqmUk,9134
141
137
  flwr/common/parameter.py,sha256=UVw6sOgehEFhFs4uUCMl2kfVq1PD6ncmWgPLMsZPKPE,2095
142
138
  flwr/common/pyproject.py,sha256=2SU6yJW7059SbMXgzjOdK1GZRWO6AixDH7BmdxbMvHI,1386
@@ -158,13 +154,14 @@ flwr/common/secure_aggregation/ndarrays_arithmetic.py,sha256=TrggOlizlny3V2KS7-3
158
154
  flwr/common/secure_aggregation/quantization.py,sha256=ssFZpiRyj9ltIh0Ai3vGkDqWFO4SoqgoD1mDU9XqMEM,2400
159
155
  flwr/common/secure_aggregation/secaggplus_constants.py,sha256=dGYhWOBMMDJcQH4_tQNC8-Efqm-ecEUNN9ANz59UnCk,2182
160
156
  flwr/common/secure_aggregation/secaggplus_utils.py,sha256=E_xU-Zd45daO1em7M6C2wOjFXVtJf-6tl7fp-7xq1wo,3214
161
- flwr/common/serde.py,sha256=IMU32jIwcF_iPej7d8PXLxZ4xZxFrsy4XPdlDyXQ9bk,23960
162
- flwr/common/serde_utils.py,sha256=ofmrgVHRBfrE1MtQwLQk0x12JS9vL-u8wHXrgZE2ueg,3985
157
+ flwr/common/serde.py,sha256=_EusvG9FGtOcUxGZWYSsPv7KzTnzadXv4jcttL40Fow,22280
158
+ flwr/common/serde_utils.py,sha256=zF99EnqTNhEd3Xh3tYy2bZ44_8B-QfwNqsuP7vfLVDs,5735
163
159
  flwr/common/telemetry.py,sha256=jF47v0SbnBd43XamHtl3wKxs3knFUY2p77cm_2lzZ8M,8762
164
160
  flwr/common/typing.py,sha256=97QRfRRS7sQnjkAI5FDZ01-38oQUSz4i1qqewQmBWRg,6886
165
161
  flwr/common/version.py,sha256=7GAGzPn73Mkh09qhrjbmjZQtcqVhBuzhFBaK4Mk4VRk,1325
166
162
  flwr/compat/__init__.py,sha256=gbfDQKKKMZzi3GswyVRgyLdDlHiWj3wU6dg7y6m5O_s,752
167
163
  flwr/compat/client/__init__.py,sha256=qpbo0lcxdNL4qy5KHqiGm8OLxSxkYgI_-dLh5rwhtcI,746
164
+ flwr/compat/client/app.py,sha256=2_-4oSkzbBdyF56PPWnBbzE9C7nG7UVaTd_5ftFQ2P8,34362
168
165
  flwr/compat/common/__init__.py,sha256=OMnKw4ad0qYMSIA9LZRa2gOkhSOXwAZCpAHnBQE_hFc,746
169
166
  flwr/compat/server/__init__.py,sha256=TGVSoOTuf5T5JHUVrK5wuorQF7L6Wvdem8B4uufvMJY,746
170
167
  flwr/compat/simulation/__init__.py,sha256=MApGa-tysDDw34iSdxZ7TWOKtGJM-z3i8fIRJa0qbZ8,750
@@ -306,7 +303,7 @@ flwr/server/superlink/linkstate/__init__.py,sha256=OtsgvDTnZLU3k0sUbkHbqoVwW6ql2
306
303
  flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=vvoOWjYlmOlbakH7AzpMh0jB70Qxx7UTlAGqjcA8ctM,25926
307
304
  flwr/server/superlink/linkstate/linkstate.py,sha256=j6nW351t07VrBhFqjO34z8tf2PuKOE9aCX9SqpW96pQ,13100
308
305
  flwr/server/superlink/linkstate/linkstate_factory.py,sha256=8RlosqSpKOoD_vhUUQPY0jtE3A84GeF96Z7sWNkRRcA,2069
309
- flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=E43YO88vdnG9GW6Rwh9Fb7oWGgEABS9RXDRg3OR3T4Q,43573
306
+ flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=z3VABMX_WtAioWJ2aUOsxi53-ecF2c8xEQ69xP3xXW8,43587
310
307
  flwr/server/superlink/linkstate/utils.py,sha256=AJs9jTAEK7JnjF2AODXnOfy0pKAKpe6oUWPCanAP57s,15382
311
308
  flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
312
309
  flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=opJ6SYwIAbu4NWEo3K-VxFO-tMSFmE4H3i2HwHIVRzw,2173
@@ -347,7 +344,11 @@ flwr/superexec/executor.py,sha256=M5ucqSE53jfRtuCNf59WFLqQvA1Mln4741TySeZE7qQ,31
347
344
  flwr/superexec/simulation.py,sha256=j6YwUvBN7EQ09ID7MYOCVZ70PGbuyBy8f9bXU0EszEM,4088
348
345
  flwr/superlink/__init__.py,sha256=GNSuJ4-N6Z8wun2iZNlXqENt5beUyzC0Gi_tN396bbM,707
349
346
  flwr/supernode/__init__.py,sha256=KgeCaVvXWrU3rptNR1y0oBp4YtXbAcrnCcJAiOoWkI4,707
350
- flwr_nightly-1.19.0.dev20250520.dist-info/METADATA,sha256=HyqyQmw4kiBnhIKntjNtkH6kQb6qQ7szzRV19cj0IMA,15910
351
- flwr_nightly-1.19.0.dev20250520.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
352
- flwr_nightly-1.19.0.dev20250520.dist-info/entry_points.txt,sha256=2-1L-GNKhwGw2_7_RoH55vHw2SIHjdAQy3HAVAWl9PY,374
353
- flwr_nightly-1.19.0.dev20250520.dist-info/RECORD,,
347
+ flwr/supernode/nodestate/__init__.py,sha256=CyLLObbmmVgfRO88UCM0VMait1dL57mUauUDfuSHsbU,976
348
+ flwr/supernode/nodestate/in_memory_nodestate.py,sha256=brV7TMMzS93tXk6ntpoYjtPK5qiSF3XD2W-uUdUVucc,1270
349
+ flwr/supernode/nodestate/nodestate.py,sha256=-LAjZOnS7VyHC05ll3b31cYDjwAt6l4WmYt7duVLRKk,1024
350
+ flwr/supernode/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
351
+ flwr_nightly-1.19.0.dev20250521.dist-info/METADATA,sha256=ly012I_t7MGbuecV64LFyv0MfIHht6Lp5QTkWuJF1-A,15910
352
+ flwr_nightly-1.19.0.dev20250521.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
353
+ flwr_nightly-1.19.0.dev20250521.dist-info/entry_points.txt,sha256=2-1L-GNKhwGw2_7_RoH55vHw2SIHjdAQy3HAVAWl9PY,374
354
+ flwr_nightly-1.19.0.dev20250521.dist-info/RECORD,,
File without changes
File without changes