flwr 1.13.1__py3-none-any.whl → 1.14.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/app.py +5 -0
- flwr/cli/build.py +1 -0
- flwr/cli/cli_user_auth_interceptor.py +86 -0
- flwr/cli/config_utils.py +19 -2
- flwr/cli/example.py +1 -0
- flwr/cli/install.py +1 -0
- flwr/cli/log.py +18 -36
- flwr/cli/login/__init__.py +22 -0
- flwr/cli/login/login.py +81 -0
- flwr/cli/ls.py +205 -106
- flwr/cli/new/__init__.py +1 -0
- flwr/cli/new/new.py +2 -1
- flwr/cli/new/templates/app/.gitignore.tpl +3 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -3
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/__init__.py +1 -0
- flwr/cli/run/run.py +89 -39
- flwr/cli/stop.py +130 -0
- flwr/cli/utils.py +172 -8
- flwr/client/app.py +14 -3
- flwr/client/client.py +1 -32
- flwr/client/clientapp/app.py +4 -1
- flwr/client/clientapp/utils.py +1 -0
- flwr/client/grpc_adapter_client/connection.py +1 -1
- flwr/client/grpc_client/connection.py +1 -1
- flwr/client/grpc_rere_client/connection.py +13 -7
- flwr/client/message_handler/message_handler.py +1 -2
- flwr/client/mod/comms_mods.py +1 -0
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/nodestate/__init__.py +1 -0
- flwr/client/nodestate/nodestate.py +1 -0
- flwr/client/nodestate/nodestate_factory.py +1 -0
- flwr/client/numpy_client.py +0 -44
- flwr/client/rest_client/connection.py +3 -3
- flwr/client/supernode/app.py +2 -2
- flwr/common/address.py +1 -0
- flwr/common/args.py +1 -0
- flwr/common/auth_plugin/__init__.py +24 -0
- flwr/common/auth_plugin/auth_plugin.py +111 -0
- flwr/common/config.py +3 -1
- flwr/common/constant.py +17 -1
- flwr/common/logger.py +40 -0
- flwr/common/message.py +1 -0
- flwr/common/object_ref.py +57 -54
- flwr/common/pyproject.py +1 -0
- flwr/common/record/__init__.py +1 -0
- flwr/common/record/parametersrecord.py +1 -0
- flwr/common/retry_invoker.py +77 -0
- flwr/common/secure_aggregation/secaggplus_utils.py +2 -2
- flwr/common/telemetry.py +15 -4
- flwr/common/typing.py +12 -0
- flwr/common/version.py +1 -0
- flwr/proto/exec_pb2.py +38 -14
- flwr/proto/exec_pb2.pyi +107 -2
- flwr/proto/exec_pb2_grpc.py +102 -0
- flwr/proto/exec_pb2_grpc.pyi +39 -0
- flwr/proto/fab_pb2.py +4 -4
- flwr/proto/fab_pb2.pyi +4 -1
- flwr/proto/serverappio_pb2.py +18 -18
- flwr/proto/serverappio_pb2.pyi +8 -2
- flwr/proto/serverappio_pb2_grpc.py +34 -0
- flwr/proto/serverappio_pb2_grpc.pyi +13 -0
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +34 -0
- flwr/proto/simulationio_pb2_grpc.pyi +13 -0
- flwr/server/app.py +54 -2
- flwr/server/compat/app_utils.py +7 -1
- flwr/server/driver/grpc_driver.py +11 -63
- flwr/server/driver/inmemory_driver.py +5 -1
- flwr/server/run_serverapp.py +8 -9
- flwr/server/serverapp/app.py +25 -3
- flwr/server/strategy/dpfedavg_fixed.py +1 -0
- flwr/server/superlink/driver/serverappio_grpc.py +1 -0
- flwr/server/superlink/driver/serverappio_servicer.py +82 -23
- flwr/server/superlink/ffs/disk_ffs.py +1 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -0
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -0
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +32 -12
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +12 -11
- flwr/server/superlink/fleet/message_handler/message_handler.py +32 -5
- flwr/server/superlink/fleet/rest_rere/rest_api.py +4 -1
- flwr/server/superlink/fleet/vce/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -0
- flwr/server/superlink/linkstate/in_memory_linkstate.py +21 -30
- flwr/server/superlink/linkstate/linkstate.py +17 -2
- flwr/server/superlink/linkstate/sqlite_linkstate.py +30 -49
- flwr/server/superlink/simulation/simulationio_servicer.py +33 -0
- flwr/server/superlink/utils.py +65 -0
- flwr/simulation/app.py +16 -4
- flwr/simulation/ray_transport/ray_actor.py +1 -0
- flwr/simulation/ray_transport/utils.py +1 -0
- flwr/simulation/run_simulation.py +36 -22
- flwr/simulation/simulationio_connection.py +3 -0
- flwr/superexec/app.py +1 -0
- flwr/superexec/deployment.py +1 -0
- flwr/superexec/exec_grpc.py +19 -1
- flwr/superexec/exec_servicer.py +76 -2
- flwr/superexec/exec_user_auth_interceptor.py +101 -0
- flwr/superexec/executor.py +1 -0
- {flwr-1.13.1.dist-info → flwr-1.14.0.dist-info}/METADATA +8 -7
- {flwr-1.13.1.dist-info → flwr-1.14.0.dist-info}/RECORD +112 -112
- flwr/proto/common_pb2.py +0 -36
- flwr/proto/common_pb2.pyi +0 -121
- flwr/proto/common_pb2_grpc.py +0 -4
- flwr/proto/common_pb2_grpc.pyi +0 -4
- flwr/proto/control_pb2.py +0 -27
- flwr/proto/control_pb2.pyi +0 -7
- flwr/proto/control_pb2_grpc.py +0 -135
- flwr/proto/control_pb2_grpc.pyi +0 -53
- {flwr-1.13.1.dist-info → flwr-1.14.0.dist-info}/LICENSE +0 -0
- {flwr-1.13.1.dist-info → flwr-1.14.0.dist-info}/WHEEL +0 -0
- {flwr-1.13.1.dist-info → flwr-1.14.0.dist-info}/entry_points.txt +0 -0
|
@@ -76,7 +76,7 @@ def grpc_connection( # pylint: disable=R0913,R0915,too-many-positional-argument
|
|
|
76
76
|
Optional[Callable[[], Optional[int]]],
|
|
77
77
|
Optional[Callable[[], None]],
|
|
78
78
|
Optional[Callable[[int], Run]],
|
|
79
|
-
Optional[Callable[[str], Fab]],
|
|
79
|
+
Optional[Callable[[str, int], Fab]],
|
|
80
80
|
]
|
|
81
81
|
]:
|
|
82
82
|
"""Establish a gRPC connection to a gRPC server.
|
|
@@ -42,7 +42,7 @@ from flwr.common.logger import log
|
|
|
42
42
|
from flwr.common.message import Message, Metadata
|
|
43
43
|
from flwr.common.retry_invoker import RetryInvoker
|
|
44
44
|
from flwr.common.serde import message_from_taskins, message_to_taskres, run_from_proto
|
|
45
|
-
from flwr.common.typing import Fab, Run
|
|
45
|
+
from flwr.common.typing import Fab, Run, RunNotRunningException
|
|
46
46
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
47
47
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
48
48
|
CreateNodeRequest,
|
|
@@ -84,7 +84,7 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
84
84
|
Optional[Callable[[], Optional[int]]],
|
|
85
85
|
Optional[Callable[[], None]],
|
|
86
86
|
Optional[Callable[[int], Run]],
|
|
87
|
-
Optional[Callable[[str], Fab]],
|
|
87
|
+
Optional[Callable[[str, int], Fab]],
|
|
88
88
|
]
|
|
89
89
|
]:
|
|
90
90
|
"""Primitives for request/response-based interaction with a server.
|
|
@@ -155,10 +155,16 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
155
155
|
ping_thread: Optional[threading.Thread] = None
|
|
156
156
|
ping_stop_event = threading.Event()
|
|
157
157
|
|
|
158
|
+
def _should_giveup_fn(e: Exception) -> bool:
|
|
159
|
+
if e.code() == grpc.StatusCode.PERMISSION_DENIED: # type: ignore
|
|
160
|
+
raise RunNotRunningException
|
|
161
|
+
if e.code() == grpc.StatusCode.UNAVAILABLE: # type: ignore
|
|
162
|
+
return False
|
|
163
|
+
return True
|
|
164
|
+
|
|
158
165
|
# Restrict retries to cases where the status code is UNAVAILABLE
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
166
|
+
# If the status code is PERMISSION_DENIED, additionally raise RunNotRunningException
|
|
167
|
+
retry_invoker.should_giveup = _should_giveup_fn
|
|
162
168
|
|
|
163
169
|
###########################################################################
|
|
164
170
|
# ping/create_node/delete_node/receive/send/get_run functions
|
|
@@ -290,9 +296,9 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
290
296
|
# Return fab_id and fab_version
|
|
291
297
|
return run_from_proto(get_run_response.run)
|
|
292
298
|
|
|
293
|
-
def get_fab(fab_hash: str) -> Fab:
|
|
299
|
+
def get_fab(fab_hash: str, run_id: int) -> Fab:
|
|
294
300
|
# Call FleetAPI
|
|
295
|
-
get_fab_request = GetFabRequest(node=node, hash_str=fab_hash)
|
|
301
|
+
get_fab_request = GetFabRequest(node=node, hash_str=fab_hash, run_id=run_id)
|
|
296
302
|
get_fab_response: GetFabResponse = retry_invoker.invoke(
|
|
297
303
|
stub.GetFab,
|
|
298
304
|
request=get_fab_request,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Client-side message handler."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
from logging import WARN
|
|
18
19
|
from typing import Optional, cast
|
|
19
20
|
|
|
@@ -104,8 +105,6 @@ def handle_legacy_message_from_msgtype(
|
|
|
104
105
|
"Please use `NumPyClient.to_client()` method to convert it to `Client`.",
|
|
105
106
|
)
|
|
106
107
|
|
|
107
|
-
client.set_context(context)
|
|
108
|
-
|
|
109
108
|
message_type = message.metadata.message_type
|
|
110
109
|
|
|
111
110
|
# Handle GetPropertiesIns
|
flwr/client/mod/comms_mods.py
CHANGED
flwr/client/mod/localdp_mod.py
CHANGED
|
@@ -136,7 +136,7 @@ class LocalDpMod:
|
|
|
136
136
|
fit_res.parameters = ndarrays_to_parameters(client_to_server_params)
|
|
137
137
|
|
|
138
138
|
# Add noise to model params
|
|
139
|
-
add_localdp_gaussian_noise_to_params(
|
|
139
|
+
fit_res.parameters = add_localdp_gaussian_noise_to_params(
|
|
140
140
|
fit_res.parameters, self.sensitivity, self.epsilon, self.delta
|
|
141
141
|
)
|
|
142
142
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower NodeState."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
from .in_memory_nodestate import InMemoryNodeState as InMemoryNodeState
|
|
18
19
|
from .nodestate import NodeState as NodeState
|
|
19
20
|
from .nodestate_factory import NodeStateFactory as NodeStateFactory
|
flwr/client/numpy_client.py
CHANGED
|
@@ -21,13 +21,11 @@ from typing import Callable
|
|
|
21
21
|
from flwr.client.client import Client
|
|
22
22
|
from flwr.common import (
|
|
23
23
|
Config,
|
|
24
|
-
Context,
|
|
25
24
|
NDArrays,
|
|
26
25
|
Scalar,
|
|
27
26
|
ndarrays_to_parameters,
|
|
28
27
|
parameters_to_ndarrays,
|
|
29
28
|
)
|
|
30
|
-
from flwr.common.logger import warn_deprecated_feature_with_example
|
|
31
29
|
from flwr.common.typing import (
|
|
32
30
|
Code,
|
|
33
31
|
EvaluateIns,
|
|
@@ -71,8 +69,6 @@ Example
|
|
|
71
69
|
class NumPyClient(ABC):
|
|
72
70
|
"""Abstract base class for Flower clients using NumPy."""
|
|
73
71
|
|
|
74
|
-
_context: Context
|
|
75
|
-
|
|
76
72
|
def get_properties(self, config: Config) -> dict[str, Scalar]:
|
|
77
73
|
"""Return a client's set of properties.
|
|
78
74
|
|
|
@@ -175,34 +171,6 @@ class NumPyClient(ABC):
|
|
|
175
171
|
_ = (self, parameters, config)
|
|
176
172
|
return 0.0, 0, {}
|
|
177
173
|
|
|
178
|
-
@property
|
|
179
|
-
def context(self) -> Context:
|
|
180
|
-
"""Getter for `Context` client attribute."""
|
|
181
|
-
warn_deprecated_feature_with_example(
|
|
182
|
-
"Accessing the context via the client's attribute is deprecated.",
|
|
183
|
-
example_message="Instead, pass it to the client's "
|
|
184
|
-
"constructor in your `client_fn()` which already "
|
|
185
|
-
"receives a context object.",
|
|
186
|
-
code_example="def client_fn(context: Context) -> Client:\n\n"
|
|
187
|
-
"\t\t# Your existing client_fn\n\n"
|
|
188
|
-
"\t\t# Pass `context` to the constructor\n"
|
|
189
|
-
"\t\treturn FlowerClient(context).to_client()",
|
|
190
|
-
)
|
|
191
|
-
return self._context
|
|
192
|
-
|
|
193
|
-
@context.setter
|
|
194
|
-
def context(self, context: Context) -> None:
|
|
195
|
-
"""Setter for `Context` client attribute."""
|
|
196
|
-
self._context = context
|
|
197
|
-
|
|
198
|
-
def get_context(self) -> Context:
|
|
199
|
-
"""Get the run context from this client."""
|
|
200
|
-
return self.context
|
|
201
|
-
|
|
202
|
-
def set_context(self, context: Context) -> None:
|
|
203
|
-
"""Apply a run context to this client."""
|
|
204
|
-
self.context = context
|
|
205
|
-
|
|
206
174
|
def to_client(self) -> Client:
|
|
207
175
|
"""Convert to object to Client type and return it."""
|
|
208
176
|
return _wrap_numpy_client(client=self)
|
|
@@ -299,21 +267,9 @@ def _evaluate(self: Client, ins: EvaluateIns) -> EvaluateRes:
|
|
|
299
267
|
)
|
|
300
268
|
|
|
301
269
|
|
|
302
|
-
def _get_context(self: Client) -> Context:
|
|
303
|
-
"""Return context of underlying NumPyClient."""
|
|
304
|
-
return self.numpy_client.get_context() # type: ignore
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def _set_context(self: Client, context: Context) -> None:
|
|
308
|
-
"""Apply context to underlying NumPyClient."""
|
|
309
|
-
self.numpy_client.set_context(context) # type: ignore
|
|
310
|
-
|
|
311
|
-
|
|
312
270
|
def _wrap_numpy_client(client: NumPyClient) -> Client:
|
|
313
271
|
member_dict: dict[str, Callable] = { # type: ignore
|
|
314
272
|
"__init__": _constructor,
|
|
315
|
-
"get_context": _get_context,
|
|
316
|
-
"set_context": _set_context,
|
|
317
273
|
}
|
|
318
274
|
|
|
319
275
|
# Add wrapper type methods (if overridden)
|
|
@@ -96,7 +96,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
96
96
|
Optional[Callable[[], Optional[int]]],
|
|
97
97
|
Optional[Callable[[], None]],
|
|
98
98
|
Optional[Callable[[int], Run]],
|
|
99
|
-
Optional[Callable[[str], Fab]],
|
|
99
|
+
Optional[Callable[[str, int], Fab]],
|
|
100
100
|
]
|
|
101
101
|
]:
|
|
102
102
|
"""Primitives for request/response-based interaction with a server.
|
|
@@ -361,9 +361,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
361
361
|
|
|
362
362
|
return run_from_proto(res.run)
|
|
363
363
|
|
|
364
|
-
def get_fab(fab_hash: str) -> Fab:
|
|
364
|
+
def get_fab(fab_hash: str, run_id: int) -> Fab:
|
|
365
365
|
# Construct the request
|
|
366
|
-
req = GetFabRequest(node=node, hash_str=fab_hash)
|
|
366
|
+
req = GetFabRequest(node=node, hash_str=fab_hash, run_id=run_id)
|
|
367
367
|
|
|
368
368
|
# Send the request
|
|
369
369
|
res = _request(req, GetFabResponse, PATH_GET_FAB)
|
flwr/client/supernode/app.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower SuperNode."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
import argparse
|
|
18
19
|
import sys
|
|
19
20
|
from logging import DEBUG, ERROR, INFO, WARN
|
|
@@ -113,9 +114,8 @@ def run_client_app() -> None:
|
|
|
113
114
|
event(EventType.RUN_CLIENT_APP_ENTER)
|
|
114
115
|
log(
|
|
115
116
|
ERROR,
|
|
116
|
-
"The command `flower-client-app` has been replaced by `
|
|
117
|
+
"The command `flower-client-app` has been replaced by `flwr run`.",
|
|
117
118
|
)
|
|
118
|
-
log(INFO, "Execute `flower-supernode --help` to learn how to use it.")
|
|
119
119
|
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
|
120
120
|
|
|
121
121
|
|
flwr/common/address.py
CHANGED
flwr/common/args.py
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
"""Auth plugin components."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .auth_plugin import CliAuthPlugin as CliAuthPlugin
|
|
19
|
+
from .auth_plugin import ExecAuthPlugin as ExecAuthPlugin
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CliAuthPlugin",
|
|
23
|
+
"ExecAuthPlugin",
|
|
24
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
"""Abstract classes for Flower User Auth Plugin."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Optional, Union
|
|
22
|
+
|
|
23
|
+
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExecAuthPlugin(ABC):
|
|
27
|
+
"""Abstract Flower Auth Plugin class for ExecServicer.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
config : dict[str, Any]
|
|
32
|
+
The authentication configuration loaded from a YAML file.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def __init__(self, config: dict[str, Any]):
|
|
37
|
+
"""Abstract constructor."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def get_login_details(self) -> dict[str, str]:
|
|
41
|
+
"""Get the login details."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def validate_tokens_in_metadata(
|
|
45
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
46
|
+
) -> bool:
|
|
47
|
+
"""Validate authentication tokens in the provided metadata."""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def get_auth_tokens(self, auth_details: dict[str, str]) -> dict[str, str]:
|
|
51
|
+
"""Get authentication tokens."""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def refresh_tokens(
|
|
55
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
56
|
+
) -> Optional[Sequence[tuple[str, Union[str, bytes]]]]:
|
|
57
|
+
"""Refresh authentication tokens in the provided metadata."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CliAuthPlugin(ABC):
|
|
61
|
+
"""Abstract Flower Auth Plugin class for CLI.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
user_auth_config_path : Path
|
|
66
|
+
The path to the user's authentication configuration file.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def login(
|
|
72
|
+
login_details: dict[str, str],
|
|
73
|
+
exec_stub: ExecStub,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Authenticate the user with the SuperLink.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
login_details : dict[str, str]
|
|
80
|
+
A dictionary containing the user's login details.
|
|
81
|
+
exec_stub : ExecStub
|
|
82
|
+
An instance of `ExecStub` used for communication with the SuperLink.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
user_auth_config : dict[str, Any]
|
|
87
|
+
A dictionary containing the user's authentication configuration
|
|
88
|
+
in JSON format.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def __init__(self, user_auth_config_path: Path):
|
|
93
|
+
"""Abstract constructor."""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def store_tokens(self, user_auth_config: dict[str, Any]) -> None:
|
|
97
|
+
"""Store authentication tokens from the provided user_auth_config.
|
|
98
|
+
|
|
99
|
+
The configuration, including tokens, will be saved as a JSON file
|
|
100
|
+
at `user_auth_config_path`.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def load_tokens(self) -> None:
|
|
105
|
+
"""Load authentication tokens from the user_auth_config_path."""
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def write_tokens_to_metadata(
|
|
109
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
|
110
|
+
) -> Sequence[tuple[str, Union[str, bytes]]]:
|
|
111
|
+
"""Write authentication tokens to the provided metadata."""
|
flwr/common/config.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Provide functions for managing global Flower config."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
import os
|
|
18
19
|
import re
|
|
19
20
|
from pathlib import Path
|
|
@@ -27,6 +28,7 @@ from flwr.common.constant import (
|
|
|
27
28
|
APP_DIR,
|
|
28
29
|
FAB_CONFIG_FILE,
|
|
29
30
|
FAB_HASH_TRUNCATION,
|
|
31
|
+
FLWR_DIR,
|
|
30
32
|
FLWR_HOME,
|
|
31
33
|
)
|
|
32
34
|
from flwr.common.typing import Run, UserConfig, UserConfigValue
|
|
@@ -38,7 +40,7 @@ def get_flwr_dir(provided_path: Optional[str] = None) -> Path:
|
|
|
38
40
|
return Path(
|
|
39
41
|
os.getenv(
|
|
40
42
|
FLWR_HOME,
|
|
41
|
-
Path(f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}") /
|
|
43
|
+
Path(f"{os.getenv('XDG_DATA_HOME', os.getenv('HOME'))}") / FLWR_DIR,
|
|
42
44
|
)
|
|
43
45
|
)
|
|
44
46
|
return Path(provided_path).absolute()
|
flwr/common/constant.py
CHANGED
|
@@ -80,7 +80,8 @@ FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
|
|
|
80
80
|
FAB_CONFIG_FILE = "pyproject.toml"
|
|
81
81
|
FAB_DATE = (2024, 10, 1, 0, 0, 0)
|
|
82
82
|
FAB_HASH_TRUNCATION = 8
|
|
83
|
-
|
|
83
|
+
FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
|
|
84
|
+
FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
|
|
84
85
|
|
|
85
86
|
# Constants entries in Node config for Simulation
|
|
86
87
|
PARTITION_ID_KEY = "partition-id"
|
|
@@ -110,6 +111,10 @@ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
|
|
|
110
111
|
# Retry configurations
|
|
111
112
|
MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
|
|
112
113
|
|
|
114
|
+
# Constants for user authentication
|
|
115
|
+
CREDENTIALS_DIR = ".credentials"
|
|
116
|
+
AUTH_TYPE = "auth_type"
|
|
117
|
+
|
|
113
118
|
|
|
114
119
|
class MessageType:
|
|
115
120
|
"""Message type."""
|
|
@@ -181,3 +186,14 @@ class SubStatus:
|
|
|
181
186
|
def __new__(cls) -> SubStatus:
|
|
182
187
|
"""Prevent instantiation."""
|
|
183
188
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CliOutputFormat:
|
|
192
|
+
"""Define output format for `flwr` CLI commands."""
|
|
193
|
+
|
|
194
|
+
DEFAULT = "default"
|
|
195
|
+
JSON = "json"
|
|
196
|
+
|
|
197
|
+
def __new__(cls) -> CliOutputFormat:
|
|
198
|
+
"""Prevent instantiation."""
|
|
199
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
flwr/common/logger.py
CHANGED
|
@@ -15,16 +15,21 @@
|
|
|
15
15
|
"""Flower Logger."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
import json as _json
|
|
18
19
|
import logging
|
|
20
|
+
import re
|
|
19
21
|
import sys
|
|
20
22
|
import threading
|
|
21
23
|
import time
|
|
24
|
+
from io import StringIO
|
|
22
25
|
from logging import WARN, LogRecord
|
|
23
26
|
from logging.handlers import HTTPHandler
|
|
24
27
|
from queue import Empty, Queue
|
|
25
28
|
from typing import TYPE_CHECKING, Any, Optional, TextIO, Union
|
|
26
29
|
|
|
27
30
|
import grpc
|
|
31
|
+
import typer
|
|
32
|
+
from rich.console import Console
|
|
28
33
|
|
|
29
34
|
from flwr.proto.log_pb2 import PushLogsRequest # pylint: disable=E0611
|
|
30
35
|
from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
|
@@ -303,6 +308,13 @@ def restore_output() -> None:
|
|
|
303
308
|
console_handler.stream = sys.stdout
|
|
304
309
|
|
|
305
310
|
|
|
311
|
+
def redirect_output(output_buffer: StringIO) -> None:
|
|
312
|
+
"""Redirect stdout and stderr to text I/O buffer."""
|
|
313
|
+
sys.stdout = output_buffer
|
|
314
|
+
sys.stderr = output_buffer
|
|
315
|
+
console_handler.stream = sys.stdout
|
|
316
|
+
|
|
317
|
+
|
|
306
318
|
def _log_uploader(
|
|
307
319
|
log_queue: Queue[Optional[str]], node_id: int, run_id: int, stub: ServerAppIoStub
|
|
308
320
|
) -> None:
|
|
@@ -366,3 +378,31 @@ def stop_log_uploader(
|
|
|
366
378
|
"""Stop the log uploader thread."""
|
|
367
379
|
log_queue.put(None)
|
|
368
380
|
log_uploader.join()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _remove_emojis(text: str) -> str:
|
|
384
|
+
"""Remove emojis from the provided text."""
|
|
385
|
+
emoji_pattern = re.compile(
|
|
386
|
+
"["
|
|
387
|
+
"\U0001F600-\U0001F64F" # Emoticons
|
|
388
|
+
"\U0001F300-\U0001F5FF" # Symbols & Pictographs
|
|
389
|
+
"\U0001F680-\U0001F6FF" # Transport & Map Symbols
|
|
390
|
+
"\U0001F1E0-\U0001F1FF" # Flags
|
|
391
|
+
"\U00002702-\U000027B0" # Dingbats
|
|
392
|
+
"\U000024C2-\U0001F251"
|
|
393
|
+
"]+",
|
|
394
|
+
flags=re.UNICODE,
|
|
395
|
+
)
|
|
396
|
+
return emoji_pattern.sub(r"", text)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def print_json_error(msg: str, e: Union[typer.Exit, Exception]) -> None:
|
|
400
|
+
"""Print error message as JSON."""
|
|
401
|
+
Console().print_json(
|
|
402
|
+
_json.dumps(
|
|
403
|
+
{
|
|
404
|
+
"success": False,
|
|
405
|
+
"error-message": _remove_emojis(str(msg) + "\n" + str(e)),
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
)
|
flwr/common/message.py
CHANGED
flwr/common/object_ref.py
CHANGED
|
@@ -21,6 +21,7 @@ import sys
|
|
|
21
21
|
from importlib.util import find_spec
|
|
22
22
|
from logging import WARN
|
|
23
23
|
from pathlib import Path
|
|
24
|
+
from threading import Lock
|
|
24
25
|
from typing import Any, Optional, Union
|
|
25
26
|
|
|
26
27
|
from .logger import log
|
|
@@ -34,6 +35,7 @@ attribute.
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
_current_sys_path: Optional[str] = None
|
|
38
|
+
_import_lock = Lock()
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def validate(
|
|
@@ -146,60 +148,61 @@ def load_app( # pylint: disable= too-many-branches
|
|
|
146
148
|
- This function will modify `sys.path` by inserting the provided `project_dir`
|
|
147
149
|
and removing the previously inserted `project_dir`.
|
|
148
150
|
"""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
project_dir
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
151
|
+
with _import_lock:
|
|
152
|
+
valid, error_msg = validate(module_attribute_str, check_module=False)
|
|
153
|
+
if not valid and error_msg:
|
|
154
|
+
raise error_type(error_msg) from None
|
|
155
|
+
|
|
156
|
+
module_str, _, attributes_str = module_attribute_str.partition(":")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
# Initialize project path
|
|
160
|
+
if project_dir is None:
|
|
161
|
+
project_dir = Path.cwd()
|
|
162
|
+
project_dir = Path(project_dir).absolute()
|
|
163
|
+
|
|
164
|
+
# Unload modules if the project directory has changed
|
|
165
|
+
if _current_sys_path and _current_sys_path != str(project_dir):
|
|
166
|
+
_unload_modules(Path(_current_sys_path))
|
|
167
|
+
|
|
168
|
+
# Set the system path
|
|
169
|
+
_set_sys_path(project_dir)
|
|
170
|
+
|
|
171
|
+
# Import the module
|
|
172
|
+
if module_str not in sys.modules:
|
|
173
|
+
module = importlib.import_module(module_str)
|
|
174
|
+
# Hack: `tabnet` does not work with `importlib.reload`
|
|
175
|
+
elif "tabnet" in sys.modules:
|
|
176
|
+
log(
|
|
177
|
+
WARN,
|
|
178
|
+
"Cannot reload module `%s` from disk due to compatibility issues "
|
|
179
|
+
"with the `tabnet` library. The module will be loaded from the "
|
|
180
|
+
"cache instead. If you experience issues, consider restarting "
|
|
181
|
+
"the application.",
|
|
182
|
+
module_str,
|
|
183
|
+
)
|
|
184
|
+
module = sys.modules[module_str]
|
|
185
|
+
else:
|
|
186
|
+
module = sys.modules[module_str]
|
|
187
|
+
_reload_modules(project_dir)
|
|
188
|
+
|
|
189
|
+
except ModuleNotFoundError as err:
|
|
190
|
+
raise error_type(
|
|
191
|
+
f"Unable to load module {module_str}{OBJECT_REF_HELP_STR}",
|
|
192
|
+
) from err
|
|
193
|
+
|
|
194
|
+
# Recursively load attribute
|
|
195
|
+
attribute = module
|
|
196
|
+
try:
|
|
197
|
+
for attribute_str in attributes_str.split("."):
|
|
198
|
+
attribute = getattr(attribute, attribute_str)
|
|
199
|
+
except AttributeError as err:
|
|
200
|
+
raise error_type(
|
|
201
|
+
f"Unable to load attribute {attributes_str} from module {module_str}"
|
|
202
|
+
f"{OBJECT_REF_HELP_STR}",
|
|
203
|
+
) from err
|
|
204
|
+
|
|
205
|
+
return attribute
|
|
203
206
|
|
|
204
207
|
|
|
205
208
|
def _unload_modules(project_dir: Path) -> None:
|