flwr-nightly 1.15.0.dev20250104__py3-none-any.whl → 1.15.0.dev20250123__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 (98) hide show
  1. flwr/cli/cli_user_auth_interceptor.py +6 -2
  2. flwr/cli/config_utils.py +23 -146
  3. flwr/cli/constant.py +27 -0
  4. flwr/cli/install.py +1 -1
  5. flwr/cli/log.py +17 -2
  6. flwr/cli/login/login.py +20 -5
  7. flwr/cli/ls.py +10 -2
  8. flwr/cli/run/run.py +20 -10
  9. flwr/cli/stop.py +9 -1
  10. flwr/cli/utils.py +4 -4
  11. flwr/client/app.py +36 -48
  12. flwr/client/clientapp/app.py +4 -6
  13. flwr/client/clientapp/utils.py +1 -1
  14. flwr/client/grpc_client/connection.py +0 -6
  15. flwr/client/grpc_rere_client/client_interceptor.py +19 -119
  16. flwr/client/grpc_rere_client/connection.py +34 -24
  17. flwr/client/grpc_rere_client/grpc_adapter.py +16 -0
  18. flwr/client/rest_client/connection.py +34 -26
  19. flwr/client/supernode/app.py +14 -20
  20. flwr/common/auth_plugin/auth_plugin.py +34 -23
  21. flwr/common/config.py +152 -15
  22. flwr/common/constant.py +11 -8
  23. flwr/common/exit/__init__.py +24 -0
  24. flwr/common/exit/exit.py +99 -0
  25. flwr/common/exit/exit_code.py +93 -0
  26. flwr/common/exit_handlers.py +24 -10
  27. flwr/common/grpc.py +161 -3
  28. flwr/common/logger.py +1 -1
  29. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
  30. flwr/common/serde.py +6 -4
  31. flwr/common/typing.py +20 -0
  32. flwr/proto/clientappio_pb2.py +13 -3
  33. flwr/proto/clientappio_pb2_grpc.py +63 -12
  34. flwr/proto/error_pb2.py +13 -3
  35. flwr/proto/error_pb2_grpc.py +20 -0
  36. flwr/proto/exec_pb2.py +27 -29
  37. flwr/proto/exec_pb2.pyi +27 -54
  38. flwr/proto/exec_pb2_grpc.py +105 -24
  39. flwr/proto/fab_pb2.py +13 -3
  40. flwr/proto/fab_pb2_grpc.py +20 -0
  41. flwr/proto/fleet_pb2.py +54 -31
  42. flwr/proto/fleet_pb2.pyi +84 -0
  43. flwr/proto/fleet_pb2_grpc.py +207 -28
  44. flwr/proto/fleet_pb2_grpc.pyi +26 -0
  45. flwr/proto/grpcadapter_pb2.py +14 -4
  46. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  47. flwr/proto/log_pb2.py +13 -3
  48. flwr/proto/log_pb2_grpc.py +20 -0
  49. flwr/proto/message_pb2.py +15 -5
  50. flwr/proto/message_pb2_grpc.py +20 -0
  51. flwr/proto/node_pb2.py +15 -5
  52. flwr/proto/node_pb2.pyi +1 -4
  53. flwr/proto/node_pb2_grpc.py +20 -0
  54. flwr/proto/recordset_pb2.py +18 -8
  55. flwr/proto/recordset_pb2_grpc.py +20 -0
  56. flwr/proto/run_pb2.py +16 -6
  57. flwr/proto/run_pb2_grpc.py +20 -0
  58. flwr/proto/serverappio_pb2.py +32 -14
  59. flwr/proto/serverappio_pb2.pyi +56 -0
  60. flwr/proto/serverappio_pb2_grpc.py +261 -44
  61. flwr/proto/serverappio_pb2_grpc.pyi +20 -0
  62. flwr/proto/simulationio_pb2.py +13 -3
  63. flwr/proto/simulationio_pb2_grpc.py +105 -24
  64. flwr/proto/task_pb2.py +13 -3
  65. flwr/proto/task_pb2_grpc.py +20 -0
  66. flwr/proto/transport_pb2.py +20 -10
  67. flwr/proto/transport_pb2_grpc.py +35 -4
  68. flwr/server/app.py +87 -38
  69. flwr/server/compat/app_utils.py +0 -1
  70. flwr/server/compat/driver_client_proxy.py +1 -2
  71. flwr/server/driver/grpc_driver.py +5 -2
  72. flwr/server/driver/inmemory_driver.py +2 -1
  73. flwr/server/serverapp/app.py +5 -6
  74. flwr/server/superlink/driver/serverappio_grpc.py +1 -1
  75. flwr/server/superlink/driver/serverappio_servicer.py +132 -14
  76. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +20 -88
  77. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
  78. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +38 -0
  79. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +95 -168
  80. flwr/server/superlink/fleet/message_handler/message_handler.py +66 -5
  81. flwr/server/superlink/fleet/rest_rere/rest_api.py +28 -3
  82. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  83. flwr/server/superlink/linkstate/in_memory_linkstate.py +40 -48
  84. flwr/server/superlink/linkstate/linkstate.py +15 -22
  85. flwr/server/superlink/linkstate/sqlite_linkstate.py +80 -99
  86. flwr/server/superlink/linkstate/utils.py +18 -8
  87. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  88. flwr/server/utils/validator.py +9 -34
  89. flwr/simulation/app.py +4 -6
  90. flwr/simulation/legacy_app.py +4 -2
  91. flwr/simulation/run_simulation.py +1 -1
  92. flwr/superexec/exec_grpc.py +1 -1
  93. flwr/superexec/exec_servicer.py +23 -2
  94. {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/METADATA +7 -7
  95. {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/RECORD +98 -94
  96. {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/LICENSE +0 -0
  97. {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/WHEEL +0 -0
  98. {flwr_nightly-1.15.0.dev20250104.dist-info → flwr_nightly-1.15.0.dev20250123.dist-info}/entry_points.txt +0 -0
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  import random
19
- import sys
20
19
  import threading
21
20
  from collections.abc import Iterator
22
21
  from contextlib import contextmanager
@@ -26,22 +25,22 @@ from typing import Callable, Optional, TypeVar, Union
26
25
 
27
26
  from cryptography.hazmat.primitives.asymmetric import ec
28
27
  from google.protobuf.message import Message as GrpcMessage
28
+ from requests.exceptions import ConnectionError as RequestsConnectionError
29
29
 
30
30
  from flwr.client.heartbeat import start_ping_loop
31
31
  from flwr.client.message_handler.message_handler import validate_out_message
32
- from flwr.client.message_handler.task_handler import get_task_ins, validate_task_ins
33
32
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
34
33
  from flwr.common.constant import (
35
- MISSING_EXTRA_REST,
36
34
  PING_BASE_MULTIPLIER,
37
35
  PING_CALL_TIMEOUT,
38
36
  PING_DEFAULT_INTERVAL,
39
37
  PING_RANDOM_RANGE,
40
38
  )
39
+ from flwr.common.exit import ExitCode, flwr_exit
41
40
  from flwr.common.logger import log
42
41
  from flwr.common.message import Message, Metadata
43
42
  from flwr.common.retry_invoker import RetryInvoker
44
- from flwr.common.serde import message_from_taskins, message_to_taskres, run_from_proto
43
+ from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
45
44
  from flwr.common.typing import Fab, Run
46
45
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
47
46
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
@@ -51,25 +50,26 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
51
50
  DeleteNodeResponse,
52
51
  PingRequest,
53
52
  PingResponse,
54
- PullTaskInsRequest,
55
- PullTaskInsResponse,
56
- PushTaskResRequest,
57
- PushTaskResResponse,
53
+ PullMessagesRequest,
54
+ PullMessagesResponse,
55
+ PushMessagesRequest,
56
+ PushMessagesResponse,
58
57
  )
59
58
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
60
59
  from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
61
- from flwr.proto.task_pb2 import TaskIns # pylint: disable=E0611
62
60
 
63
61
  try:
64
62
  import requests
65
63
  except ModuleNotFoundError:
66
- sys.exit(MISSING_EXTRA_REST)
64
+ flwr_exit(ExitCode.COMMON_MISSING_EXTRA_REST)
67
65
 
68
66
 
69
67
  PATH_CREATE_NODE: str = "api/v0/fleet/create-node"
70
68
  PATH_DELETE_NODE: str = "api/v0/fleet/delete-node"
71
69
  PATH_PULL_TASK_INS: str = "api/v0/fleet/pull-task-ins"
70
+ PATH_PULL_MESSAGES: str = "/api/v0/fleet/pull-messages"
72
71
  PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res"
72
+ PATH_PUSH_MESSAGES: str = "/api/v0/fleet/push-messages"
73
73
  PATH_PING: str = "api/v0/fleet/ping"
74
74
  PATH_GET_RUN: str = "/api/v0/fleet/get-run"
75
75
  PATH_GET_FAB: str = "/api/v0/fleet/get-fab"
@@ -286,29 +286,28 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
286
286
  log(ERROR, "Node instance missing")
287
287
  return None
288
288
 
289
- # Request instructions (task) from server
290
- req = PullTaskInsRequest(node=node)
289
+ # Request instructions (message) from server
290
+ req = PullMessagesRequest(node=node)
291
291
 
292
292
  # Send the request
293
- res = _request(req, PullTaskInsResponse, PATH_PULL_TASK_INS)
293
+ res = _request(req, PullMessagesResponse, PATH_PULL_MESSAGES)
294
294
  if res is None:
295
295
  return None
296
296
 
297
- # Get the current TaskIns
298
- task_ins: Optional[TaskIns] = get_task_ins(res)
297
+ # Get the current Messages
298
+ message_proto = None if len(res.messages_list) == 0 else res.messages_list[0]
299
299
 
300
- # Discard the current TaskIns if not valid
301
- if task_ins is not None and not (
302
- task_ins.task.consumer.node_id == node.node_id
303
- and validate_task_ins(task_ins)
300
+ # Discard the current message if not valid
301
+ if message_proto is not None and not (
302
+ message_proto.metadata.dst_node_id == node.node_id
304
303
  ):
305
- task_ins = None
304
+ message_proto = None
306
305
 
307
306
  # Return the Message if available
308
307
  nonlocal metadata
309
308
  message = None
310
- if task_ins is not None:
311
- message = message_from_taskins(task_ins)
309
+ if message_proto is not None:
310
+ message = message_from_proto(message_proto)
312
311
  metadata = copy(message.metadata)
313
312
  log(INFO, "[Node] POST /%s: success", PATH_PULL_TASK_INS)
314
313
  return message
@@ -332,14 +331,14 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
332
331
  return
333
332
  metadata = None
334
333
 
335
- # Construct TaskRes
336
- task_res = message_to_taskres(message)
334
+ # Serialize ProtoBuf to bytes
335
+ message_proto = message_to_proto(message=message)
337
336
 
338
337
  # Serialize ProtoBuf to bytes
339
- req = PushTaskResRequest(node=node, task_res_list=[task_res])
338
+ req = PushMessagesRequest(node=node, messages_list=[message_proto])
340
339
 
341
340
  # Send the request
342
- res = _request(req, PushTaskResResponse, PATH_PUSH_TASK_RES)
341
+ res = _request(req, PushMessagesResponse, PATH_PUSH_MESSAGES)
343
342
  if res is None:
344
343
  return
345
344
 
@@ -380,3 +379,12 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
380
379
  yield (receive, send, create_node, delete_node, get_run, get_fab)
381
380
  except Exception as exc: # pylint: disable=broad-except
382
381
  log(ERROR, exc)
382
+ # Cleanup
383
+ finally:
384
+ try:
385
+ if node is not None:
386
+ # Disable retrying
387
+ retry_invoker.max_tries = 1
388
+ delete_node()
389
+ except RequestsConnectionError:
390
+ pass
@@ -40,6 +40,7 @@ from flwr.common.constant import (
40
40
  TRANSPORT_TYPE_GRPC_RERE,
41
41
  TRANSPORT_TYPE_REST,
42
42
  )
43
+ from flwr.common.exit import ExitCode, flwr_exit
43
44
  from flwr.common.exit_handlers import register_exit_handlers
44
45
  from flwr.common.logger import log, warn_deprecated_feature
45
46
 
@@ -86,6 +87,12 @@ def run_supernode() -> None:
86
87
 
87
88
  log(DEBUG, "Isolation mode: %s", args.isolation)
88
89
 
90
+ # Register handlers for graceful shutdown
91
+ register_exit_handlers(
92
+ event_type=EventType.RUN_SUPERNODE_LEAVE,
93
+ exit_message="SuperNode terminated gracefully.",
94
+ )
95
+
89
96
  start_client_internal(
90
97
  server_address=args.superlink,
91
98
  load_client_app_fn=load_fn,
@@ -103,11 +110,6 @@ def run_supernode() -> None:
103
110
  clientappio_api_address=args.clientappio_api_address,
104
111
  )
105
112
 
106
- # Graceful shutdown
107
- register_exit_handlers(
108
- event_type=EventType.RUN_SUPERNODE_LEAVE,
109
- )
110
-
111
113
 
112
114
  def run_client_app() -> None:
113
115
  """Run Flower client app."""
@@ -280,11 +282,7 @@ def _try_setup_client_authentication(
280
282
  return None
281
283
 
282
284
  if not args.auth_supernode_private_key or not args.auth_supernode_public_key:
283
- sys.exit(
284
- "Authentication requires file paths to both "
285
- "'--auth-supernode-private-key' and '--auth-supernode-public-key'"
286
- "to be provided (providing only one of them is not sufficient)."
287
- )
285
+ flwr_exit(ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED)
288
286
 
289
287
  try:
290
288
  ssh_private_key = load_ssh_private_key(
@@ -294,11 +292,9 @@ def _try_setup_client_authentication(
294
292
  if not isinstance(ssh_private_key, ec.EllipticCurvePrivateKey):
295
293
  raise ValueError()
296
294
  except (ValueError, UnsupportedAlgorithm):
297
- sys.exit(
298
- "Error: Unable to parse the private key file in "
299
- "'--auth-supernode-private-key'. Authentication requires elliptic "
300
- "curve private and public key pair. Please ensure that the file "
301
- "path points to a valid private key file and try again."
295
+ flwr_exit(
296
+ ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
297
+ "Unable to parse the private key file.",
302
298
  )
303
299
 
304
300
  try:
@@ -308,11 +304,9 @@ def _try_setup_client_authentication(
308
304
  if not isinstance(ssh_public_key, ec.EllipticCurvePublicKey):
309
305
  raise ValueError()
310
306
  except (ValueError, UnsupportedAlgorithm):
311
- sys.exit(
312
- "Error: Unable to parse the public key file in "
313
- "'--auth-supernode-public-key'. Authentication requires elliptic "
314
- "curve private and public key pair. Please ensure that the file "
315
- "path points to a valid public key file and try again."
307
+ flwr_exit(
308
+ ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
309
+ "Unable to parse the public key file.",
316
310
  )
317
311
 
318
312
  return (
@@ -18,26 +18,32 @@
18
18
  from abc import ABC, abstractmethod
19
19
  from collections.abc import Sequence
20
20
  from pathlib import Path
21
- from typing import Any, Optional, Union
21
+ from typing import Optional, Union
22
22
 
23
23
  from flwr.proto.exec_pb2_grpc import ExecStub
24
24
 
25
+ from ..typing import UserAuthCredentials, UserAuthLoginDetails
26
+
25
27
 
26
28
  class ExecAuthPlugin(ABC):
27
29
  """Abstract Flower Auth Plugin class for ExecServicer.
28
30
 
29
31
  Parameters
30
32
  ----------
31
- config : dict[str, Any]
32
- The authentication configuration loaded from a YAML file.
33
+ user_auth_config_path : Path
34
+ Path to the YAML file containing the authentication configuration.
33
35
  """
34
36
 
35
37
  @abstractmethod
36
- def __init__(self, config: dict[str, Any]):
38
+ def __init__(
39
+ self,
40
+ user_auth_config_path: Path,
41
+ verify_tls_cert: bool,
42
+ ):
37
43
  """Abstract constructor."""
38
44
 
39
45
  @abstractmethod
40
- def get_login_details(self) -> dict[str, str]:
46
+ def get_login_details(self) -> Optional[UserAuthLoginDetails]:
41
47
  """Get the login details."""
42
48
 
43
49
  @abstractmethod
@@ -47,7 +53,7 @@ class ExecAuthPlugin(ABC):
47
53
  """Validate authentication tokens in the provided metadata."""
48
54
 
49
55
  @abstractmethod
50
- def get_auth_tokens(self, auth_details: dict[str, str]) -> dict[str, str]:
56
+ def get_auth_tokens(self, device_code: str) -> Optional[UserAuthCredentials]:
51
57
  """Get authentication tokens."""
52
58
 
53
59
  @abstractmethod
@@ -62,50 +68,55 @@ class CliAuthPlugin(ABC):
62
68
 
63
69
  Parameters
64
70
  ----------
65
- user_auth_config_path : Path
66
- The path to the user's authentication configuration file.
71
+ credentials_path : Path
72
+ Path to the user's authentication credentials file.
67
73
  """
68
74
 
69
75
  @staticmethod
70
76
  @abstractmethod
71
77
  def login(
72
- login_details: dict[str, str],
78
+ login_details: UserAuthLoginDetails,
73
79
  exec_stub: ExecStub,
74
- ) -> dict[str, Any]:
75
- """Authenticate the user with the SuperLink.
80
+ ) -> UserAuthCredentials:
81
+ """Authenticate the user and retrieve authentication credentials.
76
82
 
77
83
  Parameters
78
84
  ----------
79
- login_details : dict[str, str]
80
- A dictionary containing the user's login details.
85
+ login_details : UserAuthLoginDetails
86
+ An object containing the user's login details.
81
87
  exec_stub : ExecStub
82
- An instance of `ExecStub` used for communication with the SuperLink.
88
+ A stub for executing RPC calls to the server.
83
89
 
84
90
  Returns
85
91
  -------
86
- user_auth_config : dict[str, Any]
87
- A dictionary containing the user's authentication configuration
88
- in JSON format.
92
+ UserAuthCredentials
93
+ The authentication credentials obtained after login.
89
94
  """
90
95
 
91
96
  @abstractmethod
92
- def __init__(self, user_auth_config_path: Path):
97
+ def __init__(self, credentials_path: Path):
93
98
  """Abstract constructor."""
94
99
 
95
100
  @abstractmethod
96
- def store_tokens(self, user_auth_config: dict[str, Any]) -> None:
97
- """Store authentication tokens from the provided user_auth_config.
101
+ def store_tokens(self, credentials: UserAuthCredentials) -> None:
102
+ """Store authentication tokens to the `credentials_path`.
98
103
 
99
- The configuration, including tokens, will be saved as a JSON file
100
- at `user_auth_config_path`.
104
+ The credentials, including tokens, will be saved as a JSON file
105
+ at `credentials_path`.
101
106
  """
102
107
 
103
108
  @abstractmethod
104
109
  def load_tokens(self) -> None:
105
- """Load authentication tokens from the user_auth_config_path."""
110
+ """Load authentication tokens from the `credentials_path`."""
106
111
 
107
112
  @abstractmethod
108
113
  def write_tokens_to_metadata(
109
114
  self, metadata: Sequence[tuple[str, Union[str, bytes]]]
110
115
  ) -> Sequence[tuple[str, Union[str, bytes]]]:
111
116
  """Write authentication tokens to the provided metadata."""
117
+
118
+ @abstractmethod
119
+ def read_tokens_from_metadata(
120
+ self, metadata: Sequence[tuple[str, Union[str, bytes]]]
121
+ ) -> Optional[UserAuthCredentials]:
122
+ """Read authentication tokens from the provided metadata."""
flwr/common/config.py CHANGED
@@ -17,13 +17,13 @@
17
17
 
18
18
  import os
19
19
  import re
20
+ import zipfile
21
+ from io import BytesIO
20
22
  from pathlib import Path
21
- from typing import Any, Optional, Union, cast, get_args
23
+ from typing import IO, Any, Optional, TypeVar, Union, cast, get_args
22
24
 
23
25
  import tomli
24
26
 
25
- from flwr.cli.config_utils import get_fab_config, validate_fields
26
- from flwr.common import ConfigsRecord
27
27
  from flwr.common.constant import (
28
28
  APP_DIR,
29
29
  FAB_CONFIG_FILE,
@@ -33,6 +33,10 @@ from flwr.common.constant import (
33
33
  )
34
34
  from flwr.common.typing import Run, UserConfig, UserConfigValue
35
35
 
36
+ from . import ConfigsRecord, object_ref
37
+
38
+ T_dict = TypeVar("T_dict", bound=dict[str, Any]) # pylint: disable=invalid-name
39
+
36
40
 
37
41
  def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
38
42
  """Return the Flower home directory based on env variables."""
@@ -80,7 +84,7 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
80
84
  config = tomli.loads(toml_file.read())
81
85
 
82
86
  # Validate pyproject.toml fields
83
- is_valid, errors, _ = validate_fields(config)
87
+ is_valid, errors, _ = validate_fields_in_config(config)
84
88
  if not is_valid:
85
89
  error_msg = "\n".join([f" - {error}" for error in errors])
86
90
  raise ValueError(
@@ -91,19 +95,28 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
91
95
 
92
96
 
93
97
  def fuse_dicts(
94
- main_dict: UserConfig,
95
- override_dict: UserConfig,
96
- ) -> UserConfig:
98
+ main_dict: T_dict,
99
+ override_dict: T_dict,
100
+ check_keys: bool = True,
101
+ ) -> T_dict:
97
102
  """Merge a config with the overrides.
98
103
 
99
- Remove the nesting by adding the nested keys as prefixes separated by dots, and fuse
100
- it with the override dict.
104
+ If `check_keys` is set to True, an error will be raised if the override
105
+ dictionary contains keys that are not present in the main dictionary.
106
+ Otherwise, only the keys present in the main dictionary will be updated.
101
107
  """
102
- fused_dict = main_dict.copy()
108
+ if not isinstance(main_dict, dict) or not isinstance(override_dict, dict):
109
+ raise ValueError("Both dictionaries must be of type dict")
110
+
111
+ fused_dict = cast(T_dict, main_dict.copy())
103
112
 
104
113
  for key, value in override_dict.items():
105
114
  if key in main_dict:
115
+ if isinstance(value, dict):
116
+ fused_dict[key] = fuse_dicts(main_dict[key], value)
106
117
  fused_dict[key] = value
118
+ elif check_keys:
119
+ raise ValueError(f"Key '{key}' is not present in the main dictionary")
107
120
 
108
121
  return fused_dict
109
122
 
@@ -192,8 +205,8 @@ def unflatten_dict(flat_dict: dict[str, Any]) -> dict[str, Any]:
192
205
 
193
206
 
194
207
  def parse_config_args(
195
- config: Optional[list[str]],
196
- ) -> UserConfig:
208
+ config: Optional[list[str]], flatten: bool = True
209
+ ) -> dict[str, Any]:
197
210
  """Parse separator separated list of key-value pairs separated by '='."""
198
211
  overrides: UserConfig = {}
199
212
 
@@ -221,16 +234,16 @@ def parse_config_args(
221
234
  matches = pattern.findall(config_line)
222
235
  toml_str = "\n".join(f"{k} = {v}" for k, v in matches)
223
236
  overrides.update(tomli.loads(toml_str))
224
- flat_overrides = flatten_dict(overrides)
237
+ flat_overrides = flatten_dict(overrides) if flatten else overrides
225
238
 
226
239
  return flat_overrides
227
240
 
228
241
 
229
242
  def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
230
- """Extract `fab_version` and `fab_id` from a project config."""
243
+ """Extract `fab_id` and `fab_version` from a project config."""
231
244
  return (
232
- config["project"]["version"],
233
245
  f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
246
+ config["project"]["version"],
234
247
  )
235
248
 
236
249
 
@@ -241,3 +254,127 @@ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
241
254
  c_record[k] = v
242
255
 
243
256
  return c_record
257
+
258
+
259
+ def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
260
+ """Extract the config from a FAB file or path.
261
+
262
+ Parameters
263
+ ----------
264
+ fab_file : Union[Path, bytes]
265
+ The Flower App Bundle file to validate and extract the metadata from.
266
+ It can either be a path to the file or the file itself as bytes.
267
+
268
+ Returns
269
+ -------
270
+ Dict[str, Any]
271
+ The `config` of the given Flower App Bundle.
272
+ """
273
+ fab_file_archive: Union[Path, IO[bytes]]
274
+ if isinstance(fab_file, bytes):
275
+ fab_file_archive = BytesIO(fab_file)
276
+ elif isinstance(fab_file, Path):
277
+ fab_file_archive = fab_file
278
+ else:
279
+ raise ValueError("fab_file must be either a Path or bytes")
280
+
281
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
282
+ with zipf.open("pyproject.toml") as file:
283
+ toml_content = file.read().decode("utf-8")
284
+ try:
285
+ conf = tomli.loads(toml_content)
286
+ except tomli.TOMLDecodeError:
287
+ raise ValueError("Invalid TOML content in pyproject.toml") from None
288
+
289
+ is_valid, errors, _ = validate_config(conf, check_module=False)
290
+ if not is_valid:
291
+ raise ValueError(errors)
292
+
293
+ return conf
294
+
295
+
296
+ def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
297
+ for key, value in config_dict.items():
298
+ if isinstance(value, dict):
299
+ _validate_run_config(config_dict[key], errors)
300
+ elif not isinstance(value, get_args(UserConfigValue)):
301
+ raise ValueError(
302
+ f"The value for key {key} needs to be of type `int`, `float`, "
303
+ "`bool, `str`, or a `dict` of those.",
304
+ )
305
+
306
+
307
+ # pylint: disable=too-many-branches
308
+ def validate_fields_in_config(
309
+ config: dict[str, Any]
310
+ ) -> tuple[bool, list[str], list[str]]:
311
+ """Validate pyproject.toml fields."""
312
+ errors = []
313
+ warnings = []
314
+
315
+ if "project" not in config:
316
+ errors.append("Missing [project] section")
317
+ else:
318
+ if "name" not in config["project"]:
319
+ errors.append('Property "name" missing in [project]')
320
+ if "version" not in config["project"]:
321
+ errors.append('Property "version" missing in [project]')
322
+ if "description" not in config["project"]:
323
+ warnings.append('Recommended property "description" missing in [project]')
324
+ if "license" not in config["project"]:
325
+ warnings.append('Recommended property "license" missing in [project]')
326
+ if "authors" not in config["project"]:
327
+ warnings.append('Recommended property "authors" missing in [project]')
328
+
329
+ if (
330
+ "tool" not in config
331
+ or "flwr" not in config["tool"]
332
+ or "app" not in config["tool"]["flwr"]
333
+ ):
334
+ errors.append("Missing [tool.flwr.app] section")
335
+ else:
336
+ if "publisher" not in config["tool"]["flwr"]["app"]:
337
+ errors.append('Property "publisher" missing in [tool.flwr.app]')
338
+ if "config" in config["tool"]["flwr"]["app"]:
339
+ _validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
340
+ if "components" not in config["tool"]["flwr"]["app"]:
341
+ errors.append("Missing [tool.flwr.app.components] section")
342
+ else:
343
+ if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
344
+ errors.append(
345
+ 'Property "serverapp" missing in [tool.flwr.app.components]'
346
+ )
347
+ if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
348
+ errors.append(
349
+ 'Property "clientapp" missing in [tool.flwr.app.components]'
350
+ )
351
+
352
+ return len(errors) == 0, errors, warnings
353
+
354
+
355
+ def validate_config(
356
+ config: dict[str, Any],
357
+ check_module: bool = True,
358
+ project_dir: Optional[Union[str, Path]] = None,
359
+ ) -> tuple[bool, list[str], list[str]]:
360
+ """Validate pyproject.toml."""
361
+ is_valid, errors, warnings = validate_fields_in_config(config)
362
+
363
+ if not is_valid:
364
+ return False, errors, warnings
365
+
366
+ # Validate serverapp
367
+ serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
368
+ is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
369
+
370
+ if not is_valid and isinstance(reason, str):
371
+ return False, [reason], []
372
+
373
+ # Validate clientapp
374
+ clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
375
+ is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
376
+
377
+ if not is_valid and isinstance(reason, str):
378
+ return False, [reason], []
379
+
380
+ return True, [], []
flwr/common/constant.py CHANGED
@@ -17,14 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- MISSING_EXTRA_REST = """
21
- Extra dependencies required for using the REST-based Fleet API are missing.
22
-
23
- To use the REST API, install `flwr` with the `rest` extra:
24
-
25
- `pip install flwr[rest]`.
26
- """
27
-
28
20
  TRANSPORT_TYPE_GRPC_BIDI = "grpc-bidi"
29
21
  TRANSPORT_TYPE_GRPC_RERE = "grpc-rere"
30
22
  TRANSPORT_TYPE_GRPC_ADAPTER = "grpc-adapter"
@@ -83,6 +75,9 @@ FAB_HASH_TRUNCATION = 8
83
75
  FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
84
76
  FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
85
77
 
78
+ # Constant for SuperLink
79
+ SUPERLINK_NODE_ID = 1
80
+
86
81
  # Constants entries in Node config for Simulation
87
82
  PARTITION_ID_KEY = "partition-id"
88
83
  NUM_PARTITIONS_KEY = "num-partitions"
@@ -114,6 +109,14 @@ MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
114
109
  # Constants for user authentication
115
110
  CREDENTIALS_DIR = ".credentials"
116
111
  AUTH_TYPE = "auth_type"
112
+ ACCESS_TOKEN_KEY = "access_token"
113
+ REFRESH_TOKEN_KEY = "refresh_token"
114
+
115
+ # Constants for node authentication
116
+ PUBLIC_KEY_HEADER = "public-key-bin" # Must end with "-bin" for binary data
117
+ SIGNATURE_HEADER = "signature-bin" # Must end with "-bin" for binary data
118
+ TIMESTAMP_HEADER = "timestamp"
119
+ TIMESTAMP_TOLERANCE = 10 # Tolerance for timestamp verification
117
120
 
118
121
 
119
122
  class MessageType:
@@ -0,0 +1,24 @@
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
+ """Flower exit functionality."""
16
+
17
+
18
+ from .exit import flwr_exit
19
+ from .exit_code import ExitCode
20
+
21
+ __all__ = [
22
+ "ExitCode",
23
+ "flwr_exit",
24
+ ]