flwr 1.21.0__py3-none-any.whl → 1.23.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 +17 -1
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +95 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
- flwr/cli/build.py +118 -47
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +34 -23
- flwr/cli/ls.py +13 -9
- flwr/cli/new/new.py +196 -42
- flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
- flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
- flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
- flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
- flwr/cli/new/templates/app/code/{task.pytorch_msg_api.py.tpl → task.pytorch_legacy_api.py.tpl} +27 -14
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/{pyproject.pytorch_msg_api.toml.tpl → pyproject.pytorch_legacy_api.toml.tpl} +3 -3
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
- flwr/cli/pull.py +100 -0
- flwr/cli/run/run.py +11 -7
- flwr/cli/stop.py +2 -2
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +260 -0
- flwr/cli/supernode/register.py +185 -0
- flwr/cli/supernode/unregister.py +138 -0
- flwr/cli/utils.py +109 -69
- flwr/client/__init__.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +6 -8
- flwr/client/grpc_rere_client/connection.py +59 -31
- flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
- flwr/client/rest_client/connection.py +82 -37
- flwr/clientapp/__init__.py +1 -2
- flwr/clientapp/mod/__init__.py +4 -1
- flwr/clientapp/mod/centraldp_mods.py +156 -40
- flwr/clientapp/mod/localdp_mod.py +169 -0
- flwr/clientapp/typing.py +22 -0
- flwr/{client/clientapp → clientapp}/utils.py +1 -1
- flwr/common/constant.py +56 -13
- flwr/common/exit/exit_code.py +24 -10
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/common/record/arrayrecord.py +10 -1
- flwr/common/record/typeddict.py +12 -0
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/serde.py +4 -2
- flwr/common/typing.py +7 -6
- flwr/compat/client/app.py +1 -1
- flwr/compat/client/grpc_client/connection.py +2 -2
- flwr/proto/control_pb2.py +48 -31
- flwr/proto/control_pb2.pyi +95 -5
- flwr/proto/control_pb2_grpc.py +136 -0
- flwr/proto/control_pb2_grpc.pyi +52 -0
- flwr/proto/fab_pb2.py +11 -7
- flwr/proto/fab_pb2.pyi +21 -1
- flwr/proto/fleet_pb2.py +31 -23
- flwr/proto/fleet_pb2.pyi +63 -23
- flwr/proto/fleet_pb2_grpc.py +98 -28
- flwr/proto/fleet_pb2_grpc.pyi +45 -13
- flwr/proto/node_pb2.py +3 -1
- flwr/proto/node_pb2.pyi +48 -0
- flwr/server/app.py +152 -114
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
- flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
- flwr/server/superlink/fleet/vce/vce_api.py +18 -5
- flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
- flwr/server/superlink/linkstate/linkstate.py +107 -24
- flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
- flwr/server/superlink/linkstate/utils.py +3 -54
- flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
- flwr/serverapp/strategy/__init__.py +26 -0
- flwr/serverapp/strategy/bulyan.py +238 -0
- flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +71 -49
- flwr/serverapp/strategy/fedadagrad.py +0 -3
- flwr/serverapp/strategy/fedadam.py +0 -3
- flwr/serverapp/strategy/fedavg.py +89 -64
- flwr/serverapp/strategy/fedavgm.py +198 -0
- flwr/serverapp/strategy/fedmedian.py +105 -0
- flwr/serverapp/strategy/fedprox.py +174 -0
- flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
- flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
- flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
- flwr/serverapp/strategy/fedyogi.py +0 -3
- flwr/serverapp/strategy/krum.py +112 -0
- flwr/serverapp/strategy/multikrum.py +247 -0
- flwr/serverapp/strategy/qfedavg.py +252 -0
- flwr/serverapp/strategy/strategy_utils.py +48 -0
- flwr/simulation/app.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/run_simulation.py +28 -32
- flwr/supercore/cli/flower_superexec.py +26 -1
- flwr/supercore/constant.py +41 -0
- flwr/supercore/object_store/in_memory_object_store.py +0 -4
- flwr/supercore/object_store/object_store_factory.py +26 -6
- flwr/supercore/object_store/sqlite_object_store.py +252 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
- flwr/supercore/sqlite_mixin.py +156 -0
- flwr/supercore/superexec/plugin/exec_plugin.py +11 -1
- flwr/supercore/superexec/run_superexec.py +16 -2
- flwr/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/__init__.py +22 -0
- flwr/superlink/artifact_provider/artifact_provider.py +37 -0
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +91 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
- flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
- flwr/superlink/servicer/control/control_grpc.py +16 -11
- flwr/superlink/servicer/control/control_servicer.py +207 -58
- flwr/supernode/cli/flower_supernode.py +19 -26
- flwr/supernode/runtime/run_clientapp.py +2 -2
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
- flwr/supernode/start_client_internal.py +17 -9
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/METADATA +6 -16
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/RECORD +170 -140
- flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +0 -80
- flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -41
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/serverapp/dp_fixed_clipping.py +0 -352
- flwr/serverapp/strategy/strategy_utils_tests.py +0 -304
- /flwr/cli/new/templates/app/code/{__init__.pytorch_msg_api.py.tpl → __init__.pytorch_legacy_api.py.tpl} +0 -0
- /flwr/{client → clientapp}/client_app.py +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
"""Asymmetric cryptography utilities."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from typing import cast
|
|
19
|
+
|
|
20
|
+
from cryptography.exceptions import InvalidSignature
|
|
21
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_key_pairs() -> (
|
|
26
|
+
tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
|
27
|
+
):
|
|
28
|
+
"""Generate private and public key pairs with Cryptography."""
|
|
29
|
+
private_key = ec.generate_private_key(ec.SECP384R1())
|
|
30
|
+
public_key = private_key.public_key()
|
|
31
|
+
return private_key, public_key
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def private_key_to_bytes(private_key: ec.EllipticCurvePrivateKey) -> bytes:
|
|
35
|
+
"""Serialize private key to bytes."""
|
|
36
|
+
return private_key.private_bytes(
|
|
37
|
+
encoding=serialization.Encoding.PEM,
|
|
38
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
39
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def bytes_to_private_key(private_key_bytes: bytes) -> ec.EllipticCurvePrivateKey:
|
|
44
|
+
"""Deserialize private key from bytes."""
|
|
45
|
+
return cast(
|
|
46
|
+
ec.EllipticCurvePrivateKey,
|
|
47
|
+
serialization.load_pem_private_key(data=private_key_bytes, password=None),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def public_key_to_bytes(public_key: ec.EllipticCurvePublicKey) -> bytes:
|
|
52
|
+
"""Serialize public key to bytes."""
|
|
53
|
+
return public_key.public_bytes(
|
|
54
|
+
encoding=serialization.Encoding.PEM,
|
|
55
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def bytes_to_public_key(public_key_bytes: bytes) -> ec.EllipticCurvePublicKey:
|
|
60
|
+
"""Deserialize public key from bytes."""
|
|
61
|
+
return cast(
|
|
62
|
+
ec.EllipticCurvePublicKey,
|
|
63
|
+
serialization.load_pem_public_key(data=public_key_bytes),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes:
|
|
68
|
+
"""Sign a message using the provided EC private key.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
private_key : ec.EllipticCurvePrivateKey
|
|
73
|
+
The EC private key to sign the message with.
|
|
74
|
+
message : bytes
|
|
75
|
+
The message to be signed.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
bytes
|
|
80
|
+
The signature of the message.
|
|
81
|
+
"""
|
|
82
|
+
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
|
|
83
|
+
return signature
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def verify_signature(
|
|
87
|
+
public_key: ec.EllipticCurvePublicKey, message: bytes, signature: bytes
|
|
88
|
+
) -> bool:
|
|
89
|
+
"""Verify a signature against a message using the provided EC public key.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
public_key : ec.EllipticCurvePublicKey
|
|
94
|
+
The EC public key to verify the signature.
|
|
95
|
+
message : bytes
|
|
96
|
+
The original message.
|
|
97
|
+
signature : bytes
|
|
98
|
+
The signature to verify.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
bool
|
|
103
|
+
True if the signature is valid, False otherwise.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
|
|
107
|
+
return True
|
|
108
|
+
except InvalidSignature:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def uses_nist_ec_curve(public_key: ec.EllipticCurvePublicKey) -> bool:
|
|
113
|
+
"""Return True if the provided key uses a NIST EC curve."""
|
|
114
|
+
return isinstance(
|
|
115
|
+
public_key.curve,
|
|
116
|
+
(ec.SECP192R1, ec.SECP224R1, ec.SECP256R1, ec.SECP384R1, ec.SECP521R1),
|
|
117
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
"""Ed25519-only asymmetric cryptography utilities."""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
|
|
19
|
+
from cryptography.exceptions import InvalidSignature
|
|
20
|
+
from cryptography.hazmat.primitives import serialization
|
|
21
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_key_pair() -> tuple[ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey]:
|
|
25
|
+
"""Generate an Ed25519 private/public key pair.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
Tuple[Ed25519PrivateKey, Ed25519PublicKey]
|
|
30
|
+
Private and public key pair.
|
|
31
|
+
"""
|
|
32
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
33
|
+
return private_key, private_key.public_key()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def private_key_to_bytes(private_key: ed25519.Ed25519PrivateKey) -> bytes:
|
|
37
|
+
"""Serialize an Ed25519 private key to PEM bytes.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
private_key : Ed25519PrivateKey
|
|
42
|
+
The private key to serialize.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
bytes
|
|
47
|
+
PEM-encoded private key.
|
|
48
|
+
"""
|
|
49
|
+
return private_key.private_bytes(
|
|
50
|
+
encoding=serialization.Encoding.PEM,
|
|
51
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
52
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def bytes_to_private_key(private_key_bytes: bytes) -> ed25519.Ed25519PrivateKey:
|
|
57
|
+
"""Deserialize an Ed25519 private key from PEM bytes.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
private_key_bytes : bytes
|
|
62
|
+
PEM-encoded private key.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
Ed25519PrivateKey
|
|
67
|
+
Deserialized private key.
|
|
68
|
+
"""
|
|
69
|
+
return serialization.load_pem_private_key(
|
|
70
|
+
private_key_bytes, password=None
|
|
71
|
+
) # type: ignore[return-value]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def public_key_to_bytes(public_key: ed25519.Ed25519PublicKey) -> bytes:
|
|
75
|
+
"""Serialize an Ed25519 public key to PEM bytes.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
public_key : Ed25519PublicKey
|
|
80
|
+
The public key to serialize.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
bytes
|
|
85
|
+
PEM-encoded public key.
|
|
86
|
+
"""
|
|
87
|
+
return public_key.public_bytes(
|
|
88
|
+
encoding=serialization.Encoding.PEM,
|
|
89
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def bytes_to_public_key(public_key_bytes: bytes) -> ed25519.Ed25519PublicKey:
|
|
94
|
+
"""Deserialize an Ed25519 public key from PEM bytes.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
public_key_bytes : bytes
|
|
99
|
+
PEM-encoded public key.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
Ed25519PublicKey
|
|
104
|
+
Deserialized public key.
|
|
105
|
+
"""
|
|
106
|
+
return serialization.load_pem_public_key(public_key_bytes) # type: ignore[return-value]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def sign_message(private_key: ed25519.Ed25519PrivateKey, message: bytes) -> bytes:
|
|
110
|
+
"""Sign a message using an Ed25519 private key.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
private_key : Ed25519PrivateKey
|
|
115
|
+
The private key used for signing.
|
|
116
|
+
message : bytes
|
|
117
|
+
The message to sign.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
bytes
|
|
122
|
+
The signature of the message.
|
|
123
|
+
"""
|
|
124
|
+
return private_key.sign(message)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def verify_signature(
|
|
128
|
+
public_key: ed25519.Ed25519PublicKey, message: bytes, signature: bytes
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Verify a signature using an Ed25519 public key.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
public_key : Ed25519PublicKey
|
|
135
|
+
The public key used for verification.
|
|
136
|
+
message : bytes
|
|
137
|
+
The original message.
|
|
138
|
+
signature : bytes
|
|
139
|
+
The signature to verify.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
bool
|
|
144
|
+
True if the signature is valid, False otherwise.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
public_key.verify(signature, message)
|
|
148
|
+
return True
|
|
149
|
+
except InvalidSignature:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_signed_message(fab_digest: bytes, timestamp: int) -> bytes:
|
|
154
|
+
"""Create a canonical message:
|
|
155
|
+
timestamp (8 bytes big-endian) + fab_digest.
|
|
156
|
+
"""
|
|
157
|
+
timestamp_bytes = timestamp.to_bytes(8, byteorder="big")
|
|
158
|
+
return timestamp_bytes + fab_digest
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def decode_base64url(sig: str) -> bytes:
|
|
162
|
+
"""Convert signature to b64 format."""
|
|
163
|
+
# add missing padding (=) to a multiple of 4
|
|
164
|
+
pad = (-len(sig)) % 4
|
|
165
|
+
return base64.urlsafe_b64decode(sig + ("=" * pad))
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
"""Mixin providing common SQLite connection and initialization logic."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import sqlite3
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from logging import DEBUG, ERROR
|
|
23
|
+
from typing import Any, Optional, Union
|
|
24
|
+
|
|
25
|
+
from flwr.common.logger import log
|
|
26
|
+
|
|
27
|
+
DictOrTuple = Union[tuple[Any, ...], dict[str, Any]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SqliteMixin(ABC):
|
|
31
|
+
"""Mixin providing common SQLite connection and initialization logic."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, database_path: str) -> None:
|
|
34
|
+
self.database_path = database_path
|
|
35
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def conn(self) -> sqlite3.Connection:
|
|
39
|
+
"""Get the SQLite connection."""
|
|
40
|
+
if self._conn is None:
|
|
41
|
+
raise AttributeError("Database not initialized. Call initialize() first.")
|
|
42
|
+
return self._conn
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
46
|
+
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
log_queries : bool
|
|
51
|
+
Log each query which is executed.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
list[tuple[str]]
|
|
56
|
+
The list of all tables in the DB.
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
Implement in subclass:
|
|
61
|
+
|
|
62
|
+
.. code:: python
|
|
63
|
+
|
|
64
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
65
|
+
return self._ensure_initialized(
|
|
66
|
+
SQL_CREATE_TABLE_FOO,
|
|
67
|
+
SQL_CREATE_TABLE_BAR,
|
|
68
|
+
log_queries=log_queries
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def _ensure_initialized(
|
|
73
|
+
self,
|
|
74
|
+
*create_statements: str,
|
|
75
|
+
log_queries: bool = False,
|
|
76
|
+
) -> list[tuple[str]]:
|
|
77
|
+
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
78
|
+
|
|
79
|
+
Subclasses should call this with their own CREATE TABLE/INDEX statements in
|
|
80
|
+
their `.initialize()` methods.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
create_statements : str
|
|
85
|
+
SQL statements to create tables and indexes.
|
|
86
|
+
log_queries : bool
|
|
87
|
+
Log each query which is executed.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
list[tuple[str]]
|
|
92
|
+
The list of all tables in the DB.
|
|
93
|
+
"""
|
|
94
|
+
self._conn = sqlite3.connect(self.database_path)
|
|
95
|
+
# Enable Write-Ahead Logging (WAL) for better concurrency
|
|
96
|
+
self._conn.execute("PRAGMA journal_mode = WAL;")
|
|
97
|
+
self._conn.execute("PRAGMA synchronous = NORMAL;")
|
|
98
|
+
self._conn.execute("PRAGMA foreign_keys = ON;")
|
|
99
|
+
self._conn.row_factory = dict_factory
|
|
100
|
+
|
|
101
|
+
if log_queries:
|
|
102
|
+
self._conn.set_trace_callback(lambda q: log(DEBUG, q))
|
|
103
|
+
|
|
104
|
+
# Create tables and indexes
|
|
105
|
+
cur = self._conn.cursor()
|
|
106
|
+
for sql in create_statements:
|
|
107
|
+
cur.execute(sql)
|
|
108
|
+
res = cur.execute("SELECT name FROM sqlite_schema;")
|
|
109
|
+
return res.fetchall()
|
|
110
|
+
|
|
111
|
+
def query(
|
|
112
|
+
self,
|
|
113
|
+
query: str,
|
|
114
|
+
data: Optional[Union[Sequence[DictOrTuple], DictOrTuple]] = None,
|
|
115
|
+
) -> list[dict[str, Any]]:
|
|
116
|
+
"""Execute a SQL query and return the results as list of dicts."""
|
|
117
|
+
if self._conn is None:
|
|
118
|
+
raise AttributeError("LinkState is not initialized.")
|
|
119
|
+
|
|
120
|
+
if data is None:
|
|
121
|
+
data = []
|
|
122
|
+
|
|
123
|
+
# Clean up whitespace to make the logs nicer
|
|
124
|
+
query = re.sub(r"\s+", " ", query)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
with self._conn:
|
|
128
|
+
if (
|
|
129
|
+
len(data) > 0
|
|
130
|
+
and isinstance(data, (tuple, list))
|
|
131
|
+
and isinstance(data[0], (tuple, dict))
|
|
132
|
+
):
|
|
133
|
+
rows = self._conn.executemany(query, data)
|
|
134
|
+
else:
|
|
135
|
+
rows = self._conn.execute(query, data)
|
|
136
|
+
|
|
137
|
+
# Extract results before committing to support
|
|
138
|
+
# INSERT/UPDATE ... RETURNING
|
|
139
|
+
# style queries
|
|
140
|
+
result = rows.fetchall()
|
|
141
|
+
except KeyError as exc:
|
|
142
|
+
log(ERROR, {"query": query, "data": data, "exception": exc})
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def dict_factory(
|
|
148
|
+
cursor: sqlite3.Cursor,
|
|
149
|
+
row: sqlite3.Row,
|
|
150
|
+
) -> dict[str, Any]:
|
|
151
|
+
"""Turn SQLite results into dicts.
|
|
152
|
+
|
|
153
|
+
Less efficent for retrival of large amounts of data but easier to use.
|
|
154
|
+
"""
|
|
155
|
+
fields = [column[0] for column in cursor.description]
|
|
156
|
+
return dict(zip(fields, row))
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
from abc import ABC, abstractmethod
|
|
19
19
|
from collections.abc import Sequence
|
|
20
|
-
from typing import Callable, Optional
|
|
20
|
+
from typing import Any, Callable, Optional
|
|
21
21
|
|
|
22
22
|
from flwr.common.typing import Run
|
|
23
23
|
|
|
@@ -69,3 +69,13 @@ class ExecPlugin(ABC):
|
|
|
69
69
|
The ID of the run associated with the token, used for tracking or
|
|
70
70
|
logging purposes.
|
|
71
71
|
"""
|
|
72
|
+
|
|
73
|
+
# This method is optional to implement
|
|
74
|
+
def load_config(self, yaml_config: dict[str, Any]) -> None:
|
|
75
|
+
"""Load configuration from a YAML dictionary.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
yaml_config : dict[str, Any]
|
|
80
|
+
A dictionary representing the YAML configuration.
|
|
81
|
+
"""
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
import time
|
|
19
19
|
from logging import WARN
|
|
20
|
-
from typing import Optional, Union
|
|
20
|
+
from typing import Any, Optional, Union
|
|
21
21
|
|
|
22
22
|
from flwr.common.config import get_flwr_dir
|
|
23
|
-
from flwr.common.exit import register_signal_handlers
|
|
23
|
+
from flwr.common.exit import ExitCode, flwr_exit, register_signal_handlers
|
|
24
24
|
from flwr.common.grpc import create_channel, on_channel_state_change
|
|
25
25
|
from flwr.common.logger import log
|
|
26
26
|
from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
|
|
@@ -47,6 +47,7 @@ def run_superexec( # pylint: disable=R0913,R0914,R0917
|
|
|
47
47
|
type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
|
|
48
48
|
],
|
|
49
49
|
appio_api_address: str,
|
|
50
|
+
plugin_config: Optional[dict[str, Any]] = None,
|
|
50
51
|
flwr_dir: Optional[str] = None,
|
|
51
52
|
parent_pid: Optional[int] = None,
|
|
52
53
|
health_server_address: Optional[str] = None,
|
|
@@ -61,6 +62,9 @@ def run_superexec( # pylint: disable=R0913,R0914,R0917
|
|
|
61
62
|
The gRPC stub class for the AppIO API.
|
|
62
63
|
appio_api_address : str
|
|
63
64
|
The address of the AppIO API.
|
|
65
|
+
plugin_config : Optional[dict[str, Any]] (default: None)
|
|
66
|
+
The configuration dictionary for the plugin. If `None`, the plugin will use
|
|
67
|
+
its default configuration.
|
|
64
68
|
flwr_dir : Optional[str] (default: None)
|
|
65
69
|
The Flower directory.
|
|
66
70
|
parent_pid : Optional[int] (default: None)
|
|
@@ -113,6 +117,16 @@ def run_superexec( # pylint: disable=R0913,R0914,R0917
|
|
|
113
117
|
get_run=get_run,
|
|
114
118
|
)
|
|
115
119
|
|
|
120
|
+
# Load plugin configuration from file if provided
|
|
121
|
+
try:
|
|
122
|
+
if plugin_config is not None:
|
|
123
|
+
plugin.load_config(plugin_config)
|
|
124
|
+
except (KeyError, ValueError) as e:
|
|
125
|
+
flwr_exit(
|
|
126
|
+
code=ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG,
|
|
127
|
+
message=f"Invalid plugin config: {e!r}",
|
|
128
|
+
)
|
|
129
|
+
|
|
116
130
|
# Start the main loop
|
|
117
131
|
try:
|
|
118
132
|
while True:
|
flwr/supercore/utils.py
CHANGED
|
@@ -30,3 +30,23 @@ def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
|
|
|
30
30
|
if len(value) <= head + tail:
|
|
31
31
|
return value
|
|
32
32
|
return f"{value[:head]}...{value[-tail:]}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def uint64_to_int64(unsigned: int) -> int:
|
|
36
|
+
"""Convert a uint64 integer to a sint64 with the same bit pattern.
|
|
37
|
+
|
|
38
|
+
For values >= 2^63, wraps around by subtracting 2^64.
|
|
39
|
+
"""
|
|
40
|
+
if unsigned >= (1 << 63):
|
|
41
|
+
return unsigned - (1 << 64)
|
|
42
|
+
return unsigned
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def int64_to_uint64(signed: int) -> int:
|
|
46
|
+
"""Convert a sint64 integer to a uint64 with the same bit pattern.
|
|
47
|
+
|
|
48
|
+
For negative values, wraps around by adding 2^64.
|
|
49
|
+
"""
|
|
50
|
+
if signed < 0:
|
|
51
|
+
return signed + (1 << 64)
|
|
52
|
+
return signed
|
|
@@ -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
|
+
"""ArtifactProvider for SuperLink."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .artifact_provider import ArtifactProvider
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ArtifactProvider",
|
|
22
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
"""Abstract base class for ArtifactProvider."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ArtifactProvider(ABC):
|
|
23
|
+
"""ArtifactProvider interface for providing artifact download links."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def get_url(self, run_id: int) -> Optional[str]:
|
|
27
|
+
"""Return the artifact download link for the given run ID."""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def output_dir(self) -> str:
|
|
32
|
+
"""Permanent storage directory."""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def tmp_dir(self) -> str:
|
|
37
|
+
"""Temporary storage directory."""
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""
|
|
15
|
+
"""Account auth plugin for ControlServicer."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from .auth_plugin import
|
|
19
|
-
from .
|
|
20
|
-
from .auth_plugin import ControlAuthzPlugin as ControlAuthzPlugin
|
|
18
|
+
from .auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
|
|
19
|
+
from .noop_auth_plugin import NoOpControlAuthnPlugin, NoOpControlAuthzPlugin
|
|
21
20
|
|
|
22
21
|
__all__ = [
|
|
23
|
-
"
|
|
24
|
-
"ControlAuthPlugin",
|
|
22
|
+
"ControlAuthnPlugin",
|
|
25
23
|
"ControlAuthzPlugin",
|
|
24
|
+
"NoOpControlAuthnPlugin",
|
|
25
|
+
"NoOpControlAuthzPlugin",
|
|
26
26
|
]
|