flwr 1.18.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/app/__init__.py +15 -0
- flwr/app/error.py +68 -0
- flwr/app/metadata.py +223 -0
- flwr/cli/build.py +94 -59
- flwr/cli/log.py +3 -3
- flwr/cli/login/login.py +3 -7
- flwr/cli/ls.py +15 -36
- 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/code/client.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +25 -17
- 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 +48 -49
- flwr/cli/stop.py +2 -2
- flwr/cli/utils.py +38 -5
- flwr/client/__init__.py +2 -2
- flwr/client/client_app.py +1 -1
- flwr/client/clientapp/__init__.py +0 -7
- flwr/client/grpc_adapter_client/connection.py +15 -8
- flwr/client/grpc_rere_client/connection.py +142 -97
- flwr/client/grpc_rere_client/grpc_adapter.py +34 -6
- flwr/client/message_handler/message_handler.py +1 -1
- flwr/client/mod/comms_mods.py +36 -17
- flwr/client/rest_client/connection.py +176 -103
- flwr/clientapp/__init__.py +15 -0
- flwr/common/__init__.py +2 -2
- flwr/common/auth_plugin/__init__.py +2 -0
- flwr/common/auth_plugin/auth_plugin.py +29 -3
- flwr/common/constant.py +39 -8
- flwr/common/event_log_plugin/event_log_plugin.py +3 -3
- flwr/common/exit/exit_code.py +16 -1
- flwr/common/exit_handlers.py +30 -0
- flwr/common/grpc.py +12 -1
- flwr/common/heartbeat.py +165 -0
- flwr/common/inflatable.py +290 -0
- flwr/common/inflatable_protobuf_utils.py +141 -0
- flwr/common/inflatable_utils.py +508 -0
- flwr/common/message.py +110 -242
- flwr/common/record/__init__.py +2 -1
- flwr/common/record/array.py +402 -0
- flwr/common/record/arraychunk.py +59 -0
- flwr/common/record/arrayrecord.py +103 -225
- flwr/common/record/configrecord.py +59 -4
- flwr/common/record/conversion_utils.py +1 -1
- flwr/common/record/metricrecord.py +55 -4
- flwr/common/record/recorddict.py +69 -1
- flwr/common/recorddict_compat.py +2 -2
- flwr/common/retry_invoker.py +5 -1
- flwr/common/serde.py +59 -211
- flwr/common/serde_utils.py +175 -0
- flwr/common/typing.py +5 -3
- flwr/compat/__init__.py +15 -0
- flwr/compat/client/__init__.py +15 -0
- flwr/{client → compat/client}/app.py +28 -185
- flwr/compat/common/__init__.py +15 -0
- flwr/compat/server/__init__.py +15 -0
- flwr/compat/server/app.py +174 -0
- flwr/compat/simulation/__init__.py +15 -0
- 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 +24 -27
- flwr/proto/fleet_pb2.pyi +19 -35
- flwr/proto/fleet_pb2_grpc.py +117 -13
- flwr/proto/fleet_pb2_grpc.pyi +47 -6
- flwr/proto/heartbeat_pb2.py +33 -0
- flwr/proto/heartbeat_pb2.pyi +66 -0
- flwr/proto/heartbeat_pb2_grpc.py +4 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +28 -11
- flwr/proto/message_pb2.pyi +125 -0
- flwr/proto/recorddict_pb2.py +16 -28
- flwr/proto/recorddict_pb2.pyi +46 -64
- flwr/proto/run_pb2.py +24 -32
- flwr/proto/run_pb2.pyi +4 -52
- flwr/proto/serverappio_pb2.py +9 -23
- flwr/proto/serverappio_pb2.pyi +0 -110
- flwr/proto/serverappio_pb2_grpc.py +177 -72
- flwr/proto/serverappio_pb2_grpc.pyi +75 -33
- flwr/proto/simulationio_pb2.py +12 -11
- flwr/proto/simulationio_pb2_grpc.py +35 -0
- flwr/proto/simulationio_pb2_grpc.pyi +14 -0
- flwr/server/__init__.py +1 -1
- flwr/server/app.py +69 -187
- flwr/server/compat/app_utils.py +50 -28
- flwr/server/fleet_event_log_interceptor.py +6 -2
- flwr/server/grid/grpc_grid.py +148 -41
- flwr/server/grid/inmemory_grid.py +5 -4
- flwr/server/serverapp/app.py +45 -17
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +21 -3
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +102 -8
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
- flwr/server/superlink/fleet/message_handler/message_handler.py +130 -19
- flwr/server/superlink/fleet/rest_rere/rest_api.py +73 -13
- flwr/server/superlink/fleet/vce/vce_api.py +6 -3
- flwr/server/superlink/linkstate/in_memory_linkstate.py +138 -43
- flwr/server/superlink/linkstate/linkstate.py +53 -20
- flwr/server/superlink/linkstate/sqlite_linkstate.py +149 -55
- flwr/server/superlink/linkstate/utils.py +33 -29
- flwr/server/superlink/serverappio/serverappio_grpc.py +4 -1
- flwr/server/superlink/serverappio/serverappio_servicer.py +230 -84
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/superlink/simulation/simulationio_servicer.py +26 -2
- flwr/server/superlink/utils.py +9 -2
- flwr/server/utils/validator.py +2 -2
- flwr/serverapp/__init__.py +15 -0
- flwr/simulation/app.py +25 -0
- flwr/simulation/run_simulation.py +17 -0
- flwr/supercore/__init__.py +15 -0
- flwr/{server/superlink → supercore}/ffs/__init__.py +2 -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/__init__.py +24 -0
- flwr/supercore/object_store/in_memory_object_store.py +229 -0
- flwr/supercore/object_store/object_store.py +170 -0
- flwr/supercore/object_store/object_store_factory.py +44 -0
- flwr/supercore/object_store/utils.py +43 -0
- flwr/supercore/scheduler/__init__.py +22 -0
- flwr/supercore/scheduler/plugin.py +71 -0
- flwr/{client/nodestate/nodestate.py → supercore/utils.py} +14 -13
- flwr/superexec/deployment.py +7 -4
- flwr/superexec/exec_event_log_interceptor.py +8 -4
- flwr/superexec/exec_grpc.py +25 -5
- flwr/superexec/exec_license_interceptor.py +82 -0
- flwr/superexec/exec_servicer.py +135 -24
- flwr/superexec/exec_user_auth_interceptor.py +45 -8
- flwr/superexec/executor.py +5 -1
- flwr/superexec/simulation.py +8 -3
- flwr/superlink/__init__.py +15 -0
- flwr/{client/supernode → supernode}/__init__.py +0 -7
- flwr/supernode/cli/__init__.py +24 -0
- flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +3 -19
- flwr/supernode/cli/flwr_clientapp.py +88 -0
- flwr/supernode/nodestate/in_memory_nodestate.py +199 -0
- flwr/supernode/nodestate/nodestate.py +227 -0
- flwr/supernode/runtime/__init__.py +15 -0
- flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +135 -89
- flwr/supernode/scheduler/__init__.py +22 -0
- flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
- flwr/supernode/servicer/__init__.py +15 -0
- flwr/supernode/servicer/clientappio/__init__.py +22 -0
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +303 -0
- flwr/supernode/start_client_internal.py +589 -0
- {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/METADATA +6 -4
- {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/RECORD +171 -123
- {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/WHEEL +1 -1
- {flwr-1.18.0.dist-info → flwr-1.20.0.dist-info}/entry_points.txt +2 -2
- flwr/client/clientapp/clientappio_servicer.py +0 -244
- flwr/client/heartbeat.py +0 -74
- flwr/client/nodestate/in_memory_nodestate.py +0 -38
- /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
- /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
- /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
- /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
- /flwr/{client → supernode}/nodestate/__init__.py +0 -0
- /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
flwr/app/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
"""Public Flower App APIs."""
|
flwr/app/error.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
"""Error."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Optional, cast
|
|
21
|
+
|
|
22
|
+
DEFAULT_TTL = 43200 # This is 12 hours
|
|
23
|
+
MESSAGE_INIT_ERROR_MESSAGE = (
|
|
24
|
+
"Invalid arguments for Message. Expected one of the documented "
|
|
25
|
+
"signatures: Message(content: RecordDict, dst_node_id: int, message_type: str,"
|
|
26
|
+
" *, [ttl: float, group_id: str]) or Message(content: RecordDict | error: Error,"
|
|
27
|
+
" *, reply_to: Message, [ttl: float])."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Error:
|
|
32
|
+
"""The class storing information about an error that occurred.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
code : int
|
|
37
|
+
An identifier for the error.
|
|
38
|
+
reason : Optional[str]
|
|
39
|
+
A reason for why the error arose (e.g. an exception stack-trace)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, code: int, reason: str | None = None) -> None:
|
|
43
|
+
var_dict = {
|
|
44
|
+
"_code": code,
|
|
45
|
+
"_reason": reason,
|
|
46
|
+
}
|
|
47
|
+
self.__dict__.update(var_dict)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def code(self) -> int:
|
|
51
|
+
"""Error code."""
|
|
52
|
+
return cast(int, self.__dict__["_code"])
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def reason(self) -> str | None:
|
|
56
|
+
"""Reason reported about the error."""
|
|
57
|
+
return cast(Optional[str], self.__dict__["_reason"])
|
|
58
|
+
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
"""Return a string representation of this instance."""
|
|
61
|
+
view = ", ".join([f"{k.lstrip('_')}={v!r}" for k, v in self.__dict__.items()])
|
|
62
|
+
return f"{self.__class__.__qualname__}({view})"
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
"""Compare two instances of the class."""
|
|
66
|
+
if not isinstance(other, self.__class__):
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
return self.__dict__ == other.__dict__
|
flwr/app/metadata.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
"""Metadata."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
from ..common.constant import MessageType, MessageTypeLegacy
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Metadata: # pylint: disable=too-many-instance-attributes
|
|
26
|
+
"""The class representing metadata associated with the current message.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
run_id : int
|
|
31
|
+
An identifier for the current run.
|
|
32
|
+
message_id : str
|
|
33
|
+
An identifier for the current message.
|
|
34
|
+
src_node_id : int
|
|
35
|
+
An identifier for the node sending this message.
|
|
36
|
+
dst_node_id : int
|
|
37
|
+
An identifier for the node receiving this message.
|
|
38
|
+
reply_to_message_id : str
|
|
39
|
+
An identifier for the message to which this message is a reply.
|
|
40
|
+
group_id : str
|
|
41
|
+
An identifier for grouping messages. In some settings,
|
|
42
|
+
this is used as the FL round.
|
|
43
|
+
created_at : float
|
|
44
|
+
Unix timestamp when the message was created.
|
|
45
|
+
ttl : float
|
|
46
|
+
Time-to-live for this message in seconds.
|
|
47
|
+
message_type : str
|
|
48
|
+
A string that encodes the action to be executed on
|
|
49
|
+
the receiving end.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
53
|
+
self,
|
|
54
|
+
run_id: int,
|
|
55
|
+
message_id: str,
|
|
56
|
+
src_node_id: int,
|
|
57
|
+
dst_node_id: int,
|
|
58
|
+
reply_to_message_id: str,
|
|
59
|
+
group_id: str,
|
|
60
|
+
created_at: float,
|
|
61
|
+
ttl: float,
|
|
62
|
+
message_type: str,
|
|
63
|
+
) -> None:
|
|
64
|
+
var_dict = {
|
|
65
|
+
"_run_id": run_id,
|
|
66
|
+
"_message_id": message_id,
|
|
67
|
+
"_src_node_id": src_node_id,
|
|
68
|
+
"_dst_node_id": dst_node_id,
|
|
69
|
+
"_reply_to_message_id": reply_to_message_id,
|
|
70
|
+
"_group_id": group_id,
|
|
71
|
+
"_created_at": created_at,
|
|
72
|
+
"_ttl": ttl,
|
|
73
|
+
"_message_type": message_type,
|
|
74
|
+
}
|
|
75
|
+
self.__dict__.update(var_dict)
|
|
76
|
+
self.message_type = message_type # Trigger validation
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def run_id(self) -> int:
|
|
80
|
+
"""An identifier for the current run."""
|
|
81
|
+
return cast(int, self.__dict__["_run_id"])
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def message_id(self) -> str:
|
|
85
|
+
"""An identifier for the current message."""
|
|
86
|
+
return cast(str, self.__dict__["_message_id"])
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def src_node_id(self) -> int:
|
|
90
|
+
"""An identifier for the node sending this message."""
|
|
91
|
+
return cast(int, self.__dict__["_src_node_id"])
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def reply_to_message_id(self) -> str:
|
|
95
|
+
"""An identifier for the message to which this message is a reply."""
|
|
96
|
+
return cast(str, self.__dict__["_reply_to_message_id"])
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def dst_node_id(self) -> int:
|
|
100
|
+
"""An identifier for the node receiving this message."""
|
|
101
|
+
return cast(int, self.__dict__["_dst_node_id"])
|
|
102
|
+
|
|
103
|
+
@dst_node_id.setter
|
|
104
|
+
def dst_node_id(self, value: int) -> None:
|
|
105
|
+
"""Set dst_node_id."""
|
|
106
|
+
self.__dict__["_dst_node_id"] = value
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def group_id(self) -> str:
|
|
110
|
+
"""An identifier for grouping messages."""
|
|
111
|
+
return cast(str, self.__dict__["_group_id"])
|
|
112
|
+
|
|
113
|
+
@group_id.setter
|
|
114
|
+
def group_id(self, value: str) -> None:
|
|
115
|
+
"""Set group_id."""
|
|
116
|
+
self.__dict__["_group_id"] = value
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def created_at(self) -> float:
|
|
120
|
+
"""Unix timestamp when the message was created."""
|
|
121
|
+
return cast(float, self.__dict__["_created_at"])
|
|
122
|
+
|
|
123
|
+
@created_at.setter
|
|
124
|
+
def created_at(self, value: float) -> None:
|
|
125
|
+
"""Set creation timestamp of this message."""
|
|
126
|
+
self.__dict__["_created_at"] = value
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def delivered_at(self) -> str:
|
|
130
|
+
"""Unix timestamp when the message was delivered."""
|
|
131
|
+
return cast(str, self.__dict__["_delivered_at"])
|
|
132
|
+
|
|
133
|
+
@delivered_at.setter
|
|
134
|
+
def delivered_at(self, value: str) -> None:
|
|
135
|
+
"""Set delivery timestamp of this message."""
|
|
136
|
+
self.__dict__["_delivered_at"] = value
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def ttl(self) -> float:
|
|
140
|
+
"""Time-to-live for this message."""
|
|
141
|
+
return cast(float, self.__dict__["_ttl"])
|
|
142
|
+
|
|
143
|
+
@ttl.setter
|
|
144
|
+
def ttl(self, value: float) -> None:
|
|
145
|
+
"""Set ttl."""
|
|
146
|
+
self.__dict__["_ttl"] = value
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def message_type(self) -> str:
|
|
150
|
+
"""A string that encodes the action to be executed on the receiving end."""
|
|
151
|
+
return cast(str, self.__dict__["_message_type"])
|
|
152
|
+
|
|
153
|
+
@message_type.setter
|
|
154
|
+
def message_type(self, value: str) -> None:
|
|
155
|
+
"""Set message_type."""
|
|
156
|
+
# Validate message type
|
|
157
|
+
if validate_legacy_message_type(value):
|
|
158
|
+
pass # Backward compatibility for legacy message types
|
|
159
|
+
elif not validate_message_type(value):
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"Invalid message type: '{value}'. "
|
|
162
|
+
"Expected format: '<category>' or '<category>.<action>', "
|
|
163
|
+
"where <category> must be 'train', 'evaluate', or 'query', "
|
|
164
|
+
"and <action> must be a valid Python identifier."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
self.__dict__["_message_type"] = value
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
"""Return a string representation of this instance."""
|
|
171
|
+
view = ", ".join([f"{k.lstrip('_')}={v!r}" for k, v in self.__dict__.items()])
|
|
172
|
+
return f"{self.__class__.__qualname__}({view})"
|
|
173
|
+
|
|
174
|
+
def __eq__(self, other: object) -> bool:
|
|
175
|
+
"""Compare two instances of the class."""
|
|
176
|
+
if not isinstance(other, self.__class__):
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
return self.__dict__ == other.__dict__
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def validate_message_type(message_type: str) -> bool:
|
|
182
|
+
"""Validate if the message type is valid.
|
|
183
|
+
|
|
184
|
+
A valid message type format must be one of the following:
|
|
185
|
+
|
|
186
|
+
- "<category>"
|
|
187
|
+
- "<category>.<action>"
|
|
188
|
+
|
|
189
|
+
where `category` must be one of "train", "evaluate", or "query",
|
|
190
|
+
and `action` must be a valid Python identifier.
|
|
191
|
+
"""
|
|
192
|
+
# Check if conforming to the format "<category>"
|
|
193
|
+
valid_types = {
|
|
194
|
+
MessageType.TRAIN,
|
|
195
|
+
MessageType.EVALUATE,
|
|
196
|
+
MessageType.QUERY,
|
|
197
|
+
MessageType.SYSTEM,
|
|
198
|
+
}
|
|
199
|
+
if message_type in valid_types:
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
# Check if conforming to the format "<category>.<action>"
|
|
203
|
+
if message_type.count(".") != 1:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
category, action = message_type.split(".")
|
|
207
|
+
if category in valid_types and action.isidentifier():
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate_legacy_message_type(message_type: str) -> bool:
|
|
214
|
+
"""Validate if the legacy message type is valid."""
|
|
215
|
+
# Backward compatibility for legacy message types
|
|
216
|
+
if message_type in (
|
|
217
|
+
MessageTypeLegacy.GET_PARAMETERS,
|
|
218
|
+
MessageTypeLegacy.GET_PROPERTIES,
|
|
219
|
+
"reconnect",
|
|
220
|
+
):
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
return False
|
flwr/cli/build.py
CHANGED
|
@@ -16,10 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import hashlib
|
|
19
|
-
import os
|
|
20
|
-
import shutil
|
|
21
|
-
import tempfile
|
|
22
19
|
import zipfile
|
|
20
|
+
from io import BytesIO
|
|
23
21
|
from pathlib import Path
|
|
24
22
|
from typing import Annotated, Any, Optional, Union
|
|
25
23
|
|
|
@@ -27,8 +25,14 @@ import pathspec
|
|
|
27
25
|
import tomli_w
|
|
28
26
|
import typer
|
|
29
27
|
|
|
30
|
-
from flwr.common.constant import
|
|
28
|
+
from flwr.common.constant import (
|
|
29
|
+
FAB_ALLOWED_EXTENSIONS,
|
|
30
|
+
FAB_DATE,
|
|
31
|
+
FAB_HASH_TRUNCATION,
|
|
32
|
+
FAB_MAX_SIZE,
|
|
33
|
+
)
|
|
31
34
|
|
|
35
|
+
from .config_utils import load as load_toml
|
|
32
36
|
from .config_utils import load_and_validate
|
|
33
37
|
from .utils import is_valid_project_name
|
|
34
38
|
|
|
@@ -43,11 +47,11 @@ def write_to_zip(
|
|
|
43
47
|
return zipfile_obj
|
|
44
48
|
|
|
45
49
|
|
|
46
|
-
def get_fab_filename(
|
|
50
|
+
def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
|
|
47
51
|
"""Get the FAB filename based on the given config and FAB hash."""
|
|
48
|
-
publisher =
|
|
49
|
-
name =
|
|
50
|
-
version =
|
|
52
|
+
publisher = config["tool"]["flwr"]["app"]["publisher"]
|
|
53
|
+
name = config["project"]["name"]
|
|
54
|
+
version = config["project"]["version"].replace(".", "-")
|
|
51
55
|
fab_hash_truncated = fab_hash[:FAB_HASH_TRUNCATION]
|
|
52
56
|
return f"{publisher}.{name}.{version}.{fab_hash_truncated}.fab"
|
|
53
57
|
|
|
@@ -58,7 +62,7 @@ def build(
|
|
|
58
62
|
Optional[Path],
|
|
59
63
|
typer.Option(help="Path of the Flower App to bundle into a FAB"),
|
|
60
64
|
] = None,
|
|
61
|
-
) ->
|
|
65
|
+
) -> None:
|
|
62
66
|
"""Build a Flower App into a Flower App Bundle (FAB).
|
|
63
67
|
|
|
64
68
|
You can run ``flwr build`` without any arguments to bundle the app located in the
|
|
@@ -89,8 +93,8 @@ def build(
|
|
|
89
93
|
)
|
|
90
94
|
raise typer.Exit(code=1)
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
if
|
|
96
|
+
config, errors, warnings = load_and_validate(app / "pyproject.toml")
|
|
97
|
+
if config is None:
|
|
94
98
|
typer.secho(
|
|
95
99
|
"Project configuration could not be loaded.\npyproject.toml is invalid:\n"
|
|
96
100
|
+ "\n".join([f"- {line}" for line in errors]),
|
|
@@ -107,70 +111,101 @@ def build(
|
|
|
107
111
|
bold=True,
|
|
108
112
|
)
|
|
109
113
|
|
|
110
|
-
#
|
|
111
|
-
|
|
114
|
+
# Build FAB
|
|
115
|
+
fab_bytes, fab_hash, _ = build_fab(app)
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
# Get the name of the zip file
|
|
118
|
+
fab_filename = get_fab_filename(config, fab_hash)
|
|
114
119
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
"tool" in conf
|
|
118
|
-
and "flwr" in conf["tool"]
|
|
119
|
-
and "federations" in conf["tool"]["flwr"]
|
|
120
|
-
):
|
|
121
|
-
del conf["tool"]["flwr"]["federations"]
|
|
120
|
+
# Write the FAB
|
|
121
|
+
Path(fab_filename).write_bytes(fab_bytes)
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
typer.secho(
|
|
124
|
+
f"🎊 Successfully built {fab_filename}", fg=typer.colors.GREEN, bold=True
|
|
125
|
+
)
|
|
124
126
|
|
|
125
|
-
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_file:
|
|
126
|
-
temp_filename = temp_file.name
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
|
|
129
|
+
"""Build a FAB in memory and return the bytes, hash, and config.
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
f
|
|
134
|
-
for f in app.rglob("*")
|
|
135
|
-
if not ignore_spec.match_file(f)
|
|
136
|
-
and f.name != temp_filename
|
|
137
|
-
and f.suffix in FAB_ALLOWED_EXTENSIONS
|
|
138
|
-
and f.name != "pyproject.toml" # Exclude the original pyproject.toml
|
|
139
|
-
]
|
|
131
|
+
This function assumes that the provided path points to a valid Flower app and
|
|
132
|
+
bundles it into a FAB without performing additional validation.
|
|
140
133
|
|
|
141
|
-
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
app : Path
|
|
137
|
+
Path to the Flower app to bundle into a FAB.
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
tuple[bytes, str, dict[str, Any]]
|
|
142
|
+
A tuple containing:
|
|
143
|
+
- the FAB as bytes
|
|
144
|
+
- the SHA256 hash of the FAB
|
|
145
|
+
- the project configuration (with the 'federations' field removed)
|
|
146
|
+
"""
|
|
147
|
+
app = app.resolve()
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
# Load the pyproject.toml file
|
|
150
|
+
config = load_toml(app / "pyproject.toml")
|
|
151
|
+
if config is None:
|
|
152
|
+
raise ValueError("Project configuration could not be loaded.")
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
# Remove the 'federations' field if it exists
|
|
155
|
+
if (
|
|
156
|
+
"tool" in config
|
|
157
|
+
and "flwr" in config["tool"]
|
|
158
|
+
and "federations" in config["tool"]["flwr"]
|
|
159
|
+
):
|
|
160
|
+
del config["tool"]["flwr"]["federations"]
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
# Load .gitignore rules if present
|
|
163
|
+
ignore_spec = _load_gitignore(app)
|
|
158
164
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
# Search for all files in the app directory
|
|
166
|
+
all_files = [
|
|
167
|
+
f
|
|
168
|
+
for f in app.rglob("*")
|
|
169
|
+
if not ignore_spec.match_file(f)
|
|
170
|
+
and f.suffix in FAB_ALLOWED_EXTENSIONS
|
|
171
|
+
and f.name != "pyproject.toml" # Exclude the original pyproject.toml
|
|
172
|
+
]
|
|
173
|
+
all_files.sort()
|
|
174
|
+
|
|
175
|
+
# Create a zip file in memory
|
|
176
|
+
list_file_content = ""
|
|
162
177
|
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
fab_buffer = BytesIO()
|
|
179
|
+
with zipfile.ZipFile(fab_buffer, "w", zipfile.ZIP_DEFLATED) as fab_file:
|
|
180
|
+
# Add pyproject.toml
|
|
181
|
+
write_to_zip(fab_file, "pyproject.toml", tomli_w.dumps(config))
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
for file_path in all_files:
|
|
184
|
+
# Read the file content manually
|
|
185
|
+
file_contents = file_path.read_bytes()
|
|
168
186
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
187
|
+
archive_path = str(file_path.relative_to(app)).replace("\\", "/")
|
|
188
|
+
write_to_zip(fab_file, archive_path, file_contents)
|
|
189
|
+
|
|
190
|
+
# Calculate file info
|
|
191
|
+
sha256_hash = hashlib.sha256(file_contents).hexdigest()
|
|
192
|
+
file_size_bits = len(file_contents) * 8 # size in bits
|
|
193
|
+
list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
|
|
194
|
+
|
|
195
|
+
# Add CONTENT and CONTENT.jwt to the zip file
|
|
196
|
+
write_to_zip(fab_file, ".info/CONTENT", list_file_content)
|
|
197
|
+
|
|
198
|
+
fab_bytes = fab_buffer.getvalue()
|
|
199
|
+
if len(fab_bytes) > FAB_MAX_SIZE:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"FAB size exceeds maximum allowed size of {FAB_MAX_SIZE:,} bytes."
|
|
202
|
+
"To reduce the package size, consider ignoring unnecessary files "
|
|
203
|
+
"via your `.gitignore` file or excluding them from the build."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
fab_hash = hashlib.sha256(fab_bytes).hexdigest()
|
|
172
207
|
|
|
173
|
-
return
|
|
208
|
+
return fab_bytes, fab_hash, config
|
|
174
209
|
|
|
175
210
|
|
|
176
211
|
def _load_gitignore(app: Path) -> pathspec.PathSpec:
|
flwr/cli/log.py
CHANGED
|
@@ -35,7 +35,7 @@ from flwr.common.logger import log as logger
|
|
|
35
35
|
from flwr.proto.exec_pb2 import StreamLogsRequest # pylint: disable=E0611
|
|
36
36
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
37
37
|
|
|
38
|
-
from .utils import init_channel, try_obtain_cli_auth_plugin
|
|
38
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class AllLogsRetrieved(BaseException):
|
|
@@ -95,7 +95,7 @@ def stream_logs(
|
|
|
95
95
|
latest_timestamp = 0.0
|
|
96
96
|
res = None
|
|
97
97
|
try:
|
|
98
|
-
with
|
|
98
|
+
with flwr_cli_grpc_exc_handler():
|
|
99
99
|
for res in stub.StreamLogs(req, timeout=duration):
|
|
100
100
|
print(res.log_output, end="")
|
|
101
101
|
raise AllLogsRetrieved()
|
|
@@ -116,7 +116,7 @@ def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
|
|
|
116
116
|
req = StreamLogsRequest(run_id=run_id, after_timestamp=0.0)
|
|
117
117
|
|
|
118
118
|
try:
|
|
119
|
-
with
|
|
119
|
+
with flwr_cli_grpc_exc_handler():
|
|
120
120
|
# Enforce timeout for graceful exit
|
|
121
121
|
for res in stub.StreamLogs(req, timeout=timeout):
|
|
122
122
|
print(res.log_output)
|
flwr/cli/login/login.py
CHANGED
|
@@ -35,11 +35,7 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
|
35
35
|
)
|
|
36
36
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
37
37
|
|
|
38
|
-
from ..utils import
|
|
39
|
-
init_channel,
|
|
40
|
-
try_obtain_cli_auth_plugin,
|
|
41
|
-
unauthenticated_exc_handler,
|
|
42
|
-
)
|
|
38
|
+
from ..utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
|
|
43
39
|
|
|
44
40
|
|
|
45
41
|
def login( # pylint: disable=R0914
|
|
@@ -96,7 +92,7 @@ def login( # pylint: disable=R0914
|
|
|
96
92
|
stub = ExecStub(channel)
|
|
97
93
|
|
|
98
94
|
login_request = GetLoginDetailsRequest()
|
|
99
|
-
with
|
|
95
|
+
with flwr_cli_grpc_exc_handler():
|
|
100
96
|
login_response: GetLoginDetailsResponse = stub.GetLoginDetails(login_request)
|
|
101
97
|
|
|
102
98
|
# Get the auth plugin
|
|
@@ -120,7 +116,7 @@ def login( # pylint: disable=R0914
|
|
|
120
116
|
expires_in=login_response.expires_in,
|
|
121
117
|
interval=login_response.interval,
|
|
122
118
|
)
|
|
123
|
-
with
|
|
119
|
+
with flwr_cli_grpc_exc_handler():
|
|
124
120
|
credentials = auth_plugin.login(details, stub)
|
|
125
121
|
|
|
126
122
|
# Store the tokens
|
flwr/cli/ls.py
CHANGED
|
@@ -44,7 +44,7 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
|
44
44
|
)
|
|
45
45
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
46
46
|
|
|
47
|
-
from .utils import init_channel, try_obtain_cli_auth_plugin
|
|
47
|
+
from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
|
|
48
48
|
|
|
49
49
|
_RunListType = tuple[int, str, str, str, str, str, str, str, str]
|
|
50
50
|
|
|
@@ -130,23 +130,16 @@ def ls( # pylint: disable=too-many-locals, too-many-branches, R0913, R0917
|
|
|
130
130
|
# Display information about a specific run ID
|
|
131
131
|
if run_id is not None:
|
|
132
132
|
typer.echo(f"🔍 Displaying information for run ID {run_id}...")
|
|
133
|
-
|
|
134
|
-
_display_one_run(stub, run_id, output_format)
|
|
133
|
+
formatted_runs = _display_one_run(stub, run_id)
|
|
135
134
|
# By default, list all runs
|
|
136
135
|
else:
|
|
137
136
|
typer.echo("📄 Listing all runs...")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
typer.secho(
|
|
145
|
-
f"❌ {err}",
|
|
146
|
-
fg=typer.colors.RED,
|
|
147
|
-
bold=True,
|
|
148
|
-
)
|
|
149
|
-
raise typer.Exit(code=1) from err
|
|
137
|
+
formatted_runs = _list_runs(stub)
|
|
138
|
+
restore_output()
|
|
139
|
+
if output_format == CliOutputFormat.JSON:
|
|
140
|
+
Console().print_json(_to_json(formatted_runs))
|
|
141
|
+
else:
|
|
142
|
+
Console().print(_to_table(formatted_runs))
|
|
150
143
|
finally:
|
|
151
144
|
if channel:
|
|
152
145
|
channel.close()
|
|
@@ -300,37 +293,23 @@ def _to_json(run_list: list[_RunListType]) -> str:
|
|
|
300
293
|
return json.dumps({"success": True, "runs": runs_list})
|
|
301
294
|
|
|
302
295
|
|
|
303
|
-
def _list_runs(
|
|
304
|
-
stub: ExecStub,
|
|
305
|
-
output_format: str = CliOutputFormat.DEFAULT,
|
|
306
|
-
) -> None:
|
|
296
|
+
def _list_runs(stub: ExecStub) -> list[_RunListType]:
|
|
307
297
|
"""List all runs."""
|
|
308
|
-
with
|
|
298
|
+
with flwr_cli_grpc_exc_handler():
|
|
309
299
|
res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
|
|
310
300
|
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
311
301
|
|
|
312
|
-
|
|
313
|
-
if output_format == CliOutputFormat.JSON:
|
|
314
|
-
Console().print_json(_to_json(formatted_runs))
|
|
315
|
-
else:
|
|
316
|
-
Console().print(_to_table(formatted_runs))
|
|
302
|
+
return _format_runs(run_dict, res.now)
|
|
317
303
|
|
|
318
304
|
|
|
319
|
-
def _display_one_run(
|
|
320
|
-
stub: ExecStub,
|
|
321
|
-
run_id: int,
|
|
322
|
-
output_format: str = CliOutputFormat.DEFAULT,
|
|
323
|
-
) -> None:
|
|
305
|
+
def _display_one_run(stub: ExecStub, run_id: int) -> list[_RunListType]:
|
|
324
306
|
"""Display information about a specific run."""
|
|
325
|
-
with
|
|
307
|
+
with flwr_cli_grpc_exc_handler():
|
|
326
308
|
res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
|
|
327
309
|
if not res.run_dict:
|
|
310
|
+
# This won't be reached as an gRPC error is raised if run_id is invalid
|
|
328
311
|
raise ValueError(f"Run ID {run_id} not found")
|
|
329
312
|
|
|
330
313
|
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
331
314
|
|
|
332
|
-
|
|
333
|
-
if output_format == CliOutputFormat.JSON:
|
|
334
|
-
Console().print_json(_to_json(formatted_runs))
|
|
335
|
-
else:
|
|
336
|
-
Console().print(_to_table(formatted_runs))
|
|
315
|
+
return _format_runs(run_dict, res.now)
|