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
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from logging import DEBUG
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from flwr.common.logger import log
|
|
22
21
|
|
|
@@ -35,7 +34,7 @@ class FfsFactory:
|
|
|
35
34
|
|
|
36
35
|
def __init__(self, base_dir: str) -> None:
|
|
37
36
|
self.base_dir = base_dir
|
|
38
|
-
self.ffs_instance:
|
|
37
|
+
self.ffs_instance: Ffs | None = None
|
|
39
38
|
|
|
40
39
|
def ffs(self) -> Ffs:
|
|
41
40
|
"""Return a Ffs instance and create it, if necessary."""
|
|
@@ -16,24 +16,27 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import random
|
|
19
|
+
import signal
|
|
19
20
|
import threading
|
|
20
|
-
from
|
|
21
|
+
from collections.abc import Callable
|
|
21
22
|
|
|
22
23
|
import grpc
|
|
23
24
|
|
|
25
|
+
from flwr.common.constant import (
|
|
26
|
+
HEARTBEAT_BASE_MULTIPLIER,
|
|
27
|
+
HEARTBEAT_CALL_TIMEOUT,
|
|
28
|
+
HEARTBEAT_DEFAULT_INTERVAL,
|
|
29
|
+
HEARTBEAT_RANDOM_RANGE,
|
|
30
|
+
)
|
|
31
|
+
from flwr.common.retry_invoker import RetryInvoker, exponential
|
|
32
|
+
from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
|
|
33
|
+
|
|
24
34
|
# pylint: disable=E0611
|
|
25
35
|
from flwr.proto.heartbeat_pb2 import SendAppHeartbeatRequest
|
|
26
36
|
from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub
|
|
27
37
|
from flwr.proto.simulationio_pb2_grpc import SimulationIoStub
|
|
28
38
|
|
|
29
39
|
# pylint: enable=E0611
|
|
30
|
-
from .constant import (
|
|
31
|
-
HEARTBEAT_BASE_MULTIPLIER,
|
|
32
|
-
HEARTBEAT_CALL_TIMEOUT,
|
|
33
|
-
HEARTBEAT_DEFAULT_INTERVAL,
|
|
34
|
-
HEARTBEAT_RANDOM_RANGE,
|
|
35
|
-
)
|
|
36
|
-
from .retry_invoker import RetryInvoker, exponential
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
class HeartbeatFailure(Exception):
|
|
@@ -116,24 +119,18 @@ class HeartbeatSender:
|
|
|
116
119
|
raise HeartbeatFailure
|
|
117
120
|
|
|
118
121
|
|
|
119
|
-
def
|
|
120
|
-
stub:
|
|
121
|
-
|
|
122
|
-
*,
|
|
123
|
-
failure_message: str,
|
|
122
|
+
def make_app_heartbeat_fn_grpc(
|
|
123
|
+
stub: ServerAppIoStub | SimulationIoStub | ClientAppIoStub,
|
|
124
|
+
token: str,
|
|
124
125
|
) -> Callable[[], bool]:
|
|
125
|
-
"""Get the function to send a heartbeat to gRPC endpoint.
|
|
126
|
-
|
|
127
|
-
This function is for app heartbeats only. It is not used for node heartbeats.
|
|
126
|
+
"""Get the function to send a heartbeat to gRPC endpoint from an app process.
|
|
128
127
|
|
|
129
128
|
Parameters
|
|
130
129
|
----------
|
|
131
130
|
stub : Union[ServerAppIoStub, SimulationIoStub]
|
|
132
131
|
gRPC stub to send the heartbeat.
|
|
133
|
-
|
|
134
|
-
The
|
|
135
|
-
failure_message : str
|
|
136
|
-
Error message to raise if the heartbeat fails.
|
|
132
|
+
token : str
|
|
133
|
+
The token to use in the heartbeat request.
|
|
137
134
|
|
|
138
135
|
Returns
|
|
139
136
|
-------
|
|
@@ -141,9 +138,7 @@ def get_grpc_app_heartbeat_fn(
|
|
|
141
138
|
Function that sends a heartbeat to the gRPC endpoint.
|
|
142
139
|
"""
|
|
143
140
|
# Construct the heartbeat request
|
|
144
|
-
req = SendAppHeartbeatRequest(
|
|
145
|
-
run_id=run_id, heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL
|
|
146
|
-
)
|
|
141
|
+
req = SendAppHeartbeatRequest(token=token)
|
|
147
142
|
|
|
148
143
|
def fn() -> bool:
|
|
149
144
|
# Call ServerAppIo API
|
|
@@ -157,9 +152,9 @@ def get_grpc_app_heartbeat_fn(
|
|
|
157
152
|
return False
|
|
158
153
|
raise
|
|
159
154
|
|
|
160
|
-
#
|
|
155
|
+
# Raise SIGINT to trigger graceful shutdown if heartbeat failed
|
|
161
156
|
if not res.success:
|
|
162
|
-
|
|
157
|
+
signal.raise_signal(signal.SIGINT)
|
|
163
158
|
return True
|
|
164
159
|
|
|
165
160
|
return fn
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
import threading
|
|
19
19
|
from dataclasses import dataclass
|
|
20
|
-
from typing import Optional
|
|
21
20
|
|
|
22
21
|
from flwr.common.inflatable import (
|
|
23
22
|
get_object_id,
|
|
@@ -154,7 +153,7 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
154
153
|
self.store[object_id].content = object_content
|
|
155
154
|
self.store[object_id].is_available = True
|
|
156
155
|
|
|
157
|
-
def get(self, object_id: str) ->
|
|
156
|
+
def get(self, object_id: str) -> bytes | None:
|
|
158
157
|
"""Get an object from the store."""
|
|
159
158
|
with self.lock_store:
|
|
160
159
|
# Check if the object ID is pre-registered
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import abc
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
22
21
|
|
|
@@ -89,7 +88,7 @@ class ObjectStore(abc.ABC):
|
|
|
89
88
|
"""
|
|
90
89
|
|
|
91
90
|
@abc.abstractmethod
|
|
92
|
-
def get(self, object_id: str) ->
|
|
91
|
+
def get(self, object_id: str) -> bytes | None:
|
|
93
92
|
"""Get an object from the store.
|
|
94
93
|
|
|
95
94
|
Parameters
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from logging import DEBUG
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from flwr.common.logger import log
|
|
22
21
|
from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME
|
|
@@ -40,7 +39,7 @@ class ObjectStoreFactory:
|
|
|
40
39
|
|
|
41
40
|
def __init__(self, database: str = FLWR_IN_MEMORY_DB_NAME) -> None:
|
|
42
41
|
self.database = database
|
|
43
|
-
self.store_instance:
|
|
42
|
+
self.store_instance: ObjectStore | None = None
|
|
44
43
|
|
|
45
44
|
def store(self) -> ObjectStore:
|
|
46
45
|
"""Return an ObjectStore instance and create it, if necessary.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"""Flower SQLite ObjectStore implementation."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from typing import
|
|
18
|
+
from typing import cast
|
|
19
19
|
|
|
20
20
|
from flwr.common.inflatable import (
|
|
21
21
|
get_object_id,
|
|
@@ -63,13 +63,12 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
|
63
63
|
super().__init__(database_path)
|
|
64
64
|
self.verify = verify
|
|
65
65
|
|
|
66
|
-
def
|
|
67
|
-
"""
|
|
68
|
-
return
|
|
66
|
+
def get_sql_statements(self) -> tuple[str, ...]:
|
|
67
|
+
"""Return SQL statements for ObjectStore tables."""
|
|
68
|
+
return (
|
|
69
69
|
SQL_CREATE_OBJECTS,
|
|
70
70
|
SQL_CREATE_OBJECT_CHILDREN,
|
|
71
71
|
SQL_CREATE_RUN_OBJECTS,
|
|
72
|
-
log_queries=log_queries,
|
|
73
72
|
)
|
|
74
73
|
|
|
75
74
|
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
@@ -126,7 +125,9 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
|
126
125
|
"SELECT object_id FROM objects WHERE object_id=?", (object_id,)
|
|
127
126
|
).fetchone()
|
|
128
127
|
if not row:
|
|
129
|
-
raise NoObjectInStoreError(
|
|
128
|
+
raise NoObjectInStoreError(
|
|
129
|
+
f"Object {object_id} was not pre-registered."
|
|
130
|
+
)
|
|
130
131
|
children = self.query(
|
|
131
132
|
"SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
|
|
132
133
|
)
|
|
@@ -176,7 +177,7 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
|
176
177
|
(object_content, object_id),
|
|
177
178
|
)
|
|
178
179
|
|
|
179
|
-
def get(self, object_id: str) ->
|
|
180
|
+
def get(self, object_id: str) -> bytes | None:
|
|
180
181
|
"""Get an object from the store."""
|
|
181
182
|
rows = self.query("SELECT content FROM objects WHERE object_id=?", (object_id,))
|
|
182
183
|
return rows[0]["content"] if rows else None
|
|
@@ -113,5 +113,5 @@ def uses_nist_ec_curve(public_key: ec.EllipticCurvePublicKey) -> bool:
|
|
|
113
113
|
"""Return True if the provided key uses a NIST EC curve."""
|
|
114
114
|
return isinstance(
|
|
115
115
|
public_key.curve,
|
|
116
|
-
(ec.SECP192R1
|
|
116
|
+
(ec.SECP192R1 | ec.SECP224R1 | ec.SECP256R1 | ec.SECP384R1 | ec.SECP521R1),
|
|
117
117
|
)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"""Ed25519-only asymmetric cryptography utilities."""
|
|
16
16
|
|
|
17
17
|
import base64
|
|
18
|
+
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
from cryptography.exceptions import InvalidSignature
|
|
20
21
|
from cryptography.hazmat.primitives import serialization
|
|
@@ -150,7 +151,7 @@ def verify_signature(
|
|
|
150
151
|
return False
|
|
151
152
|
|
|
152
153
|
|
|
153
|
-
def
|
|
154
|
+
def create_message_to_sign(fab_digest: bytes, timestamp: int) -> bytes:
|
|
154
155
|
"""Create a canonical message:
|
|
155
156
|
timestamp (8 bytes big-endian) + fab_digest.
|
|
156
157
|
"""
|
|
@@ -163,3 +164,12 @@ def decode_base64url(sig: str) -> bytes:
|
|
|
163
164
|
# add missing padding (=) to a multiple of 4
|
|
164
165
|
pad = (-len(sig)) % 4
|
|
165
166
|
return base64.urlsafe_b64decode(sig + ("=" * pad))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def load_private_key(path: Path) -> ed25519.Ed25519PrivateKey:
|
|
170
|
+
"""Load an SSH-format private key (Ed25519) using cryptography."""
|
|
171
|
+
key_bytes = path.read_bytes()
|
|
172
|
+
private_key = serialization.load_ssh_private_key(key_bytes, password=None)
|
|
173
|
+
if not isinstance(private_key, ed25519.Ed25519PrivateKey):
|
|
174
|
+
raise ValueError("Private key is not Ed25519")
|
|
175
|
+
return private_key
|
flwr/supercore/sqlite_mixin.py
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
import re
|
|
19
19
|
import sqlite3
|
|
20
|
-
from abc import ABC
|
|
20
|
+
from abc import ABC
|
|
21
21
|
from collections.abc import Sequence
|
|
22
22
|
from logging import DEBUG, ERROR
|
|
23
|
-
from typing import Any
|
|
23
|
+
from typing import Any
|
|
24
24
|
|
|
25
25
|
from flwr.common.logger import log
|
|
26
26
|
|
|
27
|
-
DictOrTuple =
|
|
27
|
+
DictOrTuple = tuple[Any, ...] | dict[str, Any]
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class SqliteMixin(ABC):
|
|
@@ -32,7 +32,7 @@ class SqliteMixin(ABC):
|
|
|
32
32
|
|
|
33
33
|
def __init__(self, database_path: str) -> None:
|
|
34
34
|
self.database_path = database_path
|
|
35
|
-
self._conn:
|
|
35
|
+
self._conn: sqlite3.Connection | None = None
|
|
36
36
|
|
|
37
37
|
@property
|
|
38
38
|
def conn(self) -> sqlite3.Connection:
|
|
@@ -41,10 +41,24 @@ class SqliteMixin(ABC):
|
|
|
41
41
|
raise AttributeError("Database not initialized. Call initialize() first.")
|
|
42
42
|
return self._conn
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
def get_sql_statements(self) -> tuple[str, ...]:
|
|
45
|
+
"""Return SQL statements for this class.
|
|
46
|
+
|
|
47
|
+
Subclasses can override this to provide their SQL CREATE statements.
|
|
48
|
+
The base implementation returns an empty tuple.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
tuple[str, ...]
|
|
53
|
+
SQL CREATE TABLE/INDEX statements for this class.
|
|
54
|
+
"""
|
|
55
|
+
return ()
|
|
56
|
+
|
|
45
57
|
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
46
58
|
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
47
59
|
|
|
60
|
+
This method executes SQL statements returned by `get_sql_statements()`.
|
|
61
|
+
|
|
48
62
|
Parameters
|
|
49
63
|
----------
|
|
50
64
|
log_queries : bool
|
|
@@ -57,45 +71,34 @@ class SqliteMixin(ABC):
|
|
|
57
71
|
|
|
58
72
|
Examples
|
|
59
73
|
--------
|
|
60
|
-
|
|
74
|
+
Override `get_sql_statements()` in your subclass:
|
|
61
75
|
|
|
62
76
|
.. code:: python
|
|
63
77
|
|
|
64
|
-
def
|
|
65
|
-
return
|
|
78
|
+
def get_sql_statements(self) -> tuple[str, ...]:
|
|
79
|
+
return (
|
|
66
80
|
SQL_CREATE_TABLE_FOO,
|
|
67
81
|
SQL_CREATE_TABLE_BAR,
|
|
68
|
-
log_queries=log_queries
|
|
69
82
|
)
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def _ensure_initialized(
|
|
73
|
-
self,
|
|
74
|
-
*create_statements: str,
|
|
75
|
-
log_queries: bool = False,
|
|
76
|
-
) -> list[tuple[str]]:
|
|
77
|
-
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
their `.initialize()` methods.
|
|
84
|
+
To include parent SQL statements, call super():
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
----------
|
|
84
|
-
create_statements : str
|
|
85
|
-
SQL statements to create tables and indexes.
|
|
86
|
-
log_queries : bool
|
|
87
|
-
Log each query which is executed.
|
|
86
|
+
.. code:: python
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
def get_sql_statements(self) -> tuple[str, ...]:
|
|
89
|
+
return super().get_sql_statements() + (
|
|
90
|
+
SQL_CREATE_TABLE_FOO,
|
|
91
|
+
SQL_CREATE_TABLE_BAR,
|
|
92
|
+
)
|
|
93
93
|
"""
|
|
94
94
|
self._conn = sqlite3.connect(self.database_path)
|
|
95
95
|
# Enable Write-Ahead Logging (WAL) for better concurrency
|
|
96
96
|
self._conn.execute("PRAGMA journal_mode = WAL;")
|
|
97
97
|
self._conn.execute("PRAGMA synchronous = NORMAL;")
|
|
98
98
|
self._conn.execute("PRAGMA foreign_keys = ON;")
|
|
99
|
+
self._conn.execute("PRAGMA cache_size = -64000;") # 64MB cache
|
|
100
|
+
self._conn.execute("PRAGMA temp_store = MEMORY;") # In-memory temp tables
|
|
101
|
+
self._conn.execute("PRAGMA mmap_size = 268435456;") # 256MB memory-mapped I/O
|
|
99
102
|
self._conn.row_factory = dict_factory
|
|
100
103
|
|
|
101
104
|
if log_queries:
|
|
@@ -103,7 +106,7 @@ class SqliteMixin(ABC):
|
|
|
103
106
|
|
|
104
107
|
# Create tables and indexes
|
|
105
108
|
cur = self._conn.cursor()
|
|
106
|
-
for sql in
|
|
109
|
+
for sql in self.get_sql_statements():
|
|
107
110
|
cur.execute(sql)
|
|
108
111
|
res = cur.execute("SELECT name FROM sqlite_schema;")
|
|
109
112
|
return res.fetchall()
|
|
@@ -111,7 +114,7 @@ class SqliteMixin(ABC):
|
|
|
111
114
|
def query(
|
|
112
115
|
self,
|
|
113
116
|
query: str,
|
|
114
|
-
data:
|
|
117
|
+
data: Sequence[DictOrTuple] | DictOrTuple | None = None,
|
|
115
118
|
) -> list[dict[str, Any]]:
|
|
116
119
|
"""Execute a SQL query and return the results as list of dicts."""
|
|
117
120
|
if self._conn is None:
|
|
@@ -127,8 +130,8 @@ class SqliteMixin(ABC):
|
|
|
127
130
|
with self._conn:
|
|
128
131
|
if (
|
|
129
132
|
len(data) > 0
|
|
130
|
-
and isinstance(data, (tuple
|
|
131
|
-
and isinstance(data[0], (tuple
|
|
133
|
+
and isinstance(data, (tuple | list))
|
|
134
|
+
and isinstance(data[0], (tuple | dict))
|
|
132
135
|
):
|
|
133
136
|
rows = self._conn.executemany(query, data)
|
|
134
137
|
else:
|
|
@@ -153,4 +156,4 @@ def dict_factory(
|
|
|
153
156
|
Less efficent for retrival of large amounts of data but easier to use.
|
|
154
157
|
"""
|
|
155
158
|
fields = [column[0] for column in cursor.description]
|
|
156
|
-
return dict(zip(fields, row))
|
|
159
|
+
return dict(zip(fields, row, strict=True))
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import os
|
|
19
19
|
import subprocess
|
|
20
20
|
from collections.abc import Sequence
|
|
21
|
-
from typing import Optional
|
|
22
21
|
|
|
23
22
|
from .exec_plugin import ExecPlugin
|
|
24
23
|
|
|
@@ -33,7 +32,7 @@ class BaseExecPlugin(ExecPlugin):
|
|
|
33
32
|
command = ""
|
|
34
33
|
appio_api_address_arg = ""
|
|
35
34
|
|
|
36
|
-
def select_run_id(self, candidate_run_ids: Sequence[int]) ->
|
|
35
|
+
def select_run_id(self, candidate_run_ids: Sequence[int]) -> int | None:
|
|
37
36
|
"""Select a run ID to execute from a sequence of candidates."""
|
|
38
37
|
if not candidate_run_ids:
|
|
39
38
|
return None
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from abc import ABC, abstractmethod
|
|
19
|
-
from collections.abc import Sequence
|
|
20
|
-
from typing import Any
|
|
19
|
+
from collections.abc import Callable, Sequence
|
|
20
|
+
from typing import Any
|
|
21
21
|
|
|
22
22
|
from flwr.common.typing import Run
|
|
23
23
|
|
|
@@ -36,7 +36,7 @@ class ExecPlugin(ABC):
|
|
|
36
36
|
self.get_run = get_run
|
|
37
37
|
|
|
38
38
|
@abstractmethod
|
|
39
|
-
def select_run_id(self, candidate_run_ids: Sequence[int]) ->
|
|
39
|
+
def select_run_id(self, candidate_run_ids: Sequence[int]) -> int | None:
|
|
40
40
|
"""Select a run ID to execute from a sequence of candidates.
|
|
41
41
|
|
|
42
42
|
A candidate run ID is one that has at least one pending message and is
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import time
|
|
19
19
|
from logging import WARN
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
21
21
|
|
|
22
22
|
from flwr.common.config import get_flwr_dir
|
|
23
23
|
from flwr.common.exit import ExitCode, flwr_exit, register_signal_handlers
|
|
@@ -43,14 +43,12 @@ from .plugin import ExecPlugin
|
|
|
43
43
|
|
|
44
44
|
def run_superexec( # pylint: disable=R0913,R0914,R0917
|
|
45
45
|
plugin_class: type[ExecPlugin],
|
|
46
|
-
stub_class:
|
|
47
|
-
type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
|
|
48
|
-
],
|
|
46
|
+
stub_class: type[ClientAppIoStub] | type[ServerAppIoStub] | type[SimulationIoStub],
|
|
49
47
|
appio_api_address: str,
|
|
50
|
-
plugin_config:
|
|
51
|
-
flwr_dir:
|
|
52
|
-
parent_pid:
|
|
53
|
-
health_server_address:
|
|
48
|
+
plugin_config: dict[str, Any] | None = None,
|
|
49
|
+
flwr_dir: str | None = None,
|
|
50
|
+
parent_pid: int | None = None,
|
|
51
|
+
health_server_address: str | None = None,
|
|
54
52
|
) -> None:
|
|
55
53
|
"""Run Flower SuperExec.
|
|
56
54
|
|
|
@@ -158,12 +156,10 @@ def run_with_deprecation_warning( # pylint: disable=R0913, R0917
|
|
|
158
156
|
cmd: str,
|
|
159
157
|
plugin_type: str,
|
|
160
158
|
plugin_class: type[ExecPlugin],
|
|
161
|
-
stub_class:
|
|
162
|
-
type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
|
|
163
|
-
],
|
|
159
|
+
stub_class: type[ClientAppIoStub] | type[ServerAppIoStub] | type[SimulationIoStub],
|
|
164
160
|
appio_api_address: str,
|
|
165
|
-
flwr_dir:
|
|
166
|
-
parent_pid:
|
|
161
|
+
flwr_dir: str | None,
|
|
162
|
+
parent_pid: int | None,
|
|
167
163
|
warn_run_once: bool,
|
|
168
164
|
) -> None:
|
|
169
165
|
"""Log a deprecation warning and run the equivalent `flower-superexec` command.
|
flwr/supercore/utils.py
CHANGED
|
@@ -15,6 +15,16 @@
|
|
|
15
15
|
"""Utility functions for the infrastructure."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
from flwr.common.version import package_version as flwr_version
|
|
24
|
+
|
|
25
|
+
from .constant import APP_ID_PATTERN, APP_VERSION_PATTERN
|
|
26
|
+
|
|
27
|
+
|
|
18
28
|
def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
|
|
19
29
|
"""Mask a string by preserving only the head and tail characters.
|
|
20
30
|
|
|
@@ -50,3 +60,183 @@ def int64_to_uint64(signed: int) -> int:
|
|
|
50
60
|
if signed < 0:
|
|
51
61
|
return signed + (1 << 64)
|
|
52
62
|
return signed
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_app_spec(app_spec: str) -> tuple[str, str | None]:
|
|
66
|
+
"""Parse app specification string into app ID and version.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
app_spec : str
|
|
71
|
+
The app specification string in the format '@account/app' or
|
|
72
|
+
'@account/app==x.y.z' (digits only).
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
tuple[str, str | None]
|
|
77
|
+
A tuple containing the app ID and optional version.
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
ValueError
|
|
82
|
+
If the app specification format is invalid.
|
|
83
|
+
"""
|
|
84
|
+
if "==" in app_spec:
|
|
85
|
+
app_id, app_version = app_spec.split("==", 1)
|
|
86
|
+
|
|
87
|
+
if not re.match(APP_VERSION_PATTERN, app_version):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"Invalid app version. Expected format: x.y.z (digits only)."
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
app_id = app_spec
|
|
93
|
+
app_version = None
|
|
94
|
+
|
|
95
|
+
if not re.match(APP_ID_PATTERN, app_id):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Invalid remote app ID. Expected format: '@account_name/app_name'."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return app_id, app_version
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def request_download_link(
|
|
104
|
+
app_id: str, app_version: str | None, in_url: str, out_url: str
|
|
105
|
+
) -> tuple[str, list[dict[str, str]] | None]:
|
|
106
|
+
"""Request a download link for the given app from the Flower Platform API.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
app_id : str
|
|
111
|
+
The application identifier in the format '@account/app'.
|
|
112
|
+
app_version : str | None
|
|
113
|
+
The application version (e.g., '1.2.3'), or None to request the latest version.
|
|
114
|
+
in_url : str
|
|
115
|
+
The Platform API endpoint URL to query.
|
|
116
|
+
out_url : str
|
|
117
|
+
The key name in the response that contains the download URL.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
tuple[str, list[dict[str, str]] | None]
|
|
122
|
+
A tuple containing:
|
|
123
|
+
- The download URL for the application.
|
|
124
|
+
- A list of verification dictionaries if provided by the API, otherwise None.
|
|
125
|
+
|
|
126
|
+
Raises
|
|
127
|
+
------
|
|
128
|
+
ValueError
|
|
129
|
+
If the API connection fails, the application or version is not found,
|
|
130
|
+
the API returns a non-200 response, or the response format is invalid.
|
|
131
|
+
"""
|
|
132
|
+
headers = {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"Accept": "application/json",
|
|
135
|
+
}
|
|
136
|
+
body = {
|
|
137
|
+
"app_id": app_id, # send raw string of app_id
|
|
138
|
+
"app_version": app_version,
|
|
139
|
+
"flwr_version": flwr_version,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
resp = requests.post(in_url, headers=headers, data=json.dumps(body), timeout=20)
|
|
144
|
+
except requests.RequestException as e:
|
|
145
|
+
raise ValueError(f"Unable to connect to Platform API: {e}") from e
|
|
146
|
+
|
|
147
|
+
if resp.status_code == 404:
|
|
148
|
+
# Expecting a JSON body with a "detail" field
|
|
149
|
+
try:
|
|
150
|
+
error_message = resp.json().get("detail")
|
|
151
|
+
except ValueError:
|
|
152
|
+
# JSON parsing failed
|
|
153
|
+
raise ValueError(f"{app_id} not found in Platform API.") from None
|
|
154
|
+
|
|
155
|
+
if isinstance(error_message, dict):
|
|
156
|
+
available_app_versions = error_message.get("available_app_versions", [])
|
|
157
|
+
available_versions_str = (
|
|
158
|
+
", ".join(map(str, available_app_versions))
|
|
159
|
+
if available_app_versions
|
|
160
|
+
else "None"
|
|
161
|
+
)
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"{app_id}=={app_version} not found in Platform API. "
|
|
164
|
+
f"Available app versions for {app_id}: {available_versions_str}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
raise ValueError(f"{app_id} not found in Platform API.")
|
|
168
|
+
|
|
169
|
+
if not resp.ok:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"Platform API request failed with status {resp.status_code}. "
|
|
172
|
+
f"Details: {resp.text}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
data = resp.json()
|
|
176
|
+
if out_url not in data:
|
|
177
|
+
raise ValueError("Invalid response from Platform API")
|
|
178
|
+
|
|
179
|
+
verifications = data["verifications"] if "verifications" in data else None
|
|
180
|
+
|
|
181
|
+
return str(data[out_url]), verifications
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def humanize_duration(seconds: float) -> str:
|
|
185
|
+
"""Convert a duration in seconds to a human-friendly string.
|
|
186
|
+
|
|
187
|
+
Rules:
|
|
188
|
+
- < 90 seconds: show seconds
|
|
189
|
+
- < 1 hour: show minutes + seconds
|
|
190
|
+
- < 1 day: show hours + minutes
|
|
191
|
+
- >= 1 day: show days + hours
|
|
192
|
+
"""
|
|
193
|
+
seconds = int(seconds)
|
|
194
|
+
|
|
195
|
+
# Under 90 seconds → Seconds only
|
|
196
|
+
if seconds < 90:
|
|
197
|
+
return f"{seconds}s"
|
|
198
|
+
|
|
199
|
+
# Under 1 hour → Minutes and seconds
|
|
200
|
+
minutes, sec = divmod(seconds, 60)
|
|
201
|
+
if minutes < 60:
|
|
202
|
+
return f"{minutes}m {sec}s"
|
|
203
|
+
|
|
204
|
+
# Under 1 day → Hours and minutes
|
|
205
|
+
hours, minutes = divmod(minutes, 60)
|
|
206
|
+
if hours < 24:
|
|
207
|
+
return f"{hours}h {minutes}m"
|
|
208
|
+
|
|
209
|
+
# 1+ days → Days and hours
|
|
210
|
+
days, hours = divmod(hours, 24)
|
|
211
|
+
return f"{days}d {hours}h"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def humanize_bytes(num_bytes: int) -> str:
|
|
215
|
+
"""Convert a number of bytes to a human-friendly string.
|
|
216
|
+
|
|
217
|
+
Uses 1024-based units and 0-1 decimal precision.
|
|
218
|
+
Rules:
|
|
219
|
+
- < 1 KB: bytes
|
|
220
|
+
- < 1 MB: KB
|
|
221
|
+
- < 1 GB: MB
|
|
222
|
+
- < 1 TB: GB
|
|
223
|
+
"""
|
|
224
|
+
value = float(num_bytes)
|
|
225
|
+
|
|
226
|
+
for suffix in ["B", "KB", "MB", "GB", "TB"]:
|
|
227
|
+
if value < 1024 or suffix == "TB":
|
|
228
|
+
# Bytes → no decimals
|
|
229
|
+
if suffix == "B":
|
|
230
|
+
return f"{int(value)} B"
|
|
231
|
+
|
|
232
|
+
# Decide precision: 1 decimal for <10, otherwise no decimal
|
|
233
|
+
if value < 10:
|
|
234
|
+
formatted = f"{value:.1f}"
|
|
235
|
+
else:
|
|
236
|
+
formatted = f"{int(value)}"
|
|
237
|
+
|
|
238
|
+
return f"{formatted} {suffix}"
|
|
239
|
+
|
|
240
|
+
value /= 1024
|
|
241
|
+
|
|
242
|
+
raise RuntimeError("Unreachable code") # Make mypy happy
|