flwr-nightly 1.8.0.dev20240315__py3-none-any.whl → 1.11.0.dev20240813__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.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

Files changed (237) hide show
  1. flwr/cli/app.py +7 -0
  2. flwr/cli/build.py +150 -0
  3. flwr/cli/config_utils.py +219 -0
  4. flwr/cli/example.py +3 -1
  5. flwr/cli/install.py +227 -0
  6. flwr/cli/new/new.py +179 -48
  7. flwr/cli/new/templates/app/.gitignore.tpl +160 -0
  8. flwr/cli/new/templates/app/README.flowertune.md.tpl +56 -0
  9. flwr/cli/new/templates/app/README.md.tpl +1 -5
  10. flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
  11. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +65 -0
  12. flwr/cli/new/templates/app/code/client.jax.py.tpl +56 -0
  13. flwr/cli/new/templates/app/code/client.mlx.py.tpl +93 -0
  14. flwr/cli/new/templates/app/code/client.numpy.py.tpl +3 -2
  15. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +23 -11
  16. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +97 -0
  17. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +60 -1
  18. flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
  19. flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +89 -0
  20. flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +126 -0
  21. flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl +34 -0
  22. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +57 -0
  23. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +59 -0
  24. flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl +48 -0
  25. flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl +11 -0
  26. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -0
  27. flwr/cli/new/templates/app/code/server.jax.py.tpl +20 -0
  28. flwr/cli/new/templates/app/code/server.mlx.py.tpl +20 -0
  29. flwr/cli/new/templates/app/code/server.numpy.py.tpl +17 -9
  30. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
  31. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +24 -0
  32. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
  33. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +99 -0
  34. flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
  35. flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
  36. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +28 -23
  37. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
  38. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +39 -0
  39. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +38 -0
  40. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +34 -0
  41. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
  42. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
  43. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
  44. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +33 -0
  45. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
  46. flwr/cli/run/run.py +168 -17
  47. flwr/cli/utils.py +75 -4
  48. flwr/client/__init__.py +6 -1
  49. flwr/client/app.py +239 -248
  50. flwr/client/client_app.py +70 -9
  51. flwr/client/dpfedavg_numpy_client.py +1 -1
  52. flwr/client/grpc_adapter_client/__init__.py +15 -0
  53. flwr/client/grpc_adapter_client/connection.py +97 -0
  54. flwr/client/grpc_client/connection.py +18 -5
  55. flwr/client/grpc_rere_client/__init__.py +1 -1
  56. flwr/client/grpc_rere_client/client_interceptor.py +158 -0
  57. flwr/client/grpc_rere_client/connection.py +127 -33
  58. flwr/client/grpc_rere_client/grpc_adapter.py +140 -0
  59. flwr/client/heartbeat.py +74 -0
  60. flwr/client/message_handler/__init__.py +1 -1
  61. flwr/client/message_handler/message_handler.py +7 -7
  62. flwr/client/mod/__init__.py +5 -5
  63. flwr/client/mod/centraldp_mods.py +4 -2
  64. flwr/client/mod/comms_mods.py +4 -4
  65. flwr/client/mod/localdp_mod.py +9 -4
  66. flwr/client/mod/secure_aggregation/__init__.py +1 -1
  67. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  68. flwr/client/mod/utils.py +1 -1
  69. flwr/client/node_state.py +60 -10
  70. flwr/client/node_state_tests.py +4 -3
  71. flwr/client/rest_client/__init__.py +1 -1
  72. flwr/client/rest_client/connection.py +177 -157
  73. flwr/client/supernode/__init__.py +26 -0
  74. flwr/client/supernode/app.py +464 -0
  75. flwr/client/typing.py +1 -0
  76. flwr/common/__init__.py +13 -11
  77. flwr/common/address.py +1 -1
  78. flwr/common/config.py +193 -0
  79. flwr/common/constant.py +42 -1
  80. flwr/common/context.py +26 -1
  81. flwr/common/date.py +1 -1
  82. flwr/common/dp.py +1 -1
  83. flwr/common/grpc.py +6 -2
  84. flwr/common/logger.py +79 -8
  85. flwr/common/message.py +167 -105
  86. flwr/common/object_ref.py +126 -25
  87. flwr/common/record/__init__.py +1 -1
  88. flwr/common/record/parametersrecord.py +0 -1
  89. flwr/common/record/recordset.py +78 -27
  90. flwr/common/recordset_compat.py +8 -1
  91. flwr/common/retry_invoker.py +25 -13
  92. flwr/common/secure_aggregation/__init__.py +1 -1
  93. flwr/common/secure_aggregation/crypto/__init__.py +1 -1
  94. flwr/common/secure_aggregation/crypto/shamir.py +1 -1
  95. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +21 -2
  96. flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
  97. flwr/common/secure_aggregation/quantization.py +1 -1
  98. flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
  99. flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
  100. flwr/common/serde.py +209 -3
  101. flwr/common/telemetry.py +25 -0
  102. flwr/common/typing.py +38 -0
  103. flwr/common/version.py +14 -0
  104. flwr/proto/clientappio_pb2.py +41 -0
  105. flwr/proto/clientappio_pb2.pyi +110 -0
  106. flwr/proto/clientappio_pb2_grpc.py +101 -0
  107. flwr/proto/clientappio_pb2_grpc.pyi +40 -0
  108. flwr/proto/common_pb2.py +36 -0
  109. flwr/proto/common_pb2.pyi +121 -0
  110. flwr/proto/common_pb2_grpc.py +4 -0
  111. flwr/proto/common_pb2_grpc.pyi +4 -0
  112. flwr/proto/driver_pb2.py +26 -19
  113. flwr/proto/driver_pb2.pyi +34 -0
  114. flwr/proto/driver_pb2_grpc.py +70 -0
  115. flwr/proto/driver_pb2_grpc.pyi +28 -0
  116. flwr/proto/exec_pb2.py +43 -0
  117. flwr/proto/exec_pb2.pyi +95 -0
  118. flwr/proto/exec_pb2_grpc.py +101 -0
  119. flwr/proto/exec_pb2_grpc.pyi +41 -0
  120. flwr/proto/fab_pb2.py +30 -0
  121. flwr/proto/fab_pb2.pyi +56 -0
  122. flwr/proto/fab_pb2_grpc.py +4 -0
  123. flwr/proto/fab_pb2_grpc.pyi +4 -0
  124. flwr/proto/fleet_pb2.py +29 -23
  125. flwr/proto/fleet_pb2.pyi +33 -0
  126. flwr/proto/fleet_pb2_grpc.py +102 -0
  127. flwr/proto/fleet_pb2_grpc.pyi +35 -0
  128. flwr/proto/grpcadapter_pb2.py +32 -0
  129. flwr/proto/grpcadapter_pb2.pyi +43 -0
  130. flwr/proto/grpcadapter_pb2_grpc.py +66 -0
  131. flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
  132. flwr/proto/message_pb2.py +41 -0
  133. flwr/proto/message_pb2.pyi +122 -0
  134. flwr/proto/message_pb2_grpc.py +4 -0
  135. flwr/proto/message_pb2_grpc.pyi +4 -0
  136. flwr/proto/run_pb2.py +35 -0
  137. flwr/proto/run_pb2.pyi +76 -0
  138. flwr/proto/run_pb2_grpc.py +4 -0
  139. flwr/proto/run_pb2_grpc.pyi +4 -0
  140. flwr/proto/task_pb2.py +7 -8
  141. flwr/proto/task_pb2.pyi +8 -5
  142. flwr/server/__init__.py +4 -8
  143. flwr/server/app.py +298 -350
  144. flwr/server/compat/app.py +6 -57
  145. flwr/server/compat/app_utils.py +5 -4
  146. flwr/server/compat/driver_client_proxy.py +29 -48
  147. flwr/server/compat/legacy_context.py +5 -4
  148. flwr/server/driver/__init__.py +2 -0
  149. flwr/server/driver/driver.py +22 -132
  150. flwr/server/driver/grpc_driver.py +224 -74
  151. flwr/server/driver/inmemory_driver.py +183 -0
  152. flwr/server/history.py +20 -20
  153. flwr/server/run_serverapp.py +121 -34
  154. flwr/server/server.py +11 -7
  155. flwr/server/server_app.py +59 -10
  156. flwr/server/serverapp_components.py +52 -0
  157. flwr/server/strategy/__init__.py +2 -2
  158. flwr/server/strategy/bulyan.py +1 -1
  159. flwr/server/strategy/dp_adaptive_clipping.py +3 -3
  160. flwr/server/strategy/dp_fixed_clipping.py +4 -3
  161. flwr/server/strategy/dpfedavg_adaptive.py +1 -1
  162. flwr/server/strategy/dpfedavg_fixed.py +1 -1
  163. flwr/server/strategy/fedadagrad.py +1 -1
  164. flwr/server/strategy/fedadam.py +1 -1
  165. flwr/server/strategy/fedavg_android.py +1 -1
  166. flwr/server/strategy/fedavgm.py +1 -1
  167. flwr/server/strategy/fedmedian.py +1 -1
  168. flwr/server/strategy/fedopt.py +1 -1
  169. flwr/server/strategy/fedprox.py +1 -1
  170. flwr/server/strategy/fedxgb_bagging.py +1 -1
  171. flwr/server/strategy/fedxgb_cyclic.py +1 -1
  172. flwr/server/strategy/fedxgb_nn_avg.py +1 -1
  173. flwr/server/strategy/fedyogi.py +1 -1
  174. flwr/server/strategy/krum.py +1 -1
  175. flwr/server/strategy/qfedavg.py +1 -1
  176. flwr/server/superlink/driver/__init__.py +1 -1
  177. flwr/server/superlink/driver/driver_grpc.py +1 -1
  178. flwr/server/superlink/driver/driver_servicer.py +51 -4
  179. flwr/server/superlink/ffs/__init__.py +24 -0
  180. flwr/server/superlink/ffs/disk_ffs.py +104 -0
  181. flwr/server/superlink/ffs/ffs.py +79 -0
  182. flwr/server/superlink/fleet/__init__.py +1 -1
  183. flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
  184. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
  185. flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
  186. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
  187. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
  188. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
  189. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +8 -2
  190. flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
  191. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +30 -2
  192. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +214 -0
  193. flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
  194. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  195. flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
  196. flwr/server/superlink/fleet/rest_rere/rest_api.py +59 -1
  197. flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
  198. flwr/server/superlink/fleet/vce/backend/backend.py +5 -5
  199. flwr/server/superlink/fleet/vce/backend/raybackend.py +53 -56
  200. flwr/server/superlink/fleet/vce/vce_api.py +190 -127
  201. flwr/server/superlink/state/__init__.py +1 -1
  202. flwr/server/superlink/state/in_memory_state.py +159 -42
  203. flwr/server/superlink/state/sqlite_state.py +243 -39
  204. flwr/server/superlink/state/state.py +81 -6
  205. flwr/server/superlink/state/state_factory.py +11 -2
  206. flwr/server/superlink/state/utils.py +62 -0
  207. flwr/server/typing.py +2 -0
  208. flwr/server/utils/__init__.py +1 -1
  209. flwr/server/utils/tensorboard.py +1 -1
  210. flwr/server/utils/validator.py +23 -9
  211. flwr/server/workflow/default_workflows.py +67 -25
  212. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +18 -6
  213. flwr/simulation/__init__.py +7 -4
  214. flwr/simulation/app.py +67 -36
  215. flwr/simulation/ray_transport/__init__.py +1 -1
  216. flwr/simulation/ray_transport/ray_actor.py +20 -46
  217. flwr/simulation/ray_transport/ray_client_proxy.py +36 -16
  218. flwr/simulation/run_simulation.py +308 -92
  219. flwr/superexec/__init__.py +21 -0
  220. flwr/superexec/app.py +184 -0
  221. flwr/superexec/deployment.py +185 -0
  222. flwr/superexec/exec_grpc.py +55 -0
  223. flwr/superexec/exec_servicer.py +70 -0
  224. flwr/superexec/executor.py +75 -0
  225. flwr/superexec/simulation.py +193 -0
  226. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/METADATA +10 -6
  227. flwr_nightly-1.11.0.dev20240813.dist-info/RECORD +288 -0
  228. flwr_nightly-1.11.0.dev20240813.dist-info/entry_points.txt +10 -0
  229. flwr/cli/flower_toml.py +0 -140
  230. flwr/cli/new/templates/app/flower.toml.tpl +0 -13
  231. flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
  232. flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
  233. flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
  234. flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
  235. flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
  236. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/LICENSE +0 -0
  237. {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/WHEEL +0 -0
flwr/cli/app.py CHANGED
@@ -15,8 +15,11 @@
15
15
  """Flower command line interface."""
16
16
 
17
17
  import typer
18
+ from typer.main import get_command
18
19
 
20
+ from .build import build
19
21
  from .example import example
22
+ from .install import install
20
23
  from .new import new
21
24
  from .run import run
22
25
 
@@ -32,6 +35,10 @@ app = typer.Typer(
32
35
  app.command()(new)
33
36
  app.command()(example)
34
37
  app.command()(run)
38
+ app.command()(build)
39
+ app.command()(install)
40
+
41
+ typer_click_object = get_command(app)
35
42
 
36
43
  if __name__ == "__main__":
37
44
  app()
flwr/cli/build.py ADDED
@@ -0,0 +1,150 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface `build` command."""
16
+
17
+ import os
18
+ import zipfile
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ import pathspec
23
+ import tomli_w
24
+ import typer
25
+ from typing_extensions import Annotated
26
+
27
+ from .config_utils import load_and_validate
28
+ from .utils import get_sha256_hash, is_valid_project_name
29
+
30
+
31
+ # pylint: disable=too-many-locals
32
+ def build(
33
+ app: Annotated[
34
+ Optional[Path],
35
+ typer.Option(help="Path of the Flower App to bundle into a FAB"),
36
+ ] = None,
37
+ ) -> str:
38
+ """Build a Flower App into a Flower App Bundle (FAB).
39
+
40
+ You can run ``flwr build`` without any arguments to bundle the app located in the
41
+ current directory. Alternatively, you can you can specify a path using the ``--app``
42
+ option to bundle an app located at the provided path. For example:
43
+
44
+ ``flwr build --app ./apps/flower-hello-world``.
45
+ """
46
+ if app is None:
47
+ app = Path.cwd()
48
+
49
+ app = app.resolve()
50
+ if not app.is_dir():
51
+ typer.secho(
52
+ f"❌ The path {app} is not a valid path to a Flower app.",
53
+ fg=typer.colors.RED,
54
+ bold=True,
55
+ )
56
+ raise typer.Exit(code=1)
57
+
58
+ if not is_valid_project_name(app.name):
59
+ typer.secho(
60
+ f"❌ The project name {app.name} is invalid, "
61
+ "a valid project name must start with a letter or an underscore, "
62
+ "and can only contain letters, digits, and underscores.",
63
+ fg=typer.colors.RED,
64
+ bold=True,
65
+ )
66
+ raise typer.Exit(code=1)
67
+
68
+ conf, errors, warnings = load_and_validate(app / "pyproject.toml")
69
+ if conf is None:
70
+ typer.secho(
71
+ "Project configuration could not be loaded.\npyproject.toml is invalid:\n"
72
+ + "\n".join([f"- {line}" for line in errors]),
73
+ fg=typer.colors.RED,
74
+ bold=True,
75
+ )
76
+ raise typer.Exit(code=1)
77
+
78
+ if warnings:
79
+ typer.secho(
80
+ "Project configuration is missing the following "
81
+ "recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
82
+ fg=typer.colors.RED,
83
+ bold=True,
84
+ )
85
+
86
+ # Load .gitignore rules if present
87
+ ignore_spec = _load_gitignore(app)
88
+
89
+ # Set the name of the zip file
90
+ fab_filename = (
91
+ f"{conf['tool']['flwr']['app']['publisher']}"
92
+ f".{app.name}"
93
+ f".{conf['project']['version'].replace('.', '-')}.fab"
94
+ )
95
+ list_file_content = ""
96
+
97
+ allowed_extensions = {".py", ".toml", ".md"}
98
+
99
+ # Remove the 'federations' field from 'tool.flwr' if it exists
100
+ if (
101
+ "tool" in conf
102
+ and "flwr" in conf["tool"]
103
+ and "federations" in conf["tool"]["flwr"]
104
+ ):
105
+ del conf["tool"]["flwr"]["federations"]
106
+
107
+ toml_contents = tomli_w.dumps(conf)
108
+
109
+ with zipfile.ZipFile(fab_filename, "w", zipfile.ZIP_DEFLATED) as fab_file:
110
+ fab_file.writestr("pyproject.toml", toml_contents)
111
+
112
+ # Continue with adding other files
113
+ for root, _, files in os.walk(app, topdown=True):
114
+ files = [
115
+ f
116
+ for f in files
117
+ if not ignore_spec.match_file(Path(root) / f)
118
+ and f != fab_filename
119
+ and Path(f).suffix in allowed_extensions
120
+ and f != "pyproject.toml" # Exclude the original pyproject.toml
121
+ ]
122
+
123
+ for file in files:
124
+ file_path = Path(root) / file
125
+ archive_path = file_path.relative_to(app)
126
+ fab_file.write(file_path, archive_path)
127
+
128
+ # Calculate file info
129
+ sha256_hash = get_sha256_hash(file_path)
130
+ file_size_bits = os.path.getsize(file_path) * 8 # size in bits
131
+ list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
132
+
133
+ # Add CONTENT and CONTENT.jwt to the zip file
134
+ fab_file.writestr(".info/CONTENT", list_file_content)
135
+
136
+ typer.secho(
137
+ f"🎊 Successfully built {fab_filename}", fg=typer.colors.GREEN, bold=True
138
+ )
139
+
140
+ return fab_filename
141
+
142
+
143
+ def _load_gitignore(app: Path) -> pathspec.PathSpec:
144
+ """Load and parse .gitignore file, returning a pathspec."""
145
+ gitignore_path = app / ".gitignore"
146
+ patterns = ["__pycache__/"] # Default pattern
147
+ if gitignore_path.exists():
148
+ with open(gitignore_path, encoding="UTF-8") as file:
149
+ patterns.extend(file.readlines())
150
+ return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
@@ -0,0 +1,219 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Utility to validate the `pyproject.toml` file."""
16
+
17
+ import zipfile
18
+ from io import BytesIO
19
+ from pathlib import Path
20
+ from typing import IO, Any, Dict, List, Optional, Tuple, Union, get_args
21
+
22
+ import tomli
23
+
24
+ from flwr.common import object_ref
25
+ from flwr.common.typing import UserConfigValue
26
+
27
+
28
+ def get_fab_config(fab_file: Union[Path, bytes]) -> Dict[str, Any]:
29
+ """Extract the config from a FAB file or path.
30
+
31
+ Parameters
32
+ ----------
33
+ fab_file : Union[Path, bytes]
34
+ The Flower App Bundle file to validate and extract the metadata from.
35
+ It can either be a path to the file or the file itself as bytes.
36
+
37
+ Returns
38
+ -------
39
+ Dict[str, Any]
40
+ The `config` of the given Flower App Bundle.
41
+ """
42
+ fab_file_archive: Union[Path, IO[bytes]]
43
+ if isinstance(fab_file, bytes):
44
+ fab_file_archive = BytesIO(fab_file)
45
+ elif isinstance(fab_file, Path):
46
+ fab_file_archive = fab_file
47
+ else:
48
+ raise ValueError("fab_file must be either a Path or bytes")
49
+
50
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
51
+ with zipf.open("pyproject.toml") as file:
52
+ toml_content = file.read().decode("utf-8")
53
+
54
+ conf = load_from_string(toml_content)
55
+ if conf is None:
56
+ raise ValueError("Invalid TOML content in pyproject.toml")
57
+
58
+ is_valid, errors, _ = validate(conf, check_module=False)
59
+ if not is_valid:
60
+ raise ValueError(errors)
61
+
62
+ return conf
63
+
64
+
65
+ def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]:
66
+ """Extract the fab_id and the fab_version from a FAB file or path.
67
+
68
+ Parameters
69
+ ----------
70
+ fab_file : Union[Path, bytes]
71
+ The Flower App Bundle file to validate and extract the metadata from.
72
+ It can either be a path to the file or the file itself as bytes.
73
+
74
+ Returns
75
+ -------
76
+ Tuple[str, str]
77
+ The `fab_version` and `fab_id` of the given Flower App Bundle.
78
+ """
79
+ conf = get_fab_config(fab_file)
80
+
81
+ return (
82
+ conf["project"]["version"],
83
+ f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}",
84
+ )
85
+
86
+
87
+ def load_and_validate(
88
+ path: Optional[Path] = None,
89
+ check_module: bool = True,
90
+ ) -> Tuple[Optional[Dict[str, Any]], List[str], List[str]]:
91
+ """Load and validate pyproject.toml as dict.
92
+
93
+ Returns
94
+ -------
95
+ Tuple[Optional[config], List[str], List[str]]
96
+ A tuple with the optional config in case it exists and is valid
97
+ and associated errors and warnings.
98
+ """
99
+ if path is None:
100
+ path = Path.cwd() / "pyproject.toml"
101
+
102
+ config = load(path)
103
+
104
+ if config is None:
105
+ errors = [
106
+ "Project configuration could not be loaded. "
107
+ "`pyproject.toml` does not exist."
108
+ ]
109
+ return (None, errors, [])
110
+
111
+ is_valid, errors, warnings = validate(config, check_module, path.parent)
112
+
113
+ if not is_valid:
114
+ return (None, errors, warnings)
115
+
116
+ return (config, errors, warnings)
117
+
118
+
119
+ def load(toml_path: Path) -> Optional[Dict[str, Any]]:
120
+ """Load pyproject.toml and return as dict."""
121
+ if not toml_path.is_file():
122
+ return None
123
+
124
+ with toml_path.open(encoding="utf-8") as toml_file:
125
+ return load_from_string(toml_file.read())
126
+
127
+
128
+ def _validate_run_config(config_dict: Dict[str, Any], errors: List[str]) -> None:
129
+ for key, value in config_dict.items():
130
+ if isinstance(value, dict):
131
+ _validate_run_config(config_dict[key], errors)
132
+ elif not isinstance(value, get_args(UserConfigValue)):
133
+ raise ValueError(
134
+ f"The value for key {key} needs to be of type `int`, `float`, "
135
+ "`bool, `str`, or a `dict` of those.",
136
+ )
137
+
138
+
139
+ # pylint: disable=too-many-branches
140
+ def validate_fields(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]:
141
+ """Validate pyproject.toml fields."""
142
+ errors = []
143
+ warnings = []
144
+
145
+ if "project" not in config:
146
+ errors.append("Missing [project] section")
147
+ else:
148
+ if "name" not in config["project"]:
149
+ errors.append('Property "name" missing in [project]')
150
+ if "version" not in config["project"]:
151
+ errors.append('Property "version" missing in [project]')
152
+ if "description" not in config["project"]:
153
+ warnings.append('Recommended property "description" missing in [project]')
154
+ if "license" not in config["project"]:
155
+ warnings.append('Recommended property "license" missing in [project]')
156
+ if "authors" not in config["project"]:
157
+ warnings.append('Recommended property "authors" missing in [project]')
158
+
159
+ if (
160
+ "tool" not in config
161
+ or "flwr" not in config["tool"]
162
+ or "app" not in config["tool"]["flwr"]
163
+ ):
164
+ errors.append("Missing [tool.flwr.app] section")
165
+ else:
166
+ if "publisher" not in config["tool"]["flwr"]["app"]:
167
+ errors.append('Property "publisher" missing in [tool.flwr.app]')
168
+ if "config" in config["tool"]["flwr"]["app"]:
169
+ _validate_run_config(config["tool"]["flwr"]["app"]["config"], errors)
170
+ if "components" not in config["tool"]["flwr"]["app"]:
171
+ errors.append("Missing [tool.flwr.app.components] section")
172
+ else:
173
+ if "serverapp" not in config["tool"]["flwr"]["app"]["components"]:
174
+ errors.append(
175
+ 'Property "serverapp" missing in [tool.flwr.app.components]'
176
+ )
177
+ if "clientapp" not in config["tool"]["flwr"]["app"]["components"]:
178
+ errors.append(
179
+ 'Property "clientapp" missing in [tool.flwr.app.components]'
180
+ )
181
+
182
+ return len(errors) == 0, errors, warnings
183
+
184
+
185
+ def validate(
186
+ config: Dict[str, Any],
187
+ check_module: bool = True,
188
+ project_dir: Optional[Union[str, Path]] = None,
189
+ ) -> Tuple[bool, List[str], List[str]]:
190
+ """Validate pyproject.toml."""
191
+ is_valid, errors, warnings = validate_fields(config)
192
+
193
+ if not is_valid:
194
+ return False, errors, warnings
195
+
196
+ # Validate serverapp
197
+ serverapp_ref = config["tool"]["flwr"]["app"]["components"]["serverapp"]
198
+ is_valid, reason = object_ref.validate(serverapp_ref, check_module, project_dir)
199
+
200
+ if not is_valid and isinstance(reason, str):
201
+ return False, [reason], []
202
+
203
+ # Validate clientapp
204
+ clientapp_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"]
205
+ is_valid, reason = object_ref.validate(clientapp_ref, check_module, project_dir)
206
+
207
+ if not is_valid and isinstance(reason, str):
208
+ return False, [reason], []
209
+
210
+ return True, [], []
211
+
212
+
213
+ def load_from_string(toml_content: str) -> Optional[Dict[str, Any]]:
214
+ """Load TOML content from a string and return as dict."""
215
+ try:
216
+ data = tomli.loads(toml_content)
217
+ return data
218
+ except tomli.TOMLDecodeError:
219
+ return None
flwr/cli/example.py CHANGED
@@ -39,7 +39,9 @@ def example() -> None:
39
39
  with urllib.request.urlopen(examples_directory_url) as res:
40
40
  data = json.load(res)
41
41
  example_names = [
42
- item["path"] for item in data["tree"] if item["path"] not in [".gitignore"]
42
+ item["path"]
43
+ for item in data["tree"]
44
+ if item["path"] not in [".gitignore", "doc"]
43
45
  ]
44
46
 
45
47
  example_name = prompt_options(
flwr/cli/install.py ADDED
@@ -0,0 +1,227 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface `install` command."""
16
+
17
+
18
+ import shutil
19
+ import subprocess
20
+ import tempfile
21
+ import zipfile
22
+ from io import BytesIO
23
+ from pathlib import Path
24
+ from typing import IO, Optional, Union
25
+
26
+ import typer
27
+ from typing_extensions import Annotated
28
+
29
+ from flwr.common.config import get_flwr_dir
30
+
31
+ from .config_utils import load_and_validate
32
+ from .utils import get_sha256_hash
33
+
34
+
35
+ def install(
36
+ source: Annotated[
37
+ Optional[Path],
38
+ typer.Argument(metavar="source", help="The source FAB file to install."),
39
+ ] = None,
40
+ flwr_dir: Annotated[
41
+ Optional[Path],
42
+ typer.Option(help="The desired install path."),
43
+ ] = None,
44
+ ) -> None:
45
+ """Install a Flower App Bundle.
46
+
47
+ It can be ran with a single FAB file argument:
48
+
49
+ ``flwr install ./target_project.fab``
50
+
51
+ The target install directory can be specified with ``--flwr-dir``:
52
+
53
+ ``flwr install ./target_project.fab --flwr-dir ./docs/flwr``
54
+
55
+ This will install ``target_project`` to ``./docs/flwr/``. By default,
56
+ ``flwr-dir`` is equal to:
57
+
58
+ - ``$FLWR_HOME/`` if ``$FLWR_HOME`` is defined
59
+ - ``$XDG_DATA_HOME/.flwr/`` if ``$XDG_DATA_HOME`` is defined
60
+ - ``$HOME/.flwr/`` in all other cases
61
+ """
62
+ if source is None:
63
+ source = Path(typer.prompt("Enter the source FAB file"))
64
+
65
+ source = source.resolve()
66
+ if not source.exists() or not source.is_file():
67
+ typer.secho(
68
+ f"❌ The source {source} does not exist or is not a file.",
69
+ fg=typer.colors.RED,
70
+ bold=True,
71
+ )
72
+ raise typer.Exit(code=1)
73
+
74
+ if source.suffix != ".fab":
75
+ typer.secho(
76
+ f"❌ The source {source} is not a `.fab` file.",
77
+ fg=typer.colors.RED,
78
+ bold=True,
79
+ )
80
+ raise typer.Exit(code=1)
81
+
82
+ install_from_fab(source, flwr_dir)
83
+
84
+
85
+ def install_from_fab(
86
+ fab_file: Union[Path, bytes],
87
+ flwr_dir: Optional[Path],
88
+ skip_prompt: bool = False,
89
+ ) -> Path:
90
+ """Install from a FAB file after extracting and validating."""
91
+ fab_file_archive: Union[Path, IO[bytes]]
92
+ fab_name: Optional[str]
93
+ if isinstance(fab_file, bytes):
94
+ fab_file_archive = BytesIO(fab_file)
95
+ fab_name = None
96
+ elif isinstance(fab_file, Path):
97
+ fab_file_archive = fab_file
98
+ fab_name = fab_file.stem
99
+ else:
100
+ raise ValueError("fab_file must be either a Path or bytes")
101
+
102
+ with tempfile.TemporaryDirectory() as tmpdir:
103
+ with zipfile.ZipFile(fab_file_archive, "r") as zipf:
104
+ zipf.extractall(tmpdir)
105
+ tmpdir_path = Path(tmpdir)
106
+ info_dir = tmpdir_path / ".info"
107
+ if not info_dir.exists():
108
+ typer.secho(
109
+ "❌ FAB file has incorrect format.",
110
+ fg=typer.colors.RED,
111
+ bold=True,
112
+ )
113
+ raise typer.Exit(code=1)
114
+
115
+ content_file = info_dir / "CONTENT"
116
+
117
+ if not content_file.exists() or not _verify_hashes(
118
+ content_file.read_text(), tmpdir_path
119
+ ):
120
+ typer.secho(
121
+ "❌ File hashes couldn't be verified.",
122
+ fg=typer.colors.RED,
123
+ bold=True,
124
+ )
125
+ raise typer.Exit(code=1)
126
+
127
+ shutil.rmtree(info_dir)
128
+
129
+ installed_path = validate_and_install(
130
+ tmpdir_path, fab_name, flwr_dir, skip_prompt
131
+ )
132
+
133
+ return installed_path
134
+
135
+
136
+ def validate_and_install(
137
+ project_dir: Path,
138
+ fab_name: Optional[str],
139
+ flwr_dir: Optional[Path],
140
+ skip_prompt: bool = False,
141
+ ) -> Path:
142
+ """Validate TOML files and install the project to the desired directory."""
143
+ config, _, _ = load_and_validate(project_dir / "pyproject.toml", check_module=False)
144
+
145
+ if config is None:
146
+ typer.secho(
147
+ "❌ Invalid config inside FAB file.",
148
+ fg=typer.colors.RED,
149
+ bold=True,
150
+ )
151
+ raise typer.Exit(code=1)
152
+
153
+ publisher = config["tool"]["flwr"]["app"]["publisher"]
154
+ project_name = config["project"]["name"]
155
+ version = config["project"]["version"]
156
+
157
+ if (
158
+ fab_name
159
+ and fab_name != f"{publisher}.{project_name}.{version.replace('.', '-')}"
160
+ ):
161
+ typer.secho(
162
+ "❌ FAB file has incorrect name. The file name must follow the format "
163
+ "`<publisher>.<project_name>.<version>.fab`.",
164
+ fg=typer.colors.RED,
165
+ bold=True,
166
+ )
167
+ raise typer.Exit(code=1)
168
+
169
+ install_dir: Path = (
170
+ (get_flwr_dir() if not flwr_dir else flwr_dir)
171
+ / "apps"
172
+ / publisher
173
+ / project_name
174
+ / version
175
+ )
176
+ if install_dir.exists() and not skip_prompt:
177
+ if not typer.confirm(
178
+ typer.style(
179
+ f"\n💬 {project_name} version {version} is already installed, "
180
+ "do you want to reinstall it?",
181
+ fg=typer.colors.MAGENTA,
182
+ bold=True,
183
+ )
184
+ ):
185
+ return install_dir
186
+
187
+ install_dir.mkdir(parents=True, exist_ok=True)
188
+
189
+ # Move contents from source directory
190
+ for item in project_dir.iterdir():
191
+ if item.is_dir():
192
+ shutil.copytree(item, install_dir / item.name, dirs_exist_ok=True)
193
+ else:
194
+ shutil.copy2(item, install_dir / item.name)
195
+
196
+ try:
197
+ subprocess.run(
198
+ ["pip", "install", "-e", install_dir, "--no-deps"],
199
+ capture_output=True,
200
+ text=True,
201
+ check=True,
202
+ )
203
+ except subprocess.CalledProcessError as e:
204
+ typer.secho(
205
+ f"❌ Failed to `pip install` package(s) from {install_dir}:\n{e.stderr}",
206
+ fg=typer.colors.RED,
207
+ bold=True,
208
+ )
209
+ raise typer.Exit(code=1) from e
210
+
211
+ typer.secho(
212
+ f"🎊 Successfully installed {project_name} to {install_dir}.",
213
+ fg=typer.colors.GREEN,
214
+ bold=True,
215
+ )
216
+
217
+ return install_dir
218
+
219
+
220
+ def _verify_hashes(list_content: str, tmpdir: Path) -> bool:
221
+ """Verify file hashes based on the LIST content."""
222
+ for line in list_content.strip().split("\n"):
223
+ rel_path, hash_expected, _ = line.split(",")
224
+ file_path = tmpdir / rel_path
225
+ if not file_path.exists() or get_sha256_hash(file_path) != hash_expected:
226
+ return False
227
+ return True