flwr-nightly 1.9.0.dev20240531__py3-none-any.whl → 1.10.0.dev20240619__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 (80) hide show
  1. flwr/cli/app.py +2 -0
  2. flwr/cli/build.py +4 -15
  3. flwr/cli/config_utils.py +64 -7
  4. flwr/cli/install.py +211 -0
  5. flwr/cli/new/templates/app/pyproject.hf.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  11. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  12. flwr/cli/run/run.py +39 -2
  13. flwr/cli/utils.py +14 -0
  14. flwr/client/__init__.py +1 -0
  15. flwr/client/app.py +153 -103
  16. flwr/client/client_app.py +1 -1
  17. flwr/client/grpc_adapter_client/__init__.py +15 -0
  18. flwr/client/grpc_adapter_client/connection.py +94 -0
  19. flwr/client/grpc_client/connection.py +5 -1
  20. flwr/client/grpc_rere_client/client_interceptor.py +1 -1
  21. flwr/client/grpc_rere_client/connection.py +9 -5
  22. flwr/client/grpc_rere_client/grpc_adapter.py +133 -0
  23. flwr/client/mod/__init__.py +4 -4
  24. flwr/client/rest_client/connection.py +10 -3
  25. flwr/client/supernode/app.py +155 -31
  26. flwr/common/__init__.py +12 -12
  27. flwr/common/config.py +71 -0
  28. flwr/common/constant.py +15 -0
  29. flwr/common/object_ref.py +52 -14
  30. flwr/common/record/__init__.py +1 -1
  31. flwr/common/telemetry.py +4 -0
  32. flwr/common/typing.py +9 -0
  33. flwr/proto/driver_pb2.py +20 -19
  34. flwr/proto/driver_pb2_grpc.py +35 -0
  35. flwr/proto/driver_pb2_grpc.pyi +14 -0
  36. flwr/proto/exec_pb2.py +34 -0
  37. flwr/proto/exec_pb2.pyi +55 -0
  38. flwr/proto/exec_pb2_grpc.py +101 -0
  39. flwr/proto/exec_pb2_grpc.pyi +41 -0
  40. flwr/proto/fab_pb2.py +30 -0
  41. flwr/proto/fab_pb2.pyi +56 -0
  42. flwr/proto/fab_pb2_grpc.py +4 -0
  43. flwr/proto/fab_pb2_grpc.pyi +4 -0
  44. flwr/proto/fleet_pb2.py +28 -33
  45. flwr/proto/fleet_pb2.pyi +0 -42
  46. flwr/proto/fleet_pb2_grpc.py +7 -6
  47. flwr/proto/fleet_pb2_grpc.pyi +5 -4
  48. flwr/proto/run_pb2.py +30 -0
  49. flwr/proto/run_pb2.pyi +52 -0
  50. flwr/proto/run_pb2_grpc.py +4 -0
  51. flwr/proto/run_pb2_grpc.pyi +4 -0
  52. flwr/server/__init__.py +2 -6
  53. flwr/server/app.py +94 -214
  54. flwr/server/run_serverapp.py +33 -7
  55. flwr/server/server_app.py +2 -2
  56. flwr/server/strategy/__init__.py +2 -2
  57. flwr/server/superlink/driver/driver_servicer.py +7 -0
  58. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  59. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
  60. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +4 -0
  61. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -2
  62. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +1 -2
  63. flwr/server/superlink/fleet/message_handler/message_handler.py +8 -6
  64. flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -1
  65. flwr/server/superlink/fleet/vce/vce_api.py +3 -1
  66. flwr/server/superlink/state/in_memory_state.py +8 -5
  67. flwr/server/superlink/state/sqlite_state.py +6 -3
  68. flwr/server/superlink/state/state.py +5 -4
  69. flwr/simulation/__init__.py +4 -1
  70. flwr/simulation/run_simulation.py +22 -0
  71. flwr/superexec/__init__.py +21 -0
  72. flwr/superexec/app.py +178 -0
  73. flwr/superexec/exec_grpc.py +51 -0
  74. flwr/superexec/exec_servicer.py +65 -0
  75. flwr/superexec/executor.py +54 -0
  76. {flwr_nightly-1.9.0.dev20240531.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/METADATA +1 -1
  77. {flwr_nightly-1.9.0.dev20240531.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/RECORD +80 -56
  78. {flwr_nightly-1.9.0.dev20240531.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/entry_points.txt +1 -2
  79. {flwr_nightly-1.9.0.dev20240531.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/LICENSE +0 -0
  80. {flwr_nightly-1.9.0.dev20240531.dist-info → flwr_nightly-1.10.0.dev20240619.dist-info}/WHEEL +0 -0
flwr/cli/app.py CHANGED
@@ -18,6 +18,7 @@ import typer
18
18
 
19
19
  from .build import build
20
20
  from .example import example
21
+ from .install import install
21
22
  from .new import new
22
23
  from .run import run
23
24
 
@@ -34,6 +35,7 @@ app.command()(new)
34
35
  app.command()(example)
35
36
  app.command()(run)
36
37
  app.command()(build)
38
+ app.command()(install)
37
39
 
38
40
  if __name__ == "__main__":
39
41
  app()
flwr/cli/build.py CHANGED
@@ -14,7 +14,6 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `build` command."""
16
16
 
17
- import hashlib
18
17
  import os
19
18
  import zipfile
20
19
  from pathlib import Path
@@ -25,7 +24,7 @@ import typer
25
24
  from typing_extensions import Annotated
26
25
 
27
26
  from .config_utils import load_and_validate
28
- from .utils import is_valid_project_name
27
+ from .utils import get_sha256_hash, is_valid_project_name
29
28
 
30
29
 
31
30
  # pylint: disable=too-many-locals
@@ -34,7 +33,7 @@ def build(
34
33
  Optional[Path],
35
34
  typer.Option(help="The Flower project directory to bundle into a FAB"),
36
35
  ] = None,
37
- ) -> None:
36
+ ) -> str:
38
37
  """Build a Flower project into a Flower App Bundle (FAB).
39
38
 
40
39
  You can run `flwr build` without any argument to bundle the current directory:
@@ -115,7 +114,7 @@ def build(
115
114
  fab_file.write(file_path, archive_path)
116
115
 
117
116
  # Calculate file info
118
- sha256_hash = _get_sha256_hash(file_path)
117
+ sha256_hash = get_sha256_hash(file_path)
119
118
  file_size_bits = os.path.getsize(file_path) * 8 # size in bits
120
119
  list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
121
120
 
@@ -126,17 +125,7 @@ def build(
126
125
  f"🎊 Successfully built {fab_filename}.", fg=typer.colors.GREEN, bold=True
127
126
  )
128
127
 
129
-
130
- def _get_sha256_hash(file_path: Path) -> str:
131
- """Calculate the SHA-256 hash of a file."""
132
- sha256 = hashlib.sha256()
133
- with open(file_path, "rb") as f:
134
- while True:
135
- data = f.read(65536) # Read in 64kB blocks
136
- if not data:
137
- break
138
- sha256.update(data)
139
- return sha256.hexdigest()
128
+ return fab_filename
140
129
 
141
130
 
142
131
  def _load_gitignore(directory: Path) -> pathspec.PathSpec:
flwr/cli/config_utils.py CHANGED
@@ -14,16 +14,59 @@
14
14
  # ==============================================================================
15
15
  """Utility to validate the `pyproject.toml` file."""
16
16
 
17
+ import zipfile
18
+ from io import BytesIO
17
19
  from pathlib import Path
18
- from typing import Any, Dict, List, Optional, Tuple
20
+ from typing import IO, Any, Dict, List, Optional, Tuple, Union
19
21
 
20
22
  import tomli
21
23
 
22
24
  from flwr.common import object_ref
23
25
 
24
26
 
27
+ def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]:
28
+ """Extract the fab_id and the fab_version from a FAB file or path.
29
+
30
+ Parameters
31
+ ----------
32
+ fab_file : Union[Path, bytes]
33
+ The Flower App Bundle file to validate and extract the metadata from.
34
+ It can either be a path to the file or the file itself as bytes.
35
+
36
+ Returns
37
+ -------
38
+ Tuple[str, str]
39
+ The `fab_version` and `fab_id` of the given Flower App Bundle.
40
+ """
41
+ fab_file_archive: Union[Path, IO[bytes]]
42
+ if isinstance(fab_file, bytes):
43
+ fab_file_archive = BytesIO(fab_file)
44
+ elif isinstance(fab_file, Path):
45
+ fab_file_archive = fab_file
46
+ else:
47
+ raise ValueError("fab_file must be either a Path or bytes")
48
+
49
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
50
+ with zipf.open("pyproject.toml") as file:
51
+ toml_content = file.read().decode("utf-8")
52
+
53
+ conf = load_from_string(toml_content)
54
+ if conf is None:
55
+ raise ValueError("Invalid TOML content in pyproject.toml")
56
+
57
+ is_valid, errors, _ = validate(conf, check_module=False)
58
+ if not is_valid:
59
+ raise ValueError(errors)
60
+
61
+ return (
62
+ conf["project"]["version"],
63
+ f"{conf['flower']['publisher']}/{conf['project']['name']}",
64
+ )
65
+
66
+
25
67
  def load_and_validate(
26
68
  path: Optional[Path] = None,
69
+ check_module: bool = True,
27
70
  ) -> Tuple[Optional[Dict[str, Any]], List[str], List[str]]:
28
71
  """Load and validate pyproject.toml as dict.
29
72
 
@@ -42,7 +85,7 @@ def load_and_validate(
42
85
  ]
43
86
  return (None, errors, [])
44
87
 
45
- is_valid, errors, warnings = validate(config)
88
+ is_valid, errors, warnings = validate(config, check_module)
46
89
 
47
90
  if not is_valid:
48
91
  return (None, errors, warnings)
@@ -62,8 +105,7 @@ def load(path: Optional[Path] = None) -> Optional[Dict[str, Any]]:
62
105
  return None
63
106
 
64
107
  with toml_path.open(encoding="utf-8") as toml_file:
65
- data = tomli.loads(toml_file.read())
66
- return data
108
+ return load_from_string(toml_file.read())
67
109
 
68
110
 
69
111
  # pylint: disable=too-many-branches
@@ -102,7 +144,9 @@ def validate_fields(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]
102
144
  return len(errors) == 0, errors, warnings
103
145
 
104
146
 
105
- def validate(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]:
147
+ def validate(
148
+ config: Dict[str, Any], check_module: bool = True
149
+ ) -> Tuple[bool, List[str], List[str]]:
106
150
  """Validate pyproject.toml."""
107
151
  is_valid, errors, warnings = validate_fields(config)
108
152
 
@@ -110,14 +154,27 @@ def validate(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]:
110
154
  return False, errors, warnings
111
155
 
112
156
  # Validate serverapp
113
- is_valid, reason = object_ref.validate(config["flower"]["components"]["serverapp"])
157
+ is_valid, reason = object_ref.validate(
158
+ config["flower"]["components"]["serverapp"], check_module
159
+ )
114
160
  if not is_valid and isinstance(reason, str):
115
161
  return False, [reason], []
116
162
 
117
163
  # Validate clientapp
118
- is_valid, reason = object_ref.validate(config["flower"]["components"]["clientapp"])
164
+ is_valid, reason = object_ref.validate(
165
+ config["flower"]["components"]["clientapp"], check_module
166
+ )
119
167
 
120
168
  if not is_valid and isinstance(reason, str):
121
169
  return False, [reason], []
122
170
 
123
171
  return True, [], []
172
+
173
+
174
+ def load_from_string(toml_content: str) -> Optional[Dict[str, Any]]:
175
+ """Load TOML content from a string and return as dict."""
176
+ try:
177
+ data = tomli.loads(toml_content)
178
+ return data
179
+ except tomli.TOMLDecodeError:
180
+ return None
flwr/cli/install.py ADDED
@@ -0,0 +1,211 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface `install` command."""
16
+
17
+
18
+ import shutil
19
+ import tempfile
20
+ import zipfile
21
+ from io import BytesIO
22
+ from pathlib import Path
23
+ from typing import IO, Optional, Union
24
+
25
+ import typer
26
+ from typing_extensions import Annotated
27
+
28
+ from flwr.common.config import get_flwr_dir
29
+
30
+ from .config_utils import load_and_validate
31
+ from .utils import get_sha256_hash
32
+
33
+
34
+ def install(
35
+ source: Annotated[
36
+ Optional[Path],
37
+ typer.Argument(metavar="source", help="The source FAB file to install."),
38
+ ] = None,
39
+ flwr_dir: Annotated[
40
+ Optional[Path],
41
+ typer.Option(help="The desired install path."),
42
+ ] = None,
43
+ ) -> None:
44
+ """Install a Flower App Bundle.
45
+
46
+ It can be ran with a single FAB file argument:
47
+
48
+ ``flwr install ./target_project.fab``
49
+
50
+ The target install directory can be specified with ``--flwr-dir``:
51
+
52
+ ``flwr install ./target_project.fab --flwr-dir ./docs/flwr``
53
+
54
+ This will install ``target_project`` to ``./docs/flwr/``. By default,
55
+ ``flwr-dir`` is equal to:
56
+
57
+ - ``$FLWR_HOME/`` if ``$FLWR_HOME`` is defined
58
+ - ``$XDG_DATA_HOME/.flwr/`` if ``$XDG_DATA_HOME`` is defined
59
+ - ``$HOME/.flwr/`` in all other cases
60
+ """
61
+ if source is None:
62
+ source = Path(typer.prompt("Enter the source FAB file"))
63
+
64
+ source = source.resolve()
65
+ if not source.exists() or not source.is_file():
66
+ typer.secho(
67
+ f"❌ The source {source} does not exist or is not a file.",
68
+ fg=typer.colors.RED,
69
+ bold=True,
70
+ )
71
+ raise typer.Exit(code=1)
72
+
73
+ if source.suffix != ".fab":
74
+ typer.secho(
75
+ f"❌ The source {source} is not a `.fab` file.",
76
+ fg=typer.colors.RED,
77
+ bold=True,
78
+ )
79
+ raise typer.Exit(code=1)
80
+
81
+ install_from_fab(source, flwr_dir)
82
+
83
+
84
+ def install_from_fab(
85
+ fab_file: Union[Path, bytes],
86
+ flwr_dir: Optional[Path],
87
+ skip_prompt: bool = False,
88
+ ) -> Path:
89
+ """Install from a FAB file after extracting and validating."""
90
+ fab_file_archive: Union[Path, IO[bytes]]
91
+ fab_name: Optional[str]
92
+ if isinstance(fab_file, bytes):
93
+ fab_file_archive = BytesIO(fab_file)
94
+ fab_name = None
95
+ elif isinstance(fab_file, Path):
96
+ fab_file_archive = fab_file
97
+ fab_name = fab_file.stem
98
+ else:
99
+ raise ValueError("fab_file must be either a Path or bytes")
100
+
101
+ with tempfile.TemporaryDirectory() as tmpdir:
102
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
103
+ zipf.extractall(tmpdir)
104
+ tmpdir_path = Path(tmpdir)
105
+ info_dir = tmpdir_path / ".info"
106
+ if not info_dir.exists():
107
+ typer.secho(
108
+ "❌ FAB file has incorrect format.",
109
+ fg=typer.colors.RED,
110
+ bold=True,
111
+ )
112
+ raise typer.Exit(code=1)
113
+
114
+ content_file = info_dir / "CONTENT"
115
+
116
+ if not content_file.exists() or not _verify_hashes(
117
+ content_file.read_text(), tmpdir_path
118
+ ):
119
+ typer.secho(
120
+ "❌ File hashes couldn't be verified.",
121
+ fg=typer.colors.RED,
122
+ bold=True,
123
+ )
124
+ raise typer.Exit(code=1)
125
+
126
+ shutil.rmtree(info_dir)
127
+
128
+ installed_path = validate_and_install(
129
+ tmpdir_path, fab_name, flwr_dir, skip_prompt
130
+ )
131
+
132
+ return installed_path
133
+
134
+
135
+ def validate_and_install(
136
+ project_dir: Path,
137
+ fab_name: Optional[str],
138
+ flwr_dir: Optional[Path],
139
+ skip_prompt: bool = False,
140
+ ) -> Path:
141
+ """Validate TOML files and install the project to the desired directory."""
142
+ config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
143
+
144
+ if config is None:
145
+ typer.secho(
146
+ "❌ Invalid config inside FAB file.",
147
+ fg=typer.colors.RED,
148
+ bold=True,
149
+ )
150
+ raise typer.Exit(code=1)
151
+
152
+ publisher = config["flower"]["publisher"]
153
+ project_name = config["project"]["name"]
154
+ version = config["project"]["version"]
155
+
156
+ if (
157
+ fab_name
158
+ and fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}"
159
+ ):
160
+ typer.secho(
161
+ "❌ FAB file has incorrect name. The file name must follow the format "
162
+ "`<publisher>.<project_name>.<version>.fab`.",
163
+ fg=typer.colors.RED,
164
+ bold=True,
165
+ )
166
+ raise typer.Exit(code=1)
167
+
168
+ install_dir: Path = (
169
+ (get_flwr_dir() if not flwr_dir else flwr_dir)
170
+ / "apps"
171
+ / publisher
172
+ / project_name
173
+ / version
174
+ )
175
+ if install_dir.exists() and not skip_prompt:
176
+ if not typer.confirm(
177
+ typer.style(
178
+ f"\n💬 {project_name} version {version} is already installed, "
179
+ "do you want to reinstall it?",
180
+ fg=typer.colors.MAGENTA,
181
+ bold=True,
182
+ )
183
+ ):
184
+ return install_dir
185
+
186
+ install_dir.mkdir(parents=True, exist_ok=True)
187
+
188
+ # Move contents from source directory
189
+ for item in project_dir.iterdir():
190
+ if item.is_dir():
191
+ shutil.copytree(item, install_dir / item.name, dirs_exist_ok=True)
192
+ else:
193
+ shutil.copy2(item, install_dir / item.name)
194
+
195
+ typer.secho(
196
+ f"🎊 Successfully installed {project_name} to {install_dir}.",
197
+ fg=typer.colors.GREEN,
198
+ bold=True,
199
+ )
200
+
201
+ return install_dir
202
+
203
+
204
+ def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
205
+ """Verify file hashes based on the LIST content."""
206
+ for line in list_content.strip().split("\n"):
207
+ rel_path, hash_expected, _ = line.split(",")
208
+ file_path = tmpdir / rel_path
209
+ if not file_path.exists() or get_sha256_hash(file_path) != hash_expected:
210
+ return False
211
+ return True
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = { text = "Apache License (2.0)" }
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "flwr-datasets>=0.0.2,<1.0.0",
16
16
  "torch==2.2.1",
17
17
  "transformers>=4.30.0,<5.0"
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = {text = "Apache License (2.0)"}
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "jax==0.4.26",
16
16
  "jaxlib==0.4.26",
17
17
  "scikit-learn==1.4.2",
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = { text = "Apache License (2.0)" }
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "flwr-datasets[vision]>=0.0.2,<1.0.0",
16
16
  "mlx==0.10.0",
17
17
  "numpy==1.24.4",
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = { text = "Apache License (2.0)" }
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "numpy>=1.21.0",
16
16
  ]
17
17
 
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = { text = "Apache License (2.0)" }
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "flwr-datasets[vision]>=0.0.2,<1.0.0",
16
16
  "torch==2.2.1",
17
17
  "torchvision==0.17.1",
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = { text = "Apache License (2.0)" }
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "flwr-datasets[vision]>=0.0.2,<1.0.0",
16
16
  "scikit-learn>=1.1.1",
17
17
  ]
@@ -11,7 +11,7 @@ authors = [
11
11
  ]
12
12
  license = { text = "Apache License (2.0)" }
13
13
  dependencies = [
14
- "flwr[simulation]>=1.8.0,<2.0",
14
+ "flwr[simulation]>=1.9.0,<2.0",
15
15
  "flwr-datasets[vision]>=0.0.2,<1.0.0",
16
16
  "tensorflow>=2.11.1",
17
17
  ]
flwr/cli/run/run.py CHANGED
@@ -16,12 +16,18 @@
16
16
 
17
17
  import sys
18
18
  from enum import Enum
19
+ from logging import DEBUG
19
20
  from typing import Optional
20
21
 
21
22
  import typer
22
23
  from typing_extensions import Annotated
23
24
 
24
25
  from flwr.cli import config_utils
26
+ from flwr.common.constant import SUPEREXEC_DEFAULT_ADDRESS
27
+ from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
28
+ from flwr.common.logger import log
29
+ from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611
30
+ from flwr.proto.exec_pb2_grpc import ExecStub
25
31
  from flwr.simulation.run_simulation import _run_simulation
26
32
 
27
33
 
@@ -31,20 +37,32 @@ class Engine(str, Enum):
31
37
  SIMULATION = "simulation"
32
38
 
33
39
 
40
+ # pylint: disable-next=too-many-locals
34
41
  def run(
35
42
  engine: Annotated[
36
43
  Optional[Engine],
37
- typer.Option(case_sensitive=False, help="The ML framework to use"),
44
+ typer.Option(case_sensitive=False, help="The execution engine to run the app"),
38
45
  ] = None,
46
+ use_superexec: Annotated[
47
+ bool,
48
+ typer.Option(
49
+ case_sensitive=False, help="Use this flag to use the new SuperExec API"
50
+ ),
51
+ ] = False,
39
52
  ) -> None:
40
53
  """Run Flower project."""
54
+ if use_superexec:
55
+ _start_superexec_run()
56
+ return
57
+
41
58
  typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
42
59
 
43
60
  config, errors, warnings = config_utils.load_and_validate()
44
61
 
45
62
  if config is None:
46
63
  typer.secho(
47
- "Project configuration could not be loaded.\npyproject.toml is invalid:\n"
64
+ "Project configuration could not be loaded.\n"
65
+ "pyproject.toml is invalid:\n"
48
66
  + "\n".join([f"- {line}" for line in errors]),
49
67
  fg=typer.colors.RED,
50
68
  bold=True,
@@ -82,3 +100,22 @@ def run(
82
100
  fg=typer.colors.RED,
83
101
  bold=True,
84
102
  )
103
+
104
+
105
+ def _start_superexec_run() -> None:
106
+ def on_channel_state_change(channel_connectivity: str) -> None:
107
+ """Log channel connectivity."""
108
+ log(DEBUG, channel_connectivity)
109
+
110
+ channel = create_channel(
111
+ server_address=SUPEREXEC_DEFAULT_ADDRESS,
112
+ insecure=True,
113
+ root_certificates=None,
114
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
115
+ interceptors=None,
116
+ )
117
+ channel.subscribe(on_channel_state_change)
118
+ stub = ExecStub(channel)
119
+
120
+ req = StartRunRequest()
121
+ stub.StartRun(req)
flwr/cli/utils.py CHANGED
@@ -14,7 +14,9 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface utils."""
16
16
 
17
+ import hashlib
17
18
  import re
19
+ from pathlib import Path
18
20
  from typing import Callable, List, Optional, cast
19
21
 
20
22
  import typer
@@ -122,3 +124,15 @@ def sanitize_project_name(name: str) -> str:
122
124
  sanitized_name = sanitized_name[1:]
123
125
 
124
126
  return sanitized_name
127
+
128
+
129
+ def get_sha256_hash(file_path: Path) -> str:
130
+ """Calculate the SHA-256 hash of a file."""
131
+ sha256 = hashlib.sha256()
132
+ with open(file_path, "rb") as f:
133
+ while True:
134
+ data = f.read(65536) # Read in 64kB blocks
135
+ if not data:
136
+ break
137
+ sha256.update(data)
138
+ return sha256.hexdigest()
flwr/client/__init__.py CHANGED
@@ -29,6 +29,7 @@ __all__ = [
29
29
  "ClientApp",
30
30
  "ClientFn",
31
31
  "NumPyClient",
32
+ "mod",
32
33
  "run_client_app",
33
34
  "run_supernode",
34
35
  "start_client",