flwr-nightly 1.10.0.dev20240721__py3-none-any.whl → 1.10.0.dev20240723__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 (74) hide show
  1. flwr/cli/config_utils.py +20 -18
  2. flwr/cli/new/new.py +1 -1
  3. flwr/cli/new/templates/app/code/{client.hf.py.tpl → client.huggingface.py.tpl} +7 -5
  4. flwr/cli/new/templates/app/code/client.mlx.py.tpl +28 -10
  5. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +7 -5
  6. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +2 -2
  7. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +17 -7
  8. flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +20 -17
  9. flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +5 -3
  10. flwr/cli/new/templates/app/code/{server.hf.py.tpl → server.huggingface.py.tpl} +2 -1
  11. flwr/cli/new/templates/app/code/server.jax.py.tpl +2 -1
  12. flwr/cli/new/templates/app/code/server.mlx.py.tpl +2 -1
  13. flwr/cli/new/templates/app/code/server.numpy.py.tpl +2 -1
  14. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +1 -1
  15. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +2 -1
  16. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +1 -1
  17. flwr/cli/new/templates/app/code/{task.hf.py.tpl → task.huggingface.py.tpl} +13 -1
  18. flwr/cli/new/templates/app/code/task.mlx.py.tpl +14 -1
  19. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +13 -2
  20. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +13 -1
  21. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  22. flwr/cli/new/templates/app/{pyproject.hf.toml.tpl → pyproject.huggingface.toml.tpl} +2 -2
  23. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  24. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +6 -6
  25. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +2 -2
  27. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +4 -4
  29. flwr/cli/run/run.py +35 -28
  30. flwr/client/app.py +3 -3
  31. flwr/client/grpc_rere_client/connection.py +6 -2
  32. flwr/client/node_state.py +3 -3
  33. flwr/client/rest_client/connection.py +6 -2
  34. flwr/client/supernode/app.py +12 -43
  35. flwr/common/config.py +23 -17
  36. flwr/common/context.py +7 -7
  37. flwr/common/object_ref.py +84 -21
  38. flwr/common/serde.py +45 -0
  39. flwr/common/telemetry.py +17 -0
  40. flwr/common/typing.py +5 -1
  41. flwr/proto/common_pb2.py +13 -1
  42. flwr/proto/common_pb2.pyi +114 -0
  43. flwr/proto/driver_pb2.py +22 -21
  44. flwr/proto/driver_pb2.pyi +7 -4
  45. flwr/proto/exec_pb2.py +18 -13
  46. flwr/proto/exec_pb2.pyi +27 -5
  47. flwr/proto/run_pb2.py +10 -9
  48. flwr/proto/run_pb2.pyi +7 -4
  49. flwr/proto/task_pb2.py +7 -8
  50. flwr/server/compat/legacy_context.py +5 -4
  51. flwr/server/driver/grpc_driver.py +6 -2
  52. flwr/server/run_serverapp.py +3 -5
  53. flwr/server/superlink/driver/driver_servicer.py +14 -3
  54. flwr/server/superlink/fleet/message_handler/message_handler.py +13 -2
  55. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  56. flwr/server/superlink/fleet/vce/vce_api.py +4 -4
  57. flwr/server/superlink/state/in_memory_state.py +2 -2
  58. flwr/server/superlink/state/sqlite_state.py +2 -2
  59. flwr/server/superlink/state/state.py +3 -3
  60. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +18 -2
  61. flwr/simulation/__init__.py +1 -1
  62. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  63. flwr/simulation/run_simulation.py +39 -11
  64. flwr/superexec/app.py +4 -5
  65. flwr/superexec/deployment.py +19 -8
  66. flwr/superexec/exec_grpc.py +3 -2
  67. flwr/superexec/exec_servicer.py +3 -1
  68. flwr/superexec/executor.py +10 -5
  69. flwr/superexec/simulation.py +41 -15
  70. {flwr_nightly-1.10.0.dev20240721.dist-info → flwr_nightly-1.10.0.dev20240723.dist-info}/METADATA +1 -1
  71. {flwr_nightly-1.10.0.dev20240721.dist-info → flwr_nightly-1.10.0.dev20240723.dist-info}/RECORD +74 -74
  72. {flwr_nightly-1.10.0.dev20240721.dist-info → flwr_nightly-1.10.0.dev20240723.dist-info}/LICENSE +0 -0
  73. {flwr_nightly-1.10.0.dev20240721.dist-info → flwr_nightly-1.10.0.dev20240723.dist-info}/WHEEL +0 -0
  74. {flwr_nightly-1.10.0.dev20240721.dist-info → flwr_nightly-1.10.0.dev20240723.dist-info}/entry_points.txt +0 -0
@@ -25,12 +25,12 @@ serverapp = "$import_name.server_app:app"
25
25
  clientapp = "$import_name.client_app:app"
26
26
 
27
27
  [tool.flwr.app.config]
28
- num-server-rounds = "3"
29
- local-epochs = "1"
30
- num-layers = "2"
31
- hidden-dim = "32"
32
- batch-size = "256"
33
- lr = "0.1"
28
+ num-server-rounds = 3
29
+ local-epochs = 1
30
+ num-layers = 2
31
+ hidden-dim = 32
32
+ batch-size = 256
33
+ lr = 0.1
34
34
 
35
35
  [tool.flwr.federations]
36
36
  default = "localhost"
@@ -23,7 +23,7 @@ serverapp = "$import_name.server_app:app"
23
23
  clientapp = "$import_name.client_app:app"
24
24
 
25
25
  [tool.flwr.app.config]
26
- num-server-rounds = "3"
26
+ num-server-rounds = 3
27
27
 
28
28
  [tool.flwr.federations]
29
29
  default = "localhost"
@@ -25,8 +25,8 @@ serverapp = "$import_name.server_app:app"
25
25
  clientapp = "$import_name.client_app:app"
26
26
 
27
27
  [tool.flwr.app.config]
28
- num-server-rounds = "3"
29
- local-epochs = "1"
28
+ num-server-rounds = 3
29
+ local-epochs = 1
30
30
 
31
31
  [tool.flwr.federations]
32
32
  default = "localhost"
@@ -24,7 +24,7 @@ serverapp = "$import_name.server_app:app"
24
24
  clientapp = "$import_name.client_app:app"
25
25
 
26
26
  [tool.flwr.app.config]
27
- num-server-rounds = "3"
27
+ num-server-rounds = 3
28
28
 
29
29
  [tool.flwr.federations]
30
30
  default = "localhost"
@@ -24,10 +24,10 @@ serverapp = "$import_name.server_app:app"
24
24
  clientapp = "$import_name.client_app:app"
25
25
 
26
26
  [tool.flwr.app.config]
27
- num-server-rounds = "3"
28
- local-epochs = "1"
29
- batch-size = "32"
30
- verbose = "" # Empty string means False
27
+ num-server-rounds = 3
28
+ local-epochs = 1
29
+ batch-size = 32
30
+ verbose = false
31
31
 
32
32
  [tool.flwr.federations]
33
33
  default = "localhost"
flwr/cli/run/run.py CHANGED
@@ -25,36 +25,40 @@ from typing_extensions import Annotated
25
25
 
26
26
  from flwr.cli.build import build
27
27
  from flwr.cli.config_utils import load_and_validate
28
- from flwr.common.config import parse_config_args
28
+ from flwr.common.config import flatten_dict, parse_config_args
29
29
  from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
30
30
  from flwr.common.logger import log
31
+ from flwr.common.serde import user_config_to_proto
31
32
  from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
32
33
  from flwr.proto.exec_pb2_grpc import ExecStub
33
34
 
34
35
 
35
36
  # pylint: disable-next=too-many-locals
36
37
  def run(
37
- directory: Annotated[
38
+ app_dir: Annotated[
38
39
  Path,
39
- typer.Argument(help="Path of the Flower project to run"),
40
+ typer.Argument(help="Path of the Flower project to run."),
40
41
  ] = Path("."),
41
- federation_name: Annotated[
42
+ federation: Annotated[
42
43
  Optional[str],
43
- typer.Argument(help="Name of the federation to run the app on"),
44
+ typer.Argument(help="Name of the federation to run the app on."),
44
45
  ] = None,
45
46
  config_overrides: Annotated[
46
47
  Optional[List[str]],
47
48
  typer.Option(
48
49
  "--run-config",
49
50
  "-c",
50
- help="Override configuration key-value pairs",
51
+ help="Override configuration key-value pairs, should be of the format:\n\n"
52
+ "`--run-config key1=value1,key2=value2 --run-config key3=value3`\n\n"
53
+ "Note that `key1`, `key2`, and `key3` in this example need to exist "
54
+ "inside the `pyproject.toml` in order to be properly overriden.",
51
55
  ),
52
56
  ] = None,
53
57
  ) -> None:
54
58
  """Run Flower project."""
55
59
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
56
60
 
57
- pyproject_path = directory / "pyproject.toml" if directory else None
61
+ pyproject_path = app_dir / "pyproject.toml" if app_dir else None
58
62
  config, errors, warnings = load_and_validate(path=pyproject_path)
59
63
 
60
64
  if config is None:
@@ -77,11 +81,9 @@ def run(
77
81
 
78
82
  typer.secho("Success", fg=typer.colors.GREEN)
79
83
 
80
- federation_name = federation_name or config["tool"]["flwr"]["federations"].get(
81
- "default"
82
- )
84
+ federation = federation or config["tool"]["flwr"]["federations"].get("default")
83
85
 
84
- if federation_name is None:
86
+ if federation is None:
85
87
  typer.secho(
86
88
  "❌ No federation name was provided and the project's `pyproject.toml` "
87
89
  "doesn't declare a default federation (with a SuperExec address or an "
@@ -92,13 +94,13 @@ def run(
92
94
  raise typer.Exit(code=1)
93
95
 
94
96
  # Validate the federation exists in the configuration
95
- federation = config["tool"]["flwr"]["federations"].get(federation_name)
96
- if federation is None:
97
+ federation_config = config["tool"]["flwr"]["federations"].get(federation)
98
+ if federation_config is None:
97
99
  available_feds = {
98
100
  fed for fed in config["tool"]["flwr"]["federations"] if fed != "default"
99
101
  }
100
102
  typer.secho(
101
- f"❌ There is no `{federation_name}` federation declared in the "
103
+ f"❌ There is no `{federation}` federation declared in "
102
104
  "`pyproject.toml`.\n The following federations were found:\n\n"
103
105
  + "\n".join(available_feds),
104
106
  fg=typer.colors.RED,
@@ -106,15 +108,15 @@ def run(
106
108
  )
107
109
  raise typer.Exit(code=1)
108
110
 
109
- if "address" in federation:
110
- _run_with_superexec(federation, directory, config_overrides)
111
+ if "address" in federation_config:
112
+ _run_with_superexec(federation_config, app_dir, config_overrides)
111
113
  else:
112
- _run_without_superexec(directory, federation, federation_name, config_overrides)
114
+ _run_without_superexec(app_dir, federation_config, federation, config_overrides)
113
115
 
114
116
 
115
117
  def _run_with_superexec(
116
- federation: Dict[str, str],
117
- directory: Optional[Path],
118
+ federation_config: Dict[str, Any],
119
+ app_dir: Optional[Path],
118
120
  config_overrides: Optional[List[str]],
119
121
  ) -> None:
120
122
 
@@ -122,8 +124,8 @@ def _run_with_superexec(
122
124
  """Log channel connectivity."""
123
125
  log(DEBUG, channel_connectivity)
124
126
 
125
- insecure_str = federation.get("insecure")
126
- if root_certificates := federation.get("root-certificates"):
127
+ insecure_str = federation_config.get("insecure")
128
+ if root_certificates := federation_config.get("root-certificates"):
127
129
  root_certificates_bytes = Path(root_certificates).read_bytes()
128
130
  if insecure := bool(insecure_str):
129
131
  typer.secho(
@@ -151,7 +153,7 @@ def _run_with_superexec(
151
153
  raise typer.Exit(code=1)
152
154
 
153
155
  channel = create_channel(
154
- server_address=federation["address"],
156
+ server_address=federation_config["address"],
155
157
  insecure=insecure,
156
158
  root_certificates=root_certificates_bytes,
157
159
  max_message_length=GRPC_MAX_MESSAGE_LENGTH,
@@ -160,11 +162,16 @@ def _run_with_superexec(
160
162
  channel.subscribe(on_channel_state_change)
161
163
  stub = ExecStub(channel)
162
164
 
163
- fab_path = build(directory)
165
+ fab_path = build(app_dir)
164
166
 
165
167
  req = StartRunRequest(
166
168
  fab_file=Path(fab_path).read_bytes(),
167
- override_config=parse_config_args(config_overrides, separator=","),
169
+ override_config=user_config_to_proto(
170
+ parse_config_args(config_overrides, separator=",")
171
+ ),
172
+ federation_config=user_config_to_proto(
173
+ flatten_dict(federation_config.get("options"))
174
+ ),
168
175
  )
169
176
  res = stub.StartRun(req)
170
177
  typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
@@ -172,18 +179,18 @@ def _run_with_superexec(
172
179
 
173
180
  def _run_without_superexec(
174
181
  app_path: Optional[Path],
175
- federation: Dict[str, Any],
176
- federation_name: str,
182
+ federation_config: Dict[str, Any],
183
+ federation: str,
177
184
  config_overrides: Optional[List[str]],
178
185
  ) -> None:
179
186
  try:
180
- num_supernodes = federation["options"]["num-supernodes"]
187
+ num_supernodes = federation_config["options"]["num-supernodes"]
181
188
  except KeyError as err:
182
189
  typer.secho(
183
190
  "❌ The project's `pyproject.toml` needs to declare the number of"
184
191
  " SuperNodes in the simulation. To simulate 10 SuperNodes,"
185
192
  " use the following notation:\n\n"
186
- f"[tool.flwr.federations.{federation_name}]\n"
193
+ f"[tool.flwr.federations.{federation}]\n"
187
194
  "options.num-supernodes = 10\n",
188
195
  fg=typer.colors.RED,
189
196
  bold=True,
flwr/client/app.py CHANGED
@@ -42,7 +42,7 @@ from flwr.common.constant import (
42
42
  from flwr.common.logger import log, warn_deprecated_feature
43
43
  from flwr.common.message import Error
44
44
  from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
45
- from flwr.common.typing import Run
45
+ from flwr.common.typing import Run, UserConfig
46
46
 
47
47
  from .grpc_adapter_client.connection import grpc_adapter
48
48
  from .grpc_client.connection import grpc_connection
@@ -182,7 +182,7 @@ def start_client(
182
182
  def _start_client_internal(
183
183
  *,
184
184
  server_address: str,
185
- node_config: Dict[str, str],
185
+ node_config: UserConfig,
186
186
  load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
187
187
  client_fn: Optional[ClientFnExt] = None,
188
188
  client: Optional[Client] = None,
@@ -205,7 +205,7 @@ def _start_client_internal(
205
205
  The IPv4 or IPv6 address of the server. If the Flower
206
206
  server runs on the same machine on port 8080, then `server_address`
207
207
  would be `"[::]:8080"`.
208
- node_config: Dict[str, str]
208
+ node_config: UserConfig
209
209
  The configuration of the node.
210
210
  load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
211
211
  A function that can be used to load a `ClientApp` instance.
@@ -40,7 +40,11 @@ from flwr.common.grpc import create_channel
40
40
  from flwr.common.logger import log
41
41
  from flwr.common.message import Message, Metadata
42
42
  from flwr.common.retry_invoker import RetryInvoker
43
- from flwr.common.serde import message_from_taskins, message_to_taskres
43
+ from flwr.common.serde import (
44
+ message_from_taskins,
45
+ message_to_taskres,
46
+ user_config_from_proto,
47
+ )
44
48
  from flwr.common.typing import Run
45
49
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
46
50
  CreateNodeRequest,
@@ -281,7 +285,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
281
285
  run_id,
282
286
  get_run_response.run.fab_id,
283
287
  get_run_response.run.fab_version,
284
- dict(get_run_response.run.override_config.items()),
288
+ user_config_from_proto(get_run_response.run.override_config),
285
289
  )
286
290
 
287
291
  try:
flwr/client/node_state.py CHANGED
@@ -21,7 +21,7 @@ from typing import Dict, Optional
21
21
 
22
22
  from flwr.common import Context, RecordSet
23
23
  from flwr.common.config import get_fused_config, get_fused_config_from_dir
24
- from flwr.common.typing import Run
24
+ from flwr.common.typing import Run, UserConfig
25
25
 
26
26
 
27
27
  @dataclass()
@@ -29,7 +29,7 @@ class RunInfo:
29
29
  """Contains the Context and initial run_config of a Run."""
30
30
 
31
31
  context: Context
32
- initial_run_config: Dict[str, str]
32
+ initial_run_config: UserConfig
33
33
 
34
34
 
35
35
  class NodeState:
@@ -38,7 +38,7 @@ class NodeState:
38
38
  def __init__(
39
39
  self,
40
40
  node_id: int,
41
- node_config: Dict[str, str],
41
+ node_config: UserConfig,
42
42
  ) -> None:
43
43
  self.node_id = node_id
44
44
  self.node_config = node_config
@@ -40,7 +40,11 @@ from flwr.common.constant import (
40
40
  from flwr.common.logger import log
41
41
  from flwr.common.message import Message, Metadata
42
42
  from flwr.common.retry_invoker import RetryInvoker
43
- from flwr.common.serde import message_from_taskins, message_to_taskres
43
+ from flwr.common.serde import (
44
+ message_from_taskins,
45
+ message_to_taskres,
46
+ user_config_from_proto,
47
+ )
44
48
  from flwr.common.typing import Run
45
49
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
46
50
  CreateNodeRequest,
@@ -359,7 +363,7 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
359
363
  run_id,
360
364
  res.run.fab_id,
361
365
  res.run.fab_version,
362
- dict(res.run.override_config.items()),
366
+ user_config_from_proto(res.run.override_config),
363
367
  )
364
368
 
365
369
  try:
@@ -62,8 +62,8 @@ def run_supernode() -> None:
62
62
  root_certificates = _get_certificates(args)
63
63
  load_fn = _get_load_client_app_fn(
64
64
  default_app_ref=getattr(args, "client-app"),
65
- dir_arg=args.dir,
66
- flwr_dir_arg=args.flwr_dir,
65
+ project_dir=args.dir,
66
+ flwr_dir=args.flwr_dir,
67
67
  multi_app=True,
68
68
  )
69
69
  authentication_keys = _try_setup_client_authentication(args)
@@ -100,7 +100,7 @@ def run_client_app() -> None:
100
100
  root_certificates = _get_certificates(args)
101
101
  load_fn = _get_load_client_app_fn(
102
102
  default_app_ref=getattr(args, "client-app"),
103
- dir_arg=args.dir,
103
+ project_dir=args.dir,
104
104
  multi_app=False,
105
105
  )
106
106
  authentication_keys = _try_setup_client_authentication(args)
@@ -176,9 +176,9 @@ def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
176
176
 
177
177
  def _get_load_client_app_fn(
178
178
  default_app_ref: str,
179
- dir_arg: str,
179
+ project_dir: str,
180
180
  multi_app: bool,
181
- flwr_dir_arg: Optional[str] = None,
181
+ flwr_dir: Optional[str] = None,
182
182
  ) -> Callable[[str, str], ClientApp]:
183
183
  """Get the load_client_app_fn function.
184
184
 
@@ -189,38 +189,21 @@ def _get_load_client_app_fn(
189
189
  If `multi_app` is False, it ignores `fab_id` and `fab_version` and
190
190
  loads a default ClientApp.
191
191
  """
192
- # Find the Flower directory containing Flower Apps (only for multi-app)
193
- if not multi_app:
194
- flwr_dir = Path("")
195
- else:
196
- if flwr_dir_arg is None:
197
- flwr_dir = get_flwr_dir()
198
- else:
199
- flwr_dir = Path(flwr_dir_arg).absolute()
200
-
201
- inserted_path = None
202
-
203
192
  if not multi_app:
204
193
  log(
205
194
  DEBUG,
206
195
  "Flower SuperNode will load and validate ClientApp `%s`",
207
196
  default_app_ref,
208
197
  )
209
- # Insert sys.path
210
- dir_path = Path(dir_arg).absolute()
211
- sys.path.insert(0, str(dir_path))
212
- inserted_path = str(dir_path)
213
198
 
214
- valid, error_msg = validate(default_app_ref)
199
+ valid, error_msg = validate(default_app_ref, project_dir=project_dir)
215
200
  if not valid and error_msg:
216
201
  raise LoadClientAppError(error_msg) from None
217
202
 
218
203
  def _load(fab_id: str, fab_version: str) -> ClientApp:
204
+ runtime_project_dir = Path(project_dir).absolute()
219
205
  # If multi-app feature is disabled
220
206
  if not multi_app:
221
- # Get sys path to be inserted
222
- dir_path = Path(dir_arg).absolute()
223
-
224
207
  # Set app reference
225
208
  client_app_ref = default_app_ref
226
209
  # If multi-app feature is enabled but the fab id is not specified
@@ -231,43 +214,29 @@ def _get_load_client_app_fn(
231
214
  ) from None
232
215
 
233
216
  log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.")
234
- # Get sys path to be inserted
235
- dir_path = Path(dir_arg).absolute()
236
217
 
237
218
  # Set app reference
238
219
  client_app_ref = default_app_ref
239
220
  # If multi-app feature is enabled
240
221
  else:
241
222
  try:
242
- project_dir = get_project_dir(fab_id, fab_version, flwr_dir)
243
- config = get_project_config(project_dir)
223
+ runtime_project_dir = get_project_dir(
224
+ fab_id, fab_version, get_flwr_dir(flwr_dir)
225
+ )
226
+ config = get_project_config(runtime_project_dir)
244
227
  except Exception as e:
245
228
  raise LoadClientAppError("Failed to load ClientApp") from e
246
229
 
247
- # Get sys path to be inserted
248
- dir_path = Path(project_dir).absolute()
249
-
250
230
  # Set app reference
251
231
  client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
252
232
 
253
- # Set sys.path
254
- nonlocal inserted_path
255
- if inserted_path != str(dir_path):
256
- # Remove the previously inserted path
257
- if inserted_path is not None:
258
- sys.path.remove(inserted_path)
259
- # Insert the new path
260
- sys.path.insert(0, str(dir_path))
261
-
262
- inserted_path = str(dir_path)
263
-
264
233
  # Load ClientApp
265
234
  log(
266
235
  DEBUG,
267
236
  "Loading ClientApp `%s`",
268
237
  client_app_ref,
269
238
  )
270
- client_app = load_app(client_app_ref, LoadClientAppError, dir_path)
239
+ client_app = load_app(client_app_ref, LoadClientAppError, runtime_project_dir)
271
240
 
272
241
  if not isinstance(client_app, ClientApp):
273
242
  raise LoadClientAppError(
flwr/common/config.py CHANGED
@@ -16,13 +16,13 @@
16
16
 
17
17
  import os
18
18
  from pathlib import Path
19
- from typing import Any, Dict, List, Optional, Tuple, Union
19
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast, get_args
20
20
 
21
21
  import tomli
22
22
 
23
23
  from flwr.cli.config_utils import validate_fields
24
24
  from flwr.common.constant import APP_DIR, FAB_CONFIG_FILE, FLWR_HOME
25
- from flwr.common.typing import Run
25
+ from flwr.common.typing import Run, UserConfig, UserConfigValue
26
26
 
27
27
 
28
28
  def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
@@ -75,8 +75,9 @@ def get_project_config(project_dir: Union[str, Path]) -> Dict[str, Any]:
75
75
 
76
76
 
77
77
  def _fuse_dicts(
78
- main_dict: Dict[str, str], override_dict: Dict[str, str]
79
- ) -> Dict[str, str]:
78
+ main_dict: UserConfig,
79
+ override_dict: UserConfig,
80
+ ) -> UserConfig:
80
81
  fused_dict = main_dict.copy()
81
82
 
82
83
  for key, value in override_dict.items():
@@ -87,8 +88,8 @@ def _fuse_dicts(
87
88
 
88
89
 
89
90
  def get_fused_config_from_dir(
90
- project_dir: Path, override_config: Dict[str, str]
91
- ) -> Dict[str, str]:
91
+ project_dir: Path, override_config: UserConfig
92
+ ) -> UserConfig:
92
93
  """Merge the overrides from a given dict with the config from a Flower App."""
93
94
  default_config = get_project_config(project_dir)["tool"]["flwr"]["app"].get(
94
95
  "config", {}
@@ -98,7 +99,7 @@ def get_fused_config_from_dir(
98
99
  return _fuse_dicts(flat_default_config, override_config)
99
100
 
100
101
 
101
- def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> Dict[str, str]:
102
+ def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig:
102
103
  """Merge the overrides from a `Run` with the config from a FAB.
103
104
 
104
105
  Get the config using the fab_id and the fab_version, remove the nesting by adding
@@ -112,19 +113,25 @@ def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> Dict[str, str]:
112
113
  return get_fused_config_from_dir(project_dir, run.override_config)
113
114
 
114
115
 
115
- def flatten_dict(raw_dict: Dict[str, Any], parent_key: str = "") -> Dict[str, str]:
116
+ def flatten_dict(
117
+ raw_dict: Optional[Dict[str, Any]], parent_key: str = ""
118
+ ) -> UserConfig:
116
119
  """Flatten dict by joining nested keys with a given separator."""
117
- items: List[Tuple[str, str]] = []
120
+ if raw_dict is None:
121
+ return {}
122
+
123
+ items: List[Tuple[str, UserConfigValue]] = []
118
124
  separator: str = "."
119
125
  for k, v in raw_dict.items():
120
126
  new_key = f"{parent_key}{separator}{k}" if parent_key else k
121
127
  if isinstance(v, dict):
122
128
  items.extend(flatten_dict(v, parent_key=new_key).items())
123
- elif isinstance(v, str):
124
- items.append((new_key, v))
129
+ elif isinstance(v, get_args(UserConfigValue)):
130
+ items.append((new_key, cast(UserConfigValue, v)))
125
131
  else:
126
132
  raise ValueError(
127
- f"The value for key {k} needs to be a `str` or a `dict`.",
133
+ f"The value for key {k} needs to be of type `int`, `float`, "
134
+ "`bool, `str`, or a `dict` of those.",
128
135
  )
129
136
  return dict(items)
130
137
 
@@ -132,9 +139,9 @@ def flatten_dict(raw_dict: Dict[str, Any], parent_key: str = "") -> Dict[str, st
132
139
  def parse_config_args(
133
140
  config: Optional[List[str]],
134
141
  separator: str = ",",
135
- ) -> Dict[str, str]:
142
+ ) -> UserConfig:
136
143
  """Parse separator separated list of key-value pairs separated by '='."""
137
- overrides: Dict[str, str] = {}
144
+ overrides: UserConfig = {}
138
145
 
139
146
  if config is None:
140
147
  return overrides
@@ -150,8 +157,7 @@ def parse_config_args(
150
157
  with Path(overrides_list[0]).open("rb") as config_file:
151
158
  overrides = flatten_dict(tomli.load(config_file))
152
159
  else:
153
- for kv_pair in overrides_list:
154
- key, value = kv_pair.split("=")
155
- overrides[key] = value
160
+ toml_str = "\n".join(overrides_list)
161
+ overrides.update(tomli.loads(toml_str))
156
162
 
157
163
  return overrides
flwr/common/context.py CHANGED
@@ -16,9 +16,9 @@
16
16
 
17
17
 
18
18
  from dataclasses import dataclass
19
- from typing import Dict
20
19
 
21
20
  from .record import RecordSet
21
+ from .typing import UserConfig
22
22
 
23
23
 
24
24
  @dataclass
@@ -29,7 +29,7 @@ class Context:
29
29
  ----------
30
30
  node_id : int
31
31
  The ID that identifies the node.
32
- node_config : Dict[str, str]
32
+ node_config : UserConfig
33
33
  A config (key/value mapping) unique to the node and independent of the
34
34
  `run_config`. This config persists across all runs this node participates in.
35
35
  state : RecordSet
@@ -39,23 +39,23 @@ class Context:
39
39
  executing mods. It can also be used as a memory to access
40
40
  at different points during the lifecycle of this entity (e.g. across
41
41
  multiple rounds)
42
- run_config : Dict[str, str]
42
+ run_config : UserConfig
43
43
  A config (key/value mapping) held by the entity in a given run and that will
44
44
  stay local. It can be used at any point during the lifecycle of this entity
45
45
  (e.g. across multiple rounds)
46
46
  """
47
47
 
48
48
  node_id: int
49
- node_config: Dict[str, str]
49
+ node_config: UserConfig
50
50
  state: RecordSet
51
- run_config: Dict[str, str]
51
+ run_config: UserConfig
52
52
 
53
53
  def __init__( # pylint: disable=too-many-arguments
54
54
  self,
55
55
  node_id: int,
56
- node_config: Dict[str, str],
56
+ node_config: UserConfig,
57
57
  state: RecordSet,
58
- run_config: Dict[str, str],
58
+ run_config: UserConfig,
59
59
  ) -> None:
60
60
  self.node_id = node_id
61
61
  self.node_config = node_config