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.
- flwr/cli/app.py +7 -0
- flwr/cli/build.py +150 -0
- flwr/cli/config_utils.py +219 -0
- flwr/cli/example.py +3 -1
- flwr/cli/install.py +227 -0
- flwr/cli/new/new.py +179 -48
- flwr/cli/new/templates/app/.gitignore.tpl +160 -0
- flwr/cli/new/templates/app/README.flowertune.md.tpl +56 -0
- flwr/cli/new/templates/app/README.md.tpl +1 -5
- flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +65 -0
- flwr/cli/new/templates/app/code/client.jax.py.tpl +56 -0
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +93 -0
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +3 -2
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +23 -11
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +97 -0
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +60 -1
- flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
- flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +89 -0
- flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +126 -0
- flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl +34 -0
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +57 -0
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +59 -0
- flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl +48 -0
- flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl +11 -0
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -0
- flwr/cli/new/templates/app/code/server.jax.py.tpl +20 -0
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +20 -0
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +17 -9
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +24 -0
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +99 -0
- flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +28 -23
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +39 -0
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +38 -0
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +34 -0
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +33 -0
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
- flwr/cli/run/run.py +168 -17
- flwr/cli/utils.py +75 -4
- flwr/client/__init__.py +6 -1
- flwr/client/app.py +239 -248
- flwr/client/client_app.py +70 -9
- flwr/client/dpfedavg_numpy_client.py +1 -1
- flwr/client/grpc_adapter_client/__init__.py +15 -0
- flwr/client/grpc_adapter_client/connection.py +97 -0
- flwr/client/grpc_client/connection.py +18 -5
- flwr/client/grpc_rere_client/__init__.py +1 -1
- flwr/client/grpc_rere_client/client_interceptor.py +158 -0
- flwr/client/grpc_rere_client/connection.py +127 -33
- flwr/client/grpc_rere_client/grpc_adapter.py +140 -0
- flwr/client/heartbeat.py +74 -0
- flwr/client/message_handler/__init__.py +1 -1
- flwr/client/message_handler/message_handler.py +7 -7
- flwr/client/mod/__init__.py +5 -5
- flwr/client/mod/centraldp_mods.py +4 -2
- flwr/client/mod/comms_mods.py +4 -4
- flwr/client/mod/localdp_mod.py +9 -4
- flwr/client/mod/secure_aggregation/__init__.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/mod/utils.py +1 -1
- flwr/client/node_state.py +60 -10
- flwr/client/node_state_tests.py +4 -3
- flwr/client/rest_client/__init__.py +1 -1
- flwr/client/rest_client/connection.py +177 -157
- flwr/client/supernode/__init__.py +26 -0
- flwr/client/supernode/app.py +464 -0
- flwr/client/typing.py +1 -0
- flwr/common/__init__.py +13 -11
- flwr/common/address.py +1 -1
- flwr/common/config.py +193 -0
- flwr/common/constant.py +42 -1
- flwr/common/context.py +26 -1
- flwr/common/date.py +1 -1
- flwr/common/dp.py +1 -1
- flwr/common/grpc.py +6 -2
- flwr/common/logger.py +79 -8
- flwr/common/message.py +167 -105
- flwr/common/object_ref.py +126 -25
- flwr/common/record/__init__.py +1 -1
- flwr/common/record/parametersrecord.py +0 -1
- flwr/common/record/recordset.py +78 -27
- flwr/common/recordset_compat.py +8 -1
- flwr/common/retry_invoker.py +25 -13
- flwr/common/secure_aggregation/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/shamir.py +1 -1
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +21 -2
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
- flwr/common/secure_aggregation/quantization.py +1 -1
- flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
- flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
- flwr/common/serde.py +209 -3
- flwr/common/telemetry.py +25 -0
- flwr/common/typing.py +38 -0
- flwr/common/version.py +14 -0
- flwr/proto/clientappio_pb2.py +41 -0
- flwr/proto/clientappio_pb2.pyi +110 -0
- flwr/proto/clientappio_pb2_grpc.py +101 -0
- flwr/proto/clientappio_pb2_grpc.pyi +40 -0
- flwr/proto/common_pb2.py +36 -0
- flwr/proto/common_pb2.pyi +121 -0
- flwr/proto/common_pb2_grpc.py +4 -0
- flwr/proto/common_pb2_grpc.pyi +4 -0
- flwr/proto/driver_pb2.py +26 -19
- flwr/proto/driver_pb2.pyi +34 -0
- flwr/proto/driver_pb2_grpc.py +70 -0
- flwr/proto/driver_pb2_grpc.pyi +28 -0
- flwr/proto/exec_pb2.py +43 -0
- flwr/proto/exec_pb2.pyi +95 -0
- flwr/proto/exec_pb2_grpc.py +101 -0
- flwr/proto/exec_pb2_grpc.pyi +41 -0
- flwr/proto/fab_pb2.py +30 -0
- flwr/proto/fab_pb2.pyi +56 -0
- flwr/proto/fab_pb2_grpc.py +4 -0
- flwr/proto/fab_pb2_grpc.pyi +4 -0
- flwr/proto/fleet_pb2.py +29 -23
- flwr/proto/fleet_pb2.pyi +33 -0
- flwr/proto/fleet_pb2_grpc.py +102 -0
- flwr/proto/fleet_pb2_grpc.pyi +35 -0
- flwr/proto/grpcadapter_pb2.py +32 -0
- flwr/proto/grpcadapter_pb2.pyi +43 -0
- flwr/proto/grpcadapter_pb2_grpc.py +66 -0
- flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
- flwr/proto/message_pb2.py +41 -0
- flwr/proto/message_pb2.pyi +122 -0
- flwr/proto/message_pb2_grpc.py +4 -0
- flwr/proto/message_pb2_grpc.pyi +4 -0
- flwr/proto/run_pb2.py +35 -0
- flwr/proto/run_pb2.pyi +76 -0
- flwr/proto/run_pb2_grpc.py +4 -0
- flwr/proto/run_pb2_grpc.pyi +4 -0
- flwr/proto/task_pb2.py +7 -8
- flwr/proto/task_pb2.pyi +8 -5
- flwr/server/__init__.py +4 -8
- flwr/server/app.py +298 -350
- flwr/server/compat/app.py +6 -57
- flwr/server/compat/app_utils.py +5 -4
- flwr/server/compat/driver_client_proxy.py +29 -48
- flwr/server/compat/legacy_context.py +5 -4
- flwr/server/driver/__init__.py +2 -0
- flwr/server/driver/driver.py +22 -132
- flwr/server/driver/grpc_driver.py +224 -74
- flwr/server/driver/inmemory_driver.py +183 -0
- flwr/server/history.py +20 -20
- flwr/server/run_serverapp.py +121 -34
- flwr/server/server.py +11 -7
- flwr/server/server_app.py +59 -10
- flwr/server/serverapp_components.py +52 -0
- flwr/server/strategy/__init__.py +2 -2
- flwr/server/strategy/bulyan.py +1 -1
- flwr/server/strategy/dp_adaptive_clipping.py +3 -3
- flwr/server/strategy/dp_fixed_clipping.py +4 -3
- flwr/server/strategy/dpfedavg_adaptive.py +1 -1
- flwr/server/strategy/dpfedavg_fixed.py +1 -1
- flwr/server/strategy/fedadagrad.py +1 -1
- flwr/server/strategy/fedadam.py +1 -1
- flwr/server/strategy/fedavg_android.py +1 -1
- flwr/server/strategy/fedavgm.py +1 -1
- flwr/server/strategy/fedmedian.py +1 -1
- flwr/server/strategy/fedopt.py +1 -1
- flwr/server/strategy/fedprox.py +1 -1
- flwr/server/strategy/fedxgb_bagging.py +1 -1
- flwr/server/strategy/fedxgb_cyclic.py +1 -1
- flwr/server/strategy/fedxgb_nn_avg.py +1 -1
- flwr/server/strategy/fedyogi.py +1 -1
- flwr/server/strategy/krum.py +1 -1
- flwr/server/strategy/qfedavg.py +1 -1
- flwr/server/superlink/driver/__init__.py +1 -1
- flwr/server/superlink/driver/driver_grpc.py +1 -1
- flwr/server/superlink/driver/driver_servicer.py +51 -4
- flwr/server/superlink/ffs/__init__.py +24 -0
- flwr/server/superlink/ffs/disk_ffs.py +104 -0
- flwr/server/superlink/ffs/ffs.py +79 -0
- flwr/server/superlink/fleet/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
- flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +8 -2
- flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +30 -2
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +214 -0
- flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
- flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
- flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/rest_rere/rest_api.py +59 -1
- flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
- flwr/server/superlink/fleet/vce/backend/backend.py +5 -5
- flwr/server/superlink/fleet/vce/backend/raybackend.py +53 -56
- flwr/server/superlink/fleet/vce/vce_api.py +190 -127
- flwr/server/superlink/state/__init__.py +1 -1
- flwr/server/superlink/state/in_memory_state.py +159 -42
- flwr/server/superlink/state/sqlite_state.py +243 -39
- flwr/server/superlink/state/state.py +81 -6
- flwr/server/superlink/state/state_factory.py +11 -2
- flwr/server/superlink/state/utils.py +62 -0
- flwr/server/typing.py +2 -0
- flwr/server/utils/__init__.py +1 -1
- flwr/server/utils/tensorboard.py +1 -1
- flwr/server/utils/validator.py +23 -9
- flwr/server/workflow/default_workflows.py +67 -25
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +18 -6
- flwr/simulation/__init__.py +7 -4
- flwr/simulation/app.py +67 -36
- flwr/simulation/ray_transport/__init__.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +20 -46
- flwr/simulation/ray_transport/ray_client_proxy.py +36 -16
- flwr/simulation/run_simulation.py +308 -92
- flwr/superexec/__init__.py +21 -0
- flwr/superexec/app.py +184 -0
- flwr/superexec/deployment.py +185 -0
- flwr/superexec/exec_grpc.py +55 -0
- flwr/superexec/exec_servicer.py +70 -0
- flwr/superexec/executor.py +75 -0
- flwr/superexec/simulation.py +193 -0
- {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/METADATA +10 -6
- flwr_nightly-1.11.0.dev20240813.dist-info/RECORD +288 -0
- flwr_nightly-1.11.0.dev20240813.dist-info/entry_points.txt +10 -0
- flwr/cli/flower_toml.py +0 -140
- flwr/cli/new/templates/app/flower.toml.tpl +0 -13
- flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
- flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
- flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
- flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
- flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
- {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/LICENSE +0 -0
- {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)
|
flwr/cli/config_utils.py
ADDED
|
@@ -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"]
|
|
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
|