flwr 1.23.0__py3-none-any.whl → 1.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/__init__.py +16 -5
- flwr/app/error.py +2 -2
- flwr/app/exception.py +3 -3
- flwr/cli/app.py +19 -0
- flwr/cli/app_cmd/__init__.py +23 -0
- flwr/cli/app_cmd/publish.py +285 -0
- flwr/cli/app_cmd/review.py +252 -0
- flwr/cli/auth_plugin/auth_plugin.py +4 -5
- flwr/cli/auth_plugin/noop_auth_plugin.py +54 -11
- flwr/cli/auth_plugin/oidc_cli_plugin.py +32 -9
- flwr/cli/build.py +60 -18
- flwr/cli/cli_account_auth_interceptor.py +24 -7
- flwr/cli/config_utils.py +101 -13
- flwr/cli/federation/__init__.py +24 -0
- flwr/cli/federation/ls.py +140 -0
- flwr/cli/federation/show.py +317 -0
- flwr/cli/install.py +91 -13
- flwr/cli/log.py +52 -9
- flwr/cli/login/login.py +7 -4
- flwr/cli/ls.py +170 -130
- flwr/cli/new/new.py +33 -50
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
- flwr/cli/pull.py +10 -5
- flwr/cli/run/run.py +77 -30
- flwr/cli/run_utils.py +130 -0
- flwr/cli/stop.py +25 -7
- flwr/cli/supernode/ls.py +16 -8
- flwr/cli/supernode/register.py +9 -4
- flwr/cli/supernode/unregister.py +5 -3
- flwr/cli/utils.py +376 -16
- flwr/client/__init__.py +1 -1
- flwr/client/dpfedavg_numpy_client.py +4 -1
- flwr/client/grpc_adapter_client/connection.py +6 -7
- flwr/client/grpc_rere_client/connection.py +10 -11
- flwr/client/grpc_rere_client/grpc_adapter.py +6 -2
- flwr/client/grpc_rere_client/node_auth_client_interceptor.py +2 -1
- flwr/client/message_handler/message_handler.py +2 -2
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +3 -3
- flwr/client/numpy_client.py +1 -1
- flwr/client/rest_client/connection.py +12 -14
- flwr/client/run_info_store.py +4 -5
- flwr/client/typing.py +1 -1
- flwr/clientapp/client_app.py +9 -10
- flwr/clientapp/mod/centraldp_mods.py +16 -17
- flwr/clientapp/mod/localdp_mod.py +8 -9
- flwr/clientapp/typing.py +1 -1
- flwr/clientapp/utils.py +3 -3
- flwr/common/address.py +1 -2
- flwr/common/args.py +3 -4
- flwr/common/config.py +13 -16
- flwr/common/constant.py +5 -2
- flwr/common/differential_privacy.py +3 -4
- flwr/common/event_log_plugin/event_log_plugin.py +3 -4
- flwr/common/exit/exit.py +15 -2
- flwr/common/exit/exit_code.py +19 -0
- flwr/common/exit/exit_handler.py +6 -2
- flwr/common/exit/signal_handler.py +5 -5
- flwr/common/grpc.py +6 -6
- flwr/common/inflatable_protobuf_utils.py +1 -1
- flwr/common/inflatable_utils.py +38 -21
- flwr/common/logger.py +19 -19
- flwr/common/message.py +4 -4
- flwr/common/object_ref.py +7 -7
- flwr/common/record/array.py +3 -3
- flwr/common/record/arrayrecord.py +18 -30
- flwr/common/record/configrecord.py +3 -3
- flwr/common/record/recorddict.py +5 -5
- flwr/common/record/typeddict.py +9 -2
- flwr/common/recorddict_compat.py +7 -10
- flwr/common/retry_invoker.py +20 -20
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
- flwr/common/serde.py +5 -4
- flwr/common/serde_utils.py +2 -2
- flwr/common/telemetry.py +9 -5
- flwr/common/typing.py +52 -37
- flwr/compat/client/app.py +38 -37
- flwr/compat/client/grpc_client/connection.py +11 -11
- flwr/compat/server/app.py +5 -6
- flwr/proto/appio_pb2.py +13 -3
- flwr/proto/appio_pb2.pyi +134 -65
- flwr/proto/appio_pb2_grpc.py +20 -0
- flwr/proto/appio_pb2_grpc.pyi +27 -0
- flwr/proto/clientappio_pb2.py +17 -7
- flwr/proto/clientappio_pb2.pyi +15 -0
- flwr/proto/clientappio_pb2_grpc.py +206 -40
- flwr/proto/clientappio_pb2_grpc.pyi +168 -53
- flwr/proto/control_pb2.py +71 -52
- flwr/proto/control_pb2.pyi +277 -111
- flwr/proto/control_pb2_grpc.py +249 -40
- flwr/proto/control_pb2_grpc.pyi +185 -52
- flwr/proto/error_pb2.py +13 -3
- flwr/proto/error_pb2.pyi +24 -6
- flwr/proto/error_pb2_grpc.py +20 -0
- flwr/proto/error_pb2_grpc.pyi +27 -0
- flwr/proto/fab_pb2.py +14 -4
- flwr/proto/fab_pb2.pyi +59 -31
- flwr/proto/fab_pb2_grpc.py +20 -0
- flwr/proto/fab_pb2_grpc.pyi +27 -0
- flwr/proto/federation_pb2.py +38 -0
- flwr/proto/federation_pb2.pyi +56 -0
- flwr/proto/federation_pb2_grpc.py +24 -0
- flwr/proto/federation_pb2_grpc.pyi +31 -0
- flwr/proto/fleet_pb2.py +14 -4
- flwr/proto/fleet_pb2.pyi +137 -61
- flwr/proto/fleet_pb2_grpc.py +189 -48
- flwr/proto/fleet_pb2_grpc.pyi +175 -61
- flwr/proto/grpcadapter_pb2.py +14 -4
- flwr/proto/grpcadapter_pb2.pyi +38 -16
- flwr/proto/grpcadapter_pb2_grpc.py +35 -4
- flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
- flwr/proto/heartbeat_pb2.py +17 -7
- flwr/proto/heartbeat_pb2.pyi +51 -22
- flwr/proto/heartbeat_pb2_grpc.py +20 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
- flwr/proto/log_pb2.py +13 -3
- flwr/proto/log_pb2.pyi +34 -11
- flwr/proto/log_pb2_grpc.py +20 -0
- flwr/proto/log_pb2_grpc.pyi +27 -0
- flwr/proto/message_pb2.py +15 -5
- flwr/proto/message_pb2.pyi +154 -86
- flwr/proto/message_pb2_grpc.py +20 -0
- flwr/proto/message_pb2_grpc.pyi +27 -0
- flwr/proto/node_pb2.py +15 -5
- flwr/proto/node_pb2.pyi +50 -25
- flwr/proto/node_pb2_grpc.py +20 -0
- flwr/proto/node_pb2_grpc.pyi +27 -0
- flwr/proto/recorddict_pb2.py +13 -3
- flwr/proto/recorddict_pb2.pyi +184 -107
- flwr/proto/recorddict_pb2_grpc.py +20 -0
- flwr/proto/recorddict_pb2_grpc.pyi +27 -0
- flwr/proto/run_pb2.py +40 -31
- flwr/proto/run_pb2.pyi +149 -84
- flwr/proto/run_pb2_grpc.py +20 -0
- flwr/proto/run_pb2_grpc.pyi +27 -0
- flwr/proto/serverappio_pb2.py +13 -3
- flwr/proto/serverappio_pb2.pyi +32 -8
- flwr/proto/serverappio_pb2_grpc.py +246 -65
- flwr/proto/serverappio_pb2_grpc.pyi +221 -85
- flwr/proto/simulationio_pb2.py +16 -8
- flwr/proto/simulationio_pb2.pyi +15 -0
- flwr/proto/simulationio_pb2_grpc.py +162 -41
- flwr/proto/simulationio_pb2_grpc.pyi +149 -55
- flwr/proto/transport_pb2.py +20 -10
- flwr/proto/transport_pb2.pyi +249 -160
- flwr/proto/transport_pb2_grpc.py +35 -4
- flwr/proto/transport_pb2_grpc.pyi +38 -8
- flwr/server/app.py +38 -17
- flwr/server/client_manager.py +4 -5
- flwr/server/client_proxy.py +10 -11
- flwr/server/compat/app.py +4 -5
- flwr/server/compat/app_utils.py +2 -1
- flwr/server/compat/grid_client_proxy.py +10 -12
- flwr/server/compat/legacy_context.py +3 -4
- flwr/server/fleet_event_log_interceptor.py +2 -1
- flwr/server/grid/grid.py +2 -3
- flwr/server/grid/grpc_grid.py +10 -8
- flwr/server/grid/inmemory_grid.py +4 -4
- flwr/server/run_serverapp.py +2 -3
- flwr/server/server.py +34 -39
- flwr/server/server_app.py +7 -8
- flwr/server/server_config.py +1 -2
- flwr/server/serverapp/app.py +34 -28
- flwr/server/serverapp_components.py +4 -5
- flwr/server/strategy/aggregate.py +9 -8
- flwr/server/strategy/bulyan.py +13 -11
- flwr/server/strategy/dp_adaptive_clipping.py +16 -20
- flwr/server/strategy/dp_fixed_clipping.py +12 -17
- flwr/server/strategy/dpfedavg_adaptive.py +3 -4
- flwr/server/strategy/dpfedavg_fixed.py +6 -10
- flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
- flwr/server/strategy/fedadagrad.py +18 -14
- flwr/server/strategy/fedadam.py +16 -14
- flwr/server/strategy/fedavg.py +16 -17
- flwr/server/strategy/fedavg_android.py +15 -15
- flwr/server/strategy/fedavgm.py +21 -18
- flwr/server/strategy/fedmedian.py +2 -3
- flwr/server/strategy/fedopt.py +11 -10
- flwr/server/strategy/fedprox.py +10 -9
- flwr/server/strategy/fedtrimmedavg.py +12 -11
- flwr/server/strategy/fedxgb_bagging.py +13 -11
- flwr/server/strategy/fedxgb_cyclic.py +6 -6
- flwr/server/strategy/fedxgb_nn_avg.py +4 -4
- flwr/server/strategy/fedyogi.py +16 -14
- flwr/server/strategy/krum.py +12 -11
- flwr/server/strategy/qfedavg.py +16 -15
- flwr/server/strategy/strategy.py +6 -9
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +2 -1
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -4
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +3 -2
- flwr/server/superlink/fleet/message_handler/message_handler.py +34 -28
- flwr/server/superlink/fleet/rest_rere/rest_api.py +2 -2
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +5 -5
- flwr/server/superlink/fleet/vce/vce_api.py +15 -9
- flwr/server/superlink/linkstate/in_memory_linkstate.py +115 -150
- flwr/server/superlink/linkstate/linkstate.py +59 -43
- flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
- flwr/server/superlink/linkstate/sqlite_linkstate.py +447 -438
- flwr/server/superlink/linkstate/utils.py +6 -6
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +26 -21
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +18 -13
- flwr/server/superlink/utils.py +4 -6
- flwr/server/typing.py +1 -1
- flwr/server/utils/tensorboard.py +15 -8
- flwr/server/workflow/default_workflows.py +5 -5
- flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +8 -8
- flwr/serverapp/strategy/bulyan.py +16 -15
- flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
- flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
- flwr/serverapp/strategy/fedadagrad.py +10 -11
- flwr/serverapp/strategy/fedadam.py +10 -11
- flwr/serverapp/strategy/fedavg.py +9 -10
- flwr/serverapp/strategy/fedavgm.py +17 -16
- flwr/serverapp/strategy/fedmedian.py +2 -2
- flwr/serverapp/strategy/fedopt.py +10 -11
- flwr/serverapp/strategy/fedprox.py +7 -8
- flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
- flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
- flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
- flwr/serverapp/strategy/fedyogi.py +9 -11
- flwr/serverapp/strategy/krum.py +7 -7
- flwr/serverapp/strategy/multikrum.py +9 -9
- flwr/serverapp/strategy/qfedavg.py +17 -16
- flwr/serverapp/strategy/strategy.py +6 -9
- flwr/serverapp/strategy/strategy_utils.py +7 -8
- flwr/simulation/app.py +46 -42
- flwr/simulation/legacy_app.py +12 -12
- flwr/simulation/ray_transport/ray_actor.py +10 -11
- flwr/simulation/ray_transport/ray_client_proxy.py +11 -12
- flwr/simulation/run_simulation.py +43 -43
- flwr/simulation/simulationio_connection.py +4 -4
- flwr/supercore/cli/flower_superexec.py +3 -4
- flwr/supercore/constant.py +31 -1
- flwr/supercore/corestate/corestate.py +24 -3
- flwr/supercore/corestate/in_memory_corestate.py +138 -0
- flwr/supercore/corestate/sqlite_corestate.py +157 -0
- flwr/supercore/ffs/disk_ffs.py +1 -2
- flwr/supercore/ffs/ffs.py +1 -2
- flwr/supercore/ffs/ffs_factory.py +1 -2
- flwr/{common → supercore}/heartbeat.py +20 -25
- flwr/supercore/object_store/in_memory_object_store.py +1 -2
- flwr/supercore/object_store/object_store.py +1 -2
- flwr/supercore/object_store/object_store_factory.py +1 -2
- flwr/supercore/object_store/sqlite_object_store.py +8 -7
- flwr/supercore/primitives/asymmetric.py +1 -1
- flwr/supercore/primitives/asymmetric_ed25519.py +11 -1
- flwr/supercore/sqlite_mixin.py +37 -34
- flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
- flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
- flwr/supercore/superexec/run_superexec.py +9 -13
- flwr/superlink/artifact_provider/artifact_provider.py +1 -2
- flwr/superlink/auth_plugin/auth_plugin.py +6 -9
- flwr/superlink/auth_plugin/noop_auth_plugin.py +6 -9
- flwr/superlink/federation/__init__.py +24 -0
- flwr/superlink/federation/federation_manager.py +64 -0
- flwr/superlink/federation/noop_federation_manager.py +71 -0
- flwr/superlink/servicer/control/control_account_auth_interceptor.py +22 -13
- flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
- flwr/superlink/servicer/control/control_grpc.py +5 -6
- flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
- flwr/superlink/servicer/control/control_servicer.py +102 -18
- flwr/supernode/cli/flower_supernode.py +58 -3
- flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
- flwr/supernode/nodestate/nodestate.py +7 -8
- flwr/supernode/nodestate/nodestate_factory.py +7 -4
- flwr/supernode/runtime/run_clientapp.py +41 -22
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
- flwr/supernode/start_client_internal.py +158 -42
- {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
- flwr-1.24.0.dist-info/RECORD +454 -0
- flwr/supercore/object_store/utils.py +0 -43
- flwr-1.23.0.dist-info/RECORD +0 -439
- {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
- {flwr-1.23.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
flwr/cli/new/new.py
CHANGED
|
@@ -16,23 +16,24 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import io
|
|
19
|
-
import json
|
|
20
19
|
import re
|
|
21
20
|
import zipfile
|
|
22
21
|
from enum import Enum
|
|
23
22
|
from pathlib import Path
|
|
24
23
|
from string import Template
|
|
25
|
-
from typing import Annotated
|
|
24
|
+
from typing import Annotated
|
|
26
25
|
|
|
27
26
|
import requests
|
|
28
27
|
import typer
|
|
29
28
|
|
|
30
|
-
from flwr.supercore.constant import
|
|
29
|
+
from flwr.supercore.constant import PLATFORM_API_URL
|
|
31
30
|
|
|
32
31
|
from ..utils import (
|
|
33
32
|
is_valid_project_name,
|
|
33
|
+
parse_app_spec,
|
|
34
34
|
prompt_options,
|
|
35
35
|
prompt_text,
|
|
36
|
+
request_download_link,
|
|
36
37
|
sanitize_project_name,
|
|
37
38
|
)
|
|
38
39
|
|
|
@@ -100,7 +101,7 @@ def render_and_create(file_path: Path, template: str, context: dict[str, str]) -
|
|
|
100
101
|
|
|
101
102
|
|
|
102
103
|
def print_success_prompt(
|
|
103
|
-
package_name: str, llm_challenge_str:
|
|
104
|
+
package_name: str, llm_challenge_str: str | None = None
|
|
104
105
|
) -> None:
|
|
105
106
|
"""Print styled setup instructions for running a new Flower App after creation."""
|
|
106
107
|
prompt = typer.style(
|
|
@@ -181,55 +182,31 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
|
|
|
181
182
|
r = requests.get(presigned_url, timeout=60)
|
|
182
183
|
r.raise_for_status()
|
|
183
184
|
except requests.RequestException as e:
|
|
184
|
-
|
|
185
|
+
typer.secho(
|
|
186
|
+
f"ZIP download failed: {e}",
|
|
187
|
+
fg=typer.colors.RED,
|
|
188
|
+
err=True,
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(code=1) from e
|
|
185
191
|
|
|
186
192
|
buf = io.BytesIO(r.content)
|
|
187
193
|
# Validate it's a zip
|
|
188
194
|
if not zipfile.is_zipfile(buf):
|
|
189
|
-
|
|
195
|
+
typer.secho(
|
|
196
|
+
"Downloaded file is not a valid ZIP",
|
|
197
|
+
fg=typer.colors.RED,
|
|
198
|
+
err=True,
|
|
199
|
+
)
|
|
200
|
+
raise typer.Exit(code=1)
|
|
190
201
|
buf.seek(0)
|
|
191
202
|
return buf
|
|
192
203
|
|
|
193
204
|
|
|
194
|
-
def
|
|
195
|
-
"""Request download link from Flower platform API."""
|
|
196
|
-
url = f"{PLATFORM_API_URL}/hub/fetch-zip"
|
|
197
|
-
headers = {
|
|
198
|
-
"Content-Type": "application/json",
|
|
199
|
-
"Accept": "application/json",
|
|
200
|
-
}
|
|
201
|
-
body = {
|
|
202
|
-
"identifier": identifier, # send raw string of identifier
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
try:
|
|
206
|
-
resp = requests.post(url, headers=headers, data=json.dumps(body), timeout=20)
|
|
207
|
-
except requests.RequestException as e:
|
|
208
|
-
raise typer.BadParameter(f"Unable to connect to Platform API: {e}") from e
|
|
209
|
-
|
|
210
|
-
if resp.status_code == 404:
|
|
211
|
-
raise typer.BadParameter(f"'{identifier}' not found in Platform API")
|
|
212
|
-
if not resp.ok:
|
|
213
|
-
raise typer.BadParameter(
|
|
214
|
-
f"Platform API request failed with "
|
|
215
|
-
f"status {resp.status_code}. Details: {resp.text}"
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
data = resp.json()
|
|
219
|
-
if "zip_url" not in data:
|
|
220
|
-
raise typer.BadParameter("Invalid response from Platform API")
|
|
221
|
-
return str(data["zip_url"])
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def download_remote_app_via_api(identifier: str) -> None:
|
|
205
|
+
def download_remote_app_via_api(app_spec: str) -> None:
|
|
225
206
|
"""Download App from Platform API."""
|
|
226
|
-
#
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
raise typer.BadParameter(
|
|
230
|
-
"Invalid remote app ID. Expected format: '@user_name/app_name'."
|
|
231
|
-
)
|
|
232
|
-
app_name = m.group("app")
|
|
207
|
+
# Validate app version and ID format
|
|
208
|
+
app_id, app_version = parse_app_spec(app_spec)
|
|
209
|
+
app_name = app_id.split("/")[1]
|
|
233
210
|
|
|
234
211
|
project_dir = Path.cwd() / app_name
|
|
235
212
|
if project_dir.exists():
|
|
@@ -244,12 +221,14 @@ def download_remote_app_via_api(identifier: str) -> None:
|
|
|
244
221
|
|
|
245
222
|
print(
|
|
246
223
|
typer.style(
|
|
247
|
-
f"\n🔗 Requesting download link for {
|
|
224
|
+
f"\n🔗 Requesting download link for {app_id}...",
|
|
248
225
|
fg=typer.colors.GREEN,
|
|
249
226
|
bold=True,
|
|
250
227
|
)
|
|
251
228
|
)
|
|
252
|
-
|
|
229
|
+
# Fetch ZIP downloading URL
|
|
230
|
+
url = f"{PLATFORM_API_URL}/hub/fetch-zip"
|
|
231
|
+
presigned_url = request_download_link(app_id, app_version, url, "zip_url")
|
|
253
232
|
|
|
254
233
|
print(
|
|
255
234
|
typer.style(
|
|
@@ -276,15 +255,19 @@ def download_remote_app_via_api(identifier: str) -> None:
|
|
|
276
255
|
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
277
256
|
def new(
|
|
278
257
|
app_name: Annotated[
|
|
279
|
-
|
|
280
|
-
typer.Argument(
|
|
258
|
+
str | None,
|
|
259
|
+
typer.Argument(
|
|
260
|
+
help="Flower app name. For remote apps, use the format "
|
|
261
|
+
"'@account_name/app_name' or '@account_name/app_name==x.y.z'. "
|
|
262
|
+
"Version is optional (defaults to latest)."
|
|
263
|
+
),
|
|
281
264
|
] = None,
|
|
282
265
|
framework: Annotated[
|
|
283
|
-
|
|
266
|
+
MlFramework | None,
|
|
284
267
|
typer.Option(case_sensitive=False, help="The ML framework to use"),
|
|
285
268
|
] = None,
|
|
286
269
|
username: Annotated[
|
|
287
|
-
|
|
270
|
+
str | None,
|
|
288
271
|
typer.Option(case_sensitive=False, help="The Flower username of the author"),
|
|
289
272
|
] = None,
|
|
290
273
|
) -> None:
|
|
@@ -14,10 +14,10 @@ description = ""
|
|
|
14
14
|
license = "Apache-2.0"
|
|
15
15
|
# Dependencies for your Flower App
|
|
16
16
|
dependencies = [
|
|
17
|
-
"flwr[simulation]>=1.
|
|
17
|
+
"flwr[simulation]>=1.24.0",
|
|
18
18
|
"flwr-datasets[vision]>=0.5.0",
|
|
19
|
-
"torch
|
|
20
|
-
"torchvision
|
|
19
|
+
"torch>=2.7.1",
|
|
20
|
+
"torchvision>=0.22.1",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[tool.hatch.build.targets.wheel]
|
|
@@ -14,9 +14,9 @@ description = ""
|
|
|
14
14
|
license = "Apache-2.0"
|
|
15
15
|
# Dependencies for your Flower App
|
|
16
16
|
dependencies = [
|
|
17
|
-
"flwr[simulation]>=1.
|
|
17
|
+
"flwr[simulation]>=1.24.0",
|
|
18
18
|
"flwr-datasets[vision]>=0.5.0",
|
|
19
|
-
"tensorflow>=2.
|
|
19
|
+
"tensorflow>=2.18.0",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[tool.hatch.build.targets.wheel]
|
flwr/cli/pull.py
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Annotated
|
|
19
|
+
from typing import Annotated
|
|
20
20
|
|
|
21
21
|
import typer
|
|
22
22
|
|
|
@@ -50,22 +50,26 @@ def pull( # pylint: disable=R0914
|
|
|
50
50
|
typer.Argument(help="Path of the Flower App to run."),
|
|
51
51
|
] = Path("."),
|
|
52
52
|
federation: Annotated[
|
|
53
|
-
|
|
53
|
+
str | None,
|
|
54
54
|
typer.Argument(help="Name of the federation."),
|
|
55
55
|
] = None,
|
|
56
56
|
federation_config_overrides: Annotated[
|
|
57
|
-
|
|
57
|
+
list[str] | None,
|
|
58
58
|
typer.Option(
|
|
59
59
|
"--federation-config",
|
|
60
60
|
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
61
61
|
),
|
|
62
62
|
] = None,
|
|
63
63
|
) -> None:
|
|
64
|
-
"""Pull artifacts from a Flower run.
|
|
64
|
+
"""Pull artifacts from a Flower run.
|
|
65
|
+
|
|
66
|
+
Retrieve a download URL for artifacts generated during a completed Flower run. The
|
|
67
|
+
artifacts can then be downloaded from the provided URL.
|
|
68
|
+
"""
|
|
65
69
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
66
70
|
|
|
67
71
|
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
68
|
-
config, errors, warnings = load_and_validate(
|
|
72
|
+
config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
|
|
69
73
|
config = process_loaded_project_config(config, errors, warnings)
|
|
70
74
|
federation, federation_config = validate_federation_in_project_config(
|
|
71
75
|
federation, config, federation_config_overrides
|
|
@@ -88,6 +92,7 @@ def pull( # pylint: disable=R0914
|
|
|
88
92
|
"obtained.",
|
|
89
93
|
fg=typer.colors.RED,
|
|
90
94
|
bold=True,
|
|
95
|
+
err=True,
|
|
91
96
|
)
|
|
92
97
|
raise typer.Exit(code=1)
|
|
93
98
|
|
flwr/cli/run/run.py
CHANGED
|
@@ -20,7 +20,7 @@ import io
|
|
|
20
20
|
import json
|
|
21
21
|
import subprocess
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Annotated, Any,
|
|
23
|
+
from typing import Annotated, Any, cast
|
|
24
24
|
|
|
25
25
|
import typer
|
|
26
26
|
from rich.console import Console
|
|
@@ -45,25 +45,31 @@ from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_
|
|
|
45
45
|
from flwr.common.typing import Fab
|
|
46
46
|
from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
|
|
47
47
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
48
|
+
from flwr.supercore.constant import NOOP_FEDERATION
|
|
48
49
|
|
|
49
50
|
from ..log import start_stream
|
|
50
|
-
from ..utils import
|
|
51
|
+
from ..utils import (
|
|
52
|
+
flwr_cli_grpc_exc_handler,
|
|
53
|
+
init_channel,
|
|
54
|
+
load_cli_auth_plugin,
|
|
55
|
+
parse_app_spec,
|
|
56
|
+
)
|
|
51
57
|
|
|
52
58
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
|
53
59
|
|
|
54
60
|
|
|
55
|
-
# pylint: disable-next=too-many-locals, R0913, R0917
|
|
61
|
+
# pylint: disable-next=too-many-locals, too-many-branches, R0913, R0917
|
|
56
62
|
def run(
|
|
57
63
|
app: Annotated[
|
|
58
64
|
Path,
|
|
59
65
|
typer.Argument(help="Path of the Flower App to run."),
|
|
60
66
|
] = Path("."),
|
|
61
67
|
federation: Annotated[
|
|
62
|
-
|
|
68
|
+
str | None,
|
|
63
69
|
typer.Argument(help="Name of the federation to run the app on."),
|
|
64
70
|
] = None,
|
|
65
71
|
run_config_overrides: Annotated[
|
|
66
|
-
|
|
72
|
+
list[str] | None,
|
|
67
73
|
typer.Option(
|
|
68
74
|
"--run-config",
|
|
69
75
|
"-c",
|
|
@@ -71,7 +77,7 @@ def run(
|
|
|
71
77
|
),
|
|
72
78
|
] = None,
|
|
73
79
|
federation_config_overrides: Annotated[
|
|
74
|
-
|
|
80
|
+
list[str] | None,
|
|
75
81
|
typer.Option(
|
|
76
82
|
"--federation-config",
|
|
77
83
|
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
@@ -100,11 +106,25 @@ def run(
|
|
|
100
106
|
try:
|
|
101
107
|
if suppress_output:
|
|
102
108
|
redirect_output(captured_output)
|
|
109
|
+
|
|
110
|
+
# Determine if app is remote
|
|
111
|
+
app_spec = None
|
|
112
|
+
if (app_str := str(app)).startswith("@"):
|
|
113
|
+
# Validate app version and ID format
|
|
114
|
+
_ = parse_app_spec(app_str)
|
|
115
|
+
app_spec = app_str
|
|
116
|
+
is_remote_app = app_spec is not None
|
|
117
|
+
|
|
103
118
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
# Disable the validation for remote apps
|
|
121
|
+
pyproject_path = app / "pyproject.toml" if not is_remote_app else None
|
|
122
|
+
# `./pyproject.toml` will be loaded when `pyproject_path` is None
|
|
123
|
+
config, errors, warnings = load_and_validate(
|
|
124
|
+
pyproject_path, check_module=not is_remote_app
|
|
125
|
+
)
|
|
107
126
|
config = process_loaded_project_config(config, errors, warnings)
|
|
127
|
+
|
|
108
128
|
federation, federation_config = validate_federation_in_project_config(
|
|
109
129
|
federation, config, federation_config_overrides
|
|
110
130
|
)
|
|
@@ -117,6 +137,7 @@ def run(
|
|
|
117
137
|
run_config_overrides,
|
|
118
138
|
stream,
|
|
119
139
|
output_format,
|
|
140
|
+
app_spec,
|
|
120
141
|
)
|
|
121
142
|
else:
|
|
122
143
|
_run_without_control_api(
|
|
@@ -132,6 +153,7 @@ def run(
|
|
|
132
153
|
f"{err}",
|
|
133
154
|
fg=typer.colors.RED,
|
|
134
155
|
bold=True,
|
|
156
|
+
err=True,
|
|
135
157
|
)
|
|
136
158
|
finally:
|
|
137
159
|
if suppress_output:
|
|
@@ -144,22 +166,32 @@ def _run_with_control_api(
|
|
|
144
166
|
app: Path,
|
|
145
167
|
federation: str,
|
|
146
168
|
federation_config: dict[str, Any],
|
|
147
|
-
config_overrides:
|
|
169
|
+
config_overrides: list[str] | None,
|
|
148
170
|
stream: bool,
|
|
149
171
|
output_format: str,
|
|
172
|
+
app_spec: str | None,
|
|
150
173
|
) -> None:
|
|
151
174
|
channel = None
|
|
175
|
+
is_remote_app = app_spec is not None
|
|
152
176
|
try:
|
|
153
177
|
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
|
154
178
|
channel = init_channel(app, federation_config, auth_plugin)
|
|
155
179
|
stub = ControlStub(channel)
|
|
156
180
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
181
|
+
# Build FAB if local app
|
|
182
|
+
if not is_remote_app:
|
|
183
|
+
fab_bytes = build_fab_from_disk(app)
|
|
184
|
+
fab_hash = hashlib.sha256(fab_bytes).hexdigest()
|
|
185
|
+
config = cast(dict[str, Any], load_toml(app / FAB_CONFIG_FILE))
|
|
186
|
+
fab_id, fab_version = get_metadata_from_config(config)
|
|
187
|
+
fab = Fab(fab_hash, fab_bytes, {})
|
|
188
|
+
# Skip FAB build if remote app
|
|
189
|
+
else:
|
|
190
|
+
# Use empty values for FAB
|
|
191
|
+
fab_id = fab_version = fab_hash = ""
|
|
192
|
+
fab = Fab(fab_hash, b"", {})
|
|
161
193
|
|
|
162
|
-
|
|
194
|
+
real_federation: str = federation_config.get("federation", NOOP_FEDERATION)
|
|
163
195
|
|
|
164
196
|
# Construct a `ConfigRecord` out of a flattened `UserConfig`
|
|
165
197
|
fed_config = flatten_dict(federation_config.get("options", {}))
|
|
@@ -168,7 +200,9 @@ def _run_with_control_api(
|
|
|
168
200
|
req = StartRunRequest(
|
|
169
201
|
fab=fab_to_proto(fab),
|
|
170
202
|
override_config=user_config_to_proto(parse_config_args(config_overrides)),
|
|
203
|
+
federation=real_federation,
|
|
171
204
|
federation_options=config_record_to_proto(c_record),
|
|
205
|
+
app_spec=app_spec or "",
|
|
172
206
|
)
|
|
173
207
|
with flwr_cli_grpc_exc_handler():
|
|
174
208
|
res = stub.StartRun(req)
|
|
@@ -178,23 +212,35 @@ def _run_with_control_api(
|
|
|
178
212
|
f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN
|
|
179
213
|
)
|
|
180
214
|
else:
|
|
181
|
-
|
|
215
|
+
if is_remote_app:
|
|
216
|
+
typer.secho(
|
|
217
|
+
"❌ Failed to start run. Please check that the provided "
|
|
218
|
+
"app identifier (@account_name/app_name) is correct.",
|
|
219
|
+
fg=typer.colors.RED,
|
|
220
|
+
err=True,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
typer.secho("❌ Failed to start run", fg=typer.colors.RED, err=True)
|
|
182
224
|
raise typer.Exit(code=1)
|
|
183
225
|
|
|
184
226
|
if output_format == CliOutputFormat.JSON:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
227
|
+
# Only include FAB metadata if we actually built a local FAB
|
|
228
|
+
payload: dict[str, Any] = {
|
|
229
|
+
"success": res.HasField("run_id"),
|
|
230
|
+
"run-id": res.run_id if res.HasField("run_id") else None,
|
|
231
|
+
}
|
|
232
|
+
if not is_remote_app:
|
|
233
|
+
payload.update(
|
|
234
|
+
{
|
|
235
|
+
"fab-id": fab_id,
|
|
236
|
+
"fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
|
|
237
|
+
"fab-version": fab_version,
|
|
238
|
+
"fab-hash": fab_hash[:8],
|
|
239
|
+
"fab-filename": get_fab_filename(config, fab_hash),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
196
242
|
restore_output()
|
|
197
|
-
Console().print_json(
|
|
243
|
+
Console().print_json(json.dumps(payload))
|
|
198
244
|
|
|
199
245
|
if stream:
|
|
200
246
|
start_stream(res.run_id, channel, CONN_REFRESH_PERIOD)
|
|
@@ -204,14 +250,14 @@ def _run_with_control_api(
|
|
|
204
250
|
|
|
205
251
|
|
|
206
252
|
def _run_without_control_api(
|
|
207
|
-
app:
|
|
253
|
+
app: Path | None,
|
|
208
254
|
federation_config: dict[str, Any],
|
|
209
|
-
config_overrides:
|
|
255
|
+
config_overrides: list[str] | None,
|
|
210
256
|
federation: str,
|
|
211
257
|
) -> None:
|
|
212
258
|
try:
|
|
213
259
|
num_supernodes = federation_config["options"]["num-supernodes"]
|
|
214
|
-
verbose:
|
|
260
|
+
verbose: bool | None = federation_config["options"].get("verbose")
|
|
215
261
|
backend_cfg = federation_config["options"].get("backend", {})
|
|
216
262
|
except KeyError as err:
|
|
217
263
|
typer.secho(
|
|
@@ -222,6 +268,7 @@ def _run_without_control_api(
|
|
|
222
268
|
"options.num-supernodes = 10\n",
|
|
223
269
|
fg=typer.colors.RED,
|
|
224
270
|
bold=True,
|
|
271
|
+
err=True,
|
|
225
272
|
)
|
|
226
273
|
raise typer.Exit(code=1) from err
|
|
227
274
|
|
flwr/cli/run_utils.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright 2025 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 utils."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
|
|
21
|
+
from flwr.common.date import format_timedelta, isoformat8601_utc
|
|
22
|
+
from flwr.common.typing import Run
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RunRow: # pylint: disable=too-many-instance-attributes
|
|
27
|
+
"""Represents a single run's data for display.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
run_id : int
|
|
32
|
+
The unique identifier for the run.
|
|
33
|
+
federation : str
|
|
34
|
+
The federation name.
|
|
35
|
+
fab_id : str
|
|
36
|
+
The Flower App Bundle identifier.
|
|
37
|
+
fab_version : str
|
|
38
|
+
The FAB version string.
|
|
39
|
+
fab_hash : str
|
|
40
|
+
The SHA-256 hash of the FAB.
|
|
41
|
+
status_text : str
|
|
42
|
+
The formatted status text.
|
|
43
|
+
elapsed : str
|
|
44
|
+
The formatted elapsed time.
|
|
45
|
+
pending_at : str
|
|
46
|
+
Timestamp when run entered pending state.
|
|
47
|
+
starting_at : str
|
|
48
|
+
Timestamp when run entered starting state.
|
|
49
|
+
running_at : str
|
|
50
|
+
Timestamp when run entered running state.
|
|
51
|
+
finished_at : str
|
|
52
|
+
Timestamp when run finished.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
run_id: int
|
|
56
|
+
federation: str
|
|
57
|
+
fab_id: str
|
|
58
|
+
fab_version: str
|
|
59
|
+
fab_hash: str
|
|
60
|
+
status_text: str
|
|
61
|
+
elapsed: str
|
|
62
|
+
pending_at: str
|
|
63
|
+
starting_at: str
|
|
64
|
+
running_at: str
|
|
65
|
+
finished_at: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_runs(runs: list[Run], now_isoformat: str) -> list[RunRow]:
|
|
69
|
+
"""Format runs to a list of RunRow objects.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
runs : list[Run]
|
|
74
|
+
List of Run objects to format.
|
|
75
|
+
now_isoformat : str
|
|
76
|
+
Current timestamp in ISO format for calculating elapsed time.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
list[RunRow]
|
|
81
|
+
List of formatted RunRow objects sorted by pending_at timestamp.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def _format_datetime(dt: datetime | None) -> str:
|
|
85
|
+
return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
|
|
86
|
+
|
|
87
|
+
run_list: list[RunRow] = []
|
|
88
|
+
|
|
89
|
+
# Add rows
|
|
90
|
+
for run in sorted(runs, key=lambda x: datetime.fromisoformat(x.pending_at)):
|
|
91
|
+
# Combine status and sub-status into a single string
|
|
92
|
+
if run.status.sub_status == "":
|
|
93
|
+
status_text = run.status.status
|
|
94
|
+
else:
|
|
95
|
+
status_text = f"{run.status.status}:{run.status.sub_status}"
|
|
96
|
+
|
|
97
|
+
# Convert isoformat to datetime
|
|
98
|
+
pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
|
|
99
|
+
starting_at = (
|
|
100
|
+
datetime.fromisoformat(run.starting_at) if run.starting_at else None
|
|
101
|
+
)
|
|
102
|
+
running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
|
|
103
|
+
finished_at = (
|
|
104
|
+
datetime.fromisoformat(run.finished_at) if run.finished_at else None
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Calculate elapsed time
|
|
108
|
+
elapsed_time = timedelta()
|
|
109
|
+
if running_at:
|
|
110
|
+
if finished_at:
|
|
111
|
+
end_time = finished_at
|
|
112
|
+
else:
|
|
113
|
+
end_time = datetime.fromisoformat(now_isoformat)
|
|
114
|
+
elapsed_time = end_time - running_at
|
|
115
|
+
|
|
116
|
+
row = RunRow(
|
|
117
|
+
run_id=run.run_id,
|
|
118
|
+
federation=run.federation,
|
|
119
|
+
fab_id=run.fab_id,
|
|
120
|
+
fab_version=run.fab_version,
|
|
121
|
+
fab_hash=run.fab_hash,
|
|
122
|
+
status_text=status_text,
|
|
123
|
+
elapsed=format_timedelta(elapsed_time),
|
|
124
|
+
pending_at=_format_datetime(pending_at),
|
|
125
|
+
starting_at=_format_datetime(starting_at),
|
|
126
|
+
running_at=_format_datetime(running_at),
|
|
127
|
+
finished_at=_format_datetime(finished_at),
|
|
128
|
+
)
|
|
129
|
+
run_list.append(row)
|
|
130
|
+
return run_list
|