flwr 1.23.0__py3-none-any.whl → 1.25.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/{new/templates → app_cmd}/__init__.py +9 -1
- flwr/cli/app_cmd/publish.py +285 -0
- flwr/cli/app_cmd/review.py +262 -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/{new/templates/app/code/flwr_tune → federation}/__init__.py +10 -1
- flwr/cli/federation/ls.py +140 -0
- flwr/cli/federation/show.py +318 -0
- flwr/cli/install.py +91 -13
- flwr/cli/log.py +52 -9
- flwr/cli/login/login.py +7 -4
- flwr/cli/ls.py +211 -130
- flwr/cli/new/new.py +123 -331
- flwr/cli/pull.py +10 -5
- flwr/cli/run/run.py +71 -29
- flwr/cli/run_utils.py +148 -0
- flwr/cli/stop.py +26 -8
- flwr/cli/supernode/ls.py +25 -12
- flwr/cli/supernode/register.py +9 -4
- flwr/cli/supernode/unregister.py +5 -3
- flwr/cli/utils.py +239 -16
- flwr/client/__init__.py +1 -1
- flwr/client/dpfedavg_numpy_client.py +4 -1
- flwr/client/grpc_adapter_client/connection.py +8 -9
- flwr/client/grpc_rere_client/connection.py +16 -14
- 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 +18 -18
- 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 +11 -4
- flwr/common/serde_utils.py +2 -2
- flwr/common/telemetry.py +9 -5
- flwr/common/typing.py +58 -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 +24 -14
- flwr/proto/fleet_pb2.pyi +141 -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 +158 -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 +39 -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 +75 -30
- 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 +148 -149
- flwr/server/superlink/linkstate/linkstate.py +91 -43
- flwr/server/superlink/linkstate/linkstate_factory.py +22 -5
- flwr/server/superlink/linkstate/sqlite_linkstate.py +502 -436
- 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 +34 -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/supercore/utils.py +190 -0
- 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/{cli/new/templates/app → superlink/federation}/__init__.py +10 -1
- 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 +7 -6
- flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
- flwr/superlink/servicer/control/control_servicer.py +190 -23
- flwr/supernode/cli/flower_supernode.py +58 -3
- flwr/supernode/nodestate/in_memory_nodestate.py +121 -49
- flwr/supernode/nodestate/nodestate.py +52 -8
- flwr/supernode/nodestate/nodestate_factory.py +7 -4
- flwr/supernode/runtime/run_clientapp.py +41 -22
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +46 -10
- flwr/supernode/start_client_internal.py +165 -46
- {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/METADATA +9 -11
- flwr-1.25.0.dist-info/RECORD +393 -0
- flwr/cli/new/templates/app/.gitignore.tpl +0 -163
- flwr/cli/new/templates/app/LICENSE.tpl +0 -202
- flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
- flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
- flwr/cli/new/templates/app/README.md.tpl +0 -37
- flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.py +0 -15
- flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
- flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
- flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
- flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
- flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
- flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -98
- flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
- flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
- flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
- 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.25.0.dist-info}/WHEEL +0 -0
- {flwr-1.23.0.dist-info → flwr-1.25.0.dist-info}/entry_points.txt +0 -0
flwr/__init__.py
CHANGED
|
@@ -15,18 +15,29 @@
|
|
|
15
15
|
"""Flower main package."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
import importlib
|
|
19
|
+
|
|
18
20
|
from flwr.common.version import package_version as _package_version
|
|
19
21
|
|
|
20
|
-
from . import app,
|
|
22
|
+
from . import app, clientapp, serverapp
|
|
21
23
|
|
|
22
24
|
__all__ = [
|
|
23
25
|
"app",
|
|
24
|
-
"client",
|
|
25
26
|
"clientapp",
|
|
26
|
-
"common",
|
|
27
|
-
"server",
|
|
28
27
|
"serverapp",
|
|
29
|
-
"simulation",
|
|
30
28
|
]
|
|
31
29
|
|
|
32
30
|
__version__ = _package_version
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Lazy imports for legacy support
|
|
34
|
+
_lazy_imports = {"simulation", "server", "client", "common"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __getattr__(name: str) -> object:
|
|
38
|
+
"""Lazy import for legacy support."""
|
|
39
|
+
if name in _lazy_imports:
|
|
40
|
+
module = importlib.import_module(f"{__name__}.{name}")
|
|
41
|
+
globals()[name] = module
|
|
42
|
+
return module
|
|
43
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
flwr/app/error.py
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import
|
|
20
|
+
from typing import cast
|
|
21
21
|
|
|
22
22
|
DEFAULT_TTL = 43200 # This is 12 hours
|
|
23
23
|
MESSAGE_INIT_ERROR_MESSAGE = (
|
|
@@ -54,7 +54,7 @@ class Error:
|
|
|
54
54
|
@property
|
|
55
55
|
def reason(self) -> str | None:
|
|
56
56
|
"""Reason reported about the error."""
|
|
57
|
-
return cast(
|
|
57
|
+
return cast(str | None, self.__dict__["_reason"])
|
|
58
58
|
|
|
59
59
|
def __repr__(self) -> str:
|
|
60
60
|
"""Return a string representation of this instance."""
|
flwr/app/exception.py
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"""Flower application exceptions."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class AppExitException(
|
|
18
|
+
class AppExitException(Exception):
|
|
19
19
|
"""Base exception for all application-level errors in ServerApp and ClientApp.
|
|
20
20
|
|
|
21
|
-
When raised, the process will exit and report a telemetry event
|
|
22
|
-
|
|
21
|
+
When raised (and not suppressed), the process will exit and report a telemetry event
|
|
22
|
+
with the associated exit code.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
# Default exit code — subclasses must override
|
flwr/cli/app.py
CHANGED
|
@@ -19,7 +19,11 @@ from typer.main import get_command
|
|
|
19
19
|
|
|
20
20
|
from flwr.common.version import package_version
|
|
21
21
|
|
|
22
|
+
from .app_cmd import publish as app_publish
|
|
23
|
+
from .app_cmd import review as app_review
|
|
22
24
|
from .build import build
|
|
25
|
+
from .federation import ls as federation_list
|
|
26
|
+
from .federation import show as federation_show
|
|
23
27
|
from .install import install
|
|
24
28
|
from .log import log
|
|
25
29
|
from .login import login
|
|
@@ -63,6 +67,21 @@ supernode_app.command("list")(supernode_list)
|
|
|
63
67
|
supernode_app.command(hidden=True)(supernode_list)
|
|
64
68
|
app.add_typer(supernode_app, name="supernode")
|
|
65
69
|
|
|
70
|
+
# Create app command group
|
|
71
|
+
app_app = typer.Typer(help="Manage Apps")
|
|
72
|
+
app_app.command()(app_review)
|
|
73
|
+
app_app.command()(app_publish)
|
|
74
|
+
app.add_typer(app_app, name="app")
|
|
75
|
+
|
|
76
|
+
# Create federation command group
|
|
77
|
+
federation_app = typer.Typer(help="Manage Federations")
|
|
78
|
+
# Make it appear as "list"
|
|
79
|
+
federation_app.command("list")(federation_list)
|
|
80
|
+
# Hide "ls" command (left as alias)
|
|
81
|
+
federation_app.command(hidden=True)(federation_list)
|
|
82
|
+
app.add_typer(federation_app, name="federation")
|
|
83
|
+
federation_app.command()(federation_show)
|
|
84
|
+
|
|
66
85
|
typer_click_object = get_command(app)
|
|
67
86
|
|
|
68
87
|
|
|
@@ -12,4 +12,12 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""Flower
|
|
15
|
+
"""Flower command line interface `app` command."""
|
|
16
|
+
|
|
17
|
+
from .publish import publish as publish
|
|
18
|
+
from .review import review as review
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"publish",
|
|
22
|
+
"review",
|
|
23
|
+
]
|
|
@@ -0,0 +1,285 @@
|
|
|
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 `app publish` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from contextlib import ExitStack
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import IO, Annotated
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
import typer
|
|
24
|
+
from requests import Response
|
|
25
|
+
|
|
26
|
+
from flwr.common.constant import FAB_CONFIG_FILE
|
|
27
|
+
from flwr.common.version import package_version as flwr_version
|
|
28
|
+
from flwr.supercore.constant import (
|
|
29
|
+
APP_PUBLISH_EXCLUDE_PATTERNS,
|
|
30
|
+
APP_PUBLISH_INCLUDE_PATTERNS,
|
|
31
|
+
MAX_DIR_DEPTH,
|
|
32
|
+
MAX_FILE_BYTES,
|
|
33
|
+
MAX_FILE_COUNT,
|
|
34
|
+
MAX_TOTAL_BYTES,
|
|
35
|
+
MIME_MAP,
|
|
36
|
+
PLATFORM_API_URL,
|
|
37
|
+
UTF8,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from ..auth_plugin.oidc_cli_plugin import OidcCliPlugin
|
|
41
|
+
from ..config_utils import (
|
|
42
|
+
load_and_validate,
|
|
43
|
+
process_loaded_project_config,
|
|
44
|
+
validate_federation_in_project_config,
|
|
45
|
+
)
|
|
46
|
+
from ..constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
47
|
+
from ..utils import build_pathspec, load_cli_auth_plugin, load_gitignore_patterns
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# pylint: disable=too-many-locals
|
|
51
|
+
def publish(
|
|
52
|
+
app: Annotated[
|
|
53
|
+
Path,
|
|
54
|
+
typer.Argument(
|
|
55
|
+
help="Project directory to upload (defaults to current directory)."
|
|
56
|
+
),
|
|
57
|
+
] = Path("."),
|
|
58
|
+
federation: Annotated[
|
|
59
|
+
str | None,
|
|
60
|
+
typer.Argument(
|
|
61
|
+
help="Name of the federation used for login before publishing app."
|
|
62
|
+
),
|
|
63
|
+
] = None,
|
|
64
|
+
federation_config_overrides: Annotated[
|
|
65
|
+
list[str] | None,
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--federation-config",
|
|
68
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
69
|
+
),
|
|
70
|
+
] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Publish a Flower App to the Flower Platform.
|
|
73
|
+
|
|
74
|
+
This command uploads your app project to the Flower Platform. Files are filtered
|
|
75
|
+
based on .gitignore patterns and allowed file extensions.
|
|
76
|
+
"""
|
|
77
|
+
# Load configs
|
|
78
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
79
|
+
config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
|
|
80
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
81
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
82
|
+
federation, config, federation_config_overrides
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Load the authentication plugin
|
|
86
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
|
87
|
+
auth_plugin.load_tokens()
|
|
88
|
+
if not isinstance(auth_plugin, OidcCliPlugin) or not auth_plugin.access_token:
|
|
89
|
+
typer.secho(
|
|
90
|
+
"❌ Please log in before publishing app.",
|
|
91
|
+
fg=typer.colors.RED,
|
|
92
|
+
err=True,
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(code=1)
|
|
95
|
+
|
|
96
|
+
# Load token from the plugin
|
|
97
|
+
token = auth_plugin.access_token
|
|
98
|
+
|
|
99
|
+
# Collect & validate app files
|
|
100
|
+
file_paths = _collect_file_paths(app)
|
|
101
|
+
_validate_files(file_paths)
|
|
102
|
+
|
|
103
|
+
# Build and POST multipart
|
|
104
|
+
with ExitStack() as stack:
|
|
105
|
+
files_param = _build_multipart_files_param(app, file_paths, stack)
|
|
106
|
+
try:
|
|
107
|
+
resp = _post_files(files_param, token)
|
|
108
|
+
except requests.RequestException as err:
|
|
109
|
+
typer.secho(f"❌ Network error: {err}", fg=typer.colors.RED, err=True)
|
|
110
|
+
raise typer.Exit(code=1) from err
|
|
111
|
+
|
|
112
|
+
if resp.ok:
|
|
113
|
+
typer.secho("🎊 Upload successful", fg=typer.colors.GREEN, bold=True)
|
|
114
|
+
return # success
|
|
115
|
+
|
|
116
|
+
# Error path:
|
|
117
|
+
msg = f"❌ Upload failed with status {resp.status_code}"
|
|
118
|
+
if resp.text:
|
|
119
|
+
msg += f": {resp.text}"
|
|
120
|
+
typer.secho(msg, fg=typer.colors.RED, err=True)
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _depth_of(relative_path_to_root: Path) -> int:
|
|
125
|
+
"""Return depth that is number of parts (directories) in the relative path
|
|
126
|
+
(excluding filename).
|
|
127
|
+
|
|
128
|
+
Example: "a/b/c.py" -> depth 2
|
|
129
|
+
Interpret "directory depth" as number of directories: len(parts) - 1
|
|
130
|
+
"""
|
|
131
|
+
return max(0, len(relative_path_to_root.parts) - 1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _detect_mime(path: Path) -> str:
|
|
135
|
+
"""Detect files' MIME."""
|
|
136
|
+
return MIME_MAP.get(path.suffix.lower(), "text/plain; charset=utf-8")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _collect_file_paths(root: Path) -> list[Path]:
|
|
140
|
+
"""Return list of file paths that match include/exclude patterns."""
|
|
141
|
+
# Build include/exclude pathspecs
|
|
142
|
+
# Note: This should be a temporary solution until we have a complete mechanism
|
|
143
|
+
# for configurable inclusion and exclusion rules.
|
|
144
|
+
# Note: Unlike Git, we do not support nested .gitignore files in subdirectories.
|
|
145
|
+
gitignore_patterns = tuple(load_gitignore_patterns(root / ".gitignore"))
|
|
146
|
+
exclude_pathspec = build_pathspec(gitignore_patterns + APP_PUBLISH_EXCLUDE_PATTERNS)
|
|
147
|
+
include_pathspec = build_pathspec(APP_PUBLISH_INCLUDE_PATTERNS)
|
|
148
|
+
|
|
149
|
+
# Walk the directory tree
|
|
150
|
+
file_paths: list[Path] = []
|
|
151
|
+
for path in root.rglob("*"):
|
|
152
|
+
if not path.is_file():
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# Skip excluded or not included files
|
|
156
|
+
# Note: pathspec requires POSIX style relative paths
|
|
157
|
+
relative_path = path.relative_to(root)
|
|
158
|
+
posix = relative_path.as_posix()
|
|
159
|
+
if exclude_pathspec.match_file(posix) or not include_pathspec.match_file(posix):
|
|
160
|
+
typer.echo(typer.style(f"Skip: {path}", fg=typer.colors.YELLOW))
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Check max depth
|
|
164
|
+
if _depth_of(relative_path) > MAX_DIR_DEPTH:
|
|
165
|
+
typer.secho(
|
|
166
|
+
f"Error: '{path}' "
|
|
167
|
+
f"exceeds the maximum directory depth "
|
|
168
|
+
f"of {MAX_DIR_DEPTH}.",
|
|
169
|
+
fg=typer.colors.RED,
|
|
170
|
+
err=True,
|
|
171
|
+
)
|
|
172
|
+
raise typer.Exit(code=2)
|
|
173
|
+
|
|
174
|
+
file_paths.append(path)
|
|
175
|
+
|
|
176
|
+
# Sort for deterministic ordering
|
|
177
|
+
file_paths.sort(key=lambda path: path.as_posix())
|
|
178
|
+
return file_paths
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _validate_files(file_paths: list[Path]) -> None:
|
|
182
|
+
"""Validate files against upload constraints.
|
|
183
|
+
|
|
184
|
+
Checks file count, individual file size, total size, and UTF-8 encoding.
|
|
185
|
+
"""
|
|
186
|
+
if len(file_paths) == 0:
|
|
187
|
+
typer.secho(
|
|
188
|
+
"Nothing to upload: no files matched after applying .gitignore and "
|
|
189
|
+
"allowed extensions.",
|
|
190
|
+
fg=typer.colors.RED,
|
|
191
|
+
err=True,
|
|
192
|
+
)
|
|
193
|
+
raise typer.Exit(code=2)
|
|
194
|
+
|
|
195
|
+
if len(file_paths) > MAX_FILE_COUNT:
|
|
196
|
+
typer.secho(
|
|
197
|
+
f"Too many files: {len(file_paths)} > allowed maximum of {MAX_FILE_COUNT}.",
|
|
198
|
+
fg=typer.colors.RED,
|
|
199
|
+
err=True,
|
|
200
|
+
)
|
|
201
|
+
raise typer.Exit(code=2)
|
|
202
|
+
|
|
203
|
+
# Calculate files size
|
|
204
|
+
total_size = 0
|
|
205
|
+
for path in file_paths:
|
|
206
|
+
file_size = path.stat().st_size
|
|
207
|
+
total_size += file_size
|
|
208
|
+
|
|
209
|
+
# Check single file size
|
|
210
|
+
if file_size > MAX_FILE_BYTES:
|
|
211
|
+
typer.secho(
|
|
212
|
+
f"File too large: '{path.as_posix()}' is {file_size:,} bytes, "
|
|
213
|
+
f"exceeding the per-file limit of {MAX_FILE_BYTES:,} bytes.",
|
|
214
|
+
fg=typer.colors.RED,
|
|
215
|
+
err=True,
|
|
216
|
+
)
|
|
217
|
+
raise typer.Exit(code=2)
|
|
218
|
+
|
|
219
|
+
# Ensure we can decode as UTF-8.
|
|
220
|
+
try:
|
|
221
|
+
path.read_text(encoding=UTF8)
|
|
222
|
+
except UnicodeDecodeError as err:
|
|
223
|
+
typer.secho(
|
|
224
|
+
f"Encoding error: '{path}' is not UTF-8 encoded.",
|
|
225
|
+
fg=typer.colors.RED,
|
|
226
|
+
err=True,
|
|
227
|
+
)
|
|
228
|
+
raise typer.Exit(code=2) from err
|
|
229
|
+
|
|
230
|
+
# Check total files size
|
|
231
|
+
if total_size > MAX_TOTAL_BYTES:
|
|
232
|
+
typer.secho(
|
|
233
|
+
"Total size of all files is too large: "
|
|
234
|
+
f"{total_size:,} bytes > {MAX_TOTAL_BYTES:,} bytes.",
|
|
235
|
+
fg=typer.colors.RED,
|
|
236
|
+
err=True,
|
|
237
|
+
)
|
|
238
|
+
raise typer.Exit(code=2)
|
|
239
|
+
|
|
240
|
+
# Print validation passed prompt
|
|
241
|
+
typer.echo(typer.style("✅ Validation passed", fg=typer.colors.GREEN, bold=True))
|
|
242
|
+
typer.echo(f"{len(file_paths)} files, {total_size:,} bytes in total")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _build_multipart_files_param(
|
|
246
|
+
root: Path,
|
|
247
|
+
file_paths: list[Path],
|
|
248
|
+
stack: ExitStack,
|
|
249
|
+
) -> list[tuple[str, tuple[str, IO[bytes], str]]]:
|
|
250
|
+
"""Build multipart/form-data files parameter for HTTP upload.
|
|
251
|
+
|
|
252
|
+
Returns list of tuples: (field_name, (filename, file_object, content_type)).
|
|
253
|
+
File handles are registered with ExitStack for proper cleanup.
|
|
254
|
+
"""
|
|
255
|
+
form: list[tuple[str, tuple[str, IO[bytes], str]]] = []
|
|
256
|
+
for path in file_paths:
|
|
257
|
+
# Detect MIME (content type)
|
|
258
|
+
mime = _detect_mime(path)
|
|
259
|
+
|
|
260
|
+
# Open file and register with ExitStack
|
|
261
|
+
# pylint: disable-next=consider-using-with
|
|
262
|
+
fobj = stack.enter_context(open(path.resolve(), "rb"))
|
|
263
|
+
typer.echo(f"Attach {path} ({mime}, {path.stat().st_size:,} bytes)")
|
|
264
|
+
|
|
265
|
+
# Get relative POSIX path
|
|
266
|
+
relative_posix = path.relative_to(root).as_posix()
|
|
267
|
+
|
|
268
|
+
# Append to form data (key, (filename, fileobj, mime))
|
|
269
|
+
form.append(("files", (relative_posix, fobj, mime)))
|
|
270
|
+
return form
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _post_files(
|
|
274
|
+
files_param: list[tuple[str, tuple[str, IO[bytes], str]]],
|
|
275
|
+
token: str,
|
|
276
|
+
) -> Response:
|
|
277
|
+
"""POST multipart with one part per file."""
|
|
278
|
+
url = f"{PLATFORM_API_URL}/hub/apps/publish"
|
|
279
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
280
|
+
body = {"flwr_version": flwr_version}
|
|
281
|
+
|
|
282
|
+
resp = requests.post(
|
|
283
|
+
url, files=files_param, headers=headers, json=body, timeout=120
|
|
284
|
+
)
|
|
285
|
+
return resp
|
|
@@ -0,0 +1,262 @@
|
|
|
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 `app review` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import hashlib
|
|
20
|
+
import re
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Annotated
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
import typer
|
|
26
|
+
from cryptography.exceptions import UnsupportedAlgorithm
|
|
27
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
28
|
+
|
|
29
|
+
from flwr.common import now
|
|
30
|
+
from flwr.common.config import get_flwr_dir
|
|
31
|
+
from flwr.common.constant import FAB_CONFIG_FILE
|
|
32
|
+
from flwr.common.version import package_version as flwr_version
|
|
33
|
+
from flwr.supercore.constant import PLATFORM_API_URL
|
|
34
|
+
from flwr.supercore.primitives.asymmetric_ed25519 import (
|
|
35
|
+
create_message_to_sign,
|
|
36
|
+
load_private_key,
|
|
37
|
+
sign_message,
|
|
38
|
+
)
|
|
39
|
+
from flwr.supercore.utils import parse_app_spec, request_download_link
|
|
40
|
+
|
|
41
|
+
from ..auth_plugin.oidc_cli_plugin import OidcCliPlugin
|
|
42
|
+
from ..config_utils import (
|
|
43
|
+
load_and_validate,
|
|
44
|
+
process_loaded_project_config,
|
|
45
|
+
validate_federation_in_project_config,
|
|
46
|
+
)
|
|
47
|
+
from ..constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
48
|
+
from ..install import install_from_fab
|
|
49
|
+
from ..utils import load_cli_auth_plugin
|
|
50
|
+
|
|
51
|
+
TRY_AGAIN_MESSAGE = "Please try again or press CTRL+C to abort.\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# pylint: disable-next=too-many-locals, too-many-statements
|
|
55
|
+
def review(
|
|
56
|
+
app_spec: Annotated[
|
|
57
|
+
str,
|
|
58
|
+
typer.Argument(
|
|
59
|
+
help="App specifier (e.g., '@account/app' or '@account/app==1.0.0'). "
|
|
60
|
+
"Version is optional; defaults to the latest."
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
app_dir_login: Annotated[
|
|
64
|
+
Path,
|
|
65
|
+
typer.Argument(
|
|
66
|
+
help="Project directory to used for login before reviewing app."
|
|
67
|
+
),
|
|
68
|
+
] = Path("."),
|
|
69
|
+
federation: Annotated[
|
|
70
|
+
str | None,
|
|
71
|
+
typer.Argument(
|
|
72
|
+
help="Name of the federation used for login before reviewing app."
|
|
73
|
+
),
|
|
74
|
+
] = None,
|
|
75
|
+
federation_config_overrides: Annotated[
|
|
76
|
+
list[str] | None,
|
|
77
|
+
typer.Option(
|
|
78
|
+
"--federation-config",
|
|
79
|
+
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
80
|
+
),
|
|
81
|
+
] = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Download a FAB for <APP-ID>, unpack it for manual review, and upon confirmation
|
|
84
|
+
sign & submit the review to the Platform."""
|
|
85
|
+
# Load configs
|
|
86
|
+
pyproject_path = app_dir_login / FAB_CONFIG_FILE if app_dir_login else None
|
|
87
|
+
config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
|
|
88
|
+
config = process_loaded_project_config(config, errors, warnings)
|
|
89
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
90
|
+
federation, config, federation_config_overrides
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Load the authentication plugin
|
|
94
|
+
auth_plugin = load_cli_auth_plugin(app_dir_login, federation, federation_config)
|
|
95
|
+
auth_plugin.load_tokens()
|
|
96
|
+
if not isinstance(auth_plugin, OidcCliPlugin) or not auth_plugin.access_token:
|
|
97
|
+
typer.secho(
|
|
98
|
+
"❌ Please log in before reviewing app.",
|
|
99
|
+
fg=typer.colors.RED,
|
|
100
|
+
err=True,
|
|
101
|
+
)
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
# Load token from the plugin
|
|
105
|
+
token = auth_plugin.access_token
|
|
106
|
+
|
|
107
|
+
# Validate app version and ID format
|
|
108
|
+
try:
|
|
109
|
+
app_id, app_version = parse_app_spec(app_spec)
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
|
|
112
|
+
raise typer.Exit(code=1) from e
|
|
113
|
+
|
|
114
|
+
# Download FAB
|
|
115
|
+
typer.secho("Downloading FAB... ", fg=typer.colors.BLUE)
|
|
116
|
+
url = f"{PLATFORM_API_URL}/hub/fetch-fab"
|
|
117
|
+
try:
|
|
118
|
+
presigned_url, _ = request_download_link(app_id, app_version, url, "fab_url")
|
|
119
|
+
except ValueError as e:
|
|
120
|
+
typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
|
|
121
|
+
raise typer.Exit(code=1) from e
|
|
122
|
+
|
|
123
|
+
fab_bytes = _download_fab(presigned_url)
|
|
124
|
+
|
|
125
|
+
# Unpack FAB
|
|
126
|
+
typer.secho("Unpacking FAB... ", fg=typer.colors.BLUE)
|
|
127
|
+
review_dir = _create_review_dir()
|
|
128
|
+
review_app_path = install_from_fab(fab_bytes, review_dir)
|
|
129
|
+
|
|
130
|
+
# Extract app version
|
|
131
|
+
version_pattern = re.compile(r"\b(\d+\.\d+\.\d+)\b")
|
|
132
|
+
match = version_pattern.search(str(review_app_path))
|
|
133
|
+
assert match is not None
|
|
134
|
+
app_version = match.group(1)
|
|
135
|
+
|
|
136
|
+
# Prompt to ask for sign
|
|
137
|
+
typer.secho(
|
|
138
|
+
f"""
|
|
139
|
+
Review the unpacked app in the following directory:
|
|
140
|
+
|
|
141
|
+
{typer.style(review_app_path, fg=typer.colors.GREEN, bold=True)}
|
|
142
|
+
|
|
143
|
+
If you have reviewed the app and want to continue to sign it,
|
|
144
|
+
type {typer.style("SIGN", fg=typer.colors.GREEN, bold=True)} or abort with CTRL+C.
|
|
145
|
+
""",
|
|
146
|
+
fg=typer.colors.BLUE,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
confirmation = typer.prompt("Type SIGN to continue").strip()
|
|
150
|
+
if confirmation.upper() != "SIGN":
|
|
151
|
+
typer.secho("Aborted (user did not type SIGN).", fg=typer.colors.YELLOW)
|
|
152
|
+
raise typer.Exit(code=130)
|
|
153
|
+
|
|
154
|
+
# Ask for private key path (retry until valid)
|
|
155
|
+
while True:
|
|
156
|
+
try:
|
|
157
|
+
key_path_str = typer.prompt(
|
|
158
|
+
"Please specify the path of Ed25519 OpenSSH private key for signing"
|
|
159
|
+
)
|
|
160
|
+
except typer.Abort as e:
|
|
161
|
+
typer.secho("Aborted by user.", fg=typer.colors.YELLOW, err=True)
|
|
162
|
+
raise typer.Exit(code=130) from e
|
|
163
|
+
|
|
164
|
+
key_path = Path(key_path_str).expanduser().resolve()
|
|
165
|
+
|
|
166
|
+
if not key_path.is_file():
|
|
167
|
+
typer.secho(
|
|
168
|
+
f"❌ Private key not found: {key_path}",
|
|
169
|
+
fg=typer.colors.RED,
|
|
170
|
+
err=True,
|
|
171
|
+
)
|
|
172
|
+
typer.secho(TRY_AGAIN_MESSAGE, fg=typer.colors.YELLOW)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Load private key
|
|
176
|
+
try:
|
|
177
|
+
private_key = load_private_key(key_path)
|
|
178
|
+
except (OSError, ValueError, UnsupportedAlgorithm) as e:
|
|
179
|
+
typer.secho(
|
|
180
|
+
f"❌ Failed to load the private key: {e}", fg=typer.colors.RED, err=True
|
|
181
|
+
)
|
|
182
|
+
typer.secho(TRY_AGAIN_MESSAGE, fg=typer.colors.YELLOW)
|
|
183
|
+
continue
|
|
184
|
+
break # valid
|
|
185
|
+
|
|
186
|
+
# Sign FAB
|
|
187
|
+
signature, signed_at = _sign_fab(fab_bytes, private_key)
|
|
188
|
+
|
|
189
|
+
# Submit review
|
|
190
|
+
_submit_review(app_id, app_version, signature, signed_at, token)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _create_review_dir() -> Path:
|
|
194
|
+
"""Create a directory for reviewing code."""
|
|
195
|
+
home = get_flwr_dir()
|
|
196
|
+
review_dir = home / "reviews"
|
|
197
|
+
review_dir.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
return review_dir
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _download_fab(url: str) -> bytes:
|
|
202
|
+
"""Download FAB file from given URL."""
|
|
203
|
+
try:
|
|
204
|
+
r = requests.get(url, timeout=60)
|
|
205
|
+
r.raise_for_status()
|
|
206
|
+
except requests.RequestException as e:
|
|
207
|
+
typer.secho(
|
|
208
|
+
f"❌ FAB download failed: {e}",
|
|
209
|
+
fg=typer.colors.RED,
|
|
210
|
+
err=True,
|
|
211
|
+
)
|
|
212
|
+
raise typer.Exit(code=1) from e
|
|
213
|
+
return r.content
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _sign_fab(
|
|
217
|
+
fab_bytes: bytes, private_key: ed25519.Ed25519PrivateKey
|
|
218
|
+
) -> tuple[bytes, int]:
|
|
219
|
+
"""Sign the given FAB hash bytes."""
|
|
220
|
+
# Get current timestamp
|
|
221
|
+
timestamp = int(now().timestamp())
|
|
222
|
+
message_to_sign = create_message_to_sign(
|
|
223
|
+
hashlib.sha256(fab_bytes).digest(),
|
|
224
|
+
timestamp,
|
|
225
|
+
)
|
|
226
|
+
return sign_message(private_key, message_to_sign), timestamp
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _submit_review(
|
|
230
|
+
app_id: str, app_version: str, signature: bytes, signed_at: int, token: str
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Submit review to Flower Platform API."""
|
|
233
|
+
signature_b64 = base64.urlsafe_b64encode(signature).rstrip(b"=").decode("ascii")
|
|
234
|
+
url = f"{PLATFORM_API_URL}/hub/apps/signature"
|
|
235
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
236
|
+
payload = {
|
|
237
|
+
"app_id": app_id,
|
|
238
|
+
"app_version": app_version,
|
|
239
|
+
"signature_b64": signature_b64,
|
|
240
|
+
"signed_at": signed_at,
|
|
241
|
+
"flwr_version": flwr_version,
|
|
242
|
+
}
|
|
243
|
+
try:
|
|
244
|
+
resp = requests.post(url, headers=headers, json=payload, timeout=120)
|
|
245
|
+
except requests.RequestException as e:
|
|
246
|
+
typer.secho(
|
|
247
|
+
f"❌ Network error while submitting review: {e}",
|
|
248
|
+
fg=typer.colors.RED,
|
|
249
|
+
err=True,
|
|
250
|
+
)
|
|
251
|
+
raise typer.Exit(code=1) from e
|
|
252
|
+
|
|
253
|
+
if resp.ok:
|
|
254
|
+
typer.secho("🎊 Review submitted", fg=typer.colors.GREEN, bold=True)
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
# Error path:
|
|
258
|
+
msg = f"❌ Review submission failed (HTTP {resp.status_code})"
|
|
259
|
+
if resp.text:
|
|
260
|
+
msg += f": {resp.text}"
|
|
261
|
+
typer.secho(msg, fg=typer.colors.RED, err=True)
|
|
262
|
+
raise typer.Exit(code=1)
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
from abc import ABC, abstractmethod
|
|
19
19
|
from collections.abc import Sequence
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Optional, Union
|
|
22
21
|
|
|
23
22
|
from flwr.common.typing import AccountAuthCredentials, AccountAuthLoginDetails
|
|
24
23
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
@@ -84,12 +83,12 @@ class CliAuthPlugin(ABC):
|
|
|
84
83
|
|
|
85
84
|
@abstractmethod
|
|
86
85
|
def write_tokens_to_metadata(
|
|
87
|
-
self, metadata: Sequence[tuple[str,
|
|
88
|
-
) -> Sequence[tuple[str,
|
|
86
|
+
self, metadata: Sequence[tuple[str, str | bytes]]
|
|
87
|
+
) -> Sequence[tuple[str, str | bytes]]:
|
|
89
88
|
"""Write authentication tokens to the provided metadata."""
|
|
90
89
|
|
|
91
90
|
@abstractmethod
|
|
92
91
|
def read_tokens_from_metadata(
|
|
93
|
-
self, metadata: Sequence[tuple[str,
|
|
94
|
-
) ->
|
|
92
|
+
self, metadata: Sequence[tuple[str, str | bytes]]
|
|
93
|
+
) -> AccountAuthCredentials | None:
|
|
95
94
|
"""Read authentication tokens from the provided metadata."""
|