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.
Files changed (204) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +25 -66
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +72 -61
  25. flwr/cli/new/new.py +98 -309
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +87 -100
  28. flwr/cli/run_utils.py +23 -5
  29. flwr/cli/stop.py +33 -74
  30. flwr/cli/supernode/ls.py +35 -62
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -412
  35. flwr/client/grpc_adapter_client/connection.py +2 -2
  36. flwr/client/grpc_rere_client/connection.py +9 -6
  37. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  38. flwr/client/message_handler/message_handler.py +2 -1
  39. flwr/client/mod/centraldp_mods.py +1 -1
  40. flwr/client/mod/localdp_mod.py +1 -1
  41. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  42. flwr/client/rest_client/connection.py +6 -4
  43. flwr/client/run_info_store.py +2 -1
  44. flwr/clientapp/client_app.py +2 -1
  45. flwr/common/__init__.py +3 -2
  46. flwr/common/args.py +5 -5
  47. flwr/common/config.py +12 -17
  48. flwr/common/constant.py +3 -16
  49. flwr/common/context.py +2 -1
  50. flwr/common/exit/exit.py +4 -4
  51. flwr/common/exit/exit_code.py +6 -0
  52. flwr/common/grpc.py +2 -1
  53. flwr/common/logger.py +1 -1
  54. flwr/common/message.py +1 -1
  55. flwr/common/retry_invoker.py +13 -5
  56. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  57. flwr/common/serde.py +13 -5
  58. flwr/common/telemetry.py +1 -1
  59. flwr/common/typing.py +10 -3
  60. flwr/compat/client/app.py +6 -9
  61. flwr/compat/client/grpc_client/connection.py +2 -1
  62. flwr/compat/common/constant.py +29 -0
  63. flwr/compat/server/app.py +1 -1
  64. flwr/proto/clientappio_pb2.py +2 -2
  65. flwr/proto/clientappio_pb2_grpc.py +104 -88
  66. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  67. flwr/proto/federation_pb2.py +5 -3
  68. flwr/proto/federation_pb2.pyi +32 -2
  69. flwr/proto/fleet_pb2.py +10 -10
  70. flwr/proto/fleet_pb2.pyi +5 -1
  71. flwr/proto/run_pb2.py +18 -26
  72. flwr/proto/run_pb2.pyi +10 -58
  73. flwr/proto/serverappio_pb2.py +2 -2
  74. flwr/proto/serverappio_pb2_grpc.py +138 -207
  75. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  76. flwr/proto/simulationio_pb2.py +2 -2
  77. flwr/proto/simulationio_pb2_grpc.py +62 -90
  78. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  79. flwr/server/app.py +7 -13
  80. flwr/server/compat/grid_client_proxy.py +2 -1
  81. flwr/server/grid/grpc_grid.py +5 -5
  82. flwr/server/serverapp/app.py +11 -4
  83. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  84. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  85. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  86. flwr/server/superlink/linkstate/__init__.py +2 -2
  87. flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
  88. flwr/server/superlink/linkstate/linkstate.py +34 -21
  89. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  90. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
  91. flwr/server/superlink/linkstate/utils.py +49 -2
  92. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  93. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  94. flwr/server/utils/validator.py +1 -1
  95. flwr/server/workflow/default_workflows.py +2 -1
  96. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  97. flwr/serverapp/strategy/bulyan.py +7 -1
  98. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  99. flwr/serverapp/strategy/fedavg.py +1 -1
  100. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  101. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  102. flwr/simulation/run_simulation.py +3 -12
  103. flwr/simulation/simulationio_connection.py +3 -3
  104. flwr/{common → supercore}/address.py +7 -33
  105. flwr/supercore/app_utils.py +2 -1
  106. flwr/supercore/constant.py +27 -2
  107. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  108. flwr/supercore/credential_store/__init__.py +33 -0
  109. flwr/supercore/credential_store/credential_store.py +34 -0
  110. flwr/supercore/credential_store/file_credential_store.py +76 -0
  111. flwr/{common → supercore}/date.py +0 -11
  112. flwr/supercore/ffs/disk_ffs.py +1 -1
  113. flwr/supercore/object_store/object_store_factory.py +14 -6
  114. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  115. flwr/supercore/sql_mixin.py +315 -0
  116. flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
  117. flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
  118. flwr/supercore/state/alembic/env.py +103 -0
  119. flwr/supercore/state/alembic/script.py.mako +43 -0
  120. flwr/supercore/state/alembic/utils.py +239 -0
  121. flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
  122. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  123. flwr/supercore/state/schema/README.md +121 -0
  124. flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
  125. flwr/supercore/state/schema/corestate_tables.py +36 -0
  126. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  127. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  128. flwr/supercore/superexec/run_superexec.py +2 -2
  129. flwr/supercore/utils.py +225 -0
  130. flwr/superlink/federation/federation_manager.py +2 -2
  131. flwr/superlink/federation/noop_federation_manager.py +8 -6
  132. flwr/superlink/servicer/control/control_grpc.py +2 -0
  133. flwr/superlink/servicer/control/control_servicer.py +106 -21
  134. flwr/supernode/cli/flower_supernode.py +2 -1
  135. flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
  136. flwr/supernode/nodestate/nodestate.py +45 -0
  137. flwr/supernode/runtime/run_clientapp.py +14 -14
  138. flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
  139. flwr/supernode/start_client_internal.py +17 -10
  140. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
  141. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
  142. flwr/cli/federation/show.py +0 -317
  143. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  144. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  145. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  146. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  147. flwr/cli/new/templates/app/README.md.tpl +0 -37
  148. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  149. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  150. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  151. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  152. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  153. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  154. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  155. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  156. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  157. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  158. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  159. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  160. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  161. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  162. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  163. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  164. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  165. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  166. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  167. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  168. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  169. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  170. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  171. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  172. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  173. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  174. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  175. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  176. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  177. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  178. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  179. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  180. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  181. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  182. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  183. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
  184. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  185. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  186. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  187. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  188. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  189. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  190. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  191. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  192. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  193. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  194. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  195. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  196. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  197. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  198. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  199. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  200. flwr/common/pyproject.py +0 -42
  201. flwr/supercore/sqlite_mixin.py +0 -159
  202. /flwr/{common → supercore}/version.py +0 -0
  203. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  204. {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 string import Template
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
- class MlFramework(str, Enum):
42
- """Available frameworks."""
43
-
44
- PYTORCH = "PyTorch"
45
- TENSORFLOW = "TensorFlow"
46
- SKLEARN = "sklearn"
47
- HUGGINGFACE = "HuggingFace"
48
- JAX = "JAX"
49
- MLX = "MLX"
50
- NUMPY = "NumPy"
51
- XGBOOST = "XGBoost"
52
- FLOWERTUNE = "FlowerTune"
53
- BASELINE = "Flower Baseline"
54
- PYTORCH_LEGACY_API = "PyTorch (Legacy API, deprecated)"
55
-
56
-
57
- class LlmChallengeName(str, Enum):
58
- """Available LLM challenges."""
59
-
60
- GENERALNLP = "GeneralNLP"
61
- FINANCE = "Finance"
62
- MEDICAL = "Medical"
63
- CODE = "Code"
64
-
65
-
66
- class TemplateNotFound(Exception):
67
- """Raised when template does not exist."""
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
- def create_file(file_path: Path, content: str) -> None:
92
- """Create file including all nessecary directories and write content into file."""
93
- file_path.parent.mkdir(exist_ok=True)
94
- file_path.write_text(content, encoding="utf-8")
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
- def render_and_create(file_path: Path, template: str, context: dict[str, str]) -> None:
98
- """Render template and write to file."""
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" + _add + "\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
- 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
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
- 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)
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
- app_id, app_version = parse_app_spec(app_spec)
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
- print(
223
- typer.style(
224
- f"\n🔗 Requesting download link for {app_id}...",
225
- fg=typer.colors.GREEN,
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
- presigned_url = request_download_link(app_id, app_version, url, "zip_url")
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
- print(
234
- typer.style(
235
- "⬇️ Downloading ZIP into memory...",
236
- fg=typer.colors.GREEN,
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
- print(
243
- typer.style(
244
- f"📦 Unpacking into {project_dir}...",
245
- fg=typer.colors.GREEN,
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.config_utils import (
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.common.constant import FAB_CONFIG_FILE
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, init_channel, load_cli_auth_plugin
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.Option(
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
- app: Annotated[
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 federation."),
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
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
59
+ # Warn `--federation-config` is ignored
60
+ warn_if_federation_config_overrides(federation_config_overrides)
70
61
 
71
- pyproject_path = app / FAB_CONFIG_FILE if app else None
72
- config, errors, warnings = load_and_validate(pyproject_path, check_module=False)
73
- config = process_loaded_project_config(config, errors, warnings)
74
- federation, federation_config = validate_federation_in_project_config(
75
- federation, config, federation_config_overrides
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
- typer.secho(
91
- f"A download URL for artifacts from run {run_id} couldn't be "
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}",