flwr-nightly 1.10.0.dev20240612__py3-none-any.whl → 1.10.0.dev20240613__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.

flwr/cli/build.py CHANGED
@@ -33,7 +33,7 @@ def build(
33
33
  Optional[Path],
34
34
  typer.Option(help="The Flower project directory to bundle into a FAB"),
35
35
  ] = None,
36
- ) -> None:
36
+ ) -> str:
37
37
  """Build a Flower project into a Flower App Bundle (FAB).
38
38
 
39
39
  You can run `flwr build` without any argument to bundle the current directory:
@@ -125,6 +125,8 @@ def build(
125
125
  f"🎊 Successfully built {fab_filename}.", fg=typer.colors.GREEN, bold=True
126
126
  )
127
127
 
128
+ return fab_filename
129
+
128
130
 
129
131
  def _load_gitignore(directory: Path) -> pathspec.PathSpec:
130
132
  """Load and parse .gitignore file, returning a pathspec."""
flwr/cli/config_utils.py CHANGED
@@ -14,14 +14,56 @@
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)
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,
27
69
  check_module: bool = True,
@@ -63,8 +105,7 @@ def load(path: Optional[Path] = None) -> Optional[Dict[str, Any]]:
63
105
  return None
64
106
 
65
107
  with toml_path.open(encoding="utf-8") as toml_file:
66
- data = tomli.loads(toml_file.read())
67
- return data
108
+ return load_from_string(toml_file.read())
68
109
 
69
110
 
70
111
  # pylint: disable=too-many-branches
@@ -128,3 +169,12 @@ def validate(
128
169
  return False, [reason], []
129
170
 
130
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 CHANGED
@@ -15,16 +15,18 @@
15
15
  """Flower command line interface `install` command."""
16
16
 
17
17
 
18
- import os
19
18
  import shutil
20
19
  import tempfile
21
20
  import zipfile
21
+ from io import BytesIO
22
22
  from pathlib import Path
23
- from typing import Optional
23
+ from typing import IO, Optional, Union
24
24
 
25
25
  import typer
26
26
  from typing_extensions import Annotated
27
27
 
28
+ from flwr.common.config import get_flwr_dir
29
+
28
30
  from .config_utils import load_and_validate
29
31
  from .utils import get_sha256_hash
30
32
 
@@ -80,11 +82,24 @@ def install(
80
82
 
81
83
 
82
84
  def install_from_fab(
83
- fab_file: Path, flwr_dir: Optional[Path], skip_prompt: bool = False
84
- ) -> None:
85
+ fab_file: Union[Path, bytes],
86
+ flwr_dir: Optional[Path],
87
+ skip_prompt: bool = False,
88
+ ) -> Path:
85
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
+
86
101
  with tempfile.TemporaryDirectory() as tmpdir:
87
- with zipfile.ZipFile(fab_file, "r") as zipf:
102
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
88
103
  zipf.extractall(tmpdir)
89
104
  tmpdir_path = Path(tmpdir)
90
105
  info_dir = tmpdir_path / ".info"
@@ -110,15 +125,19 @@ def install_from_fab(
110
125
 
111
126
  shutil.rmtree(info_dir)
112
127
 
113
- validate_and_install(tmpdir_path, fab_file.stem, flwr_dir, skip_prompt)
128
+ installed_path = validate_and_install(
129
+ tmpdir_path, fab_name, flwr_dir, skip_prompt
130
+ )
131
+
132
+ return installed_path
114
133
 
115
134
 
116
135
  def validate_and_install(
117
136
  project_dir: Path,
118
- fab_name: str,
137
+ fab_name: Optional[str],
119
138
  flwr_dir: Optional[Path],
120
139
  skip_prompt: bool = False,
121
- ) -> None:
140
+ ) -> Path:
122
141
  """Validate TOML files and install the project to the desired directory."""
123
142
  config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
124
143
 
@@ -134,7 +153,10 @@ def validate_and_install(
134
153
  project_name = config["project"]["name"]
135
154
  version = config["project"]["version"]
136
155
 
137
- if fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}":
156
+ if (
157
+ fab_name
158
+ and fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}"
159
+ ):
138
160
  typer.secho(
139
161
  "❌ FAB file has incorrect name. The file name must follow the format "
140
162
  "`<publisher>.<project_name>.<version>.fab`.",
@@ -144,16 +166,7 @@ def validate_and_install(
144
166
  raise typer.Exit(code=1)
145
167
 
146
168
  install_dir: Path = (
147
- (
148
- Path(
149
- os.getenv(
150
- "FLWR_HOME",
151
- f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}/.flwr",
152
- )
153
- )
154
- if not flwr_dir
155
- else flwr_dir
156
- )
169
+ (get_flwr_dir() if not flwr_dir else flwr_dir)
157
170
  / "apps"
158
171
  / publisher
159
172
  / project_name
@@ -168,7 +181,7 @@ def validate_and_install(
168
181
  bold=True,
169
182
  )
170
183
  ):
171
- return
184
+ return install_dir
172
185
 
173
186
  install_dir.mkdir(parents=True, exist_ok=True)
174
187
 
@@ -185,6 +198,8 @@ def validate_and_install(
185
198
  bold=True,
186
199
  )
187
200
 
201
+ return install_dir
202
+
188
203
 
189
204
  def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
190
205
  """Verify file hashes based on the LIST content."""
flwr/client/app.py CHANGED
@@ -19,7 +19,7 @@ import sys
19
19
  import time
20
20
  from dataclasses import dataclass
21
21
  from logging import DEBUG, ERROR, INFO, WARN
22
- from typing import Callable, ContextManager, Optional, Tuple, Type, Union
22
+ from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union
23
23
 
24
24
  from cryptography.hazmat.primitives.asymmetric import ec
25
25
  from grpc import RpcError
@@ -177,7 +177,7 @@ def start_client(
177
177
  def _start_client_internal(
178
178
  *,
179
179
  server_address: str,
180
- load_client_app_fn: Optional[Callable[[], ClientApp]] = None,
180
+ load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
181
181
  client_fn: Optional[ClientFn] = None,
182
182
  client: Optional[Client] = None,
183
183
  grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
@@ -252,7 +252,7 @@ def _start_client_internal(
252
252
 
253
253
  client_fn = single_client_factory
254
254
 
255
- def _load_client_app() -> ClientApp:
255
+ def _load_client_app(_1: str, _2: str) -> ClientApp:
256
256
  return ClientApp(client_fn=client_fn)
257
257
 
258
258
  load_client_app_fn = _load_client_app
@@ -308,6 +308,8 @@ def _start_client_internal(
308
308
  )
309
309
 
310
310
  node_state = NodeState()
311
+ # run_id -> (fab_id, fab_version)
312
+ run_info: Dict[int, Tuple[str, str]] = {}
311
313
 
312
314
  while not app_state_tracker.interrupt:
313
315
  sleep_duration: int = 0
@@ -319,7 +321,6 @@ def _start_client_internal(
319
321
  root_certificates,
320
322
  authentication_keys,
321
323
  ) as conn:
322
- # pylint: disable-next=W0612
323
324
  receive, send, create_node, delete_node, get_run = conn
324
325
 
325
326
  # Register node
@@ -356,13 +357,20 @@ def _start_client_internal(
356
357
  send(out_message)
357
358
  break
358
359
 
360
+ # Get run info
361
+ run_id = message.metadata.run_id
362
+ if run_id not in run_info:
363
+ if get_run is not None:
364
+ run_info[run_id] = get_run(run_id)
365
+ # If get_run is None, i.e., in grpc-bidi mode
366
+ else:
367
+ run_info[run_id] = ("", "")
368
+
359
369
  # Register context for this run
360
- node_state.register_context(run_id=message.metadata.run_id)
370
+ node_state.register_context(run_id=run_id)
361
371
 
362
372
  # Retrieve context for this run
363
- context = node_state.retrieve_context(
364
- run_id=message.metadata.run_id
365
- )
373
+ context = node_state.retrieve_context(run_id=run_id)
366
374
 
367
375
  # Create an error reply message that will never be used to prevent
368
376
  # the used-before-assignment linting error
@@ -373,7 +381,7 @@ def _start_client_internal(
373
381
  # Handle app loading and task message
374
382
  try:
375
383
  # Load ClientApp instance
376
- client_app: ClientApp = load_client_app_fn()
384
+ client_app: ClientApp = load_client_app_fn(*run_info[run_id])
377
385
 
378
386
  # Execute ClientApp
379
387
  reply_message = client_app(message=message, context=context)
@@ -411,7 +419,7 @@ def _start_client_internal(
411
419
  else:
412
420
  # No exception, update node state
413
421
  node_state.update_context(
414
- run_id=message.metadata.run_id,
422
+ run_id=run_id,
415
423
  context=context,
416
424
  )
417
425
 
@@ -20,6 +20,7 @@ from logging import DEBUG, INFO, WARN
20
20
  from pathlib import Path
21
21
  from typing import Callable, Optional, Tuple
22
22
 
23
+ import tomli
23
24
  from cryptography.exceptions import UnsupportedAlgorithm
24
25
  from cryptography.hazmat.primitives.asymmetric import ec
25
26
  from cryptography.hazmat.primitives.serialization import (
@@ -27,8 +28,10 @@ from cryptography.hazmat.primitives.serialization import (
27
28
  load_ssh_public_key,
28
29
  )
29
30
 
31
+ from flwr.cli.config_utils import validate_fields
30
32
  from flwr.client.client_app import ClientApp, LoadClientAppError
31
33
  from flwr.common import EventType, event
34
+ from flwr.common.config import get_flwr_dir
32
35
  from flwr.common.exit_handlers import register_exit_handlers
33
36
  from flwr.common.logger import log, warn_deprecated_feature
34
37
  from flwr.common.object_ref import load_app, validate
@@ -44,11 +47,23 @@ def run_supernode() -> None:
44
47
 
45
48
  event(EventType.RUN_SUPERNODE_ENTER)
46
49
 
47
- _ = _parse_args_run_supernode().parse_args()
50
+ args = _parse_args_run_supernode().parse_args()
48
51
 
49
- log(
50
- DEBUG,
51
- "Flower SuperNode starting...",
52
+ _warn_deprecated_server_arg(args)
53
+
54
+ root_certificates = _get_certificates(args)
55
+ load_fn = _get_load_client_app_fn(args, multi_app=True)
56
+ authentication_keys = _try_setup_client_authentication(args)
57
+
58
+ _start_client_internal(
59
+ server_address=args.server,
60
+ load_client_app_fn=load_fn,
61
+ transport="rest" if args.rest else "grpc-rere",
62
+ root_certificates=root_certificates,
63
+ insecure=args.insecure,
64
+ authentication_keys=authentication_keys,
65
+ max_retries=args.max_retries,
66
+ max_wait_time=args.max_wait_time,
52
67
  )
53
68
 
54
69
  # Graceful shutdown
@@ -65,6 +80,27 @@ def run_client_app() -> None:
65
80
 
66
81
  args = _parse_args_run_client_app().parse_args()
67
82
 
83
+ _warn_deprecated_server_arg(args)
84
+
85
+ root_certificates = _get_certificates(args)
86
+ load_fn = _get_load_client_app_fn(args, multi_app=False)
87
+ authentication_keys = _try_setup_client_authentication(args)
88
+
89
+ _start_client_internal(
90
+ server_address=args.superlink,
91
+ load_client_app_fn=load_fn,
92
+ transport="rest" if args.rest else "grpc-rere",
93
+ root_certificates=root_certificates,
94
+ insecure=args.insecure,
95
+ authentication_keys=authentication_keys,
96
+ max_retries=args.max_retries,
97
+ max_wait_time=args.max_wait_time,
98
+ )
99
+ register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
100
+
101
+
102
+ def _warn_deprecated_server_arg(args: argparse.Namespace) -> None:
103
+ """Warn about the deprecated argument `--server`."""
68
104
  if args.server != ADDRESS_FLEET_API_GRPC_RERE:
69
105
  warn = "Passing flag --server is deprecated. Use --superlink instead."
70
106
  warn_deprecated_feature(warn)
@@ -82,27 +118,6 @@ def run_client_app() -> None:
82
118
  else:
83
119
  args.superlink = args.server
84
120
 
85
- root_certificates = _get_certificates(args)
86
- log(
87
- DEBUG,
88
- "Flower will load ClientApp `%s`",
89
- getattr(args, "client-app"),
90
- )
91
- load_fn = _get_load_client_app_fn(args)
92
- authentication_keys = _try_setup_client_authentication(args)
93
-
94
- _start_client_internal(
95
- server_address=args.superlink,
96
- load_client_app_fn=load_fn,
97
- transport="rest" if args.rest else "grpc-rere",
98
- root_certificates=root_certificates,
99
- insecure=args.insecure,
100
- authentication_keys=authentication_keys,
101
- max_retries=args.max_retries,
102
- max_wait_time=args.max_wait_time,
103
- )
104
- register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
105
-
106
121
 
107
122
  def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
108
123
  """Load certificates if specified in args."""
@@ -140,24 +155,112 @@ def _get_certificates(args: argparse.Namespace) -> Optional[bytes]:
140
155
 
141
156
 
142
157
  def _get_load_client_app_fn(
143
- args: argparse.Namespace,
144
- ) -> Callable[[], ClientApp]:
145
- """Get the load_client_app_fn function."""
146
- client_app_dir = args.dir
147
- if client_app_dir is not None:
148
- sys.path.insert(0, client_app_dir)
158
+ args: argparse.Namespace, multi_app: bool
159
+ ) -> Callable[[str, str], ClientApp]:
160
+ """Get the load_client_app_fn function.
161
+
162
+ If `multi_app` is True, this function loads the specified ClientApp
163
+ based on `fab_id` and `fab_version`. If `fab_id` is empty, a default
164
+ ClientApp will be loaded.
165
+
166
+ If `multi_app` is False, it ignores `fab_id` and `fab_version` and
167
+ loads a default ClientApp.
168
+ """
169
+ # Find the Flower directory containing Flower Apps (only for multi-app)
170
+ flwr_dir = Path("")
171
+ if "flwr_dir" in args:
172
+ if args.flwr_dir is None:
173
+ flwr_dir = get_flwr_dir()
174
+ else:
175
+ flwr_dir = Path(args.flwr_dir)
176
+
177
+ sys.path.insert(0, str(flwr_dir))
149
178
 
150
- app_ref: str = getattr(args, "client-app")
151
- valid, error_msg = validate(app_ref)
152
- if not valid and error_msg:
153
- raise LoadClientAppError(error_msg) from None
179
+ default_app_ref: str = getattr(args, "client-app")
154
180
 
155
- def _load() -> ClientApp:
156
- client_app = load_app(app_ref, LoadClientAppError)
181
+ if not multi_app:
182
+ log(
183
+ DEBUG,
184
+ "Flower SuperNode will load and validate ClientApp `%s`",
185
+ getattr(args, "client-app"),
186
+ )
187
+ valid, error_msg = validate(default_app_ref)
188
+ if not valid and error_msg:
189
+ raise LoadClientAppError(error_msg) from None
190
+
191
+ def _load(fab_id: str, fab_version: str) -> ClientApp:
192
+ # If multi-app feature is disabled
193
+ if not multi_app:
194
+ # Set sys.path
195
+ sys.path[0] = args.dir
196
+
197
+ # Set app reference
198
+ client_app_ref = default_app_ref
199
+ # If multi-app feature is enabled but the fab id is not specified
200
+ elif fab_id == "":
201
+ if default_app_ref == "":
202
+ raise LoadClientAppError(
203
+ "Invalid FAB ID: The FAB ID is empty.",
204
+ ) from None
205
+
206
+ log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.")
207
+ # Set sys.path
208
+ sys.path[0] = args.dir
209
+
210
+ # Set app reference
211
+ client_app_ref = default_app_ref
212
+ # If multi-app feature is enabled
213
+ else:
214
+ # Check the fab_id
215
+ if fab_id.count("/") != 1:
216
+ raise LoadClientAppError(
217
+ f"Invalid FAB ID: {fab_id}",
218
+ ) from None
219
+ username, project_name = fab_id.split("/")
220
+
221
+ # Locate the directory
222
+ project_dir = flwr_dir / "apps" / username / project_name / fab_version
223
+
224
+ # Check if the directory exists
225
+ if not project_dir.exists():
226
+ raise LoadClientAppError(
227
+ f"Invalid Flower App directory: {project_dir}",
228
+ ) from None
229
+
230
+ # Load pyproject.toml file
231
+ toml_path = project_dir / "pyproject.toml"
232
+ if not toml_path.is_file():
233
+ raise LoadClientAppError(
234
+ f"Cannot find pyproject.toml in {project_dir}",
235
+ ) from None
236
+ with open(toml_path, encoding="utf-8") as toml_file:
237
+ config = tomli.loads(toml_file.read())
238
+
239
+ # Validate pyproject.toml fields
240
+ is_valid, errors, _ = validate_fields(config)
241
+ if not is_valid:
242
+ error_msg = "\n".join([f" - {error}" for error in errors])
243
+ raise LoadClientAppError(
244
+ f"Invalid pyproject.toml:\n{error_msg}",
245
+ ) from None
246
+
247
+ # Set sys.path
248
+ sys.path[0] = str(project_dir)
249
+
250
+ # Set app reference
251
+ client_app_ref = config["flower"]["components"]["clientapp"]
252
+
253
+ # Load ClientApp
254
+ log(
255
+ DEBUG,
256
+ "Loading ClientApp `%s`",
257
+ client_app_ref,
258
+ )
259
+ client_app = load_app(client_app_ref, LoadClientAppError)
157
260
 
158
261
  if not isinstance(client_app, ClientApp):
159
262
  raise LoadClientAppError(
160
- f"Attribute {app_ref} is not of type {ClientApp}",
263
+ f"Attribute {client_app_ref} is not of type {ClientApp}",
161
264
  ) from None
162
265
 
163
266
  return client_app
flwr/common/config.py ADDED
@@ -0,0 +1,28 @@
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
+ """Provide functions for managing global Flower config."""
16
+
17
+ import os
18
+ from pathlib import Path
19
+
20
+
21
+ def get_flwr_dir() -> Path:
22
+ """Return the Flower home directory based on env variables."""
23
+ return Path(
24
+ os.getenv(
25
+ "FLWR_HOME",
26
+ f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}/.flwr",
27
+ )
28
+ )
flwr/common/constant.py CHANGED
@@ -36,6 +36,8 @@ TRANSPORT_TYPES = [
36
36
  TRANSPORT_TYPE_VCE,
37
37
  ]
38
38
 
39
+ SUPEREXEC_DEFAULT_ADDRESS = "0.0.0.0:9093"
40
+
39
41
  # Constants for ping
40
42
  PING_DEFAULT_INTERVAL = 30
41
43
  PING_CALL_TIMEOUT = 5
flwr/common/telemetry.py CHANGED
@@ -164,6 +164,10 @@ class EventType(str, Enum):
164
164
  RUN_SUPERNODE_ENTER = auto()
165
165
  RUN_SUPERNODE_LEAVE = auto()
166
166
 
167
+ # SuperExec
168
+ RUN_SUPEREXEC_ENTER = auto()
169
+ RUN_SUPEREXEC_LEAVE = auto()
170
+
167
171
 
168
172
  # Use the ThreadPoolExecutor with max_workers=1 to have a queue
169
173
  # and also ensure that telemetry calls are not blocking.
flwr/proto/exec_pb2.py ADDED
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: flwr/proto/exec.proto
4
+ # Protobuf Python Version: 4.25.0
5
+ """Generated protocol buffer code."""
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ from google.protobuf.internal import builder as _builder
10
+ # @@protoc_insertion_point(imports)
11
+
12
+ _sym_db = _symbol_database.Default()
13
+
14
+
15
+
16
+
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/exec.proto\x12\nflwr.proto\"#\n\x0fStartRunRequest\x12\x10\n\x08\x66\x61\x62_file\x18\x01 \x01(\x0c\"\"\n\x10StartRunResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x32O\n\x04\x45xec\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x62\x06proto3')
18
+
19
+ _globals = globals()
20
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.exec_pb2', _globals)
22
+ if _descriptor._USE_C_DESCRIPTORS == False:
23
+ DESCRIPTOR._options = None
24
+ _globals['_STARTRUNREQUEST']._serialized_start=37
25
+ _globals['_STARTRUNREQUEST']._serialized_end=72
26
+ _globals['_STARTRUNRESPONSE']._serialized_start=74
27
+ _globals['_STARTRUNRESPONSE']._serialized_end=108
28
+ _globals['_EXEC']._serialized_start=110
29
+ _globals['_EXEC']._serialized_end=189
30
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,32 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+ import builtins
6
+ import google.protobuf.descriptor
7
+ import google.protobuf.message
8
+ import typing_extensions
9
+
10
+ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
11
+
12
+ class StartRunRequest(google.protobuf.message.Message):
13
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
14
+ FAB_FILE_FIELD_NUMBER: builtins.int
15
+ fab_file: builtins.bytes
16
+ def __init__(self,
17
+ *,
18
+ fab_file: builtins.bytes = ...,
19
+ ) -> None: ...
20
+ def ClearField(self, field_name: typing_extensions.Literal["fab_file",b"fab_file"]) -> None: ...
21
+ global___StartRunRequest = StartRunRequest
22
+
23
+ class StartRunResponse(google.protobuf.message.Message):
24
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
25
+ RUN_ID_FIELD_NUMBER: builtins.int
26
+ run_id: builtins.int
27
+ def __init__(self,
28
+ *,
29
+ run_id: builtins.int = ...,
30
+ ) -> None: ...
31
+ def ClearField(self, field_name: typing_extensions.Literal["run_id",b"run_id"]) -> None: ...
32
+ global___StartRunResponse = StartRunResponse
@@ -0,0 +1,67 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+
5
+ from flwr.proto import exec_pb2 as flwr_dot_proto_dot_exec__pb2
6
+
7
+
8
+ class ExecStub(object):
9
+ """Missing associated documentation comment in .proto file."""
10
+
11
+ def __init__(self, channel):
12
+ """Constructor.
13
+
14
+ Args:
15
+ channel: A grpc.Channel.
16
+ """
17
+ self.StartRun = channel.unary_unary(
18
+ '/flwr.proto.Exec/StartRun',
19
+ request_serializer=flwr_dot_proto_dot_exec__pb2.StartRunRequest.SerializeToString,
20
+ response_deserializer=flwr_dot_proto_dot_exec__pb2.StartRunResponse.FromString,
21
+ )
22
+
23
+
24
+ class ExecServicer(object):
25
+ """Missing associated documentation comment in .proto file."""
26
+
27
+ def StartRun(self, request, context):
28
+ """Start run upon request
29
+ """
30
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
31
+ context.set_details('Method not implemented!')
32
+ raise NotImplementedError('Method not implemented!')
33
+
34
+
35
+ def add_ExecServicer_to_server(servicer, server):
36
+ rpc_method_handlers = {
37
+ 'StartRun': grpc.unary_unary_rpc_method_handler(
38
+ servicer.StartRun,
39
+ request_deserializer=flwr_dot_proto_dot_exec__pb2.StartRunRequest.FromString,
40
+ response_serializer=flwr_dot_proto_dot_exec__pb2.StartRunResponse.SerializeToString,
41
+ ),
42
+ }
43
+ generic_handler = grpc.method_handlers_generic_handler(
44
+ 'flwr.proto.Exec', rpc_method_handlers)
45
+ server.add_generic_rpc_handlers((generic_handler,))
46
+
47
+
48
+ # This class is part of an EXPERIMENTAL API.
49
+ class Exec(object):
50
+ """Missing associated documentation comment in .proto file."""
51
+
52
+ @staticmethod
53
+ def StartRun(request,
54
+ target,
55
+ options=(),
56
+ channel_credentials=None,
57
+ call_credentials=None,
58
+ insecure=False,
59
+ compression=None,
60
+ wait_for_ready=None,
61
+ timeout=None,
62
+ metadata=None):
63
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.Exec/StartRun',
64
+ flwr_dot_proto_dot_exec__pb2.StartRunRequest.SerializeToString,
65
+ flwr_dot_proto_dot_exec__pb2.StartRunResponse.FromString,
66
+ options, channel_credentials,
67
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@@ -0,0 +1,27 @@
1
+ """
2
+ @generated by mypy-protobuf. Do not edit manually!
3
+ isort:skip_file
4
+ """
5
+ import abc
6
+ import flwr.proto.exec_pb2
7
+ import grpc
8
+
9
+ class ExecStub:
10
+ def __init__(self, channel: grpc.Channel) -> None: ...
11
+ StartRun: grpc.UnaryUnaryMultiCallable[
12
+ flwr.proto.exec_pb2.StartRunRequest,
13
+ flwr.proto.exec_pb2.StartRunResponse]
14
+ """Start run upon request"""
15
+
16
+
17
+ class ExecServicer(metaclass=abc.ABCMeta):
18
+ @abc.abstractmethod
19
+ def StartRun(self,
20
+ request: flwr.proto.exec_pb2.StartRunRequest,
21
+ context: grpc.ServicerContext,
22
+ ) -> flwr.proto.exec_pb2.StartRunResponse:
23
+ """Start run upon request"""
24
+ pass
25
+
26
+
27
+ def add_ExecServicer_to_server(servicer: ExecServicer, server: grpc.Server) -> None: ...
flwr/server/app.py CHANGED
@@ -200,15 +200,7 @@ def run_superlink() -> None:
200
200
  args = _parse_args_run_superlink().parse_args()
201
201
 
202
202
  # Parse IP address
203
- parsed_driver_address = parse_address(args.driver_api_address)
204
- if not parsed_driver_address:
205
- sys.exit(f"Driver IP address ({args.driver_api_address}) cannot be parsed.")
206
- driver_host, driver_port, driver_is_v6 = parsed_driver_address
207
- driver_address = (
208
- f"[{driver_host}]:{driver_port}"
209
- if driver_is_v6
210
- else f"{driver_host}:{driver_port}"
211
- )
203
+ driver_address, _, _ = _format_address(args.driver_api_address)
212
204
 
213
205
  # Obtain certificates
214
206
  certificates = _try_obtain_certificates(args)
@@ -231,13 +223,8 @@ def run_superlink() -> None:
231
223
  if args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE
232
224
  else ADDRESS_FLEET_API_REST
233
225
  )
234
- parsed_fleet_address = parse_address(args.fleet_api_address)
235
- if not parsed_fleet_address:
236
- sys.exit(f"Fleet IP address ({args.fleet_api_address}) cannot be parsed.")
237
- fleet_host, fleet_port, fleet_is_v6 = parsed_fleet_address
238
- fleet_address = (
239
- f"[{fleet_host}]:{fleet_port}" if fleet_is_v6 else f"{fleet_host}:{fleet_port}"
240
- )
226
+
227
+ fleet_address, host, port = _format_address(args.fleet_api_address)
241
228
 
242
229
  num_workers = args.fleet_api_num_workers
243
230
  if num_workers != 1:
@@ -267,8 +254,8 @@ def run_superlink() -> None:
267
254
  fleet_thread = threading.Thread(
268
255
  target=_run_fleet_api_rest,
269
256
  args=(
270
- fleet_host,
271
- fleet_port,
257
+ host,
258
+ port,
272
259
  ssl_keyfile,
273
260
  ssl_certfile,
274
261
  state_factory,
@@ -325,6 +312,16 @@ def run_superlink() -> None:
325
312
  driver_server.wait_for_termination(timeout=1)
326
313
 
327
314
 
315
+ def _format_address(address: str) -> Tuple[str, str, int]:
316
+ parsed_address = parse_address(address)
317
+ if not parsed_address:
318
+ sys.exit(
319
+ f"Address ({address}) cannot be parsed (expected: URL or IPv4 or IPv6)."
320
+ )
321
+ host, port, is_v6 = parsed_address
322
+ return (f"[{host}]:{port}" if is_v6 else f"{host}:{port}", host, port)
323
+
324
+
328
325
  def _try_setup_client_authentication(
329
326
  args: argparse.Namespace,
330
327
  certificates: Optional[Tuple[bytes, bytes, bytes]],
@@ -0,0 +1,19 @@
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
+ """Fower SuperExec package."""
16
+
17
+
18
+ def run_superexec() -> None:
19
+ """Empty stub."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.10.0.dev20240612
3
+ Version: 1.10.0.dev20240613
4
4
  Summary: Flower: A Friendly Federated Learning Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -1,10 +1,10 @@
1
1
  flwr/__init__.py,sha256=VmBWedrCxqmt4QvUHBLqyVEH6p7zaFMD_oCHerXHSVw,937
2
2
  flwr/cli/__init__.py,sha256=cZJVgozlkC6Ni2Hd_FAIrqefrkCGOV18fikToq-6iLw,720
3
3
  flwr/cli/app.py,sha256=FvzW2iw3iagrTmgsk4zBLjLvGANRsBvIw11dWWs5-6g,1193
4
- flwr/cli/build.py,sha256=HBlOLdUhWbD5Gu97r0WwISDLLOxl7-40RUyW4eVwbEo,4717
5
- flwr/cli/config_utils.py,sha256=9-QnORHRMmY_n2YI1qnlHzjmKFxKvDz4uqF7eIsSwh4,4411
4
+ flwr/cli/build.py,sha256=oiFnigccMkI9jjracsslvDJe-w1tomTr579SQd66_-Y,4741
5
+ flwr/cli/config_utils.py,sha256=BRYDMVOyYuOL8Ac10bjVA_RXMxSQ2KUKMcTFBaLHZlk,6017
6
6
  flwr/cli/example.py,sha256=1bGDYll3BXQY2kRqSN-oICqS5n1b9m0g0RvXTopXHl4,2215
7
- flwr/cli/install.py,sha256=amjRGefC3-0OpoSfzq9gcL-6DvBs8-60LkfwAZDvi9Q,6171
7
+ flwr/cli/install.py,sha256=Wz7Hqg2PE9N-w5CnqlH9Zr8mzADN2J7NLcUhgldZLWU,6579
8
8
  flwr/cli/new/__init__.py,sha256=cQzK1WH4JP2awef1t2UQ2xjl1agVEz9rwutV18SWV1k,789
9
9
  flwr/cli/new/new.py,sha256=7BWziuEOE15MXX4xNLH-w0-x0ytOEfYn_AUrbaDp13Y,6223
10
10
  flwr/cli/new/templates/__init__.py,sha256=4luU8RL-CK8JJCstQ_ON809W9bNTkY1l9zSaPKBkgwY,725
@@ -43,7 +43,7 @@ flwr/cli/run/__init__.py,sha256=oCd6HmQDx-sqver1gecgx-uMA38BLTSiiKpl7RGNceg,789
43
43
  flwr/cli/run/run.py,sha256=Oadt7JsJX549JG-2P1UPdF11vnblLWS8uGvuVx0modA,2687
44
44
  flwr/cli/utils.py,sha256=l65Ul0YsSBPuypk0uorAtEDmLEYiUrzpCXi6zCg9mJ4,4506
45
45
  flwr/client/__init__.py,sha256=tcgMyAW8brnmAIk4NmXkonVjYV3lafQJD4vfZ3OJ6kA,1279
46
- flwr/client/app.py,sha256=QwSlJ7duRor1X7PRqet0rjVD64Nzsw7KKYwfc7vK50Q,23793
46
+ flwr/client/app.py,sha256=GhL-eR_Y2H8e2XhUnq5AUGQWQya51b5iVr4I6RDW7Hc,24189
47
47
  flwr/client/client.py,sha256=Vp9UkOkoHdNfn6iMYZsj_5m_GICiFfUlKEVaLad-YhM,8183
48
48
  flwr/client/client_app.py,sha256=2jyVTzu8pwDtg66z4FjAa_kPzg31Q8-hx-RkDhguIqw,8635
49
49
  flwr/client/dpfedavg_numpy_client.py,sha256=9Tnig4iml2J88HBKNahegjXjbfvIQyBtaIQaqjbeqsA,7435
@@ -70,11 +70,12 @@ flwr/client/numpy_client.py,sha256=u76GWAdHmJM88Agm2EgLQSvO8Jnk225mJTk-_TmPjFE,1
70
70
  flwr/client/rest_client/__init__.py,sha256=ThwOnkMdzxo_UuyTI47Q7y9oSpuTgNT2OuFvJCfuDiw,735
71
71
  flwr/client/rest_client/connection.py,sha256=GvDPX2BdPwhBQGH6LQE50AzUxQvC7bisWK1pk_OR7eE,11567
72
72
  flwr/client/supernode/__init__.py,sha256=SUhWOzcgXRNXk1V9UgB5-FaWukqqrOEajVUHEcPkwyQ,865
73
- flwr/client/supernode/app.py,sha256=sc0VD8Il2UI1Lnh4tQwKSQMSsiS30WFrZtVMnz7WpWM,10873
73
+ flwr/client/supernode/app.py,sha256=sxRSLPkuKrihEjYMTPg9H1WbHtVl_izcg2se2fXvKR4,14701
74
74
  flwr/client/typing.py,sha256=c9EvjlEjasxn1Wqx6bGl6Xg6vM1gMFfmXht-E2i5J-k,1006
75
75
  flwr/common/__init__.py,sha256=dHOptgKxna78CEQLD5Yu0QIsoSgpIIw5AhIUZCHDWAU,3721
76
76
  flwr/common/address.py,sha256=iTAN9jtmIGMrWFnx9XZQl45ZEtQJVZZLYPRBSNVARGI,1882
77
- flwr/common/constant.py,sha256=GsixlCwohCBlQ6_mTk29HChuyRftgTELTCN06AxrOyc,2424
77
+ flwr/common/config.py,sha256=WEPvVsGPrJKVppNlEGK23p33vKbOuMqSvi8DVtyW_dU,1022
78
+ flwr/common/constant.py,sha256=k3QmlqQL4rfgjsO4xqTJ1QiGBHyJ5rTRE01FDEiNFCQ,2468
78
79
  flwr/common/context.py,sha256=ounF-mWPPtXGwtae3sg5EhF58ScviOa3MVqxRpGVu-8,1313
79
80
  flwr/common/date.py,sha256=UWhBZj49yX9LD4BmatS_ZFZu_-kweGh0KQJ1djyWWH4,891
80
81
  flwr/common/differential_privacy.py,sha256=WZWrL7C9XaB9l9NDkLDI5PvM7jwcoTTFu08ZVG8-M5Q,6113
@@ -105,7 +106,7 @@ flwr/common/secure_aggregation/quantization.py,sha256=appui7GGrkRPsupF59TkapeV4N
105
106
  flwr/common/secure_aggregation/secaggplus_constants.py,sha256=Fh7-n6pgL4TUnHpNYXo8iW-n5cOGQgQa-c7RcU80tqQ,2183
106
107
  flwr/common/secure_aggregation/secaggplus_utils.py,sha256=87bNZX6CmQekj935R4u3m5hsaEkkfKtGSA-VG2c-O9w,3221
107
108
  flwr/common/serde.py,sha256=Yn83kbSf9vJndTa5ldL4DR_bL_wy_bD4lTlD3ZbB658,22250
108
- flwr/common/telemetry.py,sha256=Q84hW6l6MCtD8sgQI4sUcp-N-zqAo607jyApeXC5RpM,7865
109
+ flwr/common/telemetry.py,sha256=IGzOp87BYReCj5bEoZS6zDSKH0aXsmhMvhHx8fdC1v0,7948
109
110
  flwr/common/typing.py,sha256=3Wu6Ol1Ja6Gb0WdlcXVEn1EHYJbc4oRRJA81vEegxBo,4382
110
111
  flwr/common/version.py,sha256=_RDSMGZPEuGKYViZuXPotDtXMvh4iyDH9XOCO4qtPO8,666
111
112
  flwr/proto/__init__.py,sha256=hbY7JYakwZwCkYgCNlmHdc8rtvfoJbAZLalMdc--CGc,683
@@ -117,6 +118,10 @@ flwr/proto/error_pb2.py,sha256=LarjKL90LbwkXKlhzNrDssgl4DXcvIPve8NVCXHpsKA,1084
117
118
  flwr/proto/error_pb2.pyi,sha256=ZNH4HhJTU_KfMXlyCeg8FwU-fcUYxTqEmoJPtWtHikc,734
118
119
  flwr/proto/error_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
119
120
  flwr/proto/error_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
121
+ flwr/proto/exec_pb2.py,sha256=fwuyyK6WEwx-RngMZm6z5dqDeYcMKKxBBhwh9MJfvMA,1450
122
+ flwr/proto/exec_pb2.pyi,sha256=IMUD1GlhK0mizORQ-lkiU-JbpAefmo9GLpDNfAib2GM,1068
123
+ flwr/proto/exec_pb2_grpc.py,sha256=7yyi_J1Sri8LXzj5_MjhI7_hoxUMcjIAQcaE-Ghnvco,2480
124
+ flwr/proto/exec_pb2_grpc.pyi,sha256=w721aoQ_ggktlIbR973bFRhwbIlEhfNMjOH4hnwgQE0,743
120
125
  flwr/proto/fleet_pb2.py,sha256=sASthuSb5R0v1NH2g6vRDC4BeJUFPAso8bjcihAflo0,4543
121
126
  flwr/proto/fleet_pb2.pyi,sha256=45kQ9YINv3VG0nxWSjCN4SppdepjKW8rRBlxKxz7ud4,7571
122
127
  flwr/proto/fleet_pb2_grpc.py,sha256=4eP1jkEVuckzSxo16yNaOBrHPzxHqQfj2MaNtPql4Wk,10655
@@ -147,7 +152,7 @@ flwr/proto/transport_pb2_grpc.py,sha256=vLN3EHtx2aEEMCO4f1Upu-l27BPzd3-5pV-u8wPc
147
152
  flwr/proto/transport_pb2_grpc.pyi,sha256=AGXf8RiIiW2J5IKMlm_3qT3AzcDa4F3P5IqUjve_esA,766
148
153
  flwr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
149
154
  flwr/server/__init__.py,sha256=qS6A16gywlLJSQWIaI41Fy_fWa5NzHi6iyrOfLf2vQc,1575
150
- flwr/server/app.py,sha256=b89N5JW2bRL9T6yp8Lqoqgj4-Ey2h1PPH3hPzvIoxIY,22512
155
+ flwr/server/app.py,sha256=a7WA24EYpZBQ7qL6DgOCGvYqgE3oJYu_SpiGBs0jI0g,22249
151
156
  flwr/server/client_manager.py,sha256=T8UDSRJBVD3fyIDI7NTAA-NA7GPrMNNgH2OAF54RRxE,6127
152
157
  flwr/server/client_proxy.py,sha256=4G-oTwhb45sfWLx2uZdcXD98IZwdTS6F88xe3akCdUg,2399
153
158
  flwr/server/compat/__init__.py,sha256=VxnJtJyOjNFQXMNi9hIuzNlZM5n0Hj1p3aq_Pm2udw4,892
@@ -234,8 +239,9 @@ flwr/simulation/ray_transport/ray_actor.py,sha256=_wv2eP7qxkCZ-6rMyYWnjLrGPBZRxj
234
239
  flwr/simulation/ray_transport/ray_client_proxy.py,sha256=oDu4sEPIOu39vrNi-fqDAe10xtNUXMO49bM2RWfRcyw,6738
235
240
  flwr/simulation/ray_transport/utils.py,sha256=TYdtfg1P9VfTdLMOJlifInGpxWHYs9UfUqIv2wfkRLA,2392
236
241
  flwr/simulation/run_simulation.py,sha256=Jmc6DyN5UCY1U1PcDvL04NgYmEQ6ufJ1JisjG5yqfY8,15098
237
- flwr_nightly-1.10.0.dev20240612.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
238
- flwr_nightly-1.10.0.dev20240612.dist-info/METADATA,sha256=y6cyp6G3w-a07uF7toQ9YHV4BYbtiUmy4zLYcYrrlP8,15518
239
- flwr_nightly-1.10.0.dev20240612.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
240
- flwr_nightly-1.10.0.dev20240612.dist-info/entry_points.txt,sha256=lnx02_kvueYDnkzBh0sdyjFxAVy0CqrIBJaDALiIXlc,290
241
- flwr_nightly-1.10.0.dev20240612.dist-info/RECORD,,
242
+ flwr/superexec/__init__.py,sha256=aoByPmwiFT1okczzloEZ3zH7BBvKlA_s1TL_mqQW2jQ,767
243
+ flwr_nightly-1.10.0.dev20240613.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
244
+ flwr_nightly-1.10.0.dev20240613.dist-info/METADATA,sha256=B1svyAE6DUOY-lBKqD6LW1qTf3jo9L8nTH7ZOJbsKmo,15518
245
+ flwr_nightly-1.10.0.dev20240613.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
246
+ flwr_nightly-1.10.0.dev20240613.dist-info/entry_points.txt,sha256=7qBQcA-bDGDxnJmLd9FYqglFQubjCNqyg9M8a-lukps,336
247
+ flwr_nightly-1.10.0.dev20240613.dist-info/RECORD,,
@@ -2,6 +2,7 @@
2
2
  flower-client-app=flwr.client:run_client_app
3
3
  flower-server-app=flwr.server:run_server_app
4
4
  flower-simulation=flwr.simulation.run_simulation:run_simulation_from_cli
5
+ flower-superexec=flwr.superexec:run_superexec
5
6
  flower-superlink=flwr.server:run_superlink
6
7
  flower-supernode=flwr.client:run_supernode
7
8
  flwr=flwr.cli.app:app