flwr 1.24.0__py3-none-any.whl → 1.26.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 +1 -1
- flwr/app/__init__.py +4 -1
- flwr/app/message_type.py +29 -0
- flwr/app/metadata.py +5 -2
- flwr/app/user_config.py +19 -0
- flwr/cli/app.py +37 -19
- flwr/cli/app_cmd/publish.py +25 -75
- flwr/cli/app_cmd/review.py +25 -66
- flwr/cli/auth_plugin/auth_plugin.py +5 -10
- flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
- flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
- flwr/cli/build.py +15 -28
- flwr/cli/config/__init__.py +21 -0
- flwr/cli/config/ls.py +71 -0
- flwr/cli/config_migration.py +297 -0
- flwr/cli/config_utils.py +63 -156
- flwr/cli/constant.py +71 -0
- flwr/cli/federation/__init__.py +0 -2
- flwr/cli/federation/ls.py +256 -64
- flwr/cli/flower_config.py +429 -0
- flwr/cli/install.py +23 -62
- flwr/cli/log.py +23 -37
- flwr/cli/login/login.py +29 -63
- flwr/cli/ls.py +72 -61
- flwr/cli/new/new.py +98 -309
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +87 -100
- flwr/cli/run_utils.py +23 -5
- flwr/cli/stop.py +33 -74
- flwr/cli/supernode/ls.py +35 -62
- flwr/cli/supernode/register.py +31 -80
- flwr/cli/supernode/unregister.py +24 -70
- flwr/cli/typing.py +200 -0
- flwr/cli/utils.py +160 -412
- flwr/client/grpc_adapter_client/connection.py +2 -2
- flwr/client/grpc_rere_client/connection.py +9 -6
- flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
- flwr/client/message_handler/message_handler.py +2 -1
- flwr/client/mod/centraldp_mods.py +1 -1
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/rest_client/connection.py +6 -4
- flwr/client/run_info_store.py +2 -1
- flwr/clientapp/client_app.py +2 -1
- flwr/common/__init__.py +3 -2
- flwr/common/args.py +5 -5
- flwr/common/config.py +12 -17
- flwr/common/constant.py +3 -16
- flwr/common/context.py +2 -1
- flwr/common/exit/exit.py +4 -4
- flwr/common/exit/exit_code.py +6 -0
- flwr/common/grpc.py +2 -1
- flwr/common/logger.py +1 -1
- flwr/common/message.py +1 -1
- flwr/common/retry_invoker.py +13 -5
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
- flwr/common/serde.py +13 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +10 -3
- flwr/compat/client/app.py +6 -9
- flwr/compat/client/grpc_client/connection.py +2 -1
- flwr/compat/common/constant.py +29 -0
- flwr/compat/server/app.py +1 -1
- flwr/proto/clientappio_pb2.py +2 -2
- flwr/proto/clientappio_pb2_grpc.py +104 -88
- flwr/proto/clientappio_pb2_grpc.pyi +140 -80
- flwr/proto/federation_pb2.py +5 -3
- flwr/proto/federation_pb2.pyi +32 -2
- flwr/proto/fleet_pb2.py +10 -10
- flwr/proto/fleet_pb2.pyi +5 -1
- flwr/proto/run_pb2.py +18 -26
- flwr/proto/run_pb2.pyi +10 -58
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +138 -207
- flwr/proto/serverappio_pb2_grpc.pyi +189 -155
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +62 -90
- flwr/proto/simulationio_pb2_grpc.pyi +95 -55
- flwr/server/app.py +7 -13
- flwr/server/compat/grid_client_proxy.py +2 -1
- flwr/server/grid/grpc_grid.py +5 -5
- flwr/server/serverapp/app.py +11 -4
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
- flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
- flwr/server/superlink/linkstate/linkstate.py +34 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
- flwr/server/superlink/linkstate/utils.py +49 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
- flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
- flwr/server/utils/validator.py +1 -1
- flwr/server/workflow/default_workflows.py +2 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/strategy/bulyan.py +7 -1
- flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
- flwr/serverapp/strategy/fedavg.py +1 -1
- flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
- flwr/simulation/run_simulation.py +3 -12
- flwr/simulation/simulationio_connection.py +3 -3
- flwr/{common → supercore}/address.py +7 -33
- flwr/supercore/app_utils.py +2 -1
- flwr/supercore/constant.py +27 -2
- flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
- flwr/supercore/credential_store/__init__.py +33 -0
- flwr/supercore/credential_store/credential_store.py +34 -0
- flwr/supercore/credential_store/file_credential_store.py +76 -0
- flwr/{common → supercore}/date.py +0 -11
- flwr/supercore/ffs/disk_ffs.py +1 -1
- flwr/supercore/object_store/object_store_factory.py +14 -6
- flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
- flwr/supercore/sql_mixin.py +315 -0
- flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
- flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
- flwr/supercore/state/alembic/env.py +103 -0
- flwr/supercore/state/alembic/script.py.mako +43 -0
- flwr/supercore/state/alembic/utils.py +239 -0
- flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
- flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
- flwr/supercore/state/schema/README.md +121 -0
- flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
- flwr/supercore/state/schema/corestate_tables.py +36 -0
- flwr/supercore/state/schema/linkstate_tables.py +152 -0
- flwr/supercore/state/schema/objectstore_tables.py +90 -0
- flwr/supercore/superexec/run_superexec.py +2 -2
- flwr/supercore/utils.py +225 -0
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_grpc.py +2 -0
- flwr/superlink/servicer/control/control_servicer.py +106 -21
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
- flwr/supernode/nodestate/nodestate.py +45 -0
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
- flwr/supernode/start_client_internal.py +17 -10
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
- flwr/cli/federation/show.py +0 -317
- 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.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 -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/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
flwr/cli/new/new.py
CHANGED
|
@@ -16,93 +16,81 @@
|
|
|
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
|
|
|
23
|
+
import click
|
|
26
24
|
import requests
|
|
27
25
|
import typer
|
|
28
26
|
|
|
29
27
|
from flwr.supercore.constant import PLATFORM_API_URL
|
|
28
|
+
from flwr.supercore.utils import parse_app_spec, request_download_link
|
|
30
29
|
|
|
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
|
-
)
|
|
30
|
+
from ..utils import prompt_options, prompt_text
|
|
39
31
|
|
|
40
32
|
|
|
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
|
-
def load_template(name: str) -> str:
|
|
71
|
-
"""Load template from template directory and return as text."""
|
|
72
|
-
tpl_dir = (Path(__file__).parent / "templates").absolute()
|
|
73
|
-
tpl_file_path = tpl_dir / name
|
|
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
|
-
|
|
33
|
+
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
34
|
+
def new(
|
|
35
|
+
app_spec: Annotated[
|
|
36
|
+
str | None,
|
|
37
|
+
typer.Argument(
|
|
38
|
+
help="Flower app specifier. Use the format "
|
|
39
|
+
"'@account_name/app_name' or '@account_name/app_name==x.y.z'. "
|
|
40
|
+
"Version is optional (defaults to latest)."
|
|
41
|
+
),
|
|
42
|
+
] = None,
|
|
43
|
+
framework: Annotated[
|
|
44
|
+
str | None,
|
|
45
|
+
typer.Option(case_sensitive=False, help="Deprecated. The ML framework to use"),
|
|
46
|
+
] = None,
|
|
47
|
+
username: Annotated[
|
|
48
|
+
str | None,
|
|
49
|
+
typer.Option(
|
|
50
|
+
case_sensitive=False, help="Deprecated. The Flower username of the author"
|
|
51
|
+
),
|
|
52
|
+
] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Create new Flower App."""
|
|
55
|
+
if framework is not None or username is not None:
|
|
56
|
+
raise click.ClickException(
|
|
57
|
+
"The --framework and --username options are deprecated and will be "
|
|
58
|
+
"removed in future versions of Flower. Please provide an app specifier "
|
|
59
|
+
"after `flwr new` instead, e.g., '@account_name/app_name' or "
|
|
60
|
+
"'@account_name/app_name==x.y.z'."
|
|
61
|
+
)
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
63
|
+
if app_spec is None:
|
|
64
|
+
# Fetch recommended apps
|
|
65
|
+
print(
|
|
66
|
+
typer.style(
|
|
67
|
+
"\n🌸 Fetching recommended apps...",
|
|
68
|
+
fg=typer.colors.GREEN,
|
|
69
|
+
bold=True,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
apps = fetch_recommended_apps()
|
|
95
73
|
|
|
74
|
+
if not apps:
|
|
75
|
+
typer.secho(
|
|
76
|
+
"No recommended apps found. Please provide an app specifier manually.",
|
|
77
|
+
fg=typer.colors.YELLOW,
|
|
78
|
+
)
|
|
79
|
+
app_spec = prompt_text("Please provide the app specifier")
|
|
80
|
+
else:
|
|
81
|
+
# Extract app_ids and show selection menu
|
|
82
|
+
app_ids = [app["app_id"] for app in apps]
|
|
83
|
+
app_spec = prompt_options(
|
|
84
|
+
"Select a Flower App to create by entering "
|
|
85
|
+
"the number from the list below:",
|
|
86
|
+
app_ids,
|
|
87
|
+
)
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
content = render_template(template, context)
|
|
100
|
-
create_file(file_path, content)
|
|
89
|
+
# Download remote app
|
|
90
|
+
download_remote_app_via_api(app_spec)
|
|
101
91
|
|
|
102
92
|
|
|
103
|
-
def print_success_prompt(
|
|
104
|
-
package_name: str, llm_challenge_str: str | None = None
|
|
105
|
-
) -> None:
|
|
93
|
+
def print_success_prompt(package_name: str) -> None:
|
|
106
94
|
"""Print styled setup instructions for running a new Flower App after creation."""
|
|
107
95
|
prompt = typer.style(
|
|
108
96
|
"🎊 Flower App creation successful.\n\n"
|
|
@@ -111,10 +99,8 @@ def print_success_prompt(
|
|
|
111
99
|
bold=True,
|
|
112
100
|
)
|
|
113
101
|
|
|
114
|
-
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
115
|
-
|
|
116
102
|
prompt += typer.style(
|
|
117
|
-
f" cd {package_name} && pip install -e .\n
|
|
103
|
+
f" cd {package_name} && pip install -e .\n\n",
|
|
118
104
|
fg=typer.colors.BRIGHT_CYAN,
|
|
119
105
|
bold=True,
|
|
120
106
|
)
|
|
@@ -141,6 +127,20 @@ def print_success_prompt(
|
|
|
141
127
|
print(prompt)
|
|
142
128
|
|
|
143
129
|
|
|
130
|
+
def fetch_recommended_apps() -> list[dict[str, str]]:
|
|
131
|
+
"""Fetch recommended apps from Platform API."""
|
|
132
|
+
url = f"{PLATFORM_API_URL}/hub/apps?tag=recommended"
|
|
133
|
+
try:
|
|
134
|
+
response = requests.get(url, headers={"accept": "application/json"}, timeout=10)
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
data = response.json()
|
|
137
|
+
apps = data.get("apps", [])
|
|
138
|
+
return cast(list[dict[str, str]], apps)
|
|
139
|
+
|
|
140
|
+
except requests.RequestException as e:
|
|
141
|
+
raise click.ClickException(f"Failed to fetch recommended apps: {e}") from e
|
|
142
|
+
|
|
143
|
+
|
|
144
144
|
# Security: prevent zip-slip
|
|
145
145
|
def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
|
|
146
146
|
"""Extract ZIP file into destination directory."""
|
|
@@ -182,22 +182,12 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
|
|
|
182
182
|
r = requests.get(presigned_url, timeout=60)
|
|
183
183
|
r.raise_for_status()
|
|
184
184
|
except requests.RequestException as e:
|
|
185
|
-
|
|
186
|
-
f"ZIP download failed: {e}",
|
|
187
|
-
fg=typer.colors.RED,
|
|
188
|
-
err=True,
|
|
189
|
-
)
|
|
190
|
-
raise typer.Exit(code=1) from e
|
|
185
|
+
raise click.ClickException(f"ZIP download failed: {e}") from e
|
|
191
186
|
|
|
192
187
|
buf = io.BytesIO(r.content)
|
|
193
188
|
# Validate it's a zip
|
|
194
189
|
if not zipfile.is_zipfile(buf):
|
|
195
|
-
|
|
196
|
-
"Downloaded file is not a valid ZIP",
|
|
197
|
-
fg=typer.colors.RED,
|
|
198
|
-
err=True,
|
|
199
|
-
)
|
|
200
|
-
raise typer.Exit(code=1)
|
|
190
|
+
raise click.ClickException("Downloaded file is not a valid ZIP")
|
|
201
191
|
buf.seek(0)
|
|
202
192
|
return buf
|
|
203
193
|
|
|
@@ -205,7 +195,11 @@ def _download_zip_to_memory(presigned_url: str) -> io.BytesIO:
|
|
|
205
195
|
def download_remote_app_via_api(app_spec: str) -> None:
|
|
206
196
|
"""Download App from Platform API."""
|
|
207
197
|
# Validate app version and ID format
|
|
208
|
-
|
|
198
|
+
try:
|
|
199
|
+
app_id, app_version = parse_app_spec(app_spec)
|
|
200
|
+
except ValueError as e:
|
|
201
|
+
raise click.ClickException(str(e)) from e
|
|
202
|
+
|
|
209
203
|
app_name = app_id.split("/")[1]
|
|
210
204
|
|
|
211
205
|
project_dir = Path.cwd() / app_name
|
|
@@ -219,236 +213,31 @@ def download_remote_app_via_api(app_spec: str) -> None:
|
|
|
219
213
|
):
|
|
220
214
|
return
|
|
221
215
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
bold=True,
|
|
227
|
-
)
|
|
216
|
+
typer.secho(
|
|
217
|
+
f"\n🔗 Requesting download link for {app_id}...",
|
|
218
|
+
fg=typer.colors.GREEN,
|
|
219
|
+
bold=True,
|
|
228
220
|
)
|
|
229
221
|
# Fetch ZIP downloading URL
|
|
230
222
|
url = f"{PLATFORM_API_URL}/hub/fetch-zip"
|
|
231
|
-
|
|
223
|
+
try:
|
|
224
|
+
presigned_url, _ = request_download_link(app_id, app_version, url, "zip_url")
|
|
225
|
+
except ValueError as e:
|
|
226
|
+
raise click.ClickException(str(e)) from e
|
|
232
227
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
bold=True,
|
|
238
|
-
)
|
|
228
|
+
typer.secho(
|
|
229
|
+
"🔽 Downloading ZIP into memory...",
|
|
230
|
+
fg=typer.colors.GREEN,
|
|
231
|
+
bold=True,
|
|
239
232
|
)
|
|
240
233
|
zip_buf = _download_zip_to_memory(presigned_url)
|
|
241
234
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
bold=True,
|
|
247
|
-
)
|
|
235
|
+
typer.secho(
|
|
236
|
+
f"📦 Unpacking into {project_dir}...",
|
|
237
|
+
fg=typer.colors.GREEN,
|
|
238
|
+
bold=True,
|
|
248
239
|
)
|
|
249
240
|
with zipfile.ZipFile(zip_buf) as zf:
|
|
250
241
|
_safe_extract_zip(zf, Path.cwd())
|
|
251
242
|
|
|
252
243
|
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/pull.py
CHANGED
|
@@ -15,49 +15,39 @@
|
|
|
15
15
|
"""Flower command line interface `pull` command."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from pathlib import Path
|
|
19
18
|
from typing import Annotated
|
|
20
19
|
|
|
20
|
+
import click
|
|
21
21
|
import typer
|
|
22
22
|
|
|
23
|
-
from flwr.cli.
|
|
24
|
-
exit_if_no_address,
|
|
25
|
-
load_and_validate,
|
|
26
|
-
process_loaded_project_config,
|
|
27
|
-
validate_federation_in_project_config,
|
|
28
|
-
)
|
|
23
|
+
from flwr.cli.config_migration import migrate, warn_if_federation_config_overrides
|
|
29
24
|
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
30
|
-
from flwr.
|
|
25
|
+
from flwr.cli.flower_config import read_superlink_connection
|
|
31
26
|
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
32
27
|
PullArtifactsRequest,
|
|
33
28
|
PullArtifactsResponse,
|
|
34
29
|
)
|
|
35
30
|
from flwr.proto.control_pb2_grpc import ControlStub
|
|
36
31
|
|
|
37
|
-
from .utils import flwr_cli_grpc_exc_handler,
|
|
32
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel_from_connection
|
|
38
33
|
|
|
39
34
|
|
|
40
35
|
def pull( # pylint: disable=R0914
|
|
36
|
+
ctx: typer.Context,
|
|
41
37
|
run_id: Annotated[
|
|
42
38
|
int,
|
|
43
|
-
typer.
|
|
44
|
-
"--run-id",
|
|
45
|
-
help="Run ID to pull artifacts from.",
|
|
46
|
-
),
|
|
39
|
+
typer.Argument(help="Run ID to pull artifacts from."),
|
|
47
40
|
],
|
|
48
|
-
|
|
49
|
-
Path,
|
|
50
|
-
typer.Argument(help="Path of the Flower App to run."),
|
|
51
|
-
] = Path("."),
|
|
52
|
-
federation: Annotated[
|
|
41
|
+
superlink: Annotated[
|
|
53
42
|
str | None,
|
|
54
|
-
typer.Argument(help="Name of the
|
|
43
|
+
typer.Argument(help="Name of the SuperLink connection."),
|
|
55
44
|
] = None,
|
|
56
45
|
federation_config_overrides: Annotated[
|
|
57
46
|
list[str] | None,
|
|
58
47
|
typer.Option(
|
|
59
48
|
"--federation-config",
|
|
60
49
|
help=FEDERATION_CONFIG_HELP_MESSAGE,
|
|
50
|
+
hidden=True,
|
|
61
51
|
),
|
|
62
52
|
] = None,
|
|
63
53
|
) -> None:
|
|
@@ -66,20 +56,17 @@ def pull( # pylint: disable=R0914
|
|
|
66
56
|
Retrieve a download URL for artifacts generated during a completed Flower run. The
|
|
67
57
|
artifacts can then be downloaded from the provided URL.
|
|
68
58
|
"""
|
|
69
|
-
|
|
59
|
+
# Warn `--federation-config` is ignored
|
|
60
|
+
warn_if_federation_config_overrides(federation_config_overrides)
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
exit_if_no_address(federation_config, "pull")
|
|
62
|
+
# Migrate legacy usage if any
|
|
63
|
+
migrate(superlink, args=ctx.args)
|
|
64
|
+
|
|
65
|
+
# Read superlink connection configuration
|
|
66
|
+
superlink_connection = read_superlink_connection(superlink)
|
|
78
67
|
channel = None
|
|
79
68
|
try:
|
|
80
|
-
|
|
81
|
-
auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
|
|
82
|
-
channel = init_channel(app, federation_config, auth_plugin)
|
|
69
|
+
channel = init_channel_from_connection(superlink_connection)
|
|
83
70
|
stub = ControlStub(channel)
|
|
84
71
|
with flwr_cli_grpc_exc_handler():
|
|
85
72
|
res: PullArtifactsResponse = stub.PullArtifacts(
|
|
@@ -87,14 +74,9 @@ def pull( # pylint: disable=R0914
|
|
|
87
74
|
)
|
|
88
75
|
|
|
89
76
|
if not res.url:
|
|
90
|
-
|
|
91
|
-
f"
|
|
92
|
-
"obtained.",
|
|
93
|
-
fg=typer.colors.RED,
|
|
94
|
-
bold=True,
|
|
95
|
-
err=True,
|
|
77
|
+
raise click.ClickException(
|
|
78
|
+
f"A download URL for artifacts from run {run_id} couldn't be obtained."
|
|
96
79
|
)
|
|
97
|
-
raise typer.Exit(code=1)
|
|
98
80
|
|
|
99
81
|
typer.secho(
|
|
100
82
|
f"✅ Artifacts for run {run_id} can be downloaded from: {res.url}",
|