flwr 1.22.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 +34 -1
- 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/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +94 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
- flwr/cli/build.py +166 -53
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
- 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 +54 -11
- flwr/cli/login/login.py +41 -27
- flwr/cli/ls.py +177 -133
- flwr/cli/new/new.py +175 -40
- 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 +12 -7
- flwr/cli/run/run.py +82 -31
- flwr/cli/run_utils.py +130 -0
- flwr/cli/stop.py +27 -9
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +268 -0
- flwr/cli/supernode/register.py +190 -0
- flwr/cli/supernode/unregister.py +140 -0
- flwr/cli/utils.py +464 -81
- flwr/client/__init__.py +2 -1
- flwr/client/dpfedavg_numpy_client.py +4 -1
- flwr/client/grpc_adapter_client/connection.py +12 -15
- flwr/client/grpc_rere_client/connection.py +68 -41
- flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
- flwr/client/message_handler/message_handler.py +2 -2
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
- flwr/client/numpy_client.py +1 -1
- flwr/client/rest_client/connection.py +94 -51
- flwr/client/run_info_store.py +4 -5
- flwr/client/typing.py +1 -1
- flwr/clientapp/__init__.py +1 -2
- flwr/{client → 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/{client/clientapp → clientapp}/utils.py +4 -4
- flwr/common/address.py +1 -2
- flwr/common/args.py +3 -4
- flwr/common/config.py +13 -16
- flwr/common/constant.py +56 -13
- 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 +39 -10
- 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 +48 -31
- flwr/common/logger.py +19 -19
- flwr/common/message.py +4 -4
- flwr/common/object_ref.py +7 -7
- flwr/common/record/array.py +6 -6
- flwr/common/record/arrayrecord.py +18 -21
- 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/crypto/symmetric_encryption.py +1 -89
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
- flwr/common/serde.py +9 -6
- flwr/common/serde_utils.py +2 -2
- flwr/common/telemetry.py +9 -5
- flwr/common/typing.py +59 -43
- flwr/compat/client/app.py +39 -38
- flwr/compat/client/grpc_client/connection.py +13 -13
- 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 +72 -40
- flwr/proto/control_pb2.pyi +319 -87
- flwr/proto/control_pb2_grpc.py +339 -28
- flwr/proto/control_pb2_grpc.pyi +209 -37
- 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 +24 -10
- flwr/proto/fab_pb2.pyi +68 -20
- 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 +45 -27
- flwr/proto/fleet_pb2.pyi +186 -70
- flwr/proto/fleet_pb2_grpc.py +277 -66
- flwr/proto/fleet_pb2_grpc.pyi +201 -55
- 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 +16 -4
- flwr/proto/node_pb2.pyi +77 -4
- 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 +173 -127
- 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 +19 -8
- 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 +136 -42
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
- flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
- flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
- flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
- flwr/server/superlink/fleet/vce/vce_api.py +32 -13
- flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
- flwr/server/superlink/linkstate/linkstate.py +161 -62
- flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
- flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
- flwr/server/superlink/linkstate/utils.py +9 -60
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
- flwr/server/superlink/utils.py +4 -6
- flwr/server/typing.py +1 -1
- flwr/server/utils/tensorboard.py +15 -8
- flwr/server/utils/validator.py +2 -3
- 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 +12 -10
- 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 +11 -12
- flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
- flwr/simulation/run_simulation.py +44 -43
- flwr/simulation/simulationio_connection.py +4 -4
- flwr/supercore/cli/flower_superexec.py +3 -4
- flwr/supercore/constant.py +52 -0
- 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 -6
- flwr/supercore/object_store/object_store.py +1 -2
- flwr/supercore/object_store/object_store_factory.py +27 -8
- flwr/supercore/object_store/sqlite_object_store.py +253 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
- flwr/supercore/sqlite_mixin.py +159 -0
- 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/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/artifact_provider.py +1 -2
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +88 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
- 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_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
- flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
- flwr/superlink/servicer/control/control_grpc.py +18 -17
- flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
- flwr/superlink/servicer/control/control_servicer.py +239 -63
- flwr/supernode/cli/flower_supernode.py +74 -26
- 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 +43 -24
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
- flwr/supernode/start_client_internal.py +175 -51
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
- flwr-1.24.0.dist-info/RECORD +454 -0
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/supercore/object_store/utils.py +0 -43
- flwr-1.22.0.dist-info/RECORD +0 -428
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
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 `federation list` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Annotated, cast
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
|
|
27
|
+
from flwr.cli.config_utils import (
|
|
28
|
+
exit_if_no_address,
|
|
29
|
+
load_and_validate,
|
|
30
|
+
process_loaded_project_config,
|
|
31
|
+
validate_federation_in_project_config,
|
|
32
|
+
)
|
|
33
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
|
34
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
35
|
+
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
36
|
+
ListFederationsRequest,
|
|
37
|
+
ListFederationsResponse,
|
|
38
|
+
)
|
|
39
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
|
40
|
+
from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
|
|
41
|
+
|
|
42
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ls( # pylint: disable=R0914, R0913, R0917
|
|
46
|
+
ctx: typer.Context,
|
|
47
|
+
app: Annotated[
|
|
48
|
+
Path,
|
|
49
|
+
typer.Argument(help="Path of the Flower project"),
|
|
50
|
+
] = Path("."),
|
|
51
|
+
federation: Annotated[
|
|
52
|
+
str | None,
|
|
53
|
+
typer.Argument(help="Name of the federation"),
|
|
54
|
+
] = None,
|
|
55
|
+
output_format: Annotated[
|
|
56
|
+
str,
|
|
57
|
+
typer.Option(
|
|
58
|
+
"--format",
|
|
59
|
+
case_sensitive=False,
|
|
60
|
+
help="Format output using 'default' view or 'json'",
|
|
61
|
+
),
|
|
62
|
+
] = CliOutputFormat.DEFAULT,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""List available federations."""
|
|
65
|
+
# Resolve command used (list or ls)
|
|
66
|
+
command_name = cast(str, ctx.command.name) if ctx.command else "ls"
|
|
67
|
+
|
|
68
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
69
|
+
captured_output = io.StringIO()
|
|
70
|
+
try:
|
|
71
|
+
if suppress_output:
|
|
72
|
+
redirect_output(captured_output)
|
|
73
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
74
|
+
|
|
75
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
76
|
+
config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
|
|
77
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
78
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
79
|
+
federation, config
|
|
80
|
+
)
|
|
81
|
+
exit_if_no_address(federation_config, f"federation {command_name}")
|
|
82
|
+
channel = None
|
|
83
|
+
try:
|
|
84
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
|
85
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
|
86
|
+
stub = ControlStub(channel)
|
|
87
|
+
typer.echo("📄 Listing federations...")
|
|
88
|
+
federations = _list_federations(stub)
|
|
89
|
+
restore_output()
|
|
90
|
+
if output_format == CliOutputFormat.JSON:
|
|
91
|
+
Console().print_json(data=_to_json(federations))
|
|
92
|
+
else:
|
|
93
|
+
Console().print(_to_table(federations))
|
|
94
|
+
finally:
|
|
95
|
+
if channel:
|
|
96
|
+
channel.close()
|
|
97
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
98
|
+
if suppress_output:
|
|
99
|
+
restore_output()
|
|
100
|
+
e_message = captured_output.getvalue()
|
|
101
|
+
print_json_error(e_message, err)
|
|
102
|
+
else:
|
|
103
|
+
typer.secho(
|
|
104
|
+
f"{err}",
|
|
105
|
+
fg=typer.colors.RED,
|
|
106
|
+
bold=True,
|
|
107
|
+
err=True,
|
|
108
|
+
)
|
|
109
|
+
finally:
|
|
110
|
+
if suppress_output:
|
|
111
|
+
restore_output()
|
|
112
|
+
captured_output.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _list_federations(stub: ControlStub) -> list[Federation]:
|
|
116
|
+
"""List all federations."""
|
|
117
|
+
with flwr_cli_grpc_exc_handler():
|
|
118
|
+
res: ListFederationsResponse = stub.ListFederations(ListFederationsRequest())
|
|
119
|
+
|
|
120
|
+
return list(res.federations)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _to_table(federations: list[Federation]) -> Table:
|
|
124
|
+
"""Format the provided federations list to a rich Table."""
|
|
125
|
+
table = Table(header_style="bold cyan", show_lines=True)
|
|
126
|
+
|
|
127
|
+
# Add columns
|
|
128
|
+
table.add_column(
|
|
129
|
+
Text("Federation", justify="center"), style="bright_black", no_wrap=True
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
for federation in federations:
|
|
133
|
+
table.add_row(federation.name)
|
|
134
|
+
|
|
135
|
+
return table
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _to_json(federations: list[Federation]) -> list[dict[str, str]]:
|
|
139
|
+
"""Format the provided federations list to JSON serializable format."""
|
|
140
|
+
return [{"name": federation.name} for federation in federations]
|
|
@@ -0,0 +1,317 @@
|
|
|
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 `federation show` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Annotated, Any
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
|
|
27
|
+
from flwr.cli.config_utils import (
|
|
28
|
+
exit_if_no_address,
|
|
29
|
+
load_and_validate,
|
|
30
|
+
process_loaded_project_config,
|
|
31
|
+
validate_federation_in_project_config,
|
|
32
|
+
)
|
|
33
|
+
from flwr.cli.ls import _get_status_style
|
|
34
|
+
from flwr.common.constant import FAB_CONFIG_FILE, NOOP_ACCOUNT_NAME, CliOutputFormat
|
|
35
|
+
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
36
|
+
from flwr.common.serde import run_from_proto
|
|
37
|
+
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
38
|
+
ShowFederationRequest,
|
|
39
|
+
ShowFederationResponse,
|
|
40
|
+
)
|
|
41
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
|
42
|
+
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
|
43
|
+
from flwr.supercore.constant import NOOP_FEDERATION
|
|
44
|
+
|
|
45
|
+
from ..run_utils import RunRow, format_runs
|
|
46
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def show( # pylint: disable=R0914, R0913, R0917
|
|
50
|
+
app: Annotated[
|
|
51
|
+
Path,
|
|
52
|
+
typer.Argument(help="Path of the Flower project"),
|
|
53
|
+
] = Path("."),
|
|
54
|
+
federation: Annotated[
|
|
55
|
+
str | None,
|
|
56
|
+
typer.Argument(help="Name of the federation"),
|
|
57
|
+
] = None,
|
|
58
|
+
output_format: Annotated[
|
|
59
|
+
str,
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--format",
|
|
62
|
+
case_sensitive=False,
|
|
63
|
+
help="Format output using 'default' view or 'json'",
|
|
64
|
+
),
|
|
65
|
+
] = CliOutputFormat.DEFAULT,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Show details of a federation.
|
|
68
|
+
|
|
69
|
+
Display comprehensive information about a federation including its members,
|
|
70
|
+
registered SuperNodes, and runs.
|
|
71
|
+
"""
|
|
72
|
+
suppress_output = output_format == CliOutputFormat.JSON
|
|
73
|
+
captured_output = io.StringIO()
|
|
74
|
+
try:
|
|
75
|
+
if suppress_output:
|
|
76
|
+
redirect_output(captured_output)
|
|
77
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
78
|
+
|
|
79
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
80
|
+
config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
|
|
81
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
82
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
83
|
+
federation, config
|
|
84
|
+
)
|
|
85
|
+
exit_if_no_address(federation_config, "federation show")
|
|
86
|
+
real_federation: str = federation_config.get("federation", NOOP_FEDERATION)
|
|
87
|
+
channel = None
|
|
88
|
+
try:
|
|
89
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
|
90
|
+
channel = init_channel(app, federation_config, auth_plugin)
|
|
91
|
+
stub = ControlStub(channel)
|
|
92
|
+
typer.echo(f"📄 Showing '{real_federation}' federation ...")
|
|
93
|
+
members, nodes, runs = _show_federation(stub, real_federation)
|
|
94
|
+
restore_output()
|
|
95
|
+
if output_format == CliOutputFormat.JSON:
|
|
96
|
+
Console().print_json(data=_to_json(members, nodes, runs))
|
|
97
|
+
else:
|
|
98
|
+
Console().print(_to_members_table(members))
|
|
99
|
+
Console().print(_to_nodes_table(nodes))
|
|
100
|
+
Console().print(_to_runs_table(runs))
|
|
101
|
+
finally:
|
|
102
|
+
if channel:
|
|
103
|
+
channel.close()
|
|
104
|
+
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
105
|
+
if suppress_output:
|
|
106
|
+
restore_output()
|
|
107
|
+
e_message = captured_output.getvalue()
|
|
108
|
+
print_json_error(e_message, err)
|
|
109
|
+
else:
|
|
110
|
+
typer.secho(
|
|
111
|
+
f"{err}",
|
|
112
|
+
fg=typer.colors.RED,
|
|
113
|
+
bold=True,
|
|
114
|
+
err=True,
|
|
115
|
+
)
|
|
116
|
+
finally:
|
|
117
|
+
if suppress_output:
|
|
118
|
+
restore_output()
|
|
119
|
+
captured_output.close()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _show_federation(
|
|
123
|
+
stub: ControlStub, federation: str
|
|
124
|
+
) -> tuple[list[str], list[NodeInfo], list[RunRow]]:
|
|
125
|
+
"""Show federation details.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
stub : ControlStub
|
|
130
|
+
The gRPC stub for Control API communication.
|
|
131
|
+
federation : str
|
|
132
|
+
Name of the federation to show.
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
-------
|
|
136
|
+
tuple[list[str], list[NodeInfo], list[RunRow]]
|
|
137
|
+
A tuple containing (member_account_ids, nodes, runs).
|
|
138
|
+
"""
|
|
139
|
+
with flwr_cli_grpc_exc_handler():
|
|
140
|
+
res: ShowFederationResponse = stub.ShowFederation(
|
|
141
|
+
ShowFederationRequest(federation_name=federation)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
fed_proto = res.federation
|
|
145
|
+
runs = [run_from_proto(run_proto) for run_proto in fed_proto.runs]
|
|
146
|
+
formatted_runs = format_runs(runs, res.now)
|
|
147
|
+
|
|
148
|
+
return list(fed_proto.member_aids), list(fed_proto.nodes), formatted_runs
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _to_members_table(member_aids: list[str]) -> Table:
|
|
152
|
+
"""Format the provided list of federation members as a rich Table.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
member_aids : list[str]
|
|
157
|
+
List of member account identifiers.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
Table
|
|
162
|
+
Rich Table object with formatted member information.
|
|
163
|
+
"""
|
|
164
|
+
table = Table(title="Federation Members", header_style="bold cyan", show_lines=True)
|
|
165
|
+
|
|
166
|
+
table.add_column(
|
|
167
|
+
Text("Account ID", justify="center"), style="bright_black", no_wrap=True
|
|
168
|
+
)
|
|
169
|
+
table.add_column(Text("Role", justify="center"), style="bright_black", no_wrap=True)
|
|
170
|
+
|
|
171
|
+
for member_aid in member_aids:
|
|
172
|
+
table.add_row(member_aid, "Member")
|
|
173
|
+
|
|
174
|
+
return table
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _to_nodes_table(nodes: list[NodeInfo]) -> Table:
|
|
178
|
+
"""Format the provided list of federation nodes as a rich Table.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
nodes : list[NodeInfo]
|
|
183
|
+
List of NodeInfo objects containing node details.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
Table
|
|
188
|
+
Rich Table object with formatted node information.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ValueError
|
|
193
|
+
If an unexpected node status is encountered.
|
|
194
|
+
"""
|
|
195
|
+
table = Table(
|
|
196
|
+
title="SuperNodes in the Federation", header_style="bold cyan", show_lines=True
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Add columns
|
|
200
|
+
table.add_column(
|
|
201
|
+
Text("Node ID", justify="center"), style="bright_black", no_wrap=True
|
|
202
|
+
)
|
|
203
|
+
table.add_column(Text("Owner", justify="center"))
|
|
204
|
+
table.add_column(Text("Status", justify="center"))
|
|
205
|
+
|
|
206
|
+
for row in nodes:
|
|
207
|
+
owner_name = row.owner_name
|
|
208
|
+
status = row.status
|
|
209
|
+
|
|
210
|
+
if status == "online":
|
|
211
|
+
status_style = "green"
|
|
212
|
+
elif status == "offline":
|
|
213
|
+
status_style = "bright_yellow"
|
|
214
|
+
elif status == "unregistered":
|
|
215
|
+
continue
|
|
216
|
+
elif status == "registered":
|
|
217
|
+
status_style = "blue"
|
|
218
|
+
else:
|
|
219
|
+
raise ValueError(f"Unexpected node status '{status}'")
|
|
220
|
+
|
|
221
|
+
formatted_row = (
|
|
222
|
+
f"[bold]{row.node_id}[/bold]",
|
|
223
|
+
(
|
|
224
|
+
f"{owner_name}"
|
|
225
|
+
if owner_name != NOOP_ACCOUNT_NAME
|
|
226
|
+
else f"[dim]{owner_name}[/dim]"
|
|
227
|
+
),
|
|
228
|
+
f"[{status_style}]{status}",
|
|
229
|
+
)
|
|
230
|
+
table.add_row(*formatted_row)
|
|
231
|
+
|
|
232
|
+
return table
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _to_runs_table(run_list: list[RunRow]) -> Table:
|
|
236
|
+
"""Format the provided list of federation runs as a rich Table.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
run_list : list[RunRow]
|
|
241
|
+
List of RunRow objects containing run details.
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
Table
|
|
246
|
+
Rich Table object with formatted run information.
|
|
247
|
+
"""
|
|
248
|
+
table = Table(
|
|
249
|
+
title="Runs in the Federation", header_style="bold cyan", show_lines=True
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Add columns
|
|
253
|
+
table.add_column(Text("Run ID", justify="center"), no_wrap=True)
|
|
254
|
+
table.add_column(Text("App", justify="center"))
|
|
255
|
+
table.add_column(Text("Status", justify="center"))
|
|
256
|
+
table.add_column(Text("Elapsed", justify="center"), style="blue")
|
|
257
|
+
|
|
258
|
+
for row in run_list:
|
|
259
|
+
status_style = _get_status_style(row.status_text)
|
|
260
|
+
|
|
261
|
+
formatted_row = (
|
|
262
|
+
f"[bold]{row.run_id}[/bold]",
|
|
263
|
+
f"@{row.fab_id}=={row.fab_version}",
|
|
264
|
+
f"[{status_style}]{row.status_text}[/{status_style}]",
|
|
265
|
+
row.elapsed,
|
|
266
|
+
)
|
|
267
|
+
table.add_row(*formatted_row)
|
|
268
|
+
|
|
269
|
+
return table
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _to_json(
|
|
273
|
+
members: list[str], nodes: list[NodeInfo], runs: list[RunRow]
|
|
274
|
+
) -> list[list[dict[str, str]]]:
|
|
275
|
+
"""Format the provided federation information to JSON serializable format.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
members : list[str]
|
|
280
|
+
List of member account identifiers.
|
|
281
|
+
nodes : list[NodeInfo]
|
|
282
|
+
List of NodeInfo objects.
|
|
283
|
+
runs : list[RunRow]
|
|
284
|
+
List of RunRow objects.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
list[list[dict[str, str]]]
|
|
289
|
+
Nested list containing dictionaries for members, nodes, and runs.
|
|
290
|
+
"""
|
|
291
|
+
members_list: list[dict[str, Any]] = []
|
|
292
|
+
nodes_list: list[dict[str, Any]] = []
|
|
293
|
+
runs_list: list[dict[str, Any]] = []
|
|
294
|
+
|
|
295
|
+
for member in members:
|
|
296
|
+
members_list.append({"member_id": member, "role": "Member"})
|
|
297
|
+
|
|
298
|
+
for node in nodes:
|
|
299
|
+
nodes_list.append(
|
|
300
|
+
{
|
|
301
|
+
"node_id": node.node_id,
|
|
302
|
+
"owner": node.owner_name,
|
|
303
|
+
"status": node.status,
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
for run in runs:
|
|
308
|
+
runs_list.append(
|
|
309
|
+
{
|
|
310
|
+
"run_id": run.run_id,
|
|
311
|
+
"app": f"@{run.fab_id}=={run.fab_version}",
|
|
312
|
+
"status": run.status_text,
|
|
313
|
+
"elapsed": run.elapsed,
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return [members_list, nodes_list, runs_list]
|
flwr/cli/install.py
CHANGED
|
@@ -21,7 +21,7 @@ import tempfile
|
|
|
21
21
|
import zipfile
|
|
22
22
|
from io import BytesIO
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import IO, Annotated
|
|
24
|
+
from typing import IO, Annotated
|
|
25
25
|
|
|
26
26
|
import typer
|
|
27
27
|
|
|
@@ -34,11 +34,11 @@ from .utils import get_sha256_hash
|
|
|
34
34
|
|
|
35
35
|
def install(
|
|
36
36
|
source: Annotated[
|
|
37
|
-
|
|
37
|
+
Path | None,
|
|
38
38
|
typer.Argument(metavar="source", help="The source FAB file to install."),
|
|
39
39
|
] = None,
|
|
40
40
|
flwr_dir: Annotated[
|
|
41
|
-
|
|
41
|
+
Path | None,
|
|
42
42
|
typer.Option(help="The desired install path."),
|
|
43
43
|
] = None,
|
|
44
44
|
) -> None:
|
|
@@ -68,6 +68,7 @@ def install(
|
|
|
68
68
|
f"❌ The source {source} does not exist or is not a file.",
|
|
69
69
|
fg=typer.colors.RED,
|
|
70
70
|
bold=True,
|
|
71
|
+
err=True,
|
|
71
72
|
)
|
|
72
73
|
raise typer.Exit(code=1)
|
|
73
74
|
|
|
@@ -76,6 +77,7 @@ def install(
|
|
|
76
77
|
f"❌ The source {source} is not a `.fab` file.",
|
|
77
78
|
fg=typer.colors.RED,
|
|
78
79
|
bold=True,
|
|
80
|
+
err=True,
|
|
79
81
|
)
|
|
80
82
|
raise typer.Exit(code=1)
|
|
81
83
|
|
|
@@ -83,13 +85,33 @@ def install(
|
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
def install_from_fab(
|
|
86
|
-
fab_file:
|
|
87
|
-
flwr_dir:
|
|
88
|
+
fab_file: Path | bytes,
|
|
89
|
+
flwr_dir: Path | None,
|
|
88
90
|
skip_prompt: bool = False,
|
|
89
91
|
) -> Path:
|
|
90
|
-
"""Install from a FAB file after extracting and validating.
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
"""Install from a FAB file after extracting and validating.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
fab_file : Path | bytes
|
|
97
|
+
Either a path to the FAB file or the FAB file content as bytes.
|
|
98
|
+
flwr_dir : Path | None
|
|
99
|
+
Target installation directory, or None to use default.
|
|
100
|
+
skip_prompt : bool
|
|
101
|
+
If True, skip confirmation prompts. Default is False.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
Path
|
|
106
|
+
Path to the installed application directory.
|
|
107
|
+
|
|
108
|
+
Raises
|
|
109
|
+
------
|
|
110
|
+
typer.Exit
|
|
111
|
+
If FAB format is invalid or hash verification fails.
|
|
112
|
+
"""
|
|
113
|
+
fab_file_archive: Path | IO[bytes]
|
|
114
|
+
fab_name: str | None
|
|
93
115
|
if isinstance(fab_file, bytes):
|
|
94
116
|
fab_file_archive = BytesIO(fab_file)
|
|
95
117
|
fab_hash = hashlib.sha256(fab_file).hexdigest()
|
|
@@ -111,6 +133,7 @@ def install_from_fab(
|
|
|
111
133
|
"❌ FAB file has incorrect format.",
|
|
112
134
|
fg=typer.colors.RED,
|
|
113
135
|
bold=True,
|
|
136
|
+
err=True,
|
|
114
137
|
)
|
|
115
138
|
raise typer.Exit(code=1)
|
|
116
139
|
|
|
@@ -123,6 +146,7 @@ def install_from_fab(
|
|
|
123
146
|
"❌ File hashes couldn't be verified.",
|
|
124
147
|
fg=typer.colors.RED,
|
|
125
148
|
bold=True,
|
|
149
|
+
err=True,
|
|
126
150
|
)
|
|
127
151
|
raise typer.Exit(code=1)
|
|
128
152
|
|
|
@@ -139,11 +163,35 @@ def install_from_fab(
|
|
|
139
163
|
def validate_and_install(
|
|
140
164
|
project_dir: Path,
|
|
141
165
|
fab_hash: str,
|
|
142
|
-
fab_name:
|
|
143
|
-
flwr_dir:
|
|
166
|
+
fab_name: str | None,
|
|
167
|
+
flwr_dir: Path | None,
|
|
144
168
|
skip_prompt: bool = False,
|
|
145
169
|
) -> Path:
|
|
146
|
-
"""Validate TOML
|
|
170
|
+
"""Validate the TOML file and install the project to the desired directory.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
project_dir : Path
|
|
175
|
+
Path to the extracted project directory.
|
|
176
|
+
fab_hash : str
|
|
177
|
+
SHA-256 hash of the FAB file.
|
|
178
|
+
fab_name : str | None
|
|
179
|
+
Name of the FAB file, or None if installing from bytes.
|
|
180
|
+
flwr_dir : Path | None
|
|
181
|
+
Target installation directory, or None to use default.
|
|
182
|
+
skip_prompt : bool (default: False)
|
|
183
|
+
If True, skip confirmation prompts.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
Path
|
|
188
|
+
Path to the installed application directory.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
typer.Exit
|
|
193
|
+
If configuration is invalid or metadata doesn't match.
|
|
194
|
+
"""
|
|
147
195
|
config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
|
|
148
196
|
|
|
149
197
|
if config is None:
|
|
@@ -151,6 +199,7 @@ def validate_and_install(
|
|
|
151
199
|
"❌ Invalid config inside FAB file.",
|
|
152
200
|
fg=typer.colors.RED,
|
|
153
201
|
bold=True,
|
|
202
|
+
err=True,
|
|
154
203
|
)
|
|
155
204
|
raise typer.Exit(code=1)
|
|
156
205
|
|
|
@@ -198,7 +247,20 @@ def validate_and_install(
|
|
|
198
247
|
|
|
199
248
|
|
|
200
249
|
def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
|
|
201
|
-
"""Verify file hashes based on the
|
|
250
|
+
"""Verify file hashes based on the CONTENT manifest.
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
list_content : str
|
|
255
|
+
Content of the CONTENT manifest file with hash information.
|
|
256
|
+
tmpdir : Path
|
|
257
|
+
Temporary directory containing extracted files.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
bool
|
|
262
|
+
True if all file hashes match, False otherwise.
|
|
263
|
+
"""
|
|
202
264
|
for line in list_content.strip().split("\n"):
|
|
203
265
|
rel_path, hash_expected, _ = line.split(",")
|
|
204
266
|
file_path = tmpdir / rel_path
|
|
@@ -210,7 +272,20 @@ def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
|
|
|
210
272
|
def _validate_fab_and_config_metadata(
|
|
211
273
|
fab_name: str, config_metadata: tuple[str, str, str, str]
|
|
212
274
|
) -> None:
|
|
213
|
-
"""Validate metadata from the FAB filename and config.
|
|
275
|
+
"""Validate metadata from the FAB filename and config.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
fab_name : str
|
|
280
|
+
The FAB filename (with or without .fab extension).
|
|
281
|
+
config_metadata : tuple[str, str, str, str]
|
|
282
|
+
Tuple of (publisher, project_name, version, fab_hash).
|
|
283
|
+
|
|
284
|
+
Raises
|
|
285
|
+
------
|
|
286
|
+
typer.Exit
|
|
287
|
+
If filename format is incorrect or hash doesn't match.
|
|
288
|
+
"""
|
|
214
289
|
publisher, project_name, version, fab_hash = config_metadata
|
|
215
290
|
|
|
216
291
|
fab_name = fab_name.removesuffix(".fab")
|
|
@@ -229,6 +304,7 @@ def _validate_fab_and_config_metadata(
|
|
|
229
304
|
"`<publisher>.<project_name>.<version>.<8hexchars>.fab`.",
|
|
230
305
|
fg=typer.colors.RED,
|
|
231
306
|
bold=True,
|
|
307
|
+
err=True,
|
|
232
308
|
)
|
|
233
309
|
raise typer.Exit(code=1)
|
|
234
310
|
|
|
@@ -240,6 +316,7 @@ def _validate_fab_and_config_metadata(
|
|
|
240
316
|
f"❌ FAB file has an invalid hexadecimal string `{fab_shorthash}`.",
|
|
241
317
|
fg=typer.colors.RED,
|
|
242
318
|
bold=True,
|
|
319
|
+
err=True,
|
|
243
320
|
)
|
|
244
321
|
raise typer.Exit(code=1) from e
|
|
245
322
|
|
|
@@ -249,5 +326,6 @@ def _validate_fab_and_config_metadata(
|
|
|
249
326
|
"❌ The hash in the FAB file name does not match the hash of the FAB.",
|
|
250
327
|
fg=typer.colors.RED,
|
|
251
328
|
bold=True,
|
|
329
|
+
err=True,
|
|
252
330
|
)
|
|
253
331
|
raise typer.Exit(code=1)
|