flwr 1.24.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/cli/app_cmd/review.py +13 -3
- flwr/cli/federation/show.py +4 -3
- flwr/cli/ls.py +44 -3
- flwr/cli/new/new.py +106 -297
- flwr/cli/run/run.py +12 -17
- flwr/cli/run_utils.py +23 -5
- flwr/cli/stop.py +1 -1
- flwr/cli/supernode/ls.py +10 -5
- flwr/cli/utils.py +0 -137
- flwr/client/grpc_adapter_client/connection.py +2 -2
- flwr/client/grpc_rere_client/connection.py +6 -3
- flwr/client/rest_client/connection.py +6 -4
- flwr/common/serde.py +6 -0
- flwr/common/typing.py +6 -0
- flwr/proto/fleet_pb2.py +10 -10
- flwr/proto/fleet_pb2.pyi +5 -1
- flwr/proto/run_pb2.py +24 -24
- flwr/proto/run_pb2.pyi +10 -1
- flwr/server/app.py +1 -0
- flwr/server/superlink/fleet/message_handler/message_handler.py +41 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -0
- flwr/server/superlink/linkstate/linkstate.py +32 -0
- flwr/server/superlink/linkstate/sqlite_linkstate.py +60 -3
- flwr/supercore/constant.py +3 -0
- flwr/supercore/utils.py +190 -0
- flwr/superlink/servicer/control/control_grpc.py +2 -0
- flwr/superlink/servicer/control/control_servicer.py +88 -5
- flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
- flwr/supernode/nodestate/nodestate.py +45 -0
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +7 -1
- flwr/supernode/start_client_internal.py +7 -4
- {flwr-1.24.0.dist-info → flwr-1.25.0.dist-info}/METADATA +2 -4
- {flwr-1.24.0.dist-info → flwr-1.25.0.dist-info}/RECORD +35 -96
- flwr/cli/new/templates/__init__.py +0 -15
- 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/__init__.py +0 -15
- 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/__init__.py +0 -15
- 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 -99
- 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-1.24.0.dist-info → flwr-1.25.0.dist-info}/WHEEL +0 -0
- {flwr-1.24.0.dist-info → flwr-1.25.0.dist-info}/entry_points.txt +0 -0
flwr/cli/app_cmd/review.py
CHANGED
|
@@ -36,6 +36,7 @@ from flwr.supercore.primitives.asymmetric_ed25519 import (
|
|
|
36
36
|
load_private_key,
|
|
37
37
|
sign_message,
|
|
38
38
|
)
|
|
39
|
+
from flwr.supercore.utils import parse_app_spec, request_download_link
|
|
39
40
|
|
|
40
41
|
from ..auth_plugin.oidc_cli_plugin import OidcCliPlugin
|
|
41
42
|
from ..config_utils import (
|
|
@@ -45,7 +46,7 @@ from ..config_utils import (
|
|
|
45
46
|
)
|
|
46
47
|
from ..constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
47
48
|
from ..install import install_from_fab
|
|
48
|
-
from ..utils import load_cli_auth_plugin
|
|
49
|
+
from ..utils import load_cli_auth_plugin
|
|
49
50
|
|
|
50
51
|
TRY_AGAIN_MESSAGE = "Please try again or press CTRL+C to abort.\n"
|
|
51
52
|
|
|
@@ -104,12 +105,21 @@ def review(
|
|
|
104
105
|
token = auth_plugin.access_token
|
|
105
106
|
|
|
106
107
|
# Validate app version and ID format
|
|
107
|
-
|
|
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
|
|
108
113
|
|
|
109
114
|
# Download FAB
|
|
110
115
|
typer.secho("Downloading FAB... ", fg=typer.colors.BLUE)
|
|
111
116
|
url = f"{PLATFORM_API_URL}/hub/fetch-fab"
|
|
112
|
-
|
|
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
|
+
|
|
113
123
|
fab_bytes = _download_fab(presigned_url)
|
|
114
124
|
|
|
115
125
|
# Unpack FAB
|
flwr/cli/federation/show.py
CHANGED
|
@@ -41,6 +41,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
41
41
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
42
42
|
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
|
43
43
|
from flwr.supercore.constant import NOOP_FEDERATION
|
|
44
|
+
from flwr.supercore.utils import humanize_duration
|
|
44
45
|
|
|
45
46
|
from ..run_utils import RunRow, format_runs
|
|
46
47
|
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
@@ -262,7 +263,7 @@ def _to_runs_table(run_list: list[RunRow]) -> Table:
|
|
|
262
263
|
f"[bold]{row.run_id}[/bold]",
|
|
263
264
|
f"@{row.fab_id}=={row.fab_version}",
|
|
264
265
|
f"[{status_style}]{row.status_text}[/{status_style}]",
|
|
265
|
-
row.elapsed,
|
|
266
|
+
f"{humanize_duration(row.elapsed)}",
|
|
266
267
|
)
|
|
267
268
|
table.add_row(*formatted_row)
|
|
268
269
|
|
|
@@ -298,7 +299,7 @@ def _to_json(
|
|
|
298
299
|
for node in nodes:
|
|
299
300
|
nodes_list.append(
|
|
300
301
|
{
|
|
301
|
-
"node_id": node.node_id,
|
|
302
|
+
"node_id": f"{node.node_id}",
|
|
302
303
|
"owner": node.owner_name,
|
|
303
304
|
"status": node.status,
|
|
304
305
|
}
|
|
@@ -307,7 +308,7 @@ def _to_json(
|
|
|
307
308
|
for run in runs:
|
|
308
309
|
runs_list.append(
|
|
309
310
|
{
|
|
310
|
-
"run_id": run.run_id,
|
|
311
|
+
"run_id": f"{run.run_id}",
|
|
311
312
|
"app": f"@{run.fab_id}=={run.fab_version}",
|
|
312
313
|
"status": run.status_text,
|
|
313
314
|
"elapsed": run.elapsed,
|
flwr/cli/ls.py
CHANGED
|
@@ -40,6 +40,7 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
40
40
|
ListRunsResponse,
|
|
41
41
|
)
|
|
42
42
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
43
|
+
from flwr.supercore.utils import humanize_bytes, humanize_duration
|
|
43
44
|
|
|
44
45
|
from .run_utils import RunRow, format_runs
|
|
45
46
|
from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
@@ -231,7 +232,7 @@ def _to_table(run_list: list[RunRow]) -> Table:
|
|
|
231
232
|
row.federation,
|
|
232
233
|
f"@{row.fab_id}=={row.fab_version}",
|
|
233
234
|
f"[{status_style}]{row.status_text}[/{status_style}]",
|
|
234
|
-
row.elapsed,
|
|
235
|
+
humanize_duration(row.elapsed),
|
|
235
236
|
status_changed_at,
|
|
236
237
|
)
|
|
237
238
|
table.add_row(*formatted_row)
|
|
@@ -265,11 +266,39 @@ def _to_detail_table(run: RunRow) -> Table:
|
|
|
265
266
|
table.add_row("App", f"@{run.fab_id}=={run.fab_version}")
|
|
266
267
|
table.add_row("FAB Hash", f"{run.fab_hash[:8]}...{run.fab_hash[-8:]}")
|
|
267
268
|
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("Elapsed", f"[blue]{humanize_duration(run.elapsed)}[/blue]")
|
|
269
270
|
table.add_row("Pending At", run.pending_at)
|
|
270
271
|
table.add_row("Starting At", run.starting_at)
|
|
271
272
|
table.add_row("Running At", run.running_at)
|
|
272
273
|
table.add_row("Finished At", run.finished_at)
|
|
274
|
+
table.add_row(
|
|
275
|
+
"Network traffic (inbound)",
|
|
276
|
+
f"[blue]{humanize_bytes(run.network_traffic_inbound)}[/blue]",
|
|
277
|
+
)
|
|
278
|
+
table.add_row(
|
|
279
|
+
"Network traffic (outbound)",
|
|
280
|
+
f"[blue]{humanize_bytes(run.network_traffic_outbound)}[/blue]",
|
|
281
|
+
)
|
|
282
|
+
table.add_row(
|
|
283
|
+
"Network Traffic (total)",
|
|
284
|
+
"[blue]"
|
|
285
|
+
f"{humanize_bytes(run.network_traffic_inbound + run.network_traffic_outbound)}"
|
|
286
|
+
"[/blue]",
|
|
287
|
+
)
|
|
288
|
+
table.add_row(
|
|
289
|
+
"Compute Time (ServerApp)",
|
|
290
|
+
f"[blue]{humanize_duration(run.compute_time_serverapp)}[/blue]",
|
|
291
|
+
)
|
|
292
|
+
table.add_row(
|
|
293
|
+
"Compute Time (ClientApp)",
|
|
294
|
+
f"[blue]{humanize_duration(run.compute_time_clientapp)}[/blue]",
|
|
295
|
+
)
|
|
296
|
+
table.add_row(
|
|
297
|
+
"Compute Time (total)",
|
|
298
|
+
"[blue]"
|
|
299
|
+
f"{humanize_duration(run.compute_time_serverapp + run.compute_time_clientapp)}"
|
|
300
|
+
"[/blue]",
|
|
301
|
+
)
|
|
273
302
|
|
|
274
303
|
return table
|
|
275
304
|
|
|
@@ -291,7 +320,7 @@ def _to_json(run_list: list[RunRow]) -> str:
|
|
|
291
320
|
for row in run_list:
|
|
292
321
|
runs_list.append(
|
|
293
322
|
{
|
|
294
|
-
"run-id": row.run_id,
|
|
323
|
+
"run-id": f"{row.run_id}",
|
|
295
324
|
"federation": row.federation,
|
|
296
325
|
"fab-id": row.fab_id,
|
|
297
326
|
"fab-name": row.fab_id.split("/")[-1],
|
|
@@ -303,6 +332,18 @@ def _to_json(run_list: list[RunRow]) -> str:
|
|
|
303
332
|
"starting-at": row.starting_at,
|
|
304
333
|
"running-at": row.running_at,
|
|
305
334
|
"finished-at": row.finished_at,
|
|
335
|
+
"network-traffic": {
|
|
336
|
+
"inbound-bytes": row.network_traffic_inbound,
|
|
337
|
+
"outbound-bytes": row.network_traffic_outbound,
|
|
338
|
+
"total-bytes": row.network_traffic_inbound
|
|
339
|
+
+ row.network_traffic_outbound,
|
|
340
|
+
},
|
|
341
|
+
"compute-time": {
|
|
342
|
+
"serverapp-seconds": row.compute_time_serverapp,
|
|
343
|
+
"clientapp-seconds": row.compute_time_clientapp,
|
|
344
|
+
"total-seconds": row.compute_time_serverapp
|
|
345
|
+
+ row.compute_time_clientapp,
|
|
346
|
+
},
|
|
306
347
|
}
|
|
307
348
|
)
|
|
308
349
|
|
flwr/cli/new/new.py
CHANGED
|
@@ -16,93 +16,84 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import io
|
|
19
|
-
import re
|
|
20
19
|
import zipfile
|
|
21
|
-
from enum import Enum
|
|
22
20
|
from pathlib import Path
|
|
23
|
-
from
|
|
24
|
-
from typing import Annotated
|
|
21
|
+
from typing import Annotated, cast
|
|
25
22
|
|
|
26
23
|
import requests
|
|
27
24
|
import typer
|
|
28
25
|
|
|
29
26
|
from flwr.supercore.constant import PLATFORM_API_URL
|
|
27
|
+
from flwr.supercore.utils import parse_app_spec, request_download_link
|
|
30
28
|
|
|
31
|
-
from ..utils import
|
|
32
|
-
is_valid_project_name,
|
|
33
|
-
parse_app_spec,
|
|
34
|
-
prompt_options,
|
|
35
|
-
prompt_text,
|
|
36
|
-
request_download_link,
|
|
37
|
-
sanitize_project_name,
|
|
38
|
-
)
|
|
29
|
+
from ..utils import prompt_options, prompt_text
|
|
39
30
|
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if not tpl_file_path.is_file():
|
|
76
|
-
raise TemplateNotFound(f"Template '{name}' not found")
|
|
77
|
-
|
|
78
|
-
with open(tpl_file_path, encoding="utf-8") as tpl_file:
|
|
79
|
-
return tpl_file.read()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def render_template(template: str, data: dict[str, str]) -> str:
|
|
83
|
-
"""Render template."""
|
|
84
|
-
tpl_file = load_template(template)
|
|
85
|
-
tpl = Template(tpl_file)
|
|
86
|
-
if ".gitignore" not in template:
|
|
87
|
-
return tpl.substitute(data)
|
|
88
|
-
return tpl.template
|
|
89
|
-
|
|
32
|
+
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
33
|
+
def new(
|
|
34
|
+
app_spec: Annotated[
|
|
35
|
+
str | None,
|
|
36
|
+
typer.Argument(
|
|
37
|
+
help="Flower app specifier. Use the format "
|
|
38
|
+
"'@account_name/app_name' or '@account_name/app_name==x.y.z'. "
|
|
39
|
+
"Version is optional (defaults to latest)."
|
|
40
|
+
),
|
|
41
|
+
] = None,
|
|
42
|
+
framework: Annotated[
|
|
43
|
+
str | None,
|
|
44
|
+
typer.Option(case_sensitive=False, help="Deprecated. The ML framework to use"),
|
|
45
|
+
] = None,
|
|
46
|
+
username: Annotated[
|
|
47
|
+
str | None,
|
|
48
|
+
typer.Option(
|
|
49
|
+
case_sensitive=False, help="Deprecated. The Flower username of the author"
|
|
50
|
+
),
|
|
51
|
+
] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Create new Flower App."""
|
|
54
|
+
if framework is not None or username is not None:
|
|
55
|
+
typer.secho(
|
|
56
|
+
"❌ The --framework and --username options are deprecated and will be "
|
|
57
|
+
"removed in future versions of Flower. Please provide an app specifier "
|
|
58
|
+
"after `flwr new` instead, e.g., '@account_name/app_name' or "
|
|
59
|
+
"'@account_name/app_name==x.y.z'.",
|
|
60
|
+
fg=typer.colors.RED,
|
|
61
|
+
bold=True,
|
|
62
|
+
err=True,
|
|
63
|
+
)
|
|
64
|
+
raise typer.Exit(code=1)
|
|
90
65
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
66
|
+
if app_spec is None:
|
|
67
|
+
# Fetch recommended apps
|
|
68
|
+
print(
|
|
69
|
+
typer.style(
|
|
70
|
+
"\n🌸 Fetching recommended apps...",
|
|
71
|
+
fg=typer.colors.GREEN,
|
|
72
|
+
bold=True,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
apps = fetch_recommended_apps()
|
|
95
76
|
|
|
77
|
+
if not apps:
|
|
78
|
+
typer.secho(
|
|
79
|
+
"No recommended apps found. Please provide an app specifier manually.",
|
|
80
|
+
fg=typer.colors.YELLOW,
|
|
81
|
+
)
|
|
82
|
+
app_spec = prompt_text("Please provide the app specifier")
|
|
83
|
+
else:
|
|
84
|
+
# Extract app_ids and show selection menu
|
|
85
|
+
app_ids = [app["app_id"] for app in apps]
|
|
86
|
+
app_spec = prompt_options(
|
|
87
|
+
"Select a Flower App to create by entering "
|
|
88
|
+
"the number from the list below:",
|
|
89
|
+
app_ids,
|
|
90
|
+
)
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
content = render_template(template, context)
|
|
100
|
-
create_file(file_path, content)
|
|
92
|
+
# Download remote app
|
|
93
|
+
download_remote_app_via_api(app_spec)
|
|
101
94
|
|
|
102
95
|
|
|
103
|
-
def print_success_prompt(
|
|
104
|
-
package_name: str, llm_challenge_str: str | None = None
|
|
105
|
-
) -> None:
|
|
96
|
+
def print_success_prompt(package_name: str) -> None:
|
|
106
97
|
"""Print styled setup instructions for running a new Flower App after creation."""
|
|
107
98
|
prompt = typer.style(
|
|
108
99
|
"🎊 Flower App creation successful.\n\n"
|
|
@@ -111,10 +102,8 @@ def print_success_prompt(
|
|
|
111
102
|
bold=True,
|
|
112
103
|
)
|
|
113
104
|
|
|
114
|
-
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
115
|
-
|
|
116
105
|
prompt += typer.style(
|
|
117
|
-
f" cd {package_name} && pip install -e .\n
|
|
106
|
+
f" cd {package_name} && pip install -e .\n\n",
|
|
118
107
|
fg=typer.colors.BRIGHT_CYAN,
|
|
119
108
|
bold=True,
|
|
120
109
|
)
|
|
@@ -141,6 +130,25 @@ def print_success_prompt(
|
|
|
141
130
|
print(prompt)
|
|
142
131
|
|
|
143
132
|
|
|
133
|
+
def fetch_recommended_apps() -> list[dict[str, str]]:
|
|
134
|
+
"""Fetch recommended apps from Platform API."""
|
|
135
|
+
url = f"{PLATFORM_API_URL}/hub/apps?tag=recommended"
|
|
136
|
+
try:
|
|
137
|
+
response = requests.get(url, headers={"accept": "application/json"}, timeout=10)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
data = response.json()
|
|
140
|
+
apps = data.get("apps", [])
|
|
141
|
+
return cast(list[dict[str, str]], apps)
|
|
142
|
+
|
|
143
|
+
except requests.RequestException as e:
|
|
144
|
+
typer.secho(
|
|
145
|
+
f"❌ Failed to fetch recommended apps: {e}",
|
|
146
|
+
fg=typer.colors.RED,
|
|
147
|
+
err=True,
|
|
148
|
+
)
|
|
149
|
+
raise typer.Exit(code=1) from e
|
|
150
|
+
|
|
151
|
+
|
|
144
152
|
# Security: prevent zip-slip
|
|
145
153
|
def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
|
|
146
154
|
"""Extract ZIP file into destination directory."""
|
|
@@ -205,7 +213,12 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
|
|
|
205
213
|
def download_remote_app_via_api(app_spec: str) -> None:
|
|
206
214
|
"""Download App from Platform API."""
|
|
207
215
|
# Validate app version and ID format
|
|
208
|
-
|
|
216
|
+
try:
|
|
217
|
+
app_id, app_version = parse_app_spec(app_spec)
|
|
218
|
+
except ValueError as e:
|
|
219
|
+
typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
|
|
220
|
+
raise typer.Exit(code=1) from e
|
|
221
|
+
|
|
209
222
|
app_name = app_id.split("/")[1]
|
|
210
223
|
|
|
211
224
|
project_dir = Path.cwd() / app_name
|
|
@@ -219,236 +232,32 @@ def download_remote_app_via_api(app_spec: str) -> None:
|
|
|
219
232
|
):
|
|
220
233
|
return
|
|
221
234
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
bold=True,
|
|
227
|
-
)
|
|
235
|
+
typer.secho(
|
|
236
|
+
f"\n🔗 Requesting download link for {app_id}...",
|
|
237
|
+
fg=typer.colors.GREEN,
|
|
238
|
+
bold=True,
|
|
228
239
|
)
|
|
229
240
|
# Fetch ZIP downloading URL
|
|
230
241
|
url = f"{PLATFORM_API_URL}/hub/fetch-zip"
|
|
231
|
-
|
|
242
|
+
try:
|
|
243
|
+
presigned_url, _ = request_download_link(app_id, app_version, url, "zip_url")
|
|
244
|
+
except ValueError as e:
|
|
245
|
+
typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
|
|
246
|
+
raise typer.Exit(code=1) from e
|
|
232
247
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
bold=True,
|
|
238
|
-
)
|
|
248
|
+
typer.secho(
|
|
249
|
+
"🔽 Downloading ZIP into memory...",
|
|
250
|
+
fg=typer.colors.GREEN,
|
|
251
|
+
bold=True,
|
|
239
252
|
)
|
|
240
253
|
zip_buf = _download_zip_to_memory(presigned_url)
|
|
241
254
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
bold=True,
|
|
247
|
-
)
|
|
255
|
+
typer.secho(
|
|
256
|
+
f"📦 Unpacking into {project_dir}...",
|
|
257
|
+
fg=typer.colors.GREEN,
|
|
258
|
+
bold=True,
|
|
248
259
|
)
|
|
249
260
|
with zipfile.ZipFile(zip_buf) as zf:
|
|
250
261
|
_safe_extract_zip(zf, Path.cwd())
|
|
251
262
|
|
|
252
263
|
print_success_prompt(app_name)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
256
|
-
def new(
|
|
257
|
-
app_name: Annotated[
|
|
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
|
-
),
|
|
264
|
-
] = None,
|
|
265
|
-
framework: Annotated[
|
|
266
|
-
MlFramework | None,
|
|
267
|
-
typer.Option(case_sensitive=False, help="The ML framework to use"),
|
|
268
|
-
] = None,
|
|
269
|
-
username: Annotated[
|
|
270
|
-
str | None,
|
|
271
|
-
typer.Option(case_sensitive=False, help="The Flower username of the author"),
|
|
272
|
-
] = None,
|
|
273
|
-
) -> None:
|
|
274
|
-
"""Create new Flower App."""
|
|
275
|
-
if app_name is None:
|
|
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
|
-
|
|
283
|
-
if not is_valid_project_name(app_name):
|
|
284
|
-
app_name = prompt_text(
|
|
285
|
-
"Please provide a name that only contains "
|
|
286
|
-
"characters in {'-', a-zA-Z', '0-9'}",
|
|
287
|
-
predicate=is_valid_project_name,
|
|
288
|
-
default=sanitize_project_name(app_name),
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
# Set project directory path
|
|
292
|
-
package_name = re.sub(r"[-_.]+", "-", app_name).lower()
|
|
293
|
-
import_name = package_name.replace("-", "_")
|
|
294
|
-
project_dir = Path.cwd() / package_name
|
|
295
|
-
|
|
296
|
-
if project_dir.exists():
|
|
297
|
-
if not typer.confirm(
|
|
298
|
-
typer.style(
|
|
299
|
-
f"\n💬 {app_name} already exists, do you want to override it?",
|
|
300
|
-
fg=typer.colors.MAGENTA,
|
|
301
|
-
bold=True,
|
|
302
|
-
)
|
|
303
|
-
):
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
if username is None:
|
|
307
|
-
username = prompt_text("Please provide your Flower username")
|
|
308
|
-
|
|
309
|
-
if framework is not None:
|
|
310
|
-
framework_str = str(framework.value)
|
|
311
|
-
else:
|
|
312
|
-
framework_str = prompt_options(
|
|
313
|
-
"Please select ML framework by typing in the number",
|
|
314
|
-
[mlf.value for mlf in MlFramework],
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
llm_challenge_str = None
|
|
318
|
-
if framework_str == MlFramework.FLOWERTUNE:
|
|
319
|
-
llm_challenge_value = prompt_options(
|
|
320
|
-
"Please select LLM challenge by typing in the number",
|
|
321
|
-
sorted([challenge.value for challenge in LlmChallengeName]),
|
|
322
|
-
)
|
|
323
|
-
llm_challenge_str = llm_challenge_value.lower()
|
|
324
|
-
|
|
325
|
-
if framework_str == MlFramework.BASELINE:
|
|
326
|
-
framework_str = "baseline"
|
|
327
|
-
|
|
328
|
-
if framework_str == MlFramework.PYTORCH_LEGACY_API:
|
|
329
|
-
framework_str = "pytorch_legacy_api"
|
|
330
|
-
|
|
331
|
-
print(
|
|
332
|
-
typer.style(
|
|
333
|
-
f"\n🔨 Creating Flower App {app_name}...",
|
|
334
|
-
fg=typer.colors.GREEN,
|
|
335
|
-
bold=True,
|
|
336
|
-
)
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
context = {
|
|
340
|
-
"framework_str": framework_str,
|
|
341
|
-
"import_name": import_name.replace("-", "_"),
|
|
342
|
-
"package_name": package_name,
|
|
343
|
-
"project_name": app_name,
|
|
344
|
-
"username": username,
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
template_name = framework_str.lower()
|
|
348
|
-
|
|
349
|
-
# List of files to render
|
|
350
|
-
if llm_challenge_str:
|
|
351
|
-
files = {
|
|
352
|
-
".gitignore": {"template": "app/.gitignore.tpl"},
|
|
353
|
-
"pyproject.toml": {"template": f"app/pyproject.{template_name}.toml.tpl"},
|
|
354
|
-
"README.md": {"template": f"app/README.{template_name}.md.tpl"},
|
|
355
|
-
f"{import_name}/__init__.py": {"template": "app/code/__init__.py.tpl"},
|
|
356
|
-
f"{import_name}/server_app.py": {
|
|
357
|
-
"template": "app/code/flwr_tune/server_app.py.tpl"
|
|
358
|
-
},
|
|
359
|
-
f"{import_name}/client_app.py": {
|
|
360
|
-
"template": "app/code/flwr_tune/client_app.py.tpl"
|
|
361
|
-
},
|
|
362
|
-
f"{import_name}/models.py": {
|
|
363
|
-
"template": "app/code/flwr_tune/models.py.tpl"
|
|
364
|
-
},
|
|
365
|
-
f"{import_name}/dataset.py": {
|
|
366
|
-
"template": "app/code/flwr_tune/dataset.py.tpl"
|
|
367
|
-
},
|
|
368
|
-
f"{import_name}/strategy.py": {
|
|
369
|
-
"template": "app/code/flwr_tune/strategy.py.tpl"
|
|
370
|
-
},
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
# Challenge specific context
|
|
374
|
-
fraction_train = "0.2" if llm_challenge_str == "code" else "0.1"
|
|
375
|
-
if llm_challenge_str == "generalnlp":
|
|
376
|
-
challenge_name = "General NLP"
|
|
377
|
-
num_clients = "20"
|
|
378
|
-
dataset_name = "flwrlabs/alpaca-gpt4"
|
|
379
|
-
elif llm_challenge_str == "finance":
|
|
380
|
-
challenge_name = "Finance"
|
|
381
|
-
num_clients = "50"
|
|
382
|
-
dataset_name = "flwrlabs/fingpt-sentiment-train"
|
|
383
|
-
elif llm_challenge_str == "medical":
|
|
384
|
-
challenge_name = "Medical"
|
|
385
|
-
num_clients = "20"
|
|
386
|
-
dataset_name = "flwrlabs/medical-meadow-medical-flashcards"
|
|
387
|
-
else:
|
|
388
|
-
challenge_name = "Code"
|
|
389
|
-
num_clients = "10"
|
|
390
|
-
dataset_name = "flwrlabs/code-alpaca-20k"
|
|
391
|
-
|
|
392
|
-
context["llm_challenge_str"] = llm_challenge_str
|
|
393
|
-
context["fraction_train"] = fraction_train
|
|
394
|
-
context["challenge_name"] = challenge_name
|
|
395
|
-
context["num_clients"] = num_clients
|
|
396
|
-
context["dataset_name"] = dataset_name
|
|
397
|
-
else:
|
|
398
|
-
files = {
|
|
399
|
-
".gitignore": {"template": "app/.gitignore.tpl"},
|
|
400
|
-
"README.md": {"template": "app/README.md.tpl"},
|
|
401
|
-
"pyproject.toml": {"template": f"app/pyproject.{template_name}.toml.tpl"},
|
|
402
|
-
f"{import_name}/__init__.py": {"template": "app/code/__init__.py.tpl"},
|
|
403
|
-
f"{import_name}/server_app.py": {
|
|
404
|
-
"template": f"app/code/server.{template_name}.py.tpl"
|
|
405
|
-
},
|
|
406
|
-
f"{import_name}/client_app.py": {
|
|
407
|
-
"template": f"app/code/client.{template_name}.py.tpl"
|
|
408
|
-
},
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
# Depending on the framework, generate task.py file
|
|
412
|
-
frameworks_with_tasks = [
|
|
413
|
-
MlFramework.PYTORCH.value,
|
|
414
|
-
MlFramework.JAX.value,
|
|
415
|
-
MlFramework.HUGGINGFACE.value,
|
|
416
|
-
MlFramework.MLX.value,
|
|
417
|
-
MlFramework.TENSORFLOW.value,
|
|
418
|
-
MlFramework.SKLEARN.value,
|
|
419
|
-
MlFramework.NUMPY.value,
|
|
420
|
-
MlFramework.XGBOOST.value,
|
|
421
|
-
"pytorch_legacy_api",
|
|
422
|
-
]
|
|
423
|
-
if framework_str in frameworks_with_tasks:
|
|
424
|
-
files[f"{import_name}/task.py"] = {
|
|
425
|
-
"template": f"app/code/task.{template_name}.py.tpl"
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if framework_str == "pytorch_legacy_api":
|
|
429
|
-
# Use custom __init__ that better captures name of framework
|
|
430
|
-
files[f"{import_name}/__init__.py"] = {
|
|
431
|
-
"template": f"app/code/__init__.{framework_str}.py.tpl"
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if framework_str == "baseline":
|
|
435
|
-
# Include additional files for baseline template
|
|
436
|
-
for file_name in ["model", "dataset", "strategy", "utils", "__init__"]:
|
|
437
|
-
files[f"{import_name}/{file_name}.py"] = {
|
|
438
|
-
"template": f"app/code/{file_name}.{template_name}.py.tpl"
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
# Replace README.md
|
|
442
|
-
files["README.md"]["template"] = f"app/README.{template_name}.md.tpl"
|
|
443
|
-
|
|
444
|
-
# Add LICENSE
|
|
445
|
-
files["LICENSE"] = {"template": "app/LICENSE.tpl"}
|
|
446
|
-
|
|
447
|
-
for file_path, value in files.items():
|
|
448
|
-
render_and_create(
|
|
449
|
-
file_path=project_dir / file_path,
|
|
450
|
-
template=value["template"],
|
|
451
|
-
context=context,
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
print_success_prompt(package_name, llm_challenge_str)
|
flwr/cli/run/run.py
CHANGED
|
@@ -46,14 +46,10 @@ from flwr.common.typing import Fab
|
|
|
46
46
|
from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
|
|
47
47
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
48
48
|
from flwr.supercore.constant import NOOP_FEDERATION
|
|
49
|
+
from flwr.supercore.utils import parse_app_spec
|
|
49
50
|
|
|
50
51
|
from ..log import start_stream
|
|
51
|
-
from ..utils import
|
|
52
|
-
flwr_cli_grpc_exc_handler,
|
|
53
|
-
init_channel,
|
|
54
|
-
load_cli_auth_plugin,
|
|
55
|
-
parse_app_spec,
|
|
56
|
-
)
|
|
52
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
57
53
|
|
|
58
54
|
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
|
59
55
|
|
|
@@ -111,8 +107,15 @@ def run(
|
|
|
111
107
|
app_spec = None
|
|
112
108
|
if (app_str := str(app)).startswith("@"):
|
|
113
109
|
# Validate app version and ID format
|
|
114
|
-
|
|
110
|
+
try:
|
|
111
|
+
_ = parse_app_spec(app_str)
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
typer.secho(f"❌ {e}", fg=typer.colors.RED, err=True)
|
|
114
|
+
raise typer.Exit(code=1) from e
|
|
115
|
+
|
|
115
116
|
app_spec = app_str
|
|
117
|
+
# Set `app` to current directory for credential storage
|
|
118
|
+
app = Path(".")
|
|
116
119
|
is_remote_app = app_spec is not None
|
|
117
120
|
|
|
118
121
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
@@ -212,22 +215,14 @@ def _run_with_control_api(
|
|
|
212
215
|
f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN
|
|
213
216
|
)
|
|
214
217
|
else:
|
|
215
|
-
|
|
216
|
-
typer.secho(
|
|
217
|
-
"❌ Failed to start run. Please check that the provided "
|
|
218
|
-
"app identifier (@account_name/app_name) is correct.",
|
|
219
|
-
fg=typer.colors.RED,
|
|
220
|
-
err=True,
|
|
221
|
-
)
|
|
222
|
-
else:
|
|
223
|
-
typer.secho("❌ Failed to start run", fg=typer.colors.RED, err=True)
|
|
218
|
+
typer.secho("❌ Failed to start run", fg=typer.colors.RED, err=True)
|
|
224
219
|
raise typer.Exit(code=1)
|
|
225
220
|
|
|
226
221
|
if output_format == CliOutputFormat.JSON:
|
|
227
222
|
# Only include FAB metadata if we actually built a local FAB
|
|
228
223
|
payload: dict[str, Any] = {
|
|
229
224
|
"success": res.HasField("run_id"),
|
|
230
|
-
"run-id": res.run_id if res.HasField("run_id") else None,
|
|
225
|
+
"run-id": f"{res.run_id}" if res.HasField("run_id") else None,
|
|
231
226
|
}
|
|
232
227
|
if not is_remote_app:
|
|
233
228
|
payload.update(
|