flwr-nightly 1.10.0.dev20240710__py3-none-any.whl → 1.10.0.dev20240712__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

Files changed (31) hide show
  1. flwr/cli/config_utils.py +10 -0
  2. flwr/cli/run/run.py +25 -8
  3. flwr/client/app.py +49 -17
  4. flwr/client/grpc_adapter_client/connection.py +1 -1
  5. flwr/client/grpc_client/connection.py +1 -1
  6. flwr/client/grpc_rere_client/connection.py +3 -2
  7. flwr/client/node_state.py +44 -11
  8. flwr/client/node_state_tests.py +4 -3
  9. flwr/client/rest_client/connection.py +4 -3
  10. flwr/client/supernode/app.py +14 -7
  11. flwr/common/config.py +3 -3
  12. flwr/common/context.py +13 -2
  13. flwr/common/logger.py +25 -0
  14. flwr/server/__init__.py +2 -0
  15. flwr/server/compat/legacy_context.py +1 -1
  16. flwr/server/run_serverapp.py +3 -1
  17. flwr/server/server_app.py +56 -10
  18. flwr/server/serverapp_components.py +52 -0
  19. flwr/server/superlink/fleet/vce/backend/backend.py +4 -4
  20. flwr/server/superlink/fleet/vce/backend/raybackend.py +8 -9
  21. flwr/server/superlink/fleet/vce/vce_api.py +88 -121
  22. flwr/server/typing.py +2 -0
  23. flwr/simulation/ray_transport/ray_actor.py +15 -19
  24. flwr/simulation/ray_transport/ray_client_proxy.py +3 -1
  25. flwr/simulation/run_simulation.py +49 -33
  26. flwr/superexec/app.py +3 -3
  27. {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/METADATA +2 -2
  28. {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/RECORD +31 -30
  29. {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/LICENSE +0 -0
  30. {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/WHEEL +0 -0
  31. {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/entry_points.txt +0 -0
flwr/cli/config_utils.py CHANGED
@@ -108,6 +108,14 @@ def load(path: Optional[Path] = None) -> Optional[Dict[str, Any]]:
108
108
  return load_from_string(toml_file.read())
109
109
 
110
110
 
111
+ def _validate_run_config(config_dict: Dict[str, Any], errors: List[str]) -> None:
112
+ for key, value in config_dict.items():
113
+ if isinstance(value, dict):
114
+ _validate_run_config(config_dict[key], errors)
115
+ elif not isinstance(value, str):
116
+ errors.append(f"Config value of key {key} is not of type `str`.")
117
+
118
+
111
119
  # pylint: disable=too-many-branches
112
120
  def validate_fields(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]:
113
121
  """Validate pyproject.toml fields."""
@@ -133,6 +141,8 @@ def validate_fields(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]
133
141
  else:
134
142
  if "publisher" not in config["flower"]:
135
143
  errors.append('Property "publisher" missing in [flower]')
144
+ if "config" in config["flower"]:
145
+ _validate_run_config(config["flower"]["config"], errors)
136
146
  if "components" not in config["flower"]:
137
147
  errors.append("Missing [flower.components] section")
138
148
  else:
flwr/cli/run/run.py CHANGED
@@ -18,13 +18,14 @@ import sys
18
18
  from enum import Enum
19
19
  from logging import DEBUG
20
20
  from pathlib import Path
21
- from typing import Optional
21
+ from typing import Dict, Optional
22
22
 
23
23
  import typer
24
24
  from typing_extensions import Annotated
25
25
 
26
26
  from flwr.cli import config_utils
27
27
  from flwr.cli.build import build
28
+ from flwr.common.config import parse_config_args
28
29
  from flwr.common.constant import SUPEREXEC_DEFAULT_ADDRESS
29
30
  from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
30
31
  from flwr.common.logger import log
@@ -58,15 +59,20 @@ def run(
58
59
  Optional[Path],
59
60
  typer.Option(help="Path of the Flower project to run"),
60
61
  ] = None,
62
+ config_overrides: Annotated[
63
+ Optional[str],
64
+ typer.Option(
65
+ "--config",
66
+ "-c",
67
+ help="Override configuration key-value pairs",
68
+ ),
69
+ ] = None,
61
70
  ) -> None:
62
71
  """Run Flower project."""
63
- if use_superexec:
64
- _start_superexec_run(directory)
65
- return
66
-
67
72
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
68
73
 
69
- config, errors, warnings = config_utils.load_and_validate()
74
+ pyproject_path = directory / "pyproject.toml" if directory else None
75
+ config, errors, warnings = config_utils.load_and_validate(path=pyproject_path)
70
76
 
71
77
  if config is None:
72
78
  typer.secho(
@@ -88,6 +94,12 @@ def run(
88
94
 
89
95
  typer.secho("Success", fg=typer.colors.GREEN)
90
96
 
97
+ if use_superexec:
98
+ _start_superexec_run(
99
+ parse_config_args(config_overrides, separator=","), directory
100
+ )
101
+ return
102
+
91
103
  server_app_ref = config["flower"]["components"]["serverapp"]
92
104
  client_app_ref = config["flower"]["components"]["clientapp"]
93
105
 
@@ -115,7 +127,9 @@ def run(
115
127
  )
116
128
 
117
129
 
118
- def _start_superexec_run(directory: Optional[Path]) -> None:
130
+ def _start_superexec_run(
131
+ override_config: Dict[str, str], directory: Optional[Path]
132
+ ) -> None:
119
133
  def on_channel_state_change(channel_connectivity: str) -> None:
120
134
  """Log channel connectivity."""
121
135
  log(DEBUG, channel_connectivity)
@@ -132,6 +146,9 @@ def _start_superexec_run(directory: Optional[Path]) -> None:
132
146
 
133
147
  fab_path = build(directory)
134
148
 
135
- req = StartRunRequest(fab_file=Path(fab_path).read_bytes())
149
+ req = StartRunRequest(
150
+ fab_file=Path(fab_path).read_bytes(),
151
+ override_config=override_config,
152
+ )
136
153
  res = stub.StartRun(req)
137
154
  typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
flwr/client/app.py CHANGED
@@ -18,7 +18,8 @@ import signal
18
18
  import sys
19
19
  import time
20
20
  from dataclasses import dataclass
21
- from logging import DEBUG, ERROR, INFO, WARN
21
+ from logging import ERROR, INFO, WARN
22
+ from pathlib import Path
22
23
  from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union
23
24
 
24
25
  from cryptography.hazmat.primitives.asymmetric import ec
@@ -159,6 +160,7 @@ def start_client(
159
160
  event(EventType.START_CLIENT_ENTER)
160
161
  _start_client_internal(
161
162
  server_address=server_address,
163
+ node_config={},
162
164
  load_client_app_fn=None,
163
165
  client_fn=client_fn,
164
166
  client=client,
@@ -180,6 +182,7 @@ def start_client(
180
182
  def _start_client_internal(
181
183
  *,
182
184
  server_address: str,
185
+ node_config: Dict[str, str],
183
186
  load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
184
187
  client_fn: Optional[ClientFnExt] = None,
185
188
  client: Optional[Client] = None,
@@ -192,7 +195,7 @@ def _start_client_internal(
192
195
  ] = None,
193
196
  max_retries: Optional[int] = None,
194
197
  max_wait_time: Optional[float] = None,
195
- partition_id: Optional[int] = None,
198
+ flwr_dir: Optional[Path] = None,
196
199
  ) -> None:
197
200
  """Start a Flower client node which connects to a Flower server.
198
201
 
@@ -202,6 +205,8 @@ def _start_client_internal(
202
205
  The IPv4 or IPv6 address of the server. If the Flower
203
206
  server runs on the same machine on port 8080, then `server_address`
204
207
  would be `"[::]:8080"`.
208
+ node_config: Dict[str, str]
209
+ The configuration of the node.
205
210
  load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
206
211
  A function that can be used to load a `ClientApp` instance.
207
212
  client_fn : Optional[ClientFnExt]
@@ -236,9 +241,8 @@ def _start_client_internal(
236
241
  The maximum duration before the client stops trying to
237
242
  connect to the server in case of connection error.
238
243
  If set to None, there is no limit to the total time.
239
- partition_id: Optional[int] (default: None)
240
- The data partition index associated with this node. Better suited for
241
- prototyping purposes.
244
+ flwr_dir: Optional[Path] (default: None)
245
+ The fully resolved path containing installed Flower Apps.
242
246
  """
243
247
  if insecure is None:
244
248
  insecure = root_certificates is None
@@ -291,7 +295,7 @@ def _start_client_internal(
291
295
  log(WARN, "Connection attempt failed, retrying...")
292
296
  else:
293
297
  log(
294
- DEBUG,
298
+ WARN,
295
299
  "Connection attempt failed, retrying in %.2f seconds",
296
300
  retry_state.actual_wait,
297
301
  )
@@ -315,8 +319,10 @@ def _start_client_internal(
315
319
  on_backoff=_on_backoff,
316
320
  )
317
321
 
318
- node_state = NodeState(partition_id=partition_id)
319
- run_info: Dict[int, Run] = {}
322
+ # NodeState gets initialized when the first connection is established
323
+ node_state: Optional[NodeState] = None
324
+
325
+ runs: Dict[int, Run] = {}
320
326
 
321
327
  while not app_state_tracker.interrupt:
322
328
  sleep_duration: int = 0
@@ -330,9 +336,33 @@ def _start_client_internal(
330
336
  ) as conn:
331
337
  receive, send, create_node, delete_node, get_run = conn
332
338
 
333
- # Register node
334
- if create_node is not None:
335
- create_node() # pylint: disable=not-callable
339
+ # Register node when connecting the first time
340
+ if node_state is None:
341
+ if create_node is None:
342
+ if transport not in ["grpc-bidi", None]:
343
+ raise NotImplementedError(
344
+ "All transports except `grpc-bidi` require "
345
+ "an implementation for `create_node()`.'"
346
+ )
347
+ # gRPC-bidi doesn't have the concept of node_id,
348
+ # so we set it to -1
349
+ node_state = NodeState(
350
+ node_id=-1,
351
+ node_config={},
352
+ partition_id=None,
353
+ )
354
+ else:
355
+ # Call create_node fn to register node
356
+ node_id: Optional[int] = ( # pylint: disable=assignment-from-none
357
+ create_node()
358
+ ) # pylint: disable=not-callable
359
+ if node_id is None:
360
+ raise ValueError("Node registration failed")
361
+ node_state = NodeState(
362
+ node_id=node_id,
363
+ node_config=node_config,
364
+ partition_id=None,
365
+ )
336
366
 
337
367
  app_state_tracker.register_signal_handler()
338
368
  while not app_state_tracker.interrupt:
@@ -366,15 +396,17 @@ def _start_client_internal(
366
396
 
367
397
  # Get run info
368
398
  run_id = message.metadata.run_id
369
- if run_id not in run_info:
399
+ if run_id not in runs:
370
400
  if get_run is not None:
371
- run_info[run_id] = get_run(run_id)
401
+ runs[run_id] = get_run(run_id)
372
402
  # If get_run is None, i.e., in grpc-bidi mode
373
403
  else:
374
- run_info[run_id] = Run(run_id, "", "", {})
404
+ runs[run_id] = Run(run_id, "", "", {})
375
405
 
376
406
  # Register context for this run
377
- node_state.register_context(run_id=run_id)
407
+ node_state.register_context(
408
+ run_id=run_id, run=runs[run_id], flwr_dir=flwr_dir
409
+ )
378
410
 
379
411
  # Retrieve context for this run
380
412
  context = node_state.retrieve_context(run_id=run_id)
@@ -388,7 +420,7 @@ def _start_client_internal(
388
420
  # Handle app loading and task message
389
421
  try:
390
422
  # Load ClientApp instance
391
- run: Run = run_info[run_id]
423
+ run: Run = runs[run_id]
392
424
  client_app: ClientApp = load_client_app_fn(
393
425
  run.fab_id, run.fab_version
394
426
  )
@@ -574,7 +606,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
574
606
  Tuple[
575
607
  Callable[[], Optional[Message]],
576
608
  Callable[[Message], None],
577
- Optional[Callable[[], None]],
609
+ Optional[Callable[[], Optional[int]]],
578
610
  Optional[Callable[[], None]],
579
611
  Optional[Callable[[int], Run]],
580
612
  ]
@@ -44,7 +44,7 @@ def grpc_adapter( # pylint: disable=R0913
44
44
  Tuple[
45
45
  Callable[[], Optional[Message]],
46
46
  Callable[[Message], None],
47
- Optional[Callable[[], None]],
47
+ Optional[Callable[[], Optional[int]]],
48
48
  Optional[Callable[[], None]],
49
49
  Optional[Callable[[int], Run]],
50
50
  ]
@@ -72,7 +72,7 @@ def grpc_connection( # pylint: disable=R0913, R0915
72
72
  Tuple[
73
73
  Callable[[], Optional[Message]],
74
74
  Callable[[Message], None],
75
- Optional[Callable[[], None]],
75
+ Optional[Callable[[], Optional[int]]],
76
76
  Optional[Callable[[], None]],
77
77
  Optional[Callable[[int], Run]],
78
78
  ]
@@ -79,7 +79,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
79
79
  Tuple[
80
80
  Callable[[], Optional[Message]],
81
81
  Callable[[Message], None],
82
- Optional[Callable[[], None]],
82
+ Optional[Callable[[], Optional[int]]],
83
83
  Optional[Callable[[], None]],
84
84
  Optional[Callable[[int], Run]],
85
85
  ]
@@ -176,7 +176,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
176
176
  if not ping_stop_event.is_set():
177
177
  ping_stop_event.wait(next_interval)
178
178
 
179
- def create_node() -> None:
179
+ def create_node() -> Optional[int]:
180
180
  """Set create_node."""
181
181
  # Call FleetAPI
182
182
  create_node_request = CreateNodeRequest(ping_interval=PING_DEFAULT_INTERVAL)
@@ -189,6 +189,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
189
189
  nonlocal node, ping_thread
190
190
  node = cast(Node, create_node_response.node)
191
191
  ping_thread = start_ping_loop(ping, ping_stop_event)
192
+ return node.node_id
192
193
 
193
194
  def delete_node() -> None:
194
195
  """Set delete_node."""
flwr/client/node_state.py CHANGED
@@ -15,30 +15,58 @@
15
15
  """Node state."""
16
16
 
17
17
 
18
- from typing import Any, Dict, Optional
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Dict, Optional
19
21
 
20
22
  from flwr.common import Context, RecordSet
23
+ from flwr.common.config import get_fused_config
24
+ from flwr.common.typing import Run
25
+
26
+
27
+ @dataclass()
28
+ class RunInfo:
29
+ """Contains the Context and initial run_config of a Run."""
30
+
31
+ context: Context
32
+ initial_run_config: Dict[str, str]
21
33
 
22
34
 
23
35
  class NodeState:
24
36
  """State of a node where client nodes execute runs."""
25
37
 
26
- def __init__(self, partition_id: Optional[int]) -> None:
27
- self._meta: Dict[str, Any] = {} # holds metadata about the node
28
- self.run_contexts: Dict[int, Context] = {}
38
+ def __init__(
39
+ self, node_id: int, node_config: Dict[str, str], partition_id: Optional[int]
40
+ ) -> None:
41
+ self.node_id = node_id
42
+ self.node_config = node_config
43
+ self.run_infos: Dict[int, RunInfo] = {}
29
44
  self._partition_id = partition_id
30
45
 
31
- def register_context(self, run_id: int) -> None:
46
+ def register_context(
47
+ self,
48
+ run_id: int,
49
+ run: Optional[Run] = None,
50
+ flwr_dir: Optional[Path] = None,
51
+ ) -> None:
32
52
  """Register new run context for this node."""
33
- if run_id not in self.run_contexts:
34
- self.run_contexts[run_id] = Context(
35
- state=RecordSet(), run_config={}, partition_id=self._partition_id
53
+ if run_id not in self.run_infos:
54
+ initial_run_config = get_fused_config(run, flwr_dir) if run else {}
55
+ self.run_infos[run_id] = RunInfo(
56
+ initial_run_config=initial_run_config,
57
+ context=Context(
58
+ node_id=self.node_id,
59
+ node_config=self.node_config,
60
+ state=RecordSet(),
61
+ run_config=initial_run_config.copy(),
62
+ partition_id=self._partition_id,
63
+ ),
36
64
  )
37
65
 
38
66
  def retrieve_context(self, run_id: int) -> Context:
39
67
  """Get run context given a run_id."""
40
- if run_id in self.run_contexts:
41
- return self.run_contexts[run_id]
68
+ if run_id in self.run_infos:
69
+ return self.run_infos[run_id].context
42
70
 
43
71
  raise RuntimeError(
44
72
  f"Context for run_id={run_id} doesn't exist."
@@ -48,4 +76,9 @@ class NodeState:
48
76
 
49
77
  def update_context(self, run_id: int, context: Context) -> None:
50
78
  """Update run context."""
51
- self.run_contexts[run_id] = context
79
+ if context.run_config != self.run_infos[run_id].initial_run_config:
80
+ raise ValueError(
81
+ "The `run_config` field of the `Context` object cannot be "
82
+ f"modified (run_id: {run_id})."
83
+ )
84
+ self.run_infos[run_id].context = context
@@ -41,7 +41,7 @@ def test_multirun_in_node_state() -> None:
41
41
  expected_values = {0: "1", 1: "1" * 3, 2: "1" * 2, 3: "1", 5: "1"}
42
42
 
43
43
  # NodeState
44
- node_state = NodeState(partition_id=None)
44
+ node_state = NodeState(node_id=0, node_config={}, partition_id=None)
45
45
 
46
46
  for task in tasks:
47
47
  run_id = task.run_id
@@ -59,7 +59,8 @@ def test_multirun_in_node_state() -> None:
59
59
  node_state.update_context(run_id=run_id, context=updated_state)
60
60
 
61
61
  # Verify values
62
- for run_id, context in node_state.run_contexts.items():
62
+ for run_id, run_info in node_state.run_infos.items():
63
63
  assert (
64
- context.state.configs_records["counter"]["count"] == expected_values[run_id]
64
+ run_info.context.state.configs_records["counter"]["count"]
65
+ == expected_values[run_id]
65
66
  )
@@ -90,7 +90,7 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
90
90
  Tuple[
91
91
  Callable[[], Optional[Message]],
92
92
  Callable[[Message], None],
93
- Optional[Callable[[], None]],
93
+ Optional[Callable[[], Optional[int]]],
94
94
  Optional[Callable[[], None]],
95
95
  Optional[Callable[[int], Run]],
96
96
  ]
@@ -237,19 +237,20 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
237
237
  if not ping_stop_event.is_set():
238
238
  ping_stop_event.wait(next_interval)
239
239
 
240
- def create_node() -> None:
240
+ def create_node() -> Optional[int]:
241
241
  """Set create_node."""
242
242
  req = CreateNodeRequest(ping_interval=PING_DEFAULT_INTERVAL)
243
243
 
244
244
  # Send the request
245
245
  res = _request(req, CreateNodeResponse, PATH_CREATE_NODE)
246
246
  if res is None:
247
- return
247
+ return None
248
248
 
249
249
  # Remember the node and the ping-loop thread
250
250
  nonlocal node, ping_thread
251
251
  node = res.node
252
252
  ping_thread = start_ping_loop(ping, ping_stop_event)
253
+ return node.node_id
253
254
 
254
255
  def delete_node() -> None:
255
256
  """Set delete_node."""
@@ -29,7 +29,12 @@ from cryptography.hazmat.primitives.serialization import (
29
29
 
30
30
  from flwr.client.client_app import ClientApp, LoadClientAppError
31
31
  from flwr.common import EventType, event
32
- from flwr.common.config import get_flwr_dir, get_project_config, get_project_dir
32
+ from flwr.common.config import (
33
+ get_flwr_dir,
34
+ get_project_config,
35
+ get_project_dir,
36
+ parse_config_args,
37
+ )
33
38
  from flwr.common.constant import (
34
39
  TRANSPORT_TYPE_GRPC_ADAPTER,
35
40
  TRANSPORT_TYPE_GRPC_RERE,
@@ -67,7 +72,8 @@ def run_supernode() -> None:
67
72
  authentication_keys=authentication_keys,
68
73
  max_retries=args.max_retries,
69
74
  max_wait_time=args.max_wait_time,
70
- partition_id=args.partition_id,
75
+ node_config=parse_config_args(args.node_config),
76
+ flwr_dir=get_flwr_dir(args.flwr_dir),
71
77
  )
72
78
 
73
79
  # Graceful shutdown
@@ -92,6 +98,7 @@ def run_client_app() -> None:
92
98
 
93
99
  _start_client_internal(
94
100
  server_address=args.superlink,
101
+ node_config=parse_config_args(args.node_config),
95
102
  load_client_app_fn=load_fn,
96
103
  transport=args.transport,
97
104
  root_certificates=root_certificates,
@@ -388,11 +395,11 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
388
395
  help="The SuperNode's public key (as a path str) to enable authentication.",
389
396
  )
390
397
  parser.add_argument(
391
- "--partition-id",
392
- type=int,
393
- help="The data partition index associated with this SuperNode. Better suited "
394
- "for prototyping purposes where a SuperNode might only load a fraction of an "
395
- "artificially partitioned dataset (e.g. using `flwr-datasets`)",
398
+ "--node-config",
399
+ type=str,
400
+ help="A comma separated list of key/value pairs (separated by `=`) to "
401
+ "configure the SuperNode. "
402
+ "E.g. --node-config 'key1=\"value1\",partition-id=0,num-partitions=100'",
396
403
  )
397
404
 
398
405
 
flwr/common/config.py CHANGED
@@ -121,16 +121,16 @@ def flatten_dict(raw_dict: Dict[str, Any], parent_key: str = "") -> Dict[str, st
121
121
 
122
122
 
123
123
  def parse_config_args(
124
- config_overrides: Optional[str],
124
+ config: Optional[str],
125
125
  separator: str = ",",
126
126
  ) -> Dict[str, str]:
127
127
  """Parse separator separated list of key-value pairs separated by '='."""
128
128
  overrides: Dict[str, str] = {}
129
129
 
130
- if config_overrides is None:
130
+ if config is None:
131
131
  return overrides
132
132
 
133
- overrides_list = config_overrides.split(separator)
133
+ overrides_list = config.split(separator)
134
134
  if (
135
135
  len(overrides_list) == 1
136
136
  and "=" not in overrides_list
flwr/common/context.py CHANGED
@@ -27,6 +27,11 @@ class Context:
27
27
 
28
28
  Parameters
29
29
  ----------
30
+ node_id : int
31
+ The ID that identifies the node.
32
+ node_config : Dict[str, str]
33
+ A config (key/value mapping) unique to the node and independent of the
34
+ `run_config`. This config persists across all runs this node participates in.
30
35
  state : RecordSet
31
36
  Holds records added by the entity in a given run and that will stay local.
32
37
  This means that the data it holds will never leave the system it's running from.
@@ -44,16 +49,22 @@ class Context:
44
49
  simulation or proto typing setups.
45
50
  """
46
51
 
52
+ node_id: int
53
+ node_config: Dict[str, str]
47
54
  state: RecordSet
48
- partition_id: Optional[int]
49
55
  run_config: Dict[str, str]
56
+ partition_id: Optional[int]
50
57
 
51
- def __init__(
58
+ def __init__( # pylint: disable=too-many-arguments
52
59
  self,
60
+ node_id: int,
61
+ node_config: Dict[str, str],
53
62
  state: RecordSet,
54
63
  run_config: Dict[str, str],
55
64
  partition_id: Optional[int] = None,
56
65
  ) -> None:
66
+ self.node_id = node_id
67
+ self.node_config = node_config
57
68
  self.state = state
58
69
  self.run_config = run_config
59
70
  self.partition_id = partition_id
flwr/common/logger.py CHANGED
@@ -197,6 +197,31 @@ def warn_deprecated_feature(name: str) -> None:
197
197
  )
198
198
 
199
199
 
200
+ def warn_deprecated_feature_with_example(
201
+ deprecation_message: str, example_message: str, code_example: str
202
+ ) -> None:
203
+ """Warn if a feature is deprecated and show code example."""
204
+ log(
205
+ WARN,
206
+ """DEPRECATED FEATURE: %s
207
+
208
+ Check the following `FEATURE UPDATE` warning message for the preferred
209
+ new mechanism to use this feature in Flower.
210
+ """,
211
+ deprecation_message,
212
+ )
213
+ log(
214
+ WARN,
215
+ """FEATURE UPDATE: %s
216
+ ------------------------------------------------------------
217
+ %s
218
+ ------------------------------------------------------------
219
+ """,
220
+ example_message,
221
+ code_example,
222
+ )
223
+
224
+
200
225
  def warn_unsupported_feature(name: str) -> None:
201
226
  """Warn the user when they use an unsupported feature."""
202
227
  log(
flwr/server/__init__.py CHANGED
@@ -28,6 +28,7 @@ from .run_serverapp import run_server_app as run_server_app
28
28
  from .server import Server as Server
29
29
  from .server_app import ServerApp as ServerApp
30
30
  from .server_config import ServerConfig as ServerConfig
31
+ from .serverapp_components import ServerAppComponents as ServerAppComponents
31
32
 
32
33
  __all__ = [
33
34
  "ClientManager",
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "LegacyContext",
37
38
  "Server",
38
39
  "ServerApp",
40
+ "ServerAppComponents",
39
41
  "ServerConfig",
40
42
  "SimpleClientManager",
41
43
  "run_server_app",
@@ -52,4 +52,4 @@ class LegacyContext(Context):
52
52
  self.strategy = strategy
53
53
  self.client_manager = client_manager
54
54
  self.history = History()
55
- super().__init__(state, run_config={})
55
+ super().__init__(node_id=0, node_config={}, state=state, run_config={})
@@ -78,7 +78,9 @@ def run(
78
78
  server_app = _load()
79
79
 
80
80
  # Initialize Context
81
- context = Context(state=RecordSet(), run_config=server_app_run_config)
81
+ context = Context(
82
+ node_id=0, node_config={}, state=RecordSet(), run_config=server_app_run_config
83
+ )
82
84
 
83
85
  # Call ServerApp
84
86
  server_app(driver=driver, context=context)