flwr 1.19.0__py3-none-any.whl → 1.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/cli/build.py +15 -5
- flwr/cli/new/new.py +12 -4
- flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
- flwr/cli/new/templates/app/README.md.tpl +5 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -3
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
- flwr/cli/run/run.py +45 -38
- flwr/cli/utils.py +12 -5
- flwr/client/grpc_adapter_client/connection.py +11 -4
- flwr/client/grpc_rere_client/connection.py +92 -117
- flwr/client/rest_client/connection.py +131 -164
- flwr/common/constant.py +3 -1
- flwr/common/exit/exit_code.py +16 -1
- flwr/common/grpc.py +12 -1
- flwr/common/{inflatable_grpc_utils.py → inflatable_protobuf_utils.py} +52 -10
- flwr/common/inflatable_utils.py +191 -24
- flwr/common/record/array.py +101 -22
- flwr/common/record/arraychunk.py +59 -0
- flwr/common/serde.py +0 -28
- flwr/compat/client/app.py +14 -31
- flwr/proto/appio_pb2.py +43 -0
- flwr/proto/appio_pb2.pyi +151 -0
- flwr/proto/appio_pb2_grpc.py +4 -0
- flwr/proto/appio_pb2_grpc.pyi +4 -0
- flwr/proto/clientappio_pb2.py +12 -19
- flwr/proto/clientappio_pb2.pyi +23 -101
- flwr/proto/clientappio_pb2_grpc.py +269 -28
- flwr/proto/clientappio_pb2_grpc.pyi +114 -20
- flwr/proto/fleet_pb2.py +12 -20
- flwr/proto/fleet_pb2.pyi +6 -36
- flwr/proto/serverappio_pb2.py +8 -31
- flwr/proto/serverappio_pb2.pyi +0 -152
- flwr/proto/serverappio_pb2_grpc.py +39 -38
- flwr/proto/serverappio_pb2_grpc.pyi +21 -20
- flwr/server/app.py +1 -1
- flwr/server/fleet_event_log_interceptor.py +4 -0
- flwr/server/grid/grpc_grid.py +91 -54
- flwr/server/serverapp/app.py +27 -17
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +8 -0
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
- flwr/server/superlink/fleet/message_handler/message_handler.py +10 -16
- flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -2
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
- flwr/server/superlink/serverappio/serverappio_servicer.py +35 -43
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/superlink/utils.py +0 -35
- flwr/simulation/app.py +8 -0
- flwr/simulation/run_simulation.py +17 -0
- flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
- flwr/supercore/grpc_health/__init__.py +22 -0
- flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
- flwr/supercore/license_plugin/__init__.py +22 -0
- flwr/supercore/license_plugin/license_plugin.py +26 -0
- flwr/supercore/object_store/in_memory_object_store.py +31 -31
- flwr/supercore/object_store/object_store.py +20 -42
- flwr/supercore/object_store/utils.py +43 -0
- flwr/supercore/scheduler/__init__.py +22 -0
- flwr/supercore/scheduler/plugin.py +71 -0
- flwr/supercore/utils.py +32 -0
- flwr/superexec/deployment.py +1 -2
- flwr/superexec/exec_event_log_interceptor.py +4 -0
- flwr/superexec/exec_grpc.py +18 -2
- flwr/superexec/exec_license_interceptor.py +82 -0
- flwr/superexec/exec_servicer.py +10 -1
- flwr/superexec/exec_user_auth_interceptor.py +10 -2
- flwr/superexec/executor.py +1 -1
- flwr/superexec/simulation.py +1 -2
- flwr/supernode/cli/flower_supernode.py +0 -7
- flwr/supernode/cli/flwr_clientapp.py +10 -3
- flwr/supernode/nodestate/in_memory_nodestate.py +11 -2
- flwr/supernode/nodestate/nodestate.py +15 -0
- flwr/supernode/runtime/run_clientapp.py +110 -33
- flwr/supernode/scheduler/__init__.py +22 -0
- flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
- flwr/supernode/servicer/clientappio/__init__.py +1 -3
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +223 -164
- flwr/supernode/start_client_internal.py +202 -104
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/METADATA +2 -1
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/RECORD +93 -78
- flwr/common/inflatable_rest_utils.py +0 -99
- /flwr/{server/superlink → supercore}/ffs/__init__.py +0 -0
- /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
- /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/WHEEL +0 -0
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright 2025 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 Exec API license interceptor."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
from typing import Any, Callable, Union
|
|
20
|
+
|
|
21
|
+
import grpc
|
|
22
|
+
from google.protobuf.message import Message as GrpcMessage
|
|
23
|
+
|
|
24
|
+
from flwr.supercore.license_plugin import LicensePlugin
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ExecLicenseInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
28
|
+
"""Exec API interceptor for license checking."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, license_plugin: LicensePlugin) -> None:
|
|
31
|
+
"""Initialize the interceptor with a license plugin."""
|
|
32
|
+
self.license_plugin = license_plugin
|
|
33
|
+
|
|
34
|
+
def intercept_service(
|
|
35
|
+
self,
|
|
36
|
+
continuation: Callable[[Any], Any],
|
|
37
|
+
handler_call_details: grpc.HandlerCallDetails,
|
|
38
|
+
) -> grpc.RpcMethodHandler:
|
|
39
|
+
"""Flower server interceptor license logic.
|
|
40
|
+
|
|
41
|
+
Intercept all unary-unary/unary-stream calls from users and check the license.
|
|
42
|
+
Continue RPC call if license check is enabled and passes, else, terminate RPC
|
|
43
|
+
call by setting context to abort.
|
|
44
|
+
"""
|
|
45
|
+
# Only apply to Exec service
|
|
46
|
+
if not handler_call_details.method.startswith("/flwr.proto.Exec/"):
|
|
47
|
+
return continuation(handler_call_details)
|
|
48
|
+
|
|
49
|
+
# One of the method handlers in
|
|
50
|
+
# `flwr.superexec.exec_servicer.ExecServicer`
|
|
51
|
+
method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
|
|
52
|
+
return self._generic_license_unary_method_handler(method_handler)
|
|
53
|
+
|
|
54
|
+
def _generic_license_unary_method_handler(
|
|
55
|
+
self, method_handler: grpc.RpcMethodHandler
|
|
56
|
+
) -> grpc.RpcMethodHandler:
|
|
57
|
+
def _generic_method_handler(
|
|
58
|
+
request: GrpcMessage,
|
|
59
|
+
context: grpc.ServicerContext,
|
|
60
|
+
) -> Union[GrpcMessage, Iterator[GrpcMessage]]:
|
|
61
|
+
"""Handle the method call with license checking."""
|
|
62
|
+
call = method_handler.unary_unary or method_handler.unary_stream
|
|
63
|
+
|
|
64
|
+
if not self.license_plugin.check_license():
|
|
65
|
+
context.abort(
|
|
66
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
67
|
+
"❗️ License check failed. Please contact the SuperLink "
|
|
68
|
+
"administrator.",
|
|
69
|
+
)
|
|
70
|
+
raise grpc.RpcError()
|
|
71
|
+
|
|
72
|
+
return call(request, context) # type: ignore
|
|
73
|
+
|
|
74
|
+
if method_handler.unary_unary:
|
|
75
|
+
message_handler = grpc.unary_unary_rpc_method_handler
|
|
76
|
+
else:
|
|
77
|
+
message_handler = grpc.unary_stream_rpc_method_handler
|
|
78
|
+
return message_handler(
|
|
79
|
+
_generic_method_handler,
|
|
80
|
+
request_deserializer=method_handler.request_deserializer,
|
|
81
|
+
response_serializer=method_handler.response_serializer,
|
|
82
|
+
)
|
flwr/superexec/exec_servicer.py
CHANGED
|
@@ -25,6 +25,7 @@ import grpc
|
|
|
25
25
|
from flwr.common import now
|
|
26
26
|
from flwr.common.auth_plugin import ExecAuthPlugin
|
|
27
27
|
from flwr.common.constant import (
|
|
28
|
+
FAB_MAX_SIZE,
|
|
28
29
|
LOG_STREAM_INTERVAL,
|
|
29
30
|
RUN_ID_NOT_FOUND_MESSAGE,
|
|
30
31
|
Status,
|
|
@@ -52,8 +53,8 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
|
52
53
|
StreamLogsRequest,
|
|
53
54
|
StreamLogsResponse,
|
|
54
55
|
)
|
|
55
|
-
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
56
56
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
57
|
+
from flwr.supercore.ffs import FfsFactory
|
|
57
58
|
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
|
|
58
59
|
|
|
59
60
|
from .exec_user_auth_interceptor import shared_account_info
|
|
@@ -84,6 +85,14 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
|
|
|
84
85
|
"""Create run ID."""
|
|
85
86
|
log(INFO, "ExecServicer.StartRun")
|
|
86
87
|
|
|
88
|
+
if len(request.fab.content) > FAB_MAX_SIZE:
|
|
89
|
+
log(
|
|
90
|
+
ERROR,
|
|
91
|
+
"FAB size exceeds maximum allowed size of %d bytes.",
|
|
92
|
+
FAB_MAX_SIZE,
|
|
93
|
+
)
|
|
94
|
+
return StartRunResponse()
|
|
95
|
+
|
|
87
96
|
flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
|
|
88
97
|
run_id = self.executor.start_run(
|
|
89
98
|
request.fab.content,
|
|
@@ -72,6 +72,10 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
72
72
|
by validating auth metadata sent by the user. Continue RPC call if user is
|
|
73
73
|
authenticated, else, terminate RPC call by setting context to abort.
|
|
74
74
|
"""
|
|
75
|
+
# Only apply to Exec service
|
|
76
|
+
if not handler_call_details.method.startswith("/flwr.proto.Exec/"):
|
|
77
|
+
return continuation(handler_call_details)
|
|
78
|
+
|
|
75
79
|
# One of the method handlers in
|
|
76
80
|
# `flwr.superexec.exec_servicer.ExecServicer`
|
|
77
81
|
method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
|
|
@@ -108,7 +112,9 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
108
112
|
# Check if the user is authorized
|
|
109
113
|
if not self.authz_plugin.verify_user_authorization(account_info):
|
|
110
114
|
context.abort(
|
|
111
|
-
grpc.StatusCode.PERMISSION_DENIED,
|
|
115
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
116
|
+
"❗️ User not authorized. "
|
|
117
|
+
"Please contact the SuperLink administrator.",
|
|
112
118
|
)
|
|
113
119
|
raise grpc.RpcError()
|
|
114
120
|
return call(request, context) # type: ignore
|
|
@@ -127,7 +133,9 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
127
133
|
# Check if the user is authorized
|
|
128
134
|
if not self.authz_plugin.verify_user_authorization(account_info):
|
|
129
135
|
context.abort(
|
|
130
|
-
grpc.StatusCode.PERMISSION_DENIED,
|
|
136
|
+
grpc.StatusCode.PERMISSION_DENIED,
|
|
137
|
+
"❗️ User not authorized. "
|
|
138
|
+
"Please contact the SuperLink administrator.",
|
|
131
139
|
)
|
|
132
140
|
raise grpc.RpcError()
|
|
133
141
|
|
flwr/superexec/executor.py
CHANGED
|
@@ -22,8 +22,8 @@ from typing import Optional
|
|
|
22
22
|
|
|
23
23
|
from flwr.common import ConfigRecord
|
|
24
24
|
from flwr.common.typing import UserConfig
|
|
25
|
-
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
26
25
|
from flwr.server.superlink.linkstate import LinkStateFactory
|
|
26
|
+
from flwr.supercore.ffs import FfsFactory
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
flwr/superexec/simulation.py
CHANGED
|
@@ -25,9 +25,8 @@ from flwr.cli.config_utils import get_fab_metadata
|
|
|
25
25
|
from flwr.common import ConfigRecord, Context, RecordDict
|
|
26
26
|
from flwr.common.logger import log
|
|
27
27
|
from flwr.common.typing import Fab, UserConfig
|
|
28
|
-
from flwr.server.superlink.ffs import Ffs
|
|
29
|
-
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
30
28
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
29
|
+
from flwr.supercore.ffs import Ffs, FfsFactory
|
|
31
30
|
|
|
32
31
|
from .executor import Executor
|
|
33
32
|
|
|
@@ -40,7 +40,6 @@ from flwr.common.constant import (
|
|
|
40
40
|
TRANSPORT_TYPE_REST,
|
|
41
41
|
)
|
|
42
42
|
from flwr.common.exit import ExitCode, flwr_exit
|
|
43
|
-
from flwr.common.exit_handlers import register_exit_handlers
|
|
44
43
|
from flwr.common.logger import log
|
|
45
44
|
from flwr.supernode.start_client_internal import start_client_internal
|
|
46
45
|
|
|
@@ -66,12 +65,6 @@ def flower_supernode() -> None:
|
|
|
66
65
|
|
|
67
66
|
log(DEBUG, "Isolation mode: %s", args.isolation)
|
|
68
67
|
|
|
69
|
-
# Register handlers for graceful shutdown
|
|
70
|
-
register_exit_handlers(
|
|
71
|
-
event_type=EventType.RUN_SUPERNODE_LEAVE,
|
|
72
|
-
exit_message="SuperNode terminated gracefully.",
|
|
73
|
-
)
|
|
74
|
-
|
|
75
68
|
start_client_internal(
|
|
76
69
|
server_address=args.superlink,
|
|
77
70
|
transport=args.transport,
|
|
@@ -22,6 +22,7 @@ from flwr.common.args import add_args_flwr_app_common
|
|
|
22
22
|
from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
|
|
23
23
|
from flwr.common.exit import ExitCode, flwr_exit
|
|
24
24
|
from flwr.common.logger import log
|
|
25
|
+
from flwr.supercore.utils import mask_string
|
|
25
26
|
from flwr.supernode.runtime.run_clientapp import run_clientapp
|
|
26
27
|
|
|
27
28
|
|
|
@@ -40,11 +41,11 @@ def flwr_clientapp() -> None:
|
|
|
40
41
|
"`flwr-clientapp` will attempt to connect to SuperNode's "
|
|
41
42
|
"ClientAppIo API at %s with token %s",
|
|
42
43
|
args.clientappio_api_address,
|
|
43
|
-
args.token,
|
|
44
|
+
mask_string(args.token) if args.token else "None",
|
|
44
45
|
)
|
|
45
46
|
run_clientapp(
|
|
46
47
|
clientappio_api_address=args.clientappio_api_address,
|
|
47
|
-
run_once=(args.token is not None),
|
|
48
|
+
run_once=(args.token is not None) or args.run_once,
|
|
48
49
|
token=args.token,
|
|
49
50
|
flwr_dir=args.flwr_dir,
|
|
50
51
|
certificates=None,
|
|
@@ -66,7 +67,7 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
|
|
|
66
67
|
)
|
|
67
68
|
parser.add_argument(
|
|
68
69
|
"--token",
|
|
69
|
-
type=
|
|
70
|
+
type=str,
|
|
70
71
|
required=False,
|
|
71
72
|
help="Unique token generated by SuperNode for each ClientApp execution",
|
|
72
73
|
)
|
|
@@ -77,5 +78,11 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
|
|
|
77
78
|
help="The PID of the parent process. When set, the process will terminate "
|
|
78
79
|
"when the parent process exits.",
|
|
79
80
|
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--run-once",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="When set, this process will start a single ClientApp for a pending "
|
|
85
|
+
"message. If there is no pending message, the process will exit.",
|
|
86
|
+
)
|
|
80
87
|
add_args_flwr_app_common(parser=parser)
|
|
81
88
|
return parser
|
|
@@ -51,8 +51,9 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
|
|
|
51
51
|
# Store run ID to Context mapping
|
|
52
52
|
self.ctx_store: dict[int, Context] = {}
|
|
53
53
|
self.lock_ctx_store = Lock()
|
|
54
|
-
# Store run ID to token mapping
|
|
54
|
+
# Store run ID to token mapping and token to run ID mapping
|
|
55
55
|
self.token_store: dict[int, str] = {}
|
|
56
|
+
self.token_to_run_id: dict[str, int] = {}
|
|
56
57
|
self.lock_token_store = Lock()
|
|
57
58
|
|
|
58
59
|
def set_node_id(self, node_id: Optional[int]) -> None:
|
|
@@ -177,6 +178,7 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
|
|
|
177
178
|
if run_id in self.token_store:
|
|
178
179
|
raise ValueError("Token already created for this run ID")
|
|
179
180
|
self.token_store[run_id] = token
|
|
181
|
+
self.token_to_run_id[token] = run_id
|
|
180
182
|
return token
|
|
181
183
|
|
|
182
184
|
def verify_token(self, run_id: int, token: str) -> bool:
|
|
@@ -187,4 +189,11 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
|
|
|
187
189
|
def delete_token(self, run_id: int) -> None:
|
|
188
190
|
"""Delete the token for the given run ID."""
|
|
189
191
|
with self.lock_token_store:
|
|
190
|
-
self.token_store.pop(run_id, None)
|
|
192
|
+
token = self.token_store.pop(run_id, None)
|
|
193
|
+
if token is not None:
|
|
194
|
+
self.token_to_run_id.pop(token, None)
|
|
195
|
+
|
|
196
|
+
def get_run_id_by_token(self, token: str) -> Optional[int]:
|
|
197
|
+
"""Get the run ID associated with a given token."""
|
|
198
|
+
with self.lock_token_store:
|
|
199
|
+
return self.token_to_run_id.get(token)
|
|
@@ -210,3 +210,18 @@ class NodeState(ABC):
|
|
|
210
210
|
run_id : int
|
|
211
211
|
The ID of the run for which to delete the token.
|
|
212
212
|
"""
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def get_run_id_by_token(self, token: str) -> Optional[int]:
|
|
216
|
+
"""Get the run ID associated with a given token.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
token : str
|
|
221
|
+
The token to look up.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
Optional[int]
|
|
226
|
+
The run ID if the token is valid, otherwise None.
|
|
227
|
+
"""
|
|
@@ -32,34 +32,54 @@ from flwr.common import Context, Message
|
|
|
32
32
|
from flwr.common.config import get_flwr_dir
|
|
33
33
|
from flwr.common.constant import ErrorCode
|
|
34
34
|
from flwr.common.grpc import create_channel, on_channel_state_change
|
|
35
|
+
from flwr.common.inflatable import (
|
|
36
|
+
get_all_nested_objects,
|
|
37
|
+
get_object_tree,
|
|
38
|
+
no_object_id_recompute,
|
|
39
|
+
)
|
|
40
|
+
from flwr.common.inflatable_protobuf_utils import (
|
|
41
|
+
make_confirm_message_received_fn_protobuf,
|
|
42
|
+
make_pull_object_fn_protobuf,
|
|
43
|
+
make_push_object_fn_protobuf,
|
|
44
|
+
)
|
|
45
|
+
from flwr.common.inflatable_utils import pull_and_inflate_object_from_tree, push_objects
|
|
35
46
|
from flwr.common.logger import log
|
|
47
|
+
from flwr.common.message import remove_content_from_message
|
|
36
48
|
from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
|
|
37
49
|
from flwr.common.serde import (
|
|
38
50
|
context_from_proto,
|
|
39
51
|
context_to_proto,
|
|
40
52
|
fab_from_proto,
|
|
41
|
-
message_from_proto,
|
|
42
53
|
message_to_proto,
|
|
43
54
|
run_from_proto,
|
|
44
55
|
)
|
|
45
56
|
from flwr.common.typing import Fab, Run
|
|
57
|
+
from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
|
|
58
|
+
PullAppInputsRequest,
|
|
59
|
+
PullAppInputsResponse,
|
|
60
|
+
PullAppMessagesRequest,
|
|
61
|
+
PullAppMessagesResponse,
|
|
62
|
+
PushAppMessagesRequest,
|
|
63
|
+
PushAppOutputsRequest,
|
|
64
|
+
PushAppOutputsResponse,
|
|
65
|
+
)
|
|
46
66
|
|
|
47
67
|
# pylint: disable=E0611
|
|
48
68
|
from flwr.proto.clientappio_pb2 import (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
PushClientAppOutputsRequest,
|
|
54
|
-
PushClientAppOutputsResponse,
|
|
69
|
+
GetRunIdsWithPendingMessagesRequest,
|
|
70
|
+
GetRunIdsWithPendingMessagesResponse,
|
|
71
|
+
RequestTokenRequest,
|
|
72
|
+
RequestTokenResponse,
|
|
55
73
|
)
|
|
56
74
|
from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
|
|
75
|
+
from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
|
76
|
+
from flwr.supercore.utils import mask_string
|
|
57
77
|
|
|
58
78
|
|
|
59
79
|
def run_clientapp( # pylint: disable=R0913, R0914, R0917
|
|
60
80
|
clientappio_api_address: str,
|
|
61
81
|
run_once: bool,
|
|
62
|
-
token: Optional[
|
|
82
|
+
token: Optional[str] = None,
|
|
63
83
|
flwr_dir: Optional[str] = None,
|
|
64
84
|
certificates: Optional[bytes] = None,
|
|
65
85
|
parent_pid: Optional[int] = None,
|
|
@@ -84,9 +104,8 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
|
|
|
84
104
|
|
|
85
105
|
while True:
|
|
86
106
|
# If token is not set, loop until token is received from SuperNode
|
|
87
|
-
|
|
107
|
+
if token is None:
|
|
88
108
|
token = get_token(stub)
|
|
89
|
-
time.sleep(1)
|
|
90
109
|
|
|
91
110
|
# Pull Message, Context, Run and (optional) FAB from SuperNode
|
|
92
111
|
message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
|
|
@@ -172,34 +191,58 @@ def start_parent_process_monitor(
|
|
|
172
191
|
threading.Thread(target=monitor, daemon=True).start()
|
|
173
192
|
|
|
174
193
|
|
|
175
|
-
def get_token(stub:
|
|
194
|
+
def get_token(stub: ClientAppIoStub) -> str:
|
|
176
195
|
"""Get a token from SuperNode."""
|
|
177
196
|
log(DEBUG, "[flwr-clientapp] Request token")
|
|
178
|
-
|
|
179
|
-
res:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
197
|
+
while True:
|
|
198
|
+
res: GetRunIdsWithPendingMessagesResponse = stub.GetRunIdsWithPendingMessages(
|
|
199
|
+
GetRunIdsWithPendingMessagesRequest()
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
for run_id in res.run_ids:
|
|
203
|
+
tk_res: RequestTokenResponse = stub.RequestToken(
|
|
204
|
+
RequestTokenRequest(run_id=run_id)
|
|
205
|
+
)
|
|
206
|
+
if tk_res.token:
|
|
207
|
+
return tk_res.token
|
|
208
|
+
|
|
209
|
+
time.sleep(1) # Wait before retrying to get run IDs
|
|
188
210
|
|
|
189
211
|
|
|
190
212
|
def pull_clientappinputs(
|
|
191
|
-
stub:
|
|
213
|
+
stub: ClientAppIoStub, token: str
|
|
192
214
|
) -> tuple[Message, Context, Run, Optional[Fab]]:
|
|
193
215
|
"""Pull ClientAppInputs from SuperNode."""
|
|
194
|
-
|
|
216
|
+
masked_token = mask_string(token)
|
|
217
|
+
log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", masked_token)
|
|
195
218
|
try:
|
|
196
|
-
|
|
197
|
-
|
|
219
|
+
# Pull Context, Run and (optional) FAB
|
|
220
|
+
res: PullAppInputsResponse = stub.PullClientAppInputs(
|
|
221
|
+
PullAppInputsRequest(token=token)
|
|
198
222
|
)
|
|
199
|
-
message = message_from_proto(res.message)
|
|
200
223
|
context = context_from_proto(res.context)
|
|
201
224
|
run = run_from_proto(res.run)
|
|
202
225
|
fab = fab_from_proto(res.fab) if res.fab else None
|
|
226
|
+
|
|
227
|
+
# Pull and inflate the message
|
|
228
|
+
pull_msg_res: PullAppMessagesResponse = stub.PullMessage(
|
|
229
|
+
PullAppMessagesRequest(token=token)
|
|
230
|
+
)
|
|
231
|
+
run_id = context.run_id
|
|
232
|
+
node = Node(node_id=context.node_id)
|
|
233
|
+
object_tree = pull_msg_res.message_object_trees[0]
|
|
234
|
+
message = pull_and_inflate_object_from_tree(
|
|
235
|
+
object_tree,
|
|
236
|
+
make_pull_object_fn_protobuf(stub.PullObject, node, run_id),
|
|
237
|
+
make_confirm_message_received_fn_protobuf(
|
|
238
|
+
stub.ConfirmMessageReceived, node, run_id
|
|
239
|
+
),
|
|
240
|
+
return_type=Message,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Set the message ID
|
|
244
|
+
# The deflated message doesn't contain the message_id (its own object_id)
|
|
245
|
+
message.metadata.__dict__["_message_id"] = object_tree.object_id
|
|
203
246
|
return message, context, run, fab
|
|
204
247
|
except grpc.RpcError as e:
|
|
205
248
|
log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e))
|
|
@@ -207,18 +250,52 @@ def pull_clientappinputs(
|
|
|
207
250
|
|
|
208
251
|
|
|
209
252
|
def push_clientappoutputs(
|
|
210
|
-
stub:
|
|
211
|
-
) ->
|
|
253
|
+
stub: ClientAppIoStub, token: str, message: Message, context: Context
|
|
254
|
+
) -> PushAppOutputsResponse:
|
|
212
255
|
"""Push ClientAppOutputs to SuperNode."""
|
|
213
|
-
|
|
214
|
-
|
|
256
|
+
masked_token = mask_string(token)
|
|
257
|
+
log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", masked_token)
|
|
258
|
+
# Set message ID
|
|
259
|
+
message.metadata.__dict__["_message_id"] = message.object_id
|
|
260
|
+
proto_message = message_to_proto(remove_content_from_message(message))
|
|
215
261
|
proto_context = context_to_proto(context)
|
|
216
262
|
|
|
217
263
|
try:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
264
|
+
|
|
265
|
+
with no_object_id_recompute():
|
|
266
|
+
# Get object tree and all objects to push
|
|
267
|
+
object_tree = get_object_tree(message)
|
|
268
|
+
|
|
269
|
+
# Push Message
|
|
270
|
+
# This is temporary. The message should not contain its content
|
|
271
|
+
push_msg_res = stub.PushMessage(
|
|
272
|
+
PushAppMessagesRequest(
|
|
273
|
+
token=token,
|
|
274
|
+
messages_list=[proto_message],
|
|
275
|
+
message_object_trees=[object_tree],
|
|
276
|
+
)
|
|
221
277
|
)
|
|
278
|
+
del proto_message
|
|
279
|
+
|
|
280
|
+
# Retrieve the object IDs to push
|
|
281
|
+
object_ids_to_push = set(push_msg_res.objects_to_push)
|
|
282
|
+
|
|
283
|
+
# Push all objects
|
|
284
|
+
all_objects = get_all_nested_objects(message)
|
|
285
|
+
del message
|
|
286
|
+
push_objects(
|
|
287
|
+
all_objects,
|
|
288
|
+
make_push_object_fn_protobuf(
|
|
289
|
+
stub.PushObject,
|
|
290
|
+
Node(node_id=context.node_id),
|
|
291
|
+
run_id=context.run_id,
|
|
292
|
+
),
|
|
293
|
+
object_ids_to_push=object_ids_to_push,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Push Context
|
|
297
|
+
res: PushAppOutputsResponse = stub.PushClientAppOutputs(
|
|
298
|
+
PushAppOutputsRequest(token=token, context=proto_context)
|
|
222
299
|
)
|
|
223
300
|
return res
|
|
224
301
|
except grpc.RpcError as e:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2025 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 ClientApp Scheduler."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .simple_clientapp_scheduler_plugin import SimpleClientAppSchedulerPlugin
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SimpleClientAppSchedulerPlugin",
|
|
22
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
"""Simple Flower ClientApp Scheduler plugin."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from flwr.supercore.scheduler import SchedulerPlugin
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SimpleClientAppSchedulerPlugin(SchedulerPlugin):
|
|
27
|
+
"""Simple Flower ClientApp Scheduler plugin.
|
|
28
|
+
|
|
29
|
+
The plugin always selects the first candidate run ID.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
|
|
33
|
+
"""Select a run ID to execute from a sequence of candidates."""
|
|
34
|
+
if not candidate_run_ids:
|
|
35
|
+
return None
|
|
36
|
+
return candidate_run_ids[0]
|
|
37
|
+
|
|
38
|
+
def launch_app(self, token: str, run_id: int) -> None:
|
|
39
|
+
"""Launch the application associated with a given run ID and token."""
|
|
40
|
+
cmds = ["flwr-clientapp", "--insecure"]
|
|
41
|
+
cmds += ["--clientappio-api-address", self.appio_api_address]
|
|
42
|
+
cmds += ["--token", token]
|
|
43
|
+
cmds += ["--parent-pid", str(os.getpid())]
|
|
44
|
+
cmds += ["--flwr-dir", self.flwr_dir]
|
|
45
|
+
# Launch the client app without waiting for it to complete.
|
|
46
|
+
# Since we don't need to manage the process, we intentionally avoid using
|
|
47
|
+
# a `with` statement. Suppress the pylint warning for it in this case.
|
|
48
|
+
# pylint: disable-next=consider-using-with
|
|
49
|
+
subprocess.Popen(cmds)
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
"""ClientAppIo API Servicer."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from .clientappio_servicer import
|
|
18
|
+
from .clientappio_servicer import ClientAppIoServicer
|
|
19
19
|
|
|
20
20
|
__all__ = [
|
|
21
|
-
"ClientAppInputs",
|
|
22
21
|
"ClientAppIoServicer",
|
|
23
|
-
"ClientAppOutputs",
|
|
24
22
|
]
|