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
flwr/cli/ls.py
CHANGED
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import io
|
|
19
19
|
import json
|
|
20
|
-
from datetime import datetime, timedelta
|
|
21
20
|
from pathlib import Path
|
|
22
|
-
from typing import Annotated,
|
|
21
|
+
from typing import Annotated, cast
|
|
23
22
|
|
|
24
23
|
import typer
|
|
25
24
|
from rich.console import Console
|
|
@@ -33,33 +32,31 @@ from flwr.cli.config_utils import (
|
|
|
33
32
|
validate_federation_in_project_config,
|
|
34
33
|
)
|
|
35
34
|
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
36
|
-
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, SubStatus
|
|
37
|
-
from flwr.common.date import format_timedelta, isoformat8601_utc
|
|
35
|
+
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat, Status, SubStatus
|
|
38
36
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
39
37
|
from flwr.common.serde import run_from_proto
|
|
40
|
-
from flwr.common.typing import Run
|
|
41
38
|
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
42
39
|
ListRunsRequest,
|
|
43
40
|
ListRunsResponse,
|
|
44
41
|
)
|
|
45
42
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
46
43
|
|
|
47
|
-
from .
|
|
48
|
-
|
|
49
|
-
_RunListType = tuple[int, str, str, str, str, str, str, str, str]
|
|
44
|
+
from .run_utils import RunRow, format_runs
|
|
45
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
50
46
|
|
|
51
47
|
|
|
52
48
|
def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
49
|
+
ctx: typer.Context,
|
|
53
50
|
app: Annotated[
|
|
54
51
|
Path,
|
|
55
52
|
typer.Argument(help="Path of the Flower project"),
|
|
56
53
|
] = Path("."),
|
|
57
54
|
federation: Annotated[
|
|
58
|
-
|
|
55
|
+
str | None,
|
|
59
56
|
typer.Argument(help="Name of the federation"),
|
|
60
57
|
] = None,
|
|
61
58
|
federation_config_overrides: Annotated[
|
|
62
|
-
|
|
59
|
+
list[str] | None,
|
|
63
60
|
typer.Option(
|
|
64
61
|
"--federation-config",
|
|
65
62
|
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
@@ -73,7 +70,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
73
70
|
),
|
|
74
71
|
] = False,
|
|
75
72
|
run_id: Annotated[
|
|
76
|
-
|
|
73
|
+
int | None,
|
|
77
74
|
typer.Option(
|
|
78
75
|
"--run-id",
|
|
79
76
|
help="Specific run ID to display",
|
|
@@ -93,15 +90,17 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
93
90
|
The following details are displayed:
|
|
94
91
|
|
|
95
92
|
- **Run ID:** Unique identifier for the run.
|
|
96
|
-
- **
|
|
93
|
+
- **Federation:** The federation to which the run belongs.
|
|
94
|
+
- **App:** The App associated with the run (``<APP_ID>==<APP_VERSION>``).
|
|
97
95
|
- **Status:** Current status of the run (pending, starting, running, finished).
|
|
98
96
|
- **Elapsed:** Time elapsed since the run started (``HH:MM:SS``).
|
|
99
|
-
- **
|
|
100
|
-
- **Running At:** Timestamp when the run started running.
|
|
101
|
-
- **Finished At:** Timestamp when the run finished.
|
|
97
|
+
- **Status Changed @:** Timestamp of the most recent status change.
|
|
102
98
|
|
|
103
99
|
All timestamps follow ISO 8601, UTC and are formatted as ``YYYY-MM-DD HH:MM:SSZ``.
|
|
104
100
|
"""
|
|
101
|
+
# Resolve command used (list or ls)
|
|
102
|
+
command_name = cast(str, ctx.command.name) if ctx.command else "list"
|
|
103
|
+
|
|
105
104
|
suppress_output = output_format == CliOutputFormat.JSON
|
|
106
105
|
captured_output = io.StringIO()
|
|
107
106
|
try:
|
|
@@ -111,19 +110,19 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
111
110
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
112
111
|
|
|
113
112
|
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
114
|
-
config, errors, warnings = load_and_validate(
|
|
113
|
+
config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
|
|
115
114
|
config = process_loaded_project_config(config, errors, warnings)
|
|
116
115
|
federation, federation_config = validate_federation_in_project_config(
|
|
117
116
|
federation, config, federation_config_overrides
|
|
118
117
|
)
|
|
119
|
-
exit_if_no_address(federation_config,
|
|
118
|
+
exit_if_no_address(federation_config, command_name)
|
|
120
119
|
channel = None
|
|
121
120
|
try:
|
|
122
121
|
if runs and run_id is not None:
|
|
123
122
|
raise ValueError(
|
|
124
123
|
"The options '--runs' and '--run-id' are mutually exclusive."
|
|
125
124
|
)
|
|
126
|
-
auth_plugin =
|
|
125
|
+
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
|
127
126
|
channel = init_channel(app, federation_config, auth_plugin)
|
|
128
127
|
stub = ControlStub(channel)
|
|
129
128
|
|
|
@@ -139,7 +138,10 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
139
138
|
if output_format == CliOutputFormat.JSON:
|
|
140
139
|
Console().print_json(_to_json(formatted_runs))
|
|
141
140
|
else:
|
|
142
|
-
|
|
141
|
+
if run_id is not None:
|
|
142
|
+
Console().print(_to_detail_table(formatted_runs[0]))
|
|
143
|
+
else:
|
|
144
|
+
Console().print(_to_table(formatted_runs))
|
|
143
145
|
finally:
|
|
144
146
|
if channel:
|
|
145
147
|
channel.close()
|
|
@@ -153,6 +155,7 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
153
155
|
f"{err}",
|
|
154
156
|
fg=typer.colors.RED,
|
|
155
157
|
bold=True,
|
|
158
|
+
err=True,
|
|
156
159
|
)
|
|
157
160
|
finally:
|
|
158
161
|
if suppress_output:
|
|
@@ -160,156 +163,197 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
160
163
|
captured_output.close()
|
|
161
164
|
|
|
162
165
|
|
|
163
|
-
def
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
def _format_datetime(dt: Optional[datetime]) -> str:
|
|
167
|
-
return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
|
|
168
|
-
|
|
169
|
-
run_list: list[_RunListType] = []
|
|
170
|
-
|
|
171
|
-
# Add rows
|
|
172
|
-
for run in sorted(
|
|
173
|
-
run_dict.values(), key=lambda x: datetime.fromisoformat(x.pending_at)
|
|
174
|
-
):
|
|
175
|
-
# Combine status and sub-status into a single string
|
|
176
|
-
if run.status.sub_status == "":
|
|
177
|
-
status_text = run.status.status
|
|
178
|
-
else:
|
|
179
|
-
status_text = f"{run.status.status}:{run.status.sub_status}"
|
|
180
|
-
|
|
181
|
-
# Convert isoformat to datetime
|
|
182
|
-
pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
|
|
183
|
-
running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
|
|
184
|
-
finished_at = (
|
|
185
|
-
datetime.fromisoformat(run.finished_at) if run.finished_at else None
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
# Calculate elapsed time
|
|
189
|
-
elapsed_time = timedelta()
|
|
190
|
-
if running_at:
|
|
191
|
-
if finished_at:
|
|
192
|
-
end_time = finished_at
|
|
193
|
-
else:
|
|
194
|
-
end_time = datetime.fromisoformat(now_isoformat)
|
|
195
|
-
elapsed_time = end_time - running_at
|
|
196
|
-
|
|
197
|
-
run_list.append(
|
|
198
|
-
(
|
|
199
|
-
run.run_id,
|
|
200
|
-
run.fab_id,
|
|
201
|
-
run.fab_version,
|
|
202
|
-
run.fab_hash,
|
|
203
|
-
status_text,
|
|
204
|
-
format_timedelta(elapsed_time),
|
|
205
|
-
_format_datetime(pending_at),
|
|
206
|
-
_format_datetime(running_at),
|
|
207
|
-
_format_datetime(finished_at),
|
|
208
|
-
)
|
|
209
|
-
)
|
|
210
|
-
return run_list
|
|
166
|
+
def _get_status_style(status_text: str) -> str:
|
|
167
|
+
"""Determine the display style/color for a status.
|
|
211
168
|
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
status_text : str
|
|
172
|
+
The status text to determine color for.
|
|
212
173
|
|
|
213
|
-
|
|
214
|
-
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
str
|
|
177
|
+
Color name for rich console styling (e.g., 'green', 'red', 'blue').
|
|
178
|
+
"""
|
|
179
|
+
status = status_text.lower()
|
|
180
|
+
sub_status = status_text.rsplit(":", maxsplit=1)[-1]
|
|
181
|
+
|
|
182
|
+
if sub_status == SubStatus.COMPLETED: # finished:completed
|
|
183
|
+
return "green"
|
|
184
|
+
if sub_status == SubStatus.FAILED: # finished:failed
|
|
185
|
+
return "red"
|
|
186
|
+
if sub_status == SubStatus.STOPPED: # finished:stopped
|
|
187
|
+
return "yellow"
|
|
188
|
+
if status in (Status.STARTING, Status.RUNNING): # starting, running
|
|
189
|
+
return "blue"
|
|
190
|
+
return "bright_black" # pending
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _to_table(run_list: list[RunRow]) -> Table:
|
|
194
|
+
"""Format the provided run list to a rich Table.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
run_list : list[RunRow]
|
|
199
|
+
List of run information to display.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
Table
|
|
204
|
+
Rich Table object with formatted run information.
|
|
205
|
+
"""
|
|
215
206
|
table = Table(header_style="bold cyan", show_lines=True)
|
|
216
207
|
|
|
217
208
|
# Add columns
|
|
218
|
-
table.add_column(
|
|
219
|
-
|
|
220
|
-
)
|
|
221
|
-
table.add_column(Text("FAB", justify="center"), style="dim white")
|
|
209
|
+
table.add_column(Text("Run ID", justify="center"), no_wrap=True)
|
|
210
|
+
table.add_column(Text("Federation", justify="center"))
|
|
211
|
+
table.add_column(Text("App", justify="center"))
|
|
222
212
|
table.add_column(Text("Status", justify="center"))
|
|
223
213
|
table.add_column(Text("Elapsed", justify="center"), style="blue")
|
|
224
|
-
table.add_column(Text("
|
|
225
|
-
table.add_column(Text("Running At", justify="center"), style="dim white")
|
|
226
|
-
table.add_column(Text("Finished At", justify="center"), style="dim white")
|
|
214
|
+
table.add_column(Text("Status Changed @", justify="center"))
|
|
227
215
|
|
|
228
216
|
for row in run_list:
|
|
229
|
-
(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
finished_at,
|
|
239
|
-
) = row
|
|
240
|
-
# Style the status based on its value
|
|
241
|
-
sub_status = status_text.rsplit(":", maxsplit=1)[-1]
|
|
242
|
-
if sub_status == SubStatus.COMPLETED:
|
|
243
|
-
status_style = "green"
|
|
244
|
-
elif sub_status == SubStatus.FAILED:
|
|
245
|
-
status_style = "red"
|
|
217
|
+
status_style = _get_status_style(row.status_text)
|
|
218
|
+
|
|
219
|
+
# Use the most recent timestamp
|
|
220
|
+
if row.finished_at != "N/A":
|
|
221
|
+
status_changed_at = row.finished_at
|
|
222
|
+
elif row.running_at != "N/A":
|
|
223
|
+
status_changed_at = row.running_at
|
|
224
|
+
elif row.starting_at != "N/A":
|
|
225
|
+
status_changed_at = row.starting_at
|
|
246
226
|
else:
|
|
247
|
-
|
|
227
|
+
status_changed_at = row.pending_at
|
|
248
228
|
|
|
249
229
|
formatted_row = (
|
|
250
|
-
f"[bold]{run_id}[/bold]",
|
|
251
|
-
|
|
252
|
-
f"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
finished_at,
|
|
230
|
+
f"[bold]{row.run_id}[/bold]",
|
|
231
|
+
row.federation,
|
|
232
|
+
f"@{row.fab_id}=={row.fab_version}",
|
|
233
|
+
f"[{status_style}]{row.status_text}[/{status_style}]",
|
|
234
|
+
row.elapsed,
|
|
235
|
+
status_changed_at,
|
|
257
236
|
)
|
|
258
237
|
table.add_row(*formatted_row)
|
|
259
238
|
|
|
260
239
|
return table
|
|
261
240
|
|
|
262
241
|
|
|
263
|
-
def
|
|
264
|
-
"""Format run
|
|
242
|
+
def _to_detail_table(run: RunRow) -> Table:
|
|
243
|
+
"""Format a single run's details in a vertical table layout.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
run : RunRow
|
|
248
|
+
The run information to display.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
Table
|
|
253
|
+
Rich Table object with detailed run information in vertical format.
|
|
254
|
+
"""
|
|
255
|
+
status_style = _get_status_style(run.status_text)
|
|
256
|
+
|
|
257
|
+
# Create vertical table with field names on the left
|
|
258
|
+
table = Table(show_header=False, show_lines=False)
|
|
259
|
+
table.add_column("Field", style="bold cyan", no_wrap=True)
|
|
260
|
+
table.add_column("Value")
|
|
261
|
+
|
|
262
|
+
# Add rows with all details
|
|
263
|
+
table.add_row("Run ID", f"[bold]{run.run_id}[/bold]")
|
|
264
|
+
table.add_row("Federation", run.federation)
|
|
265
|
+
table.add_row("App", f"@{run.fab_id}=={run.fab_version}")
|
|
266
|
+
table.add_row("FAB Hash", f"{run.fab_hash[:8]}...{run.fab_hash[-8:]}")
|
|
267
|
+
table.add_row("Status", f"[{status_style}]{run.status_text}[/{status_style}]")
|
|
268
|
+
table.add_row("Elapsed", f"[blue]{run.elapsed}[/blue]")
|
|
269
|
+
table.add_row("Pending At", run.pending_at)
|
|
270
|
+
table.add_row("Starting At", run.starting_at)
|
|
271
|
+
table.add_row("Running At", run.running_at)
|
|
272
|
+
table.add_row("Finished At", run.finished_at)
|
|
273
|
+
|
|
274
|
+
return table
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _to_json(run_list: list[RunRow]) -> str:
|
|
278
|
+
"""Format run status list to a JSON formatted string.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
run_list : list[RunRow]
|
|
283
|
+
List of run information to serialize.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
str
|
|
288
|
+
JSON string containing formatted run information.
|
|
289
|
+
"""
|
|
265
290
|
runs_list = []
|
|
266
291
|
for row in run_list:
|
|
267
|
-
(
|
|
268
|
-
run_id,
|
|
269
|
-
fab_id,
|
|
270
|
-
fab_version,
|
|
271
|
-
fab_hash,
|
|
272
|
-
status_text,
|
|
273
|
-
elapsed,
|
|
274
|
-
created_at,
|
|
275
|
-
running_at,
|
|
276
|
-
finished_at,
|
|
277
|
-
) = row
|
|
278
292
|
runs_list.append(
|
|
279
293
|
{
|
|
280
|
-
"run-id": run_id,
|
|
281
|
-
"
|
|
282
|
-
"fab-
|
|
283
|
-
"fab-
|
|
284
|
-
"fab-
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
294
|
+
"run-id": row.run_id,
|
|
295
|
+
"federation": row.federation,
|
|
296
|
+
"fab-id": row.fab_id,
|
|
297
|
+
"fab-name": row.fab_id.split("/")[-1],
|
|
298
|
+
"fab-version": row.fab_version,
|
|
299
|
+
"fab-hash": row.fab_hash[:8],
|
|
300
|
+
"status": row.status_text,
|
|
301
|
+
"elapsed": row.elapsed,
|
|
302
|
+
"pending-at": row.pending_at,
|
|
303
|
+
"starting-at": row.starting_at,
|
|
304
|
+
"running-at": row.running_at,
|
|
305
|
+
"finished-at": row.finished_at,
|
|
290
306
|
}
|
|
291
307
|
)
|
|
292
308
|
|
|
293
309
|
return json.dumps({"success": True, "runs": runs_list})
|
|
294
310
|
|
|
295
311
|
|
|
296
|
-
def _list_runs(stub: ControlStub) -> list[
|
|
297
|
-
"""List all runs.
|
|
312
|
+
def _list_runs(stub: ControlStub) -> list[RunRow]:
|
|
313
|
+
"""List all runs.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
stub : ControlStub
|
|
318
|
+
The gRPC stub for Control API communication.
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
list[RunRow]
|
|
323
|
+
List of formatted run information for all runs.
|
|
324
|
+
"""
|
|
298
325
|
with flwr_cli_grpc_exc_handler():
|
|
299
326
|
res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
|
|
300
|
-
|
|
327
|
+
runs = [run_from_proto(proto) for proto in res.run_dict.values()]
|
|
328
|
+
|
|
329
|
+
return format_runs(runs, res.now)
|
|
301
330
|
|
|
302
|
-
return _format_runs(run_dict, res.now)
|
|
303
331
|
|
|
332
|
+
def _display_one_run(stub: ControlStub, run_id: int) -> list[RunRow]:
|
|
333
|
+
"""Display information about a specific run.
|
|
304
334
|
|
|
305
|
-
|
|
306
|
-
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
stub : ControlStub
|
|
338
|
+
The gRPC stub for Control API communication.
|
|
339
|
+
run_id : int
|
|
340
|
+
The unique identifier of the run to display.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
list[RunRow]
|
|
345
|
+
List containing the formatted run information (single item).
|
|
346
|
+
|
|
347
|
+
Raises
|
|
348
|
+
------
|
|
349
|
+
ValueError
|
|
350
|
+
If the run_id is not found.
|
|
351
|
+
"""
|
|
307
352
|
with flwr_cli_grpc_exc_handler():
|
|
308
353
|
res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
|
|
309
354
|
if not res.run_dict:
|
|
310
355
|
# This won't be reached as an gRPC error is raised if run_id is invalid
|
|
311
356
|
raise ValueError(f"Run ID {run_id} not found")
|
|
312
357
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return _format_runs(run_dict, res.now)
|
|
358
|
+
runs = [run_from_proto(proto) for proto in res.run_dict.values()]
|
|
359
|
+
return format_runs(runs, res.now)
|
flwr/cli/new/new.py
CHANGED
|
@@ -15,18 +15,25 @@
|
|
|
15
15
|
"""Flower command line interface `new` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
import io
|
|
18
19
|
import re
|
|
20
|
+
import zipfile
|
|
19
21
|
from enum import Enum
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from string import Template
|
|
22
|
-
from typing import Annotated
|
|
24
|
+
from typing import Annotated
|
|
23
25
|
|
|
26
|
+
import requests
|
|
24
27
|
import typer
|
|
25
28
|
|
|
29
|
+
from flwr.supercore.constant import PLATFORM_API_URL
|
|
30
|
+
|
|
26
31
|
from ..utils import (
|
|
27
32
|
is_valid_project_name,
|
|
33
|
+
parse_app_spec,
|
|
28
34
|
prompt_options,
|
|
29
35
|
prompt_text,
|
|
36
|
+
request_download_link,
|
|
30
37
|
sanitize_project_name,
|
|
31
38
|
)
|
|
32
39
|
|
|
@@ -93,24 +100,186 @@ def render_and_create(file_path: Path, template: str, context: dict[str, str]) -
|
|
|
93
100
|
create_file(file_path, content)
|
|
94
101
|
|
|
95
102
|
|
|
103
|
+
def print_success_prompt(
|
|
104
|
+
package_name: str, llm_challenge_str: str | None = None
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Print styled setup instructions for running a new Flower App after creation."""
|
|
107
|
+
prompt = typer.style(
|
|
108
|
+
"🎊 Flower App creation successful.\n\n"
|
|
109
|
+
"To run your Flower App, first install its dependencies:\n\n",
|
|
110
|
+
fg=typer.colors.GREEN,
|
|
111
|
+
bold=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
115
|
+
|
|
116
|
+
prompt += typer.style(
|
|
117
|
+
f" cd {package_name} && pip install -e .\n" + _add + "\n",
|
|
118
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
119
|
+
bold=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
prompt += typer.style(
|
|
123
|
+
"then, run the app:\n\n ",
|
|
124
|
+
fg=typer.colors.GREEN,
|
|
125
|
+
bold=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
prompt += typer.style(
|
|
129
|
+
"\tflwr run .\n\n",
|
|
130
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
131
|
+
bold=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
prompt += typer.style(
|
|
135
|
+
"💡 Check the README in your app directory to learn how to\n"
|
|
136
|
+
"customize it and how to run it using the Deployment Runtime.\n",
|
|
137
|
+
fg=typer.colors.GREEN,
|
|
138
|
+
bold=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
print(prompt)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Security: prevent zip-slip
|
|
145
|
+
def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
|
|
146
|
+
"""Extract ZIP file into destination directory."""
|
|
147
|
+
dest_dir = dest_dir.resolve()
|
|
148
|
+
|
|
149
|
+
def _is_within_directory(base: Path, target: Path) -> bool:
|
|
150
|
+
try:
|
|
151
|
+
target.relative_to(base)
|
|
152
|
+
return True
|
|
153
|
+
except ValueError:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
for member in zf.infolist():
|
|
157
|
+
# Skip directory placeholders;
|
|
158
|
+
# ZipInfo can represent them as names ending with '/'.
|
|
159
|
+
if member.is_dir():
|
|
160
|
+
target_path = (dest_dir / member.filename).resolve()
|
|
161
|
+
if not _is_within_directory(dest_dir, target_path):
|
|
162
|
+
raise ValueError(f"Unsafe path in zip: {member.filename}")
|
|
163
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Files
|
|
167
|
+
target_path = (dest_dir / member.filename).resolve()
|
|
168
|
+
if not _is_within_directory(dest_dir, target_path):
|
|
169
|
+
raise ValueError(f"Unsafe path in zip: {member.filename}")
|
|
170
|
+
|
|
171
|
+
# Ensure parent exists
|
|
172
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
# Extract
|
|
175
|
+
with zf.open(member, "r") as src, open(target_path, "wb") as dst:
|
|
176
|
+
dst.write(src.read())
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
|
|
180
|
+
"""Download ZIP file from Platform API to memory."""
|
|
181
|
+
try:
|
|
182
|
+
r = requests.get(presigned_url, timeout=60)
|
|
183
|
+
r.raise_for_status()
|
|
184
|
+
except requests.RequestException as e:
|
|
185
|
+
typer.secho(
|
|
186
|
+
f"ZIP download failed: {e}",
|
|
187
|
+
fg=typer.colors.RED,
|
|
188
|
+
err=True,
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(code=1) from e
|
|
191
|
+
|
|
192
|
+
buf = io.BytesIO(r.content)
|
|
193
|
+
# Validate it's a zip
|
|
194
|
+
if not zipfile.is_zipfile(buf):
|
|
195
|
+
typer.secho(
|
|
196
|
+
"Downloaded file is not a valid ZIP",
|
|
197
|
+
fg=typer.colors.RED,
|
|
198
|
+
err=True,
|
|
199
|
+
)
|
|
200
|
+
raise typer.Exit(code=1)
|
|
201
|
+
buf.seek(0)
|
|
202
|
+
return buf
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def download_remote_app_via_api(app_spec: str) -> None:
|
|
206
|
+
"""Download App from Platform API."""
|
|
207
|
+
# Validate app version and ID format
|
|
208
|
+
app_id, app_version = parse_app_spec(app_spec)
|
|
209
|
+
app_name = app_id.split("/")[1]
|
|
210
|
+
|
|
211
|
+
project_dir = Path.cwd() / app_name
|
|
212
|
+
if project_dir.exists():
|
|
213
|
+
if not typer.confirm(
|
|
214
|
+
typer.style(
|
|
215
|
+
f"\n💬 {app_name} already exists, do you want to override it?",
|
|
216
|
+
fg=typer.colors.MAGENTA,
|
|
217
|
+
bold=True,
|
|
218
|
+
)
|
|
219
|
+
):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
print(
|
|
223
|
+
typer.style(
|
|
224
|
+
f"\n🔗 Requesting download link for {app_id}...",
|
|
225
|
+
fg=typer.colors.GREEN,
|
|
226
|
+
bold=True,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
# Fetch ZIP downloading URL
|
|
230
|
+
url = f"{PLATFORM_API_URL}/hub/fetch-zip"
|
|
231
|
+
presigned_url = request_download_link(app_id, app_version, url, "zip_url")
|
|
232
|
+
|
|
233
|
+
print(
|
|
234
|
+
typer.style(
|
|
235
|
+
"⬇️ Downloading ZIP into memory...",
|
|
236
|
+
fg=typer.colors.GREEN,
|
|
237
|
+
bold=True,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
zip_buf = _download_zip_to_memory(presigned_url)
|
|
241
|
+
|
|
242
|
+
print(
|
|
243
|
+
typer.style(
|
|
244
|
+
f"📦 Unpacking into {project_dir}...",
|
|
245
|
+
fg=typer.colors.GREEN,
|
|
246
|
+
bold=True,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
with zipfile.ZipFile(zip_buf) as zf:
|
|
250
|
+
_safe_extract_zip(zf, Path.cwd())
|
|
251
|
+
|
|
252
|
+
print_success_prompt(app_name)
|
|
253
|
+
|
|
254
|
+
|
|
96
255
|
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
97
256
|
def new(
|
|
98
257
|
app_name: Annotated[
|
|
99
|
-
|
|
100
|
-
typer.Argument(
|
|
258
|
+
str | None,
|
|
259
|
+
typer.Argument(
|
|
260
|
+
help="Flower app name. For remote apps, use the format "
|
|
261
|
+
"'@account_name/app_name' or '@account_name/app_name==x.y.z'. "
|
|
262
|
+
"Version is optional (defaults to latest)."
|
|
263
|
+
),
|
|
101
264
|
] = None,
|
|
102
265
|
framework: Annotated[
|
|
103
|
-
|
|
266
|
+
MlFramework | None,
|
|
104
267
|
typer.Option(case_sensitive=False, help="The ML framework to use"),
|
|
105
268
|
] = None,
|
|
106
269
|
username: Annotated[
|
|
107
|
-
|
|
270
|
+
str | None,
|
|
108
271
|
typer.Option(case_sensitive=False, help="The Flower username of the author"),
|
|
109
272
|
] = None,
|
|
110
273
|
) -> None:
|
|
111
274
|
"""Create new Flower App."""
|
|
112
275
|
if app_name is None:
|
|
113
276
|
app_name = prompt_text("Please provide the app name")
|
|
277
|
+
|
|
278
|
+
# Download remote app
|
|
279
|
+
if app_name and app_name.startswith("@"):
|
|
280
|
+
download_remote_app_via_api(app_name)
|
|
281
|
+
return
|
|
282
|
+
|
|
114
283
|
if not is_valid_project_name(app_name):
|
|
115
284
|
app_name = prompt_text(
|
|
116
285
|
"Please provide a name that only contains "
|
|
@@ -282,38 +451,4 @@ def new(
|
|
|
282
451
|
context=context,
|
|
283
452
|
)
|
|
284
453
|
|
|
285
|
-
|
|
286
|
-
"🎊 Flower App creation successful.\n\n"
|
|
287
|
-
"To run your Flower App, first install its dependencies:\n\n",
|
|
288
|
-
fg=typer.colors.GREEN,
|
|
289
|
-
bold=True,
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
293
|
-
|
|
294
|
-
prompt += typer.style(
|
|
295
|
-
f" cd {package_name} && pip install -e .\n" + _add + "\n",
|
|
296
|
-
fg=typer.colors.BRIGHT_CYAN,
|
|
297
|
-
bold=True,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
prompt += typer.style(
|
|
301
|
-
"then, run the app:\n\n ",
|
|
302
|
-
fg=typer.colors.GREEN,
|
|
303
|
-
bold=True,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
prompt += typer.style(
|
|
307
|
-
"\tflwr run .\n\n",
|
|
308
|
-
fg=typer.colors.BRIGHT_CYAN,
|
|
309
|
-
bold=True,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
prompt += typer.style(
|
|
313
|
-
"💡 Check the README in your app directory to learn how to\n"
|
|
314
|
-
"customize it and how to run it using the Deployment Runtime.\n",
|
|
315
|
-
fg=typer.colors.GREEN,
|
|
316
|
-
bold=True,
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
print(prompt)
|
|
454
|
+
print_success_prompt(package_name, llm_challenge_str)
|