flwr-nightly 1.15.0.dev20250120__py3-none-any.whl → 1.15.0.dev20250122__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/cli/config_utils.py CHANGED
@@ -15,53 +15,13 @@
15
15
  """Utility to validate the `pyproject.toml` file."""
16
16
 
17
17
 
18
- import zipfile
19
- from io import BytesIO
20
18
  from pathlib import Path
21
- from typing import IO, Any, Optional, Union, get_args
19
+ from typing import Any, Optional, Union
22
20
 
23
21
  import tomli
24
22
  import typer
25
23
 
26
- from flwr.common import object_ref
27
- from flwr.common.typing import UserConfigValue
28
-
29
-
30
- def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
31
- """Extract the config from a FAB file or path.
32
-
33
- Parameters
34
- ----------
35
- fab_file : Union[Path, bytes]
36
- The Flower App Bundle file to validate and extract the metadata from.
37
- It can either be a path to the file or the file itself as bytes.
38
-
39
- Returns
40
- -------
41
- Dict[str, Any]
42
- The `config` of the given Flower App Bundle.
43
- """
44
- fab_file_archive: Union[Path, IO[bytes]]
45
- if isinstance(fab_file, bytes):
46
- fab_file_archive = BytesIO(fab_file)
47
- elif isinstance(fab_file, Path):
48
- fab_file_archive = fab_file
49
- else:
50
- raise ValueError("fab_file must be either a Path or bytes")
51
-
52
- with zipfile.ZipFile(fab_file_archive, "r") as zipf:
53
- with zipf.open("pyproject.toml") as file:
54
- toml_content = file.read().decode("utf-8")
55
-
56
- conf = load_from_string(toml_content)
57
- if conf is None:
58
- raise ValueError("Invalid TOML content in pyproject.toml")
59
-
60
- is_valid, errors, _ = validate(conf, check_module=False)
61
- if not is_valid:
62
- raise ValueError(errors)
63
-
64
- return conf
24
+ from flwr.common.config import get_fab_config, get_metadata_from_config, validate_config
65
25
 
66
26
 
67
27
  def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
@@ -78,12 +38,7 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> tuple[str, str]:
78
38
  Tuple[str, str]
79
39
  The `fab_id` and `fab_version` of the given Flower App Bundle.
80
40
  """
81
- conf = get_fab_config(fab_file)
82
-
83
- return (
84
- f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}",
85
- conf["project"]["version"],
86
- )
41
+ return get_metadata_from_config(get_fab_config(fab_file))
87
42
 
88
43
 
89
44
  def load_and_validate(
@@ -120,7 +75,7 @@ def load_and_validate(
120
75
  ]
121
76
  return (None, errors, [])
122
77
 
123
- is_valid, errors, warnings = validate(config, check_module, path.parent)
78
+ is_valid, errors, warnings = validate_config(config, check_module, path.parent)
124
79
 
125
80
  if not is_valid:
126
81
  return (None, errors, warnings)
@@ -133,102 +88,11 @@ def load(toml_path: Path) -> Optional[dict[str, Any]]:
133
88
  if not toml_path.is_file():
134
89
  return None
135
90
 
136
- with toml_path.open(encoding="utf-8") as toml_file:
137
- return load_from_string(toml_file.read())
138
-
139
-
140
- def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
141
- for key, value in config_dict.items():
142
- if isinstance(value, dict):
143
- _validate_run_config(config_dict[key], errors)
144
- elif not isinstance(value, get_args(UserConfigValue)):
145
- raise ValueError(
146
- f"The value for key {key} needs to be of type `int`, `float`, "
147
- "`bool, `str`, or a `dict` of those.",
148
- )
149
-
150
-
151
- # pylint: disable=too-many-branches
152
- def validate_fields(config: dict[str, Any]) -> tuple[bool, list[str], list[str]]:
153
- """Validate pyproject.toml fields."""
154
- errors = []
155
- warnings = []
156
-
157
- if "project" not in config:
158
- errors.append("Missing [project] section")
159
- else:
160
- if "name" not in config["project"]:
161
- errors.append('Property "name" missing in [project]')
162
- if "version" not in config["project"]:
163
- errors.append('Property "version" missing in [project]')
164
- if "description" not in config["project"]:
165
- warnings.append('Recommended property "description" missing in [project]')
166
- if "license" not in config["project"]:
167
- warnings.append('Recommended property "license" missing in [project]')
168
- if "authors" not in config["project"]:
169
- warnings.append('Recommended property "authors" missing in [project]')
170
-
171
- if (
172
- "tool" not in config
173
- or "flwr" not in config["tool"]
174
- or "app" not in config["tool"]["flwr"]
175
- ):
176
- errors.append("Missing [tool.flwr.app] section")
177
- else:
178
- if "publisher" not in config["tool"]["flwr"]["app"]:
179
- errors.append('Property "publisher" missing in [tool.flwr.app]')
180
- if "config" in config["tool"]["flwr"]["app"]:
181
- _validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
182
- if "components" not in config["tool"]["flwr"]["app"]:
183
- errors.append("Missing [tool.flwr.app.components] section")
184
- else:
185
- if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
186
- errors.append(
187
- 'Property "serverapp" missing in [tool.flwr.app.components]'
188
- )
189
- if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
190
- errors.append(
191
- 'Property "clientapp" missing in [tool.flwr.app.components]'
192
- )
193
-
194
- return len(errors) == 0, errors, warnings
195
-
196
-
197
- def validate(
198
- config: dict[str, Any],
199
- check_module: bool = True,
200
- project_dir: Optional[Union[str, Path]] = None,
201
- ) -> tuple[bool, list[str], list[str]]:
202
- """Validate pyproject.toml."""
203
- is_valid, errors, warnings = validate_fields(config)
204
-
205
- if not is_valid:
206
- return False, errors, warnings
207
-
208
- # Validate serverapp
209
- serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
210
- is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
211
-
212
- if not is_valid and isinstance(reason, str):
213
- return False, [reason], []
214
-
215
- # Validate clientapp
216
- clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
217
- is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
218
-
219
- if not is_valid and isinstance(reason, str):
220
- return False, [reason], []
221
-
222
- return True, [], []
223
-
224
-
225
- def load_from_string(toml_content: str) -> Optional[dict[str, Any]]:
226
- """Load TOML content from a string and return as dict."""
227
- try:
228
- data = tomli.loads(toml_content)
229
- return data
230
- except tomli.TOMLDecodeError:
231
- return None
91
+ with toml_path.open("rb") as toml_file:
92
+ try:
93
+ return tomli.load(toml_file)
94
+ except tomli.TOMLDecodeError:
95
+ return None
232
96
 
233
97
 
234
98
  def process_loaded_project_config(
flwr/cli/install.py CHANGED
@@ -154,7 +154,7 @@ def validate_and_install(
154
154
  )
155
155
  raise typer.Exit(code=1)
156
156
 
157
- version, fab_id = get_metadata_from_config(config)
157
+ fab_id, version = get_metadata_from_config(config)
158
158
  publisher, project_name = fab_id.split("/")
159
159
  config_metadata = (publisher, project_name, version, fab_hash)
160
160
 
@@ -66,7 +66,7 @@ def get_load_client_app_fn(
66
66
  # `fab_hash` is not required since the app is loaded from `runtime_app_dir`.
67
67
  elif app_path is not None:
68
68
  config = get_project_config(runtime_app_dir)
69
- this_fab_version, this_fab_id = get_metadata_from_config(config)
69
+ this_fab_id, this_fab_version = get_metadata_from_config(config)
70
70
 
71
71
  if this_fab_version != fab_version or this_fab_id != fab_id:
72
72
  raise LoadClientAppError(
@@ -15,71 +15,18 @@
15
15
  """Flower client interceptor."""
16
16
 
17
17
 
18
- import base64
19
- import collections
20
- from collections.abc import Sequence
21
- from logging import WARNING
22
- from typing import Any, Callable, Optional, Union
18
+ from typing import Any, Callable
23
19
 
24
20
  import grpc
25
21
  from cryptography.hazmat.primitives.asymmetric import ec
22
+ from google.protobuf.message import Message as GrpcMessage
26
23
 
27
- from flwr.common.logger import log
24
+ from flwr.common import now
25
+ from flwr.common.constant import PUBLIC_KEY_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER
28
26
  from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
29
- bytes_to_public_key,
30
- compute_hmac,
31
- generate_shared_key,
32
27
  public_key_to_bytes,
28
+ sign_message,
33
29
  )
34
- from flwr.proto.fab_pb2 import GetFabRequest # pylint: disable=E0611
35
- from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
36
- CreateNodeRequest,
37
- DeleteNodeRequest,
38
- PingRequest,
39
- PullMessagesRequest,
40
- PullTaskInsRequest,
41
- PushMessagesRequest,
42
- PushTaskResRequest,
43
- )
44
- from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611
45
-
46
- _PUBLIC_KEY_HEADER = "public-key"
47
- _AUTH_TOKEN_HEADER = "auth-token"
48
-
49
- Request = Union[
50
- CreateNodeRequest,
51
- DeleteNodeRequest,
52
- PullTaskInsRequest,
53
- PushTaskResRequest,
54
- GetRunRequest,
55
- PingRequest,
56
- GetFabRequest,
57
- PullMessagesRequest,
58
- PushMessagesRequest,
59
- ]
60
-
61
-
62
- def _get_value_from_tuples(
63
- key_string: str, tuples: Sequence[tuple[str, Union[str, bytes]]]
64
- ) -> bytes:
65
- value = next((value for key, value in tuples if key == key_string), "")
66
- if isinstance(value, str):
67
- return value.encode()
68
-
69
- return value
70
-
71
-
72
- class _ClientCallDetails(
73
- collections.namedtuple(
74
- "_ClientCallDetails", ("method", "timeout", "metadata", "credentials")
75
- ),
76
- grpc.ClientCallDetails, # type: ignore
77
- ):
78
- """Details for each client call.
79
-
80
- The class will be passed on as the first argument in continuation function.
81
- In our case, `AuthenticateClientInterceptor` adds new metadata to the construct.
82
- """
83
30
 
84
31
 
85
32
  class AuthenticateClientInterceptor(grpc.UnaryUnaryClientInterceptor): # type: ignore
@@ -91,86 +38,33 @@ class AuthenticateClientInterceptor(grpc.UnaryUnaryClientInterceptor): # type:
91
38
  public_key: ec.EllipticCurvePublicKey,
92
39
  ):
93
40
  self.private_key = private_key
94
- self.public_key = public_key
95
- self.shared_secret: Optional[bytes] = None
96
- self.server_public_key: Optional[ec.EllipticCurvePublicKey] = None
97
- self.encoded_public_key = base64.urlsafe_b64encode(
98
- public_key_to_bytes(self.public_key)
99
- )
41
+ self.public_key_bytes = public_key_to_bytes(public_key)
100
42
 
101
43
  def intercept_unary_unary(
102
44
  self,
103
45
  continuation: Callable[[Any, Any], Any],
104
46
  client_call_details: grpc.ClientCallDetails,
105
- request: Request,
47
+ request: GrpcMessage,
106
48
  ) -> grpc.Call:
107
49
  """Flower client interceptor.
108
50
 
109
51
  Intercept unary call from client and add necessary authentication header in the
110
52
  RPC metadata.
111
53
  """
112
- metadata = []
113
- postprocess = False
114
- if client_call_details.metadata is not None:
115
- metadata = list(client_call_details.metadata)
116
-
117
- # Always add the public key header
118
- metadata.append(
119
- (
120
- _PUBLIC_KEY_HEADER,
121
- self.encoded_public_key,
122
- )
123
- )
124
-
125
- if isinstance(request, CreateNodeRequest):
126
- postprocess = True
127
- elif isinstance(
128
- request,
129
- (
130
- DeleteNodeRequest,
131
- PullTaskInsRequest,
132
- PushTaskResRequest,
133
- GetRunRequest,
134
- PingRequest,
135
- GetFabRequest,
136
- PullMessagesRequest,
137
- PushMessagesRequest,
138
- ),
139
- ):
140
- if self.shared_secret is None:
141
- raise RuntimeError("Failure to compute hmac")
142
-
143
- message_bytes = request.SerializeToString(deterministic=True)
144
- metadata.append(
145
- (
146
- _AUTH_TOKEN_HEADER,
147
- base64.urlsafe_b64encode(
148
- compute_hmac(self.shared_secret, message_bytes)
149
- ),
150
- )
151
- )
54
+ metadata = list(client_call_details.metadata or [])
152
55
 
153
- client_call_details = _ClientCallDetails(
154
- client_call_details.method,
155
- client_call_details.timeout,
156
- metadata,
157
- client_call_details.credentials,
158
- )
56
+ # Add the public key
57
+ metadata.append((PUBLIC_KEY_HEADER, self.public_key_bytes))
159
58
 
160
- response = continuation(client_call_details, request)
161
- if postprocess:
162
- server_public_key_bytes = base64.urlsafe_b64decode(
163
- _get_value_from_tuples(_PUBLIC_KEY_HEADER, response.initial_metadata())
164
- )
59
+ # Add timestamp
60
+ timestamp = now().isoformat()
61
+ metadata.append((TIMESTAMP_HEADER, timestamp))
165
62
 
166
- if server_public_key_bytes != b"":
167
- self.server_public_key = bytes_to_public_key(server_public_key_bytes)
168
- else:
169
- log(WARNING, "Can't get server public key, SuperLink may be offline")
63
+ # Sign and add the signature
64
+ signature = sign_message(self.private_key, timestamp.encode("ascii"))
65
+ metadata.append((SIGNATURE_HEADER, signature))
170
66
 
171
- if self.server_public_key is not None:
172
- self.shared_secret = generate_shared_key(
173
- self.private_key, self.server_public_key
174
- )
67
+ # Overwrite the metadata
68
+ details = client_call_details._replace(metadata=metadata)
175
69
 
176
- return response
70
+ return continuation(details, request)
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, 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,8 @@ 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
+
36
38
 
37
39
  def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
38
40
  """Return the Flower home directory based on env variables."""
@@ -80,7 +82,7 @@ def get_project_config(project_dir: Union[str, Path]) -> dict[str, Any]:
80
82
  config = tomli.loads(toml_file.read())
81
83
 
82
84
  # Validate pyproject.toml fields
83
- is_valid, errors, _ = validate_fields(config)
85
+ is_valid, errors, _ = validate_fields_in_config(config)
84
86
  if not is_valid:
85
87
  error_msg = "\n".join([f" - {error}" for error in errors])
86
88
  raise ValueError(
@@ -227,10 +229,10 @@ def parse_config_args(
227
229
 
228
230
 
229
231
  def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
230
- """Extract `fab_version` and `fab_id` from a project config."""
232
+ """Extract `fab_id` and `fab_version` from a project config."""
231
233
  return (
232
- config["project"]["version"],
233
234
  f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
235
+ config["project"]["version"],
234
236
  )
235
237
 
236
238
 
@@ -241,3 +243,127 @@ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
241
243
  c_record[k] = v
242
244
 
243
245
  return c_record
246
+
247
+
248
+ def get_fab_config(fab_file: Union[Path, bytes]) -> dict[str, Any]:
249
+ """Extract the config from a FAB file or path.
250
+
251
+ Parameters
252
+ ----------
253
+ fab_file : Union[Path, bytes]
254
+ The Flower App Bundle file to validate and extract the metadata from.
255
+ It can either be a path to the file or the file itself as bytes.
256
+
257
+ Returns
258
+ -------
259
+ Dict[str, Any]
260
+ The `config` of the given Flower App Bundle.
261
+ """
262
+ fab_file_archive: Union[Path, IO[bytes]]
263
+ if isinstance(fab_file, bytes):
264
+ fab_file_archive = BytesIO(fab_file)
265
+ elif isinstance(fab_file, Path):
266
+ fab_file_archive = fab_file
267
+ else:
268
+ raise ValueError("fab_file must be either a Path or bytes")
269
+
270
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
271
+ with zipf.open("pyproject.toml") as file:
272
+ toml_content = file.read().decode("utf-8")
273
+ try:
274
+ conf = tomli.loads(toml_content)
275
+ except tomli.TOMLDecodeError:
276
+ raise ValueError("Invalid TOML content in pyproject.toml") from None
277
+
278
+ is_valid, errors, _ = validate_config(conf, check_module=False)
279
+ if not is_valid:
280
+ raise ValueError(errors)
281
+
282
+ return conf
283
+
284
+
285
+ def _validate_run_config(config_dict: dict[str, Any], errors: list[str]) -> None:
286
+ for key, value in config_dict.items():
287
+ if isinstance(value, dict):
288
+ _validate_run_config(config_dict[key], errors)
289
+ elif not isinstance(value, get_args(UserConfigValue)):
290
+ raise ValueError(
291
+ f"The value for key {key} needs to be of type `int`, `float`, "
292
+ "`bool, `str`, or a `dict` of those.",
293
+ )
294
+
295
+
296
+ # pylint: disable=too-many-branches
297
+ def validate_fields_in_config(
298
+ config: dict[str, Any]
299
+ ) -> tuple[bool, list[str], list[str]]:
300
+ """Validate pyproject.toml fields."""
301
+ errors = []
302
+ warnings = []
303
+
304
+ if "project" not in config:
305
+ errors.append("Missing [project] section")
306
+ else:
307
+ if "name" not in config["project"]:
308
+ errors.append('Property "name" missing in [project]')
309
+ if "version" not in config["project"]:
310
+ errors.append('Property "version" missing in [project]')
311
+ if "description" not in config["project"]:
312
+ warnings.append('Recommended property "description" missing in [project]')
313
+ if "license" not in config["project"]:
314
+ warnings.append('Recommended property "license" missing in [project]')
315
+ if "authors" not in config["project"]:
316
+ warnings.append('Recommended property "authors" missing in [project]')
317
+
318
+ if (
319
+ "tool" not in config
320
+ or "flwr" not in config["tool"]
321
+ or "app" not in config["tool"]["flwr"]
322
+ ):
323
+ errors.append("Missing [tool.flwr.app] section")
324
+ else:
325
+ if "publisher" not in config["tool"]["flwr"]["app"]:
326
+ errors.append('Property "publisher" missing in [tool.flwr.app]')
327
+ if "config" in config["tool"]["flwr"]["app"]:
328
+ _validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
329
+ if "components" not in config["tool"]["flwr"]["app"]:
330
+ errors.append("Missing [tool.flwr.app.components] section")
331
+ else:
332
+ if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
333
+ errors.append(
334
+ 'Property "serverapp" missing in [tool.flwr.app.components]'
335
+ )
336
+ if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
337
+ errors.append(
338
+ 'Property "clientapp" missing in [tool.flwr.app.components]'
339
+ )
340
+
341
+ return len(errors) == 0, errors, warnings
342
+
343
+
344
+ def validate_config(
345
+ config: dict[str, Any],
346
+ check_module: bool = True,
347
+ project_dir: Optional[Union[str, Path]] = None,
348
+ ) -> tuple[bool, list[str], list[str]]:
349
+ """Validate pyproject.toml."""
350
+ is_valid, errors, warnings = validate_fields_in_config(config)
351
+
352
+ if not is_valid:
353
+ return False, errors, warnings
354
+
355
+ # Validate serverapp
356
+ serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
357
+ is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
358
+
359
+ if not is_valid and isinstance(reason, str):
360
+ return False, [reason], []
361
+
362
+ # Validate clientapp
363
+ clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
364
+ is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
365
+
366
+ if not is_valid and isinstance(reason, str):
367
+ return False, [reason], []
368
+
369
+ return True, [], []
flwr/common/constant.py CHANGED
@@ -112,6 +112,12 @@ AUTH_TYPE = "auth_type"
112
112
  ACCESS_TOKEN_KEY = "access_token"
113
113
  REFRESH_TOKEN_KEY = "refresh_token"
114
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
120
+
115
121
 
116
122
  class MessageType:
117
123
  """Message type."""
@@ -39,10 +39,12 @@ class ExitCode:
39
39
 
40
40
  # ClientApp-specific exit codes (400-499)
41
41
 
42
- # Common exit codes (500-)
43
- COMMON_ADDRESS_INVALID = 500
44
- COMMON_MISSING_EXTRA_REST = 501
45
- COMMON_TLS_NOT_SUPPORTED = 502
42
+ # Simulation-specific exit codes (500-599)
43
+
44
+ # Common exit codes (600-)
45
+ COMMON_ADDRESS_INVALID = 600
46
+ COMMON_MISSING_EXTRA_REST = 601
47
+ COMMON_TLS_NOT_SUPPORTED = 602
46
48
 
47
49
  def __new__(cls) -> ExitCode:
48
50
  """Prevent instantiation."""
@@ -75,7 +77,8 @@ EXIT_CODE_HELP = {
75
77
  "file and try again."
76
78
  ),
77
79
  # ClientApp-specific exit codes (400-499)
78
- # Common exit codes (500-)
80
+ # Simulation-specific exit codes (500-599)
81
+ # Common exit codes (600-)
79
82
  ExitCode.COMMON_ADDRESS_INVALID: (
80
83
  "Please provide a valid URL, IPv4 or IPv6 address."
81
84
  ),
flwr/server/app.py CHANGED
@@ -228,6 +228,13 @@ def start_server( # pylint: disable=too-many-arguments,too-many-locals
228
228
  "enabled" if certificates is not None else "disabled",
229
229
  )
230
230
 
231
+ # Graceful shutdown
232
+ register_exit_handlers(
233
+ event_type=EventType.START_SERVER_LEAVE,
234
+ exit_message="Flower server terminated gracefully.",
235
+ grpc_servers=[grpc_server],
236
+ )
237
+
231
238
  # Start training
232
239
  hist = run_fl(
233
240
  server=initialized_server,
@@ -15,7 +15,7 @@
15
15
  """Fleet API gRPC adapter servicer."""
16
16
 
17
17
 
18
- from logging import DEBUG, INFO
18
+ from logging import DEBUG
19
19
  from typing import Callable, TypeVar
20
20
 
21
21
  import grpc
@@ -31,35 +31,30 @@ from flwr.common.constant import (
31
31
  from flwr.common.logger import log
32
32
  from flwr.common.version import package_name, package_version
33
33
  from flwr.proto import grpcadapter_pb2_grpc # pylint: disable=E0611
34
- from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
34
+ from flwr.proto.fab_pb2 import GetFabRequest # pylint: disable=E0611
35
35
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
36
36
  CreateNodeRequest,
37
- CreateNodeResponse,
38
37
  DeleteNodeRequest,
39
- DeleteNodeResponse,
40
38
  PingRequest,
41
- PingResponse,
42
- PullTaskInsRequest,
43
- PullTaskInsResponse,
44
- PushTaskResRequest,
45
- PushTaskResResponse,
39
+ PullMessagesRequest,
40
+ PushMessagesRequest,
46
41
  )
47
42
  from flwr.proto.grpcadapter_pb2 import MessageContainer # pylint: disable=E0611
48
- from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
49
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
50
- from flwr.server.superlink.fleet.message_handler import message_handler
51
- from flwr.server.superlink.linkstate import LinkStateFactory
43
+ from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611
44
+
45
+ from ..grpc_rere.fleet_servicer import FleetServicer
52
46
 
53
47
  T = TypeVar("T", bound=GrpcMessage)
54
48
 
55
49
 
56
50
  def _handle(
57
51
  msg_container: MessageContainer,
52
+ context: grpc.ServicerContext,
58
53
  request_type: type[T],
59
- handler: Callable[[T], GrpcMessage],
54
+ handler: Callable[[T, grpc.ServicerContext], GrpcMessage],
60
55
  ) -> MessageContainer:
61
56
  req = request_type.FromString(msg_container.grpc_message_content)
62
- res = handler(req)
57
+ res = handler(req, context)
63
58
  res_cls = res.__class__
64
59
  return MessageContainer(
65
60
  metadata={
@@ -74,89 +69,26 @@ def _handle(
74
69
  )
75
70
 
76
71
 
77
- class GrpcAdapterServicer(grpcadapter_pb2_grpc.GrpcAdapterServicer):
72
+ class GrpcAdapterServicer(grpcadapter_pb2_grpc.GrpcAdapterServicer, FleetServicer):
78
73
  """Fleet API via GrpcAdapter servicer."""
79
74
 
80
- def __init__(
81
- self, state_factory: LinkStateFactory, ffs_factory: FfsFactory
82
- ) -> None:
83
- self.state_factory = state_factory
84
- self.ffs_factory = ffs_factory
85
-
86
75
  def SendReceive( # pylint: disable=too-many-return-statements
87
76
  self, request: MessageContainer, context: grpc.ServicerContext
88
77
  ) -> MessageContainer:
89
78
  """."""
90
79
  log(DEBUG, "GrpcAdapterServicer.SendReceive")
91
80
  if request.grpc_message_name == CreateNodeRequest.__qualname__:
92
- return _handle(request, CreateNodeRequest, self._create_node)
81
+ return _handle(request, context, CreateNodeRequest, self.CreateNode)
93
82
  if request.grpc_message_name == DeleteNodeRequest.__qualname__:
94
- return _handle(request, DeleteNodeRequest, self._delete_node)
83
+ return _handle(request, context, DeleteNodeRequest, self.DeleteNode)
95
84
  if request.grpc_message_name == PingRequest.__qualname__:
96
- return _handle(request, PingRequest, self._ping)
97
- if request.grpc_message_name == PullTaskInsRequest.__qualname__:
98
- return _handle(request, PullTaskInsRequest, self._pull_task_ins)
99
- if request.grpc_message_name == PushTaskResRequest.__qualname__:
100
- return _handle(request, PushTaskResRequest, self._push_task_res)
85
+ return _handle(request, context, PingRequest, self.Ping)
101
86
  if request.grpc_message_name == GetRunRequest.__qualname__:
102
- return _handle(request, GetRunRequest, self._get_run)
87
+ return _handle(request, context, GetRunRequest, self.GetRun)
103
88
  if request.grpc_message_name == GetFabRequest.__qualname__:
104
- return _handle(request, GetFabRequest, self._get_fab)
89
+ return _handle(request, context, GetFabRequest, self.GetFab)
90
+ if request.grpc_message_name == PullMessagesRequest.__qualname__:
91
+ return _handle(request, context, PullMessagesRequest, self.PullMessages)
92
+ if request.grpc_message_name == PushMessagesRequest.__qualname__:
93
+ return _handle(request, context, PushMessagesRequest, self.PushMessages)
105
94
  raise ValueError(f"Invalid grpc_message_name: {request.grpc_message_name}")
106
-
107
- def _create_node(self, request: CreateNodeRequest) -> CreateNodeResponse:
108
- """."""
109
- log(INFO, "GrpcAdapter.CreateNode")
110
- return message_handler.create_node(
111
- request=request,
112
- state=self.state_factory.state(),
113
- )
114
-
115
- def _delete_node(self, request: DeleteNodeRequest) -> DeleteNodeResponse:
116
- """."""
117
- log(INFO, "GrpcAdapter.DeleteNode")
118
- return message_handler.delete_node(
119
- request=request,
120
- state=self.state_factory.state(),
121
- )
122
-
123
- def _ping(self, request: PingRequest) -> PingResponse:
124
- """."""
125
- log(DEBUG, "GrpcAdapter.Ping")
126
- return message_handler.ping(
127
- request=request,
128
- state=self.state_factory.state(),
129
- )
130
-
131
- def _pull_task_ins(self, request: PullTaskInsRequest) -> PullTaskInsResponse:
132
- """Pull TaskIns."""
133
- log(INFO, "GrpcAdapter.PullTaskIns")
134
- return message_handler.pull_task_ins(
135
- request=request,
136
- state=self.state_factory.state(),
137
- )
138
-
139
- def _push_task_res(self, request: PushTaskResRequest) -> PushTaskResResponse:
140
- """Push TaskRes."""
141
- log(INFO, "GrpcAdapter.PushTaskRes")
142
- return message_handler.push_task_res(
143
- request=request,
144
- state=self.state_factory.state(),
145
- )
146
-
147
- def _get_run(self, request: GetRunRequest) -> GetRunResponse:
148
- """Get run information."""
149
- log(INFO, "GrpcAdapter.GetRun")
150
- return message_handler.get_run(
151
- request=request,
152
- state=self.state_factory.state(),
153
- )
154
-
155
- def _get_fab(self, request: GetFabRequest) -> GetFabResponse:
156
- """Get FAB."""
157
- log(INFO, "GrpcAdapter.GetFab")
158
- return message_handler.get_fab(
159
- request=request,
160
- ffs=self.ffs_factory.ffs(),
161
- state=self.state_factory.state(),
162
- )
@@ -15,91 +15,54 @@
15
15
  """Flower server interceptor."""
16
16
 
17
17
 
18
- import base64
19
- from collections.abc import Sequence
20
- from logging import INFO, WARNING
21
- from typing import Any, Callable, Optional, Union
18
+ import datetime
19
+ from typing import Any, Callable, Optional, cast
22
20
 
23
21
  import grpc
24
- from cryptography.hazmat.primitives.asymmetric import ec
25
-
26
- from flwr.common.logger import log
22
+ from google.protobuf.message import Message as GrpcMessage
23
+
24
+ from flwr.common import now
25
+ from flwr.common.constant import (
26
+ PUBLIC_KEY_HEADER,
27
+ SIGNATURE_HEADER,
28
+ TIMESTAMP_HEADER,
29
+ TIMESTAMP_TOLERANCE,
30
+ )
27
31
  from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
28
- bytes_to_private_key,
29
32
  bytes_to_public_key,
30
- generate_shared_key,
31
- verify_hmac,
33
+ verify_signature,
32
34
  )
33
- from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
34
35
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
35
36
  CreateNodeRequest,
36
37
  CreateNodeResponse,
37
- DeleteNodeRequest,
38
- DeleteNodeResponse,
39
- PingRequest,
40
- PingResponse,
41
- PullTaskInsRequest,
42
- PullTaskInsResponse,
43
- PushTaskResRequest,
44
- PushTaskResResponse,
45
38
  )
46
- from flwr.proto.node_pb2 import Node # pylint: disable=E0611
47
- from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
48
39
  from flwr.server.superlink.linkstate import LinkStateFactory
49
40
 
50
- _PUBLIC_KEY_HEADER = "public-key"
51
- _AUTH_TOKEN_HEADER = "auth-token"
52
-
53
- Request = Union[
54
- CreateNodeRequest,
55
- DeleteNodeRequest,
56
- PullTaskInsRequest,
57
- PushTaskResRequest,
58
- GetRunRequest,
59
- PingRequest,
60
- GetFabRequest,
61
- ]
62
-
63
- Response = Union[
64
- CreateNodeResponse,
65
- DeleteNodeResponse,
66
- PullTaskInsResponse,
67
- PushTaskResResponse,
68
- GetRunResponse,
69
- PingResponse,
70
- GetFabResponse,
71
- ]
72
-
73
41
 
74
- def _get_value_from_tuples(
75
- key_string: str, tuples: Sequence[tuple[str, Union[str, bytes]]]
76
- ) -> bytes:
77
- value = next((value for key, value in tuples if key == key_string), "")
78
- if isinstance(value, str):
79
- return value.encode()
42
+ def _unary_unary_rpc_terminator(message: str) -> grpc.RpcMethodHandler:
43
+ def terminate(_request: GrpcMessage, context: grpc.ServicerContext) -> GrpcMessage:
44
+ context.abort(grpc.StatusCode.UNAUTHENTICATED, message)
45
+ raise RuntimeError("Should not reach this point") # Make mypy happy
80
46
 
81
- return value
47
+ return grpc.unary_unary_rpc_method_handler(terminate)
82
48
 
83
49
 
84
50
  class AuthenticateServerInterceptor(grpc.ServerInterceptor): # type: ignore
85
- """Server interceptor for node authentication."""
86
-
87
- def __init__(self, state_factory: LinkStateFactory):
51
+ """Server interceptor for node authentication.
52
+
53
+ Parameters
54
+ ----------
55
+ state_factory : LinkStateFactory
56
+ A factory for creating new instances of LinkState.
57
+ auto_auth : bool (default: False)
58
+ If True, nodes are authenticated without requiring their public keys to be
59
+ pre-stored in the LinkState. If False, only nodes with pre-stored public keys
60
+ can be authenticated.
61
+ """
62
+
63
+ def __init__(self, state_factory: LinkStateFactory, auto_auth: bool = False):
88
64
  self.state_factory = state_factory
89
- state = self.state_factory.state()
90
-
91
- self.node_public_keys = state.get_node_public_keys()
92
- if len(self.node_public_keys) == 0:
93
- log(WARNING, "Authentication enabled, but no known public keys configured")
94
-
95
- private_key = state.get_server_private_key()
96
- public_key = state.get_server_public_key()
97
-
98
- if private_key is None or public_key is None:
99
- raise ValueError("Error loading authentication keys")
100
-
101
- self.server_private_key = bytes_to_private_key(private_key)
102
- self.encoded_server_public_key = base64.urlsafe_b64encode(public_key)
65
+ self.auto_auth = auto_auth
103
66
 
104
67
  def intercept_service(
105
68
  self,
@@ -112,117 +75,80 @@ class AuthenticateServerInterceptor(grpc.ServerInterceptor): # type: ignore
112
75
  metadata sent by the node. Continue RPC call if node is authenticated, else,
113
76
  terminate RPC call by setting context to abort.
114
77
  """
78
+ state = self.state_factory.state()
79
+ metadata_dict = dict(handler_call_details.invocation_metadata)
80
+
81
+ # Retrieve info from the metadata
82
+ try:
83
+ node_pk_bytes = cast(bytes, metadata_dict[PUBLIC_KEY_HEADER])
84
+ timestamp_iso = cast(str, metadata_dict[TIMESTAMP_HEADER])
85
+ signature = cast(bytes, metadata_dict[SIGNATURE_HEADER])
86
+ except KeyError:
87
+ return _unary_unary_rpc_terminator("Missing authentication metadata")
88
+
89
+ if not self.auto_auth:
90
+ # Abort the RPC call if the node public key is not found
91
+ if node_pk_bytes not in state.get_node_public_keys():
92
+ return _unary_unary_rpc_terminator("Public key not recognized")
93
+
94
+ # Verify the signature
95
+ node_pk = bytes_to_public_key(node_pk_bytes)
96
+ if not verify_signature(node_pk, timestamp_iso.encode("ascii"), signature):
97
+ return _unary_unary_rpc_terminator("Invalid signature")
98
+
99
+ # Verify the timestamp
100
+ current = now()
101
+ time_diff = current - datetime.datetime.fromisoformat(timestamp_iso)
102
+ # Abort the RPC call if the timestamp is too old or in the future
103
+ if not 0 < time_diff.total_seconds() < TIMESTAMP_TOLERANCE:
104
+ return _unary_unary_rpc_terminator("Invalid timestamp")
105
+
106
+ # Continue the RPC call
107
+ expected_node_id = state.get_node_id(node_pk_bytes)
108
+ if not handler_call_details.method.endswith("CreateNode"):
109
+ if expected_node_id is None:
110
+ return _unary_unary_rpc_terminator("Invalid node ID")
115
111
  # One of the method handlers in
116
112
  # `flwr.server.superlink.fleet.grpc_rere.fleet_server.FleetServicer`
117
113
  method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
118
- return self._generic_auth_unary_method_handler(method_handler)
114
+ return self._wrap_method_handler(
115
+ method_handler, expected_node_id, node_pk_bytes
116
+ )
119
117
 
120
- def _generic_auth_unary_method_handler(
121
- self, method_handler: grpc.RpcMethodHandler
118
+ def _wrap_method_handler(
119
+ self,
120
+ method_handler: grpc.RpcMethodHandler,
121
+ expected_node_id: Optional[int],
122
+ node_public_key: bytes,
122
123
  ) -> grpc.RpcMethodHandler:
123
124
  def _generic_method_handler(
124
- request: Request,
125
+ request: GrpcMessage,
125
126
  context: grpc.ServicerContext,
126
- ) -> Response:
127
- node_public_key_bytes = base64.urlsafe_b64decode(
128
- _get_value_from_tuples(
129
- _PUBLIC_KEY_HEADER, context.invocation_metadata()
130
- )
131
- )
132
- if node_public_key_bytes not in self.node_public_keys:
133
- context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied")
134
-
135
- if isinstance(request, CreateNodeRequest):
136
- response = self._create_authenticated_node(
137
- node_public_key_bytes, request, context
138
- )
139
- log(
140
- INFO,
141
- "AuthenticateServerInterceptor: Created node_id=%s",
142
- response.node.node_id,
143
- )
144
- return response
145
-
146
- # Verify hmac value
147
- hmac_value = base64.urlsafe_b64decode(
148
- _get_value_from_tuples(
149
- _AUTH_TOKEN_HEADER, context.invocation_metadata()
150
- )
151
- )
152
- public_key = bytes_to_public_key(node_public_key_bytes)
153
-
154
- if not self._verify_hmac(public_key, request, hmac_value):
155
- context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied")
156
-
157
- # Verify node_id
158
- node_id = self.state_factory.state().get_node_id(node_public_key_bytes)
159
-
160
- if not self._verify_node_id(node_id, request):
161
- context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied")
162
-
163
- return method_handler.unary_unary(request, context) # type: ignore
127
+ ) -> GrpcMessage:
128
+ # Verify the node ID
129
+ if not isinstance(request, CreateNodeRequest):
130
+ try:
131
+ if request.node.node_id != expected_node_id: # type: ignore
132
+ raise ValueError
133
+ except (AttributeError, ValueError):
134
+ context.abort(grpc.StatusCode.UNAUTHENTICATED, "Invalid node ID")
135
+
136
+ response: GrpcMessage = method_handler.unary_unary(request, context)
137
+
138
+ # Set the public key after a successful CreateNode request
139
+ if isinstance(response, CreateNodeResponse):
140
+ state = self.state_factory.state()
141
+ try:
142
+ state.set_node_public_key(response.node.node_id, node_public_key)
143
+ except ValueError as e:
144
+ # Remove newly created node if setting the public key fails
145
+ state.delete_node(response.node.node_id)
146
+ context.abort(grpc.StatusCode.UNAUTHENTICATED, str(e))
147
+
148
+ return response
164
149
 
165
150
  return grpc.unary_unary_rpc_method_handler(
166
151
  _generic_method_handler,
167
152
  request_deserializer=method_handler.request_deserializer,
168
153
  response_serializer=method_handler.response_serializer,
169
154
  )
170
-
171
- def _verify_node_id(
172
- self,
173
- node_id: Optional[int],
174
- request: Union[
175
- DeleteNodeRequest,
176
- PullTaskInsRequest,
177
- PushTaskResRequest,
178
- GetRunRequest,
179
- PingRequest,
180
- GetFabRequest,
181
- ],
182
- ) -> bool:
183
- if node_id is None:
184
- return False
185
- if isinstance(request, PushTaskResRequest):
186
- if len(request.task_res_list) == 0:
187
- return False
188
- return request.task_res_list[0].task.producer.node_id == node_id
189
- if isinstance(request, GetRunRequest):
190
- return node_id in self.state_factory.state().get_nodes(request.run_id)
191
- return request.node.node_id == node_id
192
-
193
- def _verify_hmac(
194
- self, public_key: ec.EllipticCurvePublicKey, request: Request, hmac_value: bytes
195
- ) -> bool:
196
- shared_secret = generate_shared_key(self.server_private_key, public_key)
197
- message_bytes = request.SerializeToString(deterministic=True)
198
- return verify_hmac(shared_secret, message_bytes, hmac_value)
199
-
200
- def _create_authenticated_node(
201
- self,
202
- public_key_bytes: bytes,
203
- request: CreateNodeRequest,
204
- context: grpc.ServicerContext,
205
- ) -> CreateNodeResponse:
206
- context.send_initial_metadata(
207
- (
208
- (
209
- _PUBLIC_KEY_HEADER,
210
- self.encoded_server_public_key,
211
- ),
212
- )
213
- )
214
- state = self.state_factory.state()
215
- node_id = state.get_node_id(public_key_bytes)
216
-
217
- # Handle `CreateNode` here instead of calling the default method handler
218
- # Return previously assigned `node_id` for the provided `public_key`
219
- if node_id is not None:
220
- state.acknowledge_ping(node_id, request.ping_interval)
221
- return CreateNodeResponse(node=Node(node_id=node_id))
222
-
223
- # No `node_id` exists for the provided `public_key`
224
- # Handle `CreateNode` here instead of calling the default method handler
225
- # Note: the innermost `CreateNode` method will never be called
226
- node_id = state.create_node(request.ping_interval)
227
- state.set_node_public_key(node_id, public_key_bytes)
228
- return CreateNodeResponse(node=Node(node_id=node_id))
flwr/simulation/app.py CHANGED
@@ -16,7 +16,6 @@
16
16
 
17
17
 
18
18
  import argparse
19
- import sys
20
19
  from logging import DEBUG, ERROR, INFO
21
20
  from queue import Queue
22
21
  from time import sleep
@@ -39,6 +38,7 @@ from flwr.common.constant import (
39
38
  Status,
40
39
  SubStatus,
41
40
  )
41
+ from flwr.common.exit import ExitCode, flwr_exit
42
42
  from flwr.common.logger import (
43
43
  log,
44
44
  mirror_output_to_queue,
@@ -81,12 +81,10 @@ def flwr_simulation() -> None:
81
81
  log(INFO, "Starting Flower Simulation")
82
82
 
83
83
  if not args.insecure:
84
- log(
85
- ERROR,
86
- "`flwr-simulation` does not support TLS yet. "
87
- "Please use the '--insecure' flag.",
84
+ flwr_exit(
85
+ ExitCode.COMMON_TLS_NOT_SUPPORTED,
86
+ "`flwr-simulation` does not support TLS yet. ",
88
87
  )
89
- sys.exit(1)
90
88
 
91
89
  log(
92
90
  DEBUG,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.15.0.dev20250120
3
+ Version: 1.15.0.dev20250122
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -3,9 +3,9 @@ flwr/cli/__init__.py,sha256=cZJVgozlkC6Ni2Hd_FAIrqefrkCGOV18fikToq-6iLw,720
3
3
  flwr/cli/app.py,sha256=UeXrW5gxrUnFViDjAMIxGNZZKwu3a1oAj83v53IWIWM,1382
4
4
  flwr/cli/build.py,sha256=4P70i_FnUs0P21aTwjTXtFQSAfY-C04hUDF-2npfJdo,6345
5
5
  flwr/cli/cli_user_auth_interceptor.py,sha256=aZepPA298s-HjGmkJGMvI_uZe72O5aLC3jri-ilG53o,3126
6
- flwr/cli/config_utils.py,sha256=I4_EMv2f68mfrL_QuOYoAG--yDfKisE7tGiIg09G2YQ,12079
6
+ flwr/cli/config_utils.py,sha256=u4VMNgNTj1mGgCVzV4KfBz3Nyn0j46KJ-Ii8dUgZ4OM,7196
7
7
  flwr/cli/example.py,sha256=uk5CoD0ZITgpY_ffsTbEKf8XOOCSUzByjHPcMSPqV18,2216
8
- flwr/cli/install.py,sha256=0AD0qJD79SKgBnWOQlphcubfr4zHk8jTpFgwZbJBI_g,8180
8
+ flwr/cli/install.py,sha256=-RnrYGejN_zyXXp_CoddSQwoQfRTWWyt9WYlxphJzyU,8180
9
9
  flwr/cli/log.py,sha256=O7MBpsJp114PIZb-7Cru-KM6fqyneFQkqoQbQsqQmZU,6121
10
10
  flwr/cli/login/__init__.py,sha256=6_9zOzbPOAH72K2wX3-9dXTAbS7Mjpa5sEn2lA6eHHI,800
11
11
  flwr/cli/login/login.py,sha256=VaBPQBdLYmSfxXEJWVyu8U5dXztQgIv6rfTJkvz3zV8,3025
@@ -75,14 +75,14 @@ flwr/client/client_app.py,sha256=cTig-N00YzTucbo9zNi6I21J8PlbflU_8J_f5CI-Wpw,103
75
75
  flwr/client/clientapp/__init__.py,sha256=kZqChGnTChQ1WGSUkIlW2S5bc0d0mzDubCAmZUGRpEY,800
76
76
  flwr/client/clientapp/app.py,sha256=O2dghU6PBXeU6kK5Ihj2-8cKAzXIC1efyZv4aulqHU4,8952
77
77
  flwr/client/clientapp/clientappio_servicer.py,sha256=5L6bjw_j3Mnx9kRFwYwxDNABKurBO5q1jZOWE_X11wQ,8522
78
- flwr/client/clientapp/utils.py,sha256=TTihPRO_AUhA3ZCszPsLyLZ30D_tnhTfe1ndMNVOBPg,4344
78
+ flwr/client/clientapp/utils.py,sha256=qqTw9PKPCldGnnbAbMhtS-Qs_GcqADE1eOtVPXeKYAo,4344
79
79
  flwr/client/dpfedavg_numpy_client.py,sha256=4KsEvzavDKyVDU1V0kMqffTwu1lNdUCYQN-i0DTYVN8,7404
80
80
  flwr/client/grpc_adapter_client/__init__.py,sha256=QyNWIbsq9DpyMk7oemiO1P3TBFfkfkctnJ1JoAkTl3s,742
81
81
  flwr/client/grpc_adapter_client/connection.py,sha256=nV-hPd5q5Eblg6PgUrGGYj74mbE1a0qjfN8G3wzJVAc,4006
82
82
  flwr/client/grpc_client/__init__.py,sha256=LsnbqXiJhgQcB0XzAlUQgPx011Uf7Y7yabIC1HxivJ8,735
83
83
  flwr/client/grpc_client/connection.py,sha256=Y6VDDDEHCPlGxSf-bEyBua_khnY_F1OoXD-8gpIcYhE,9171
84
84
  flwr/client/grpc_rere_client/__init__.py,sha256=MK-oSoV3kwUEQnIwl0GN4OpiHR7eLOrMA8ikunET130,752
85
- flwr/client/grpc_rere_client/client_interceptor.py,sha256=9BEZNOHxNJFxIuXc9KzBF0ZkXECtOm7RgnZ9yHVUxCQ,5554
85
+ flwr/client/grpc_rere_client/client_interceptor.py,sha256=8yX2jhwfX9r1PO76ZdME4tPefutnQqWPi7kELriBMUo,2451
86
86
  flwr/client/grpc_rere_client/connection.py,sha256=9XBcTn4myN_pgAGM6QFQ2Q35clSqxfgY8irrChQuF6I,11668
87
87
  flwr/client/grpc_rere_client/grpc_adapter.py,sha256=VrSqHosRcWv8xDLKEabuzyHpVnRhjAEJf_MUFQxhDh8,6155
88
88
  flwr/client/heartbeat.py,sha256=cx37mJBH8LyoIN4Lks85wtqT1mnU5GulQnr4pGCvAq0,2404
@@ -113,8 +113,8 @@ flwr/common/address.py,sha256=9KNYE69WW_QVcyumsux3Qn1wmn4J7f13Y9nHASpvzbA,3018
113
113
  flwr/common/args.py,sha256=bCvtG0hhh_hVjl9NoWsY_g7kLMIN3jCN7B883HvZ7hg,6223
114
114
  flwr/common/auth_plugin/__init__.py,sha256=1Y8Oj3iB49IHDu9tvDih1J74Ygu7k85V9s2A4WORPyA,887
115
115
  flwr/common/auth_plugin/auth_plugin.py,sha256=wgDorBUB4IkK6twQ8vNawRVz7BDPmKdXZBNLqhU9RSs,3871
116
- flwr/common/config.py,sha256=vmPwtRu7JIoGCke03pJlsyrA6zTlN43flzQx-4AX1mE,8099
117
- flwr/common/constant.py,sha256=-YIo0mQS-RExu1pEhWaQZllZxomIAjLYo7F3Zac7wEc,5917
116
+ flwr/common/config.py,sha256=GEnOmW1vw-0IF-NkekyXQLYqstSqetZFEtT_SShyBgA,12693
117
+ flwr/common/constant.py,sha256=FLqav6UCcdCG61XZr31fmAFqOu4WRFG8zcbnwUyYJ4w,6202
118
118
  flwr/common/context.py,sha256=uJ-mnoC_8y_udEb3kAX-r8CPphNTWM72z1AlsvQEu54,2403
119
119
  flwr/common/date.py,sha256=NHHpESce5wYqEwoDXf09gp9U9l_5Bmlh2BsOcwS-kDM,1554
120
120
  flwr/common/differential_privacy.py,sha256=XwcJ3rWr8S8BZUocc76vLSJAXIf6OHnWkBV6-xlIRuw,6106
@@ -122,7 +122,7 @@ flwr/common/differential_privacy_constants.py,sha256=c7b7tqgvT7yMK0XN9ndiTBs4mQf
122
122
  flwr/common/dp.py,sha256=vddkvyjV2FhRoN4VuU2LeAM1UBn7dQB8_W-Qdiveal8,1978
123
123
  flwr/common/exit/__init__.py,sha256=-ZOJYLaNnR729a7VzZiFsLiqngzKQh3xc27svYStZ_Q,826
124
124
  flwr/common/exit/exit.py,sha256=DmZFyksp-w1sFDQekq5Z-qfnr-ivCAv78aQkqj-TDps,3458
125
- flwr/common/exit/exit_code.py,sha256=ffz81LCfi5BCZwifnqZKa-2Eg3VaYpPHZBlKwv5GBUo,3375
125
+ flwr/common/exit/exit_code.py,sha256=PNEnCrZfOILjfDAFu5m-2YWEJBrk97xglq4zCUlqV7E,3470
126
126
  flwr/common/exit_handlers.py,sha256=Dke87CC6d6b6kqkC2mF0I4JsP4mHhlQTFxkS4sKKgyw,3308
127
127
  flwr/common/grpc.py,sha256=GCdiTCppW-clhzOo7OIJbsKIWKnJ9pqNTsAKhj7y4So,9646
128
128
  flwr/common/logger.py,sha256=UPyI_98EDibqgf3epgWxFHxdXgYReSWtaKFf9Mj0hd0,12306
@@ -214,7 +214,7 @@ flwr/proto/transport_pb2_grpc.py,sha256=Nvn7oxzm1g1fPiGCGhyKxILDZHYG0CcgjySTzxq-
214
214
  flwr/proto/transport_pb2_grpc.pyi,sha256=AGXf8RiIiW2J5IKMlm_3qT3AzcDa4F3P5IqUjve_esA,766
215
215
  flwr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
216
216
  flwr/server/__init__.py,sha256=cEg1oecBu4cKB69iJCqWEylC8b5XW47bl7rQiJsdTvM,1528
217
- flwr/server/app.py,sha256=fhCavxPESfYkiQuxtC80sBDwAe7xf3-tOrFH0pog8z0,32660
217
+ flwr/server/app.py,sha256=7Ru-udfJDLU3VHa47q_4ErdKROvuyRktjO6GWZMdvvg,32865
218
218
  flwr/server/client_manager.py,sha256=7Ese0tgrH-i-ms363feYZJKwB8gWnXSmg_hYF2Bju4U,6227
219
219
  flwr/server/client_proxy.py,sha256=4G-oTwhb45sfWLx2uZdcXD98IZwdTS6F88xe3akCdUg,2399
220
220
  flwr/server/compat/__init__.py,sha256=VxnJtJyOjNFQXMNi9hIuzNlZM5n0Hj1p3aq_Pm2udw4,892
@@ -269,7 +269,7 @@ flwr/server/superlink/ffs/ffs.py,sha256=qLI1UfosJugu2BKOJWqHIhafTm-YiuKqGf3OGWPH
269
269
  flwr/server/superlink/ffs/ffs_factory.py,sha256=N_eMuUZggotdGiDQ5r_Tf21xsu_ob0e3jyM6ag7d3kk,1490
270
270
  flwr/server/superlink/fleet/__init__.py,sha256=76od-HhYjOUoZFLFDFCFnNHI4JLAmaXQEAyp7LWlQpc,711
271
271
  flwr/server/superlink/fleet/grpc_adapter/__init__.py,sha256=spBQQJeYz8zPOBOfyMLv87kqWPASGB73AymcLXdFaYA,742
272
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py,sha256=u3CR2oeNRBIINksenwcZREurIPqKds3JBEAVBhJjRS8,6413
272
+ flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py,sha256=RLYHmuNBtv7DUyxrUu6hmjqkum_WEsTP3RMpbqnVGic,4160
273
273
  flwr/server/superlink/fleet/grpc_bidi/__init__.py,sha256=dkSKQMuMTYh1qSnuN87cAPv_mcdLg3f0PqTABHs8gUE,735
274
274
  flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py,sha256=ud08wi9j8OYRYVTIioL1xenOgrEbtS7afyr8MnQEk4I,6021
275
275
  flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py,sha256=JkAH_nIZaqe_9kntrg26od_jaz5XdLFuvNMgGu8xk9Q,6485
@@ -277,7 +277,7 @@ flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py,sha256=h3EhqgelegVC4E
277
277
  flwr/server/superlink/fleet/grpc_bidi/grpc_server.py,sha256=mxPxyEF0IW0vV41Bqk1zfKOdRDEvXPwzJyMiRMg7nTI,5173
278
278
  flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=j2hyC342am-_Hgp1g80Y3fGDzfTI6n8QOOn2PyWf4eg,758
279
279
  flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=EVx3rHX8WjUXVX7Svki5ihsA1aIZkpOMLv0aQv9Rjjw,6656
280
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=NXih_xTErEp13CaeQKKPoM38vcXAQl7mXZgps4no9l4,8308
280
+ flwr/server/superlink/fleet/grpc_rere/server_interceptor.py,sha256=zf58FXJ4S-k4kUh-LWcz6O6AWRcxs_ZGNQtUDDM7FVw,6307
281
281
  flwr/server/superlink/fleet/message_handler/__init__.py,sha256=h8oLD7uo5lKICPy0rRdKRjTYe62u8PKkT_fA4xF5JPA,731
282
282
  flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=5QRqE0w8Kb-M2cEiPNIdKkPc17CEGHvNYjMpGOfgOlE,6886
283
283
  flwr/server/superlink/fleet/rest_rere/__init__.py,sha256=5jbYbAn75sGv-gBwOPDySE0kz96F6dTYLeMrGqNi4lM,735
@@ -308,7 +308,7 @@ flwr/server/workflow/secure_aggregation/__init__.py,sha256=3XlgDOjD_hcukTGl6Bc1B
308
308
  flwr/server/workflow/secure_aggregation/secagg_workflow.py,sha256=l2IdMdJjs1bgHs5vQgLSOVzar7v2oxUn46oCrnVE1rM,5839
309
309
  flwr/server/workflow/secure_aggregation/secaggplus_workflow.py,sha256=rfn2etO1nb7u-1oRl-H9q3enJZz3shMINZaBB7rPsC4,29671
310
310
  flwr/simulation/__init__.py,sha256=5UcDVJNjFoSwWqHbGM1hKfTTUUNdwAtuoNvNrfvdkUY,1556
311
- flwr/simulation/app.py,sha256=cQgIJJujFUpBCcydxgakNygibf3Iww6OAWRo7Sq6y8w,9754
311
+ flwr/simulation/app.py,sha256=xRVSJBnTXQUqWIYOzENfTnJlZ24CSNhWkhVEFxIu4I0,9758
312
312
  flwr/simulation/legacy_app.py,sha256=qpZI4Vvzr5TyWSLTRrMP-jN4rH2C25JI9nVSSjhFwSQ,15861
313
313
  flwr/simulation/ray_transport/__init__.py,sha256=wzcEEwUUlulnXsg6raCA1nGpP3LlAQDtJ8zNkCXcVbA,734
314
314
  flwr/simulation/ray_transport/ray_actor.py,sha256=k11yoAPQzFGQU-KnCCP0ZrfPPdUPXXrBe-1DKM5VdW4,18997
@@ -324,8 +324,8 @@ flwr/superexec/exec_servicer.py,sha256=X10ILT-AoGMrB3IgI2mBe9i-QcIVUAl9bucuqVOPY
324
324
  flwr/superexec/exec_user_auth_interceptor.py,sha256=K06OU-l4LnYhTDg071hGJuOaQWEJbZsYi5qxUmmtiG0,3704
325
325
  flwr/superexec/executor.py,sha256=_B55WW2TD1fBINpabSSDRenVHXYmvlfhv-k8hJKU4lQ,3115
326
326
  flwr/superexec/simulation.py,sha256=WQDon15oqpMopAZnwRZoTICYCfHqtkvFSqiTQ2hLD_g,4088
327
- flwr_nightly-1.15.0.dev20250120.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
328
- flwr_nightly-1.15.0.dev20250120.dist-info/METADATA,sha256=453s8bQi7Lz8qCItA93OEcIedV3ZdOB9EqKm2N8_hBY,15864
329
- flwr_nightly-1.15.0.dev20250120.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
330
- flwr_nightly-1.15.0.dev20250120.dist-info/entry_points.txt,sha256=JlNxX3qhaV18_2yj5a3kJW1ESxm31cal9iS_N_pf1Rk,538
331
- flwr_nightly-1.15.0.dev20250120.dist-info/RECORD,,
327
+ flwr_nightly-1.15.0.dev20250122.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
328
+ flwr_nightly-1.15.0.dev20250122.dist-info/METADATA,sha256=HNrdO4R0GWS2WA5_9eQjJrKDJhObz0BZJKPsVYErnso,15864
329
+ flwr_nightly-1.15.0.dev20250122.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
330
+ flwr_nightly-1.15.0.dev20250122.dist-info/entry_points.txt,sha256=JlNxX3qhaV18_2yj5a3kJW1ESxm31cal9iS_N_pf1Rk,538
331
+ flwr_nightly-1.15.0.dev20250122.dist-info/RECORD,,