flwr-nightly 1.15.0.dev20250124__py3-none-any.whl → 1.15.0.dev20250127__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/auth_plugin/__init__.py +31 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +150 -0
- flwr/cli/login/login.py +9 -3
- flwr/cli/utils.py +19 -19
- flwr/common/constant.py +11 -1
- flwr/common/logger.py +20 -6
- flwr/server/app.py +20 -81
- flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -25
- flwr/server/superlink/linkstate/linkstate.py +2 -16
- flwr/server/superlink/linkstate/sqlite_linkstate.py +3 -48
- {flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/METADATA +1 -1
- {flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/RECORD +15 -13
- {flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
# ==============================================================================
|
15
|
+
"""Flower user auth plugins."""
|
16
|
+
|
17
|
+
|
18
|
+
from flwr.common.auth_plugin import CliAuthPlugin
|
19
|
+
from flwr.common.constant import AuthType
|
20
|
+
|
21
|
+
from .oidc_cli_plugin import OidcCliPlugin
|
22
|
+
|
23
|
+
|
24
|
+
def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
|
25
|
+
"""Return all CLI authentication plugins."""
|
26
|
+
return {AuthType.OIDC: OidcCliPlugin}
|
27
|
+
|
28
|
+
|
29
|
+
__all__ = [
|
30
|
+
"get_cli_auth_plugins",
|
31
|
+
]
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
# ==============================================================================
|
15
|
+
"""Flower CLI user auth plugin for OIDC."""
|
16
|
+
|
17
|
+
|
18
|
+
import json
|
19
|
+
import time
|
20
|
+
from collections.abc import Sequence
|
21
|
+
from pathlib import Path
|
22
|
+
from typing import Any, Optional, Union
|
23
|
+
|
24
|
+
import typer
|
25
|
+
|
26
|
+
from flwr.common.auth_plugin import CliAuthPlugin
|
27
|
+
from flwr.common.constant import (
|
28
|
+
ACCESS_TOKEN_KEY,
|
29
|
+
AUTH_TYPE_KEY,
|
30
|
+
REFRESH_TOKEN_KEY,
|
31
|
+
AuthType,
|
32
|
+
)
|
33
|
+
from flwr.common.typing import UserAuthCredentials, UserAuthLoginDetails
|
34
|
+
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
35
|
+
GetAuthTokensRequest,
|
36
|
+
GetAuthTokensResponse,
|
37
|
+
)
|
38
|
+
from flwr.proto.exec_pb2_grpc import ExecStub
|
39
|
+
|
40
|
+
|
41
|
+
class OidcCliPlugin(CliAuthPlugin):
|
42
|
+
"""Flower OIDC auth plugin for CLI."""
|
43
|
+
|
44
|
+
def __init__(self, credentials_path: Path):
|
45
|
+
self.access_token: Optional[str] = None
|
46
|
+
self.refresh_token: Optional[str] = None
|
47
|
+
self.credentials_path = credentials_path
|
48
|
+
|
49
|
+
@staticmethod
|
50
|
+
def login(
|
51
|
+
login_details: UserAuthLoginDetails,
|
52
|
+
exec_stub: ExecStub,
|
53
|
+
) -> UserAuthCredentials:
|
54
|
+
"""Authenticate the user and retrieve authentication credentials."""
|
55
|
+
typer.secho(
|
56
|
+
"Please login with your user credentials here: "
|
57
|
+
f"{login_details.verification_uri_complete}",
|
58
|
+
fg=typer.colors.BLUE,
|
59
|
+
)
|
60
|
+
start_time = time.time()
|
61
|
+
time.sleep(login_details.interval)
|
62
|
+
|
63
|
+
while (time.time() - start_time) < login_details.expires_in:
|
64
|
+
res: GetAuthTokensResponse = exec_stub.GetAuthTokens(
|
65
|
+
GetAuthTokensRequest(device_code=login_details.device_code)
|
66
|
+
)
|
67
|
+
|
68
|
+
access_token = res.access_token
|
69
|
+
refresh_token = res.refresh_token
|
70
|
+
|
71
|
+
if access_token and refresh_token:
|
72
|
+
typer.secho(
|
73
|
+
"✅ Login successful.",
|
74
|
+
fg=typer.colors.GREEN,
|
75
|
+
bold=False,
|
76
|
+
)
|
77
|
+
return UserAuthCredentials(
|
78
|
+
access_token=access_token,
|
79
|
+
refresh_token=refresh_token,
|
80
|
+
)
|
81
|
+
|
82
|
+
time.sleep(login_details.interval)
|
83
|
+
|
84
|
+
typer.secho(
|
85
|
+
"❌ Timeout, failed to sign in.",
|
86
|
+
fg=typer.colors.RED,
|
87
|
+
bold=True,
|
88
|
+
)
|
89
|
+
raise typer.Exit(code=1)
|
90
|
+
|
91
|
+
def store_tokens(self, credentials: UserAuthCredentials) -> None:
|
92
|
+
"""Store authentication tokens to the `credentials_path`.
|
93
|
+
|
94
|
+
The credentials, including tokens, will be saved as a JSON file
|
95
|
+
at `credentials_path`.
|
96
|
+
"""
|
97
|
+
self.access_token = credentials.access_token
|
98
|
+
self.refresh_token = credentials.refresh_token
|
99
|
+
json_dict = {
|
100
|
+
AUTH_TYPE_KEY: AuthType.OIDC,
|
101
|
+
ACCESS_TOKEN_KEY: credentials.access_token,
|
102
|
+
REFRESH_TOKEN_KEY: credentials.refresh_token,
|
103
|
+
}
|
104
|
+
|
105
|
+
with open(self.credentials_path, "w", encoding="utf-8") as file:
|
106
|
+
json.dump(json_dict, file, indent=4)
|
107
|
+
|
108
|
+
def load_tokens(self) -> None:
|
109
|
+
"""Load authentication tokens from the `credentials_path`."""
|
110
|
+
with open(self.credentials_path, encoding="utf-8") as file:
|
111
|
+
json_dict: dict[str, Any] = json.load(file)
|
112
|
+
access_token = json_dict.get(ACCESS_TOKEN_KEY)
|
113
|
+
refresh_token = json_dict.get(REFRESH_TOKEN_KEY)
|
114
|
+
|
115
|
+
if isinstance(access_token, str) and isinstance(refresh_token, str):
|
116
|
+
self.access_token = access_token
|
117
|
+
self.refresh_token = refresh_token
|
118
|
+
|
119
|
+
def write_tokens_to_metadata(
|
120
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
121
|
+
) -> Sequence[tuple[str, Union[str, bytes]]]:
|
122
|
+
"""Write authentication tokens to the provided metadata."""
|
123
|
+
if self.access_token is None or self.refresh_token is None:
|
124
|
+
typer.secho(
|
125
|
+
"❌ Missing authentication tokens. Please login first.",
|
126
|
+
fg=typer.colors.RED,
|
127
|
+
bold=True,
|
128
|
+
)
|
129
|
+
raise typer.Exit(code=1)
|
130
|
+
|
131
|
+
return list(metadata) + [
|
132
|
+
(ACCESS_TOKEN_KEY, self.access_token),
|
133
|
+
(REFRESH_TOKEN_KEY, self.refresh_token),
|
134
|
+
]
|
135
|
+
|
136
|
+
def read_tokens_from_metadata(
|
137
|
+
self, metadata: Sequence[tuple[str, Union[str, bytes]]]
|
138
|
+
) -> Optional[UserAuthCredentials]:
|
139
|
+
"""Read authentication tokens from the provided metadata."""
|
140
|
+
metadata_dict = dict(metadata)
|
141
|
+
access_token = metadata_dict.get(ACCESS_TOKEN_KEY)
|
142
|
+
refresh_token = metadata_dict.get(REFRESH_TOKEN_KEY)
|
143
|
+
|
144
|
+
if isinstance(access_token, str) and isinstance(refresh_token, str):
|
145
|
+
return UserAuthCredentials(
|
146
|
+
access_token=access_token,
|
147
|
+
refresh_token=refresh_token,
|
148
|
+
)
|
149
|
+
|
150
|
+
return None
|
flwr/cli/login/login.py
CHANGED
@@ -34,7 +34,11 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
34
34
|
)
|
35
35
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
36
36
|
|
37
|
-
from ..utils import
|
37
|
+
from ..utils import (
|
38
|
+
init_channel,
|
39
|
+
try_obtain_cli_auth_plugin,
|
40
|
+
unauthenticated_exc_handler,
|
41
|
+
)
|
38
42
|
|
39
43
|
|
40
44
|
def login( # pylint: disable=R0914
|
@@ -81,7 +85,8 @@ def login( # pylint: disable=R0914
|
|
81
85
|
stub = ExecStub(channel)
|
82
86
|
|
83
87
|
login_request = GetLoginDetailsRequest()
|
84
|
-
|
88
|
+
with unauthenticated_exc_handler():
|
89
|
+
login_response: GetLoginDetailsResponse = stub.GetLoginDetails(login_request)
|
85
90
|
|
86
91
|
# Get the auth plugin
|
87
92
|
auth_type = login_response.auth_type
|
@@ -104,7 +109,8 @@ def login( # pylint: disable=R0914
|
|
104
109
|
expires_in=login_response.expires_in,
|
105
110
|
interval=login_response.interval,
|
106
111
|
)
|
107
|
-
|
112
|
+
with unauthenticated_exc_handler():
|
113
|
+
credentials = auth_plugin.login(details, stub)
|
108
114
|
|
109
115
|
# Store the tokens
|
110
116
|
auth_plugin.store_tokens(credentials)
|
flwr/cli/utils.py
CHANGED
@@ -29,20 +29,13 @@ import typer
|
|
29
29
|
|
30
30
|
from flwr.cli.cli_user_auth_interceptor import CliUserAuthInterceptor
|
31
31
|
from flwr.common.auth_plugin import CliAuthPlugin
|
32
|
-
from flwr.common.constant import
|
32
|
+
from flwr.common.constant import AUTH_TYPE_KEY, CREDENTIALS_DIR, FLWR_DIR
|
33
33
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
34
34
|
from flwr.common.logger import log
|
35
35
|
|
36
|
+
from .auth_plugin import get_cli_auth_plugins
|
36
37
|
from .config_utils import validate_certificate_in_federation_config
|
37
38
|
|
38
|
-
try:
|
39
|
-
from flwr.ee import get_cli_auth_plugins
|
40
|
-
except ImportError:
|
41
|
-
|
42
|
-
def get_cli_auth_plugins() -> dict[str, type[CliAuthPlugin]]:
|
43
|
-
"""Return all CLI authentication plugins."""
|
44
|
-
raise NotImplementedError("No authentication plugins are currently supported.")
|
45
|
-
|
46
39
|
|
47
40
|
def prompt_text(
|
48
41
|
text: str,
|
@@ -244,7 +237,7 @@ def try_obtain_cli_auth_plugin(
|
|
244
237
|
try:
|
245
238
|
with config_path.open("r", encoding="utf-8") as file:
|
246
239
|
json_file = json.load(file)
|
247
|
-
auth_type = json_file[
|
240
|
+
auth_type = json_file[AUTH_TYPE_KEY]
|
248
241
|
except (FileNotFoundError, KeyError):
|
249
242
|
typer.secho(
|
250
243
|
"❌ Missing or invalid credentials for user authentication. "
|
@@ -308,12 +301,19 @@ def unauthenticated_exc_handler() -> Iterator[None]:
|
|
308
301
|
try:
|
309
302
|
yield
|
310
303
|
except grpc.RpcError as e:
|
311
|
-
if e.code()
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
304
|
+
if e.code() == grpc.StatusCode.UNAUTHENTICATED:
|
305
|
+
typer.secho(
|
306
|
+
"❌ Authentication failed. Please run `flwr login`"
|
307
|
+
" to authenticate and try again.",
|
308
|
+
fg=typer.colors.RED,
|
309
|
+
bold=True,
|
310
|
+
)
|
311
|
+
raise typer.Exit(code=1) from None
|
312
|
+
if e.code() == grpc.StatusCode.UNIMPLEMENTED:
|
313
|
+
typer.secho(
|
314
|
+
"❌ User authentication is not enabled on this SuperLink.",
|
315
|
+
fg=typer.colors.RED,
|
316
|
+
bold=True,
|
317
|
+
)
|
318
|
+
raise typer.Exit(code=1) from None
|
319
|
+
raise
|
flwr/common/constant.py
CHANGED
@@ -108,7 +108,7 @@ MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
|
|
108
108
|
|
109
109
|
# Constants for user authentication
|
110
110
|
CREDENTIALS_DIR = ".credentials"
|
111
|
-
|
111
|
+
AUTH_TYPE_KEY = "auth_type"
|
112
112
|
ACCESS_TOKEN_KEY = "access_token"
|
113
113
|
REFRESH_TOKEN_KEY = "refresh_token"
|
114
114
|
|
@@ -200,3 +200,13 @@ class CliOutputFormat:
|
|
200
200
|
def __new__(cls) -> CliOutputFormat:
|
201
201
|
"""Prevent instantiation."""
|
202
202
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
203
|
+
|
204
|
+
|
205
|
+
class AuthType:
|
206
|
+
"""User authentication types."""
|
207
|
+
|
208
|
+
OIDC = "oidc"
|
209
|
+
|
210
|
+
def __new__(cls) -> AuthType:
|
211
|
+
"""Prevent instantiation."""
|
212
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
flwr/common/logger.py
CHANGED
@@ -17,12 +17,13 @@
|
|
17
17
|
|
18
18
|
import json as _json
|
19
19
|
import logging
|
20
|
+
import os
|
20
21
|
import re
|
21
22
|
import sys
|
22
23
|
import threading
|
23
24
|
import time
|
24
25
|
from io import StringIO
|
25
|
-
from logging import WARN, LogRecord
|
26
|
+
from logging import ERROR, WARN, LogRecord
|
26
27
|
from logging.handlers import HTTPHandler
|
27
28
|
from queue import Empty, Queue
|
28
29
|
from typing import TYPE_CHECKING, Any, Optional, TextIO, Union
|
@@ -42,6 +43,7 @@ from .constant import LOG_UPLOAD_INTERVAL
|
|
42
43
|
LOGGER_NAME = "flwr"
|
43
44
|
FLOWER_LOGGER = logging.getLogger(LOGGER_NAME)
|
44
45
|
FLOWER_LOGGER.setLevel(logging.DEBUG)
|
46
|
+
log = FLOWER_LOGGER.log # pylint: disable=invalid-name
|
45
47
|
|
46
48
|
LOG_COLORS = {
|
47
49
|
"DEBUG": "\033[94m", # Blue
|
@@ -101,7 +103,7 @@ class ConsoleHandler(StreamHandler):
|
|
101
103
|
|
102
104
|
|
103
105
|
def update_console_handler(
|
104
|
-
level: Optional[int] = None,
|
106
|
+
level: Optional[Union[int, str]] = None,
|
105
107
|
timestamps: Optional[bool] = None,
|
106
108
|
colored: Optional[bool] = None,
|
107
109
|
) -> None:
|
@@ -125,6 +127,22 @@ console_handler = ConsoleHandler(
|
|
125
127
|
console_handler.setLevel(logging.INFO)
|
126
128
|
FLOWER_LOGGER.addHandler(console_handler)
|
127
129
|
|
130
|
+
# Set log level via env var (show timestamps for `DEBUG`)
|
131
|
+
if log_level := os.getenv("PYTHONLOGLEVEL"):
|
132
|
+
try:
|
133
|
+
use_time_stamps = log_level.upper() == "DEBUG"
|
134
|
+
update_console_handler(
|
135
|
+
level=log_level, timestamps=use_time_stamps, colored=True
|
136
|
+
)
|
137
|
+
except Exception: # pylint: disable=broad-exception-caught
|
138
|
+
# Alert user but don't raise exception
|
139
|
+
log(
|
140
|
+
ERROR,
|
141
|
+
"Failed to set logging level %s. Using default level: %s",
|
142
|
+
log_level,
|
143
|
+
logging.getLevelName(console_handler.level),
|
144
|
+
)
|
145
|
+
|
128
146
|
|
129
147
|
class CustomHTTPHandler(HTTPHandler):
|
130
148
|
"""Custom HTTPHandler which overrides the mapLogRecords method."""
|
@@ -185,10 +203,6 @@ def configure(
|
|
185
203
|
FLOWER_LOGGER.addHandler(http_handler)
|
186
204
|
|
187
205
|
|
188
|
-
logger = logging.getLogger(LOGGER_NAME) # pylint: disable=invalid-name
|
189
|
-
log = logger.log # pylint: disable=invalid-name
|
190
|
-
|
191
|
-
|
192
206
|
def warn_preview_feature(name: str) -> None:
|
193
207
|
"""Warn the user when they use a preview feature."""
|
194
208
|
log(
|
flwr/server/app.py
CHANGED
@@ -31,12 +31,8 @@ from typing import Any, Optional
|
|
31
31
|
|
32
32
|
import grpc
|
33
33
|
import yaml
|
34
|
-
from cryptography.exceptions import UnsupportedAlgorithm
|
35
34
|
from cryptography.hazmat.primitives.asymmetric import ec
|
36
|
-
from cryptography.hazmat.primitives.serialization import
|
37
|
-
load_ssh_private_key,
|
38
|
-
load_ssh_public_key,
|
39
|
-
)
|
35
|
+
from cryptography.hazmat.primitives.serialization import load_ssh_public_key
|
40
36
|
|
41
37
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, event
|
42
38
|
from flwr.common.address import parse_address
|
@@ -44,7 +40,7 @@ from flwr.common.args import try_obtain_server_certificates
|
|
44
40
|
from flwr.common.auth_plugin import ExecAuthPlugin
|
45
41
|
from flwr.common.config import get_flwr_dir, parse_config_args
|
46
42
|
from flwr.common.constant import (
|
47
|
-
|
43
|
+
AUTH_TYPE_KEY,
|
48
44
|
CLIENT_OCTET,
|
49
45
|
EXEC_API_DEFAULT_SERVER_ADDRESS,
|
50
46
|
FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS,
|
@@ -64,7 +60,6 @@ from flwr.common.exit_handlers import register_exit_handlers
|
|
64
60
|
from flwr.common.grpc import generic_create_grpc_server
|
65
61
|
from flwr.common.logger import log, warn_deprecated_feature
|
66
62
|
from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
|
67
|
-
private_key_to_bytes,
|
68
63
|
public_key_to_bytes,
|
69
64
|
)
|
70
65
|
from flwr.proto.fleet_pb2_grpc import ( # pylint: disable=E0611
|
@@ -378,21 +373,12 @@ def run_superlink() -> None:
|
|
378
373
|
fleet_thread.start()
|
379
374
|
bckg_threads.append(fleet_thread)
|
380
375
|
elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE:
|
381
|
-
|
376
|
+
node_public_keys = _try_load_public_keys_node_authentication(args)
|
382
377
|
interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None
|
383
|
-
if
|
384
|
-
(
|
385
|
-
node_public_keys,
|
386
|
-
server_private_key,
|
387
|
-
server_public_key,
|
388
|
-
) = maybe_keys
|
378
|
+
if node_public_keys is not None:
|
389
379
|
state = state_factory.state()
|
390
|
-
state.
|
380
|
+
state.clear_supernode_auth_keys()
|
391
381
|
state.store_node_public_keys(node_public_keys)
|
392
|
-
state.store_server_private_public_key(
|
393
|
-
private_key_to_bytes(server_private_key),
|
394
|
-
public_key_to_bytes(server_public_key),
|
395
|
-
)
|
396
382
|
log(
|
397
383
|
INFO,
|
398
384
|
"Node authentication enabled with %d known public keys",
|
@@ -541,34 +527,20 @@ def _format_address(address: str) -> tuple[str, str, int]:
|
|
541
527
|
return (f"[{host}]:{port}" if is_v6 else f"{host}:{port}", host, port)
|
542
528
|
|
543
529
|
|
544
|
-
def
|
530
|
+
def _try_load_public_keys_node_authentication(
|
545
531
|
args: argparse.Namespace,
|
546
|
-
|
547
|
-
|
548
|
-
if
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
if (
|
556
|
-
not args.auth_list_public_keys
|
557
|
-
or not args.auth_superlink_private_key
|
558
|
-
or not args.auth_superlink_public_key
|
559
|
-
):
|
560
|
-
sys.exit(
|
561
|
-
"Authentication requires providing file paths for "
|
562
|
-
"'--auth-list-public-keys', '--auth-superlink-private-key' and "
|
563
|
-
"'--auth-superlink-public-key'. Provide all three to enable authentication."
|
532
|
+
) -> Optional[set[bytes]]:
|
533
|
+
"""Return a set of node public keys."""
|
534
|
+
if args.auth_superlink_private_key or args.auth_superlink_public_key:
|
535
|
+
log(
|
536
|
+
WARN,
|
537
|
+
"The `--auth-superlink-private-key` and `--auth-superlink-public-key` "
|
538
|
+
"arguments are deprecated and will be removed in a future release. Node "
|
539
|
+
"authentication no longer requires these arguments.",
|
564
540
|
)
|
565
541
|
|
566
|
-
if
|
567
|
-
|
568
|
-
"Authentication requires secure connections. "
|
569
|
-
"Please provide certificate paths to `--ssl-certfile`, "
|
570
|
-
"`--ssl-keyfile`, and `—-ssl-ca-certfile` and try again."
|
571
|
-
)
|
542
|
+
if not args.auth_list_public_keys:
|
543
|
+
return None
|
572
544
|
|
573
545
|
node_keys_file_path = Path(args.auth_list_public_keys)
|
574
546
|
if not node_keys_file_path.exists():
|
@@ -581,35 +553,6 @@ def _try_setup_node_authentication(
|
|
581
553
|
|
582
554
|
node_public_keys: set[bytes] = set()
|
583
555
|
|
584
|
-
try:
|
585
|
-
ssh_private_key = load_ssh_private_key(
|
586
|
-
Path(args.auth_superlink_private_key).read_bytes(),
|
587
|
-
None,
|
588
|
-
)
|
589
|
-
if not isinstance(ssh_private_key, ec.EllipticCurvePrivateKey):
|
590
|
-
raise ValueError()
|
591
|
-
except (ValueError, UnsupportedAlgorithm):
|
592
|
-
sys.exit(
|
593
|
-
"Error: Unable to parse the private key file in "
|
594
|
-
"'--auth-superlink-private-key'. Authentication requires elliptic "
|
595
|
-
"curve private and public key pair. Please ensure that the file "
|
596
|
-
"path points to a valid private key file and try again."
|
597
|
-
)
|
598
|
-
|
599
|
-
try:
|
600
|
-
ssh_public_key = load_ssh_public_key(
|
601
|
-
Path(args.auth_superlink_public_key).read_bytes()
|
602
|
-
)
|
603
|
-
if not isinstance(ssh_public_key, ec.EllipticCurvePublicKey):
|
604
|
-
raise ValueError()
|
605
|
-
except (ValueError, UnsupportedAlgorithm):
|
606
|
-
sys.exit(
|
607
|
-
"Error: Unable to parse the public key file in "
|
608
|
-
"'--auth-superlink-public-key'. Authentication requires elliptic "
|
609
|
-
"curve private and public key pair. Please ensure that the file "
|
610
|
-
"path points to a valid public key file and try again."
|
611
|
-
)
|
612
|
-
|
613
556
|
with open(node_keys_file_path, newline="", encoding="utf-8") as csvfile:
|
614
557
|
reader = csv.reader(csvfile)
|
615
558
|
for row in reader:
|
@@ -623,11 +566,7 @@ def _try_setup_node_authentication(
|
|
623
566
|
"file. Please ensure that the CSV file path points to a valid "
|
624
567
|
"known SSH public keys files and try again."
|
625
568
|
)
|
626
|
-
|
627
|
-
node_public_keys,
|
628
|
-
ssh_private_key,
|
629
|
-
ssh_public_key,
|
630
|
-
)
|
569
|
+
return node_public_keys
|
631
570
|
|
632
571
|
|
633
572
|
def _try_obtain_exec_auth_plugin(
|
@@ -639,7 +578,7 @@ def _try_obtain_exec_auth_plugin(
|
|
639
578
|
|
640
579
|
# Load authentication configuration
|
641
580
|
auth_config: dict[str, Any] = config.get("authentication", {})
|
642
|
-
auth_type: str = auth_config.get(
|
581
|
+
auth_type: str = auth_config.get(AUTH_TYPE_KEY, "")
|
643
582
|
|
644
583
|
# Load authentication plugin
|
645
584
|
try:
|
@@ -840,12 +779,12 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None:
|
|
840
779
|
parser.add_argument(
|
841
780
|
"--auth-superlink-private-key",
|
842
781
|
type=str,
|
843
|
-
help="
|
782
|
+
help="This argument is deprecated and will be removed in a future release.",
|
844
783
|
)
|
845
784
|
parser.add_argument(
|
846
785
|
"--auth-superlink-public-key",
|
847
786
|
type=str,
|
848
|
-
help="
|
787
|
+
help="This argument is deprecated and will be removed in a future release.",
|
849
788
|
)
|
850
789
|
|
851
790
|
|
@@ -74,8 +74,6 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
74
74
|
self.task_ins_id_to_task_res_id: dict[UUID, UUID] = {}
|
75
75
|
|
76
76
|
self.node_public_keys: set[bytes] = set()
|
77
|
-
self.server_public_key: Optional[bytes] = None
|
78
|
-
self.server_private_key: Optional[bytes] = None
|
79
77
|
|
80
78
|
self.lock = threading.RLock()
|
81
79
|
|
@@ -403,30 +401,9 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
403
401
|
log(ERROR, "Unexpected run creation failure.")
|
404
402
|
return 0
|
405
403
|
|
406
|
-
def
|
407
|
-
|
408
|
-
) -> None:
|
409
|
-
"""Store `server_private_key` and `server_public_key` in the link state."""
|
404
|
+
def clear_supernode_auth_keys(self) -> None:
|
405
|
+
"""Clear stored `node_public_keys` in the link state if any."""
|
410
406
|
with self.lock:
|
411
|
-
if self.server_private_key is None and self.server_public_key is None:
|
412
|
-
self.server_private_key = private_key
|
413
|
-
self.server_public_key = public_key
|
414
|
-
else:
|
415
|
-
raise RuntimeError("Server private and public key already set")
|
416
|
-
|
417
|
-
def get_server_private_key(self) -> Optional[bytes]:
|
418
|
-
"""Retrieve `server_private_key` in urlsafe bytes."""
|
419
|
-
return self.server_private_key
|
420
|
-
|
421
|
-
def get_server_public_key(self) -> Optional[bytes]:
|
422
|
-
"""Retrieve `server_public_key` in urlsafe bytes."""
|
423
|
-
return self.server_public_key
|
424
|
-
|
425
|
-
def clear_supernode_auth_keys_and_credentials(self) -> None:
|
426
|
-
"""Clear stored `node_public_keys` and credentials in the link state if any."""
|
427
|
-
with self.lock:
|
428
|
-
self.server_private_key = None
|
429
|
-
self.server_public_key = None
|
430
407
|
self.node_public_keys.clear()
|
431
408
|
|
432
409
|
def store_node_public_keys(self, public_keys: set[bytes]) -> None:
|
@@ -264,22 +264,8 @@ class LinkState(abc.ABC): # pylint: disable=R0904
|
|
264
264
|
"""
|
265
265
|
|
266
266
|
@abc.abstractmethod
|
267
|
-
def
|
268
|
-
|
269
|
-
) -> None:
|
270
|
-
"""Store `server_private_key` and `server_public_key` in the link state."""
|
271
|
-
|
272
|
-
@abc.abstractmethod
|
273
|
-
def get_server_private_key(self) -> Optional[bytes]:
|
274
|
-
"""Retrieve `server_private_key` in urlsafe bytes."""
|
275
|
-
|
276
|
-
@abc.abstractmethod
|
277
|
-
def get_server_public_key(self) -> Optional[bytes]:
|
278
|
-
"""Retrieve `server_public_key` in urlsafe bytes."""
|
279
|
-
|
280
|
-
@abc.abstractmethod
|
281
|
-
def clear_supernode_auth_keys_and_credentials(self) -> None:
|
282
|
-
"""Clear stored `node_public_keys` and credentials in the link state if any."""
|
267
|
+
def clear_supernode_auth_keys(self) -> None:
|
268
|
+
"""Clear stored `node_public_keys` in the link state if any."""
|
283
269
|
|
284
270
|
@abc.abstractmethod
|
285
271
|
def store_node_public_keys(self, public_keys: set[bytes]) -> None:
|
@@ -71,13 +71,6 @@ CREATE TABLE IF NOT EXISTS node(
|
|
71
71
|
);
|
72
72
|
"""
|
73
73
|
|
74
|
-
SQL_CREATE_TABLE_CREDENTIAL = """
|
75
|
-
CREATE TABLE IF NOT EXISTS credential(
|
76
|
-
private_key BLOB PRIMARY KEY,
|
77
|
-
public_key BLOB
|
78
|
-
);
|
79
|
-
"""
|
80
|
-
|
81
74
|
SQL_CREATE_TABLE_PUBLIC_KEY = """
|
82
75
|
CREATE TABLE IF NOT EXISTS public_key(
|
83
76
|
public_key BLOB PRIMARY KEY
|
@@ -208,7 +201,6 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
208
201
|
cur.execute(SQL_CREATE_TABLE_TASK_INS)
|
209
202
|
cur.execute(SQL_CREATE_TABLE_TASK_RES)
|
210
203
|
cur.execute(SQL_CREATE_TABLE_NODE)
|
211
|
-
cur.execute(SQL_CREATE_TABLE_CREDENTIAL)
|
212
204
|
cur.execute(SQL_CREATE_TABLE_PUBLIC_KEY)
|
213
205
|
cur.execute(SQL_CREATE_INDEX_ONLINE_UNTIL)
|
214
206
|
res = cur.execute("SELECT name FROM sqlite_schema;")
|
@@ -773,46 +765,9 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
773
765
|
log(ERROR, "Unexpected run creation failure.")
|
774
766
|
return 0
|
775
767
|
|
776
|
-
def
|
777
|
-
|
778
|
-
|
779
|
-
"""Store `server_private_key` and `server_public_key` in the link state."""
|
780
|
-
query = "SELECT COUNT(*) FROM credential"
|
781
|
-
count = self.query(query)[0]["COUNT(*)"]
|
782
|
-
if count < 1:
|
783
|
-
query = (
|
784
|
-
"INSERT OR REPLACE INTO credential (private_key, public_key) "
|
785
|
-
"VALUES (:private_key, :public_key)"
|
786
|
-
)
|
787
|
-
self.query(query, {"private_key": private_key, "public_key": public_key})
|
788
|
-
else:
|
789
|
-
raise RuntimeError("Server private and public key already set")
|
790
|
-
|
791
|
-
def get_server_private_key(self) -> Optional[bytes]:
|
792
|
-
"""Retrieve `server_private_key` in urlsafe bytes."""
|
793
|
-
query = "SELECT private_key FROM credential"
|
794
|
-
rows = self.query(query)
|
795
|
-
try:
|
796
|
-
private_key: Optional[bytes] = rows[0]["private_key"]
|
797
|
-
except IndexError:
|
798
|
-
private_key = None
|
799
|
-
return private_key
|
800
|
-
|
801
|
-
def get_server_public_key(self) -> Optional[bytes]:
|
802
|
-
"""Retrieve `server_public_key` in urlsafe bytes."""
|
803
|
-
query = "SELECT public_key FROM credential"
|
804
|
-
rows = self.query(query)
|
805
|
-
try:
|
806
|
-
public_key: Optional[bytes] = rows[0]["public_key"]
|
807
|
-
except IndexError:
|
808
|
-
public_key = None
|
809
|
-
return public_key
|
810
|
-
|
811
|
-
def clear_supernode_auth_keys_and_credentials(self) -> None:
|
812
|
-
"""Clear stored `node_public_keys` and credentials in the link state if any."""
|
813
|
-
queries = ["DELETE FROM public_key;", "DELETE FROM credential;"]
|
814
|
-
for query in queries:
|
815
|
-
self.query(query)
|
768
|
+
def clear_supernode_auth_keys(self) -> None:
|
769
|
+
"""Clear stored `node_public_keys` in the link state if any."""
|
770
|
+
self.query("DELETE FROM public_key;")
|
816
771
|
|
817
772
|
def store_node_public_keys(self, public_keys: set[bytes]) -> None:
|
818
773
|
"""Store a set of `node_public_keys` in the link state."""
|
{flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/RECORD
RENAMED
@@ -1,6 +1,8 @@
|
|
1
1
|
flwr/__init__.py,sha256=VmBWedrCxqmt4QvUHBLqyVEH6p7zaFMD_oCHerXHSVw,937
|
2
2
|
flwr/cli/__init__.py,sha256=cZJVgozlkC6Ni2Hd_FAIrqefrkCGOV18fikToq-6iLw,720
|
3
3
|
flwr/cli/app.py,sha256=UeXrW5gxrUnFViDjAMIxGNZZKwu3a1oAj83v53IWIWM,1382
|
4
|
+
flwr/cli/auth_plugin/__init__.py,sha256=FyaoqPzcxlBTFfJ2sBRC5USwQLmAhFr5KuBwfMO4bmo,1052
|
5
|
+
flwr/cli/auth_plugin/oidc_cli_plugin.py,sha256=nooDDWO_3bWYqi_KBrsO3YteDIIuvTZD30XymbvRPlA,5374
|
4
6
|
flwr/cli/build.py,sha256=4P70i_FnUs0P21aTwjTXtFQSAfY-C04hUDF-2npfJdo,6345
|
5
7
|
flwr/cli/cli_user_auth_interceptor.py,sha256=aZepPA298s-HjGmkJGMvI_uZe72O5aLC3jri-ilG53o,3126
|
6
8
|
flwr/cli/config_utils.py,sha256=LelRR960I36n1IPw7BIu79fKoOh0JePA58kAtoXSTH0,7518
|
@@ -9,7 +11,7 @@ flwr/cli/example.py,sha256=uk5CoD0ZITgpY_ffsTbEKf8XOOCSUzByjHPcMSPqV18,2216
|
|
9
11
|
flwr/cli/install.py,sha256=-RnrYGejN_zyXXp_CoddSQwoQfRTWWyt9WYlxphJzyU,8180
|
10
12
|
flwr/cli/log.py,sha256=vcO-r5EIc127mOQ26uxKVITX-w_Zib7AxSVuuN70_JY,6671
|
11
13
|
flwr/cli/login/__init__.py,sha256=6_9zOzbPOAH72K2wX3-9dXTAbS7Mjpa5sEn2lA6eHHI,800
|
12
|
-
flwr/cli/login/login.py,sha256=
|
14
|
+
flwr/cli/login/login.py,sha256=iNnNF1bvV0n8Z-vNc89azFNA73JqKZlO1s5OF2atsTc,3917
|
13
15
|
flwr/cli/ls.py,sha256=5KCHdctN5f5GkCAkbZSC1OuKdhmLzobINpltsdtDtQU,11383
|
14
16
|
flwr/cli/new/__init__.py,sha256=pOQtPT9W4kCIttcKne5m-FtJbvTqdjTVJxzQ9AUYK8I,790
|
15
17
|
flwr/cli/new/new.py,sha256=scyyKt8mzkc3El1bypgkHjKwVQEc2-q4I50PxriPFdI,9922
|
@@ -68,7 +70,7 @@ flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl,sha256=r0SZnvoR5a5mEWKJ
|
|
68
70
|
flwr/cli/run/__init__.py,sha256=cCsKVB0SFzh2b3QmGba6BHckB85xlhjh3mh4pBpACtY,790
|
69
71
|
flwr/cli/run/run.py,sha256=kEOYKin9qPJy8SODxcAvIWk-OskKPsxvcbvhDhf2VD4,8299
|
70
72
|
flwr/cli/stop.py,sha256=DBCKg9AhB1WcJsyqfkHKR1_V_yT7D32zqa9QhmX9IAU,4926
|
71
|
-
flwr/cli/utils.py,sha256=
|
73
|
+
flwr/cli/utils.py,sha256=D6XmroF6iu-AT02xbp2vWK6nGGgAPBLPRlJw9Cmx0SA,11390
|
72
74
|
flwr/client/__init__.py,sha256=DGDoO0AEAfz-0CUFmLdyUUweAS64-07AOnmDfWUefK4,1192
|
73
75
|
flwr/client/app.py,sha256=tNnef5wGVfqMiiGiWzAuULyy1QpvCKukiRmNi_a2cQc,34261
|
74
76
|
flwr/client/client.py,sha256=8o58nd9o6ZFcMIaVYPGcV4MSjBG4H0oFgWiv8ZEO3oA,7895
|
@@ -115,7 +117,7 @@ flwr/common/args.py,sha256=bCvtG0hhh_hVjl9NoWsY_g7kLMIN3jCN7B883HvZ7hg,6223
|
|
115
117
|
flwr/common/auth_plugin/__init__.py,sha256=1Y8Oj3iB49IHDu9tvDih1J74Ygu7k85V9s2A4WORPyA,887
|
116
118
|
flwr/common/auth_plugin/auth_plugin.py,sha256=wgDorBUB4IkK6twQ8vNawRVz7BDPmKdXZBNLqhU9RSs,3871
|
117
119
|
flwr/common/config.py,sha256=n6T5Vi6BUFul37GUpKp9Doqnz35phJqSud_G3ySWlIQ,13336
|
118
|
-
flwr/common/constant.py,sha256=
|
120
|
+
flwr/common/constant.py,sha256=mw2H-rTFI5Lwv8EK2dlW5RDAynQWeSawcwup2p0vLN4,6419
|
119
121
|
flwr/common/context.py,sha256=uJ-mnoC_8y_udEb3kAX-r8CPphNTWM72z1AlsvQEu54,2403
|
120
122
|
flwr/common/date.py,sha256=NHHpESce5wYqEwoDXf09gp9U9l_5Bmlh2BsOcwS-kDM,1554
|
121
123
|
flwr/common/differential_privacy.py,sha256=XwcJ3rWr8S8BZUocc76vLSJAXIf6OHnWkBV6-xlIRuw,6106
|
@@ -126,7 +128,7 @@ flwr/common/exit/exit.py,sha256=DmZFyksp-w1sFDQekq5Z-qfnr-ivCAv78aQkqj-TDps,3458
|
|
126
128
|
flwr/common/exit/exit_code.py,sha256=PNEnCrZfOILjfDAFu5m-2YWEJBrk97xglq4zCUlqV7E,3470
|
127
129
|
flwr/common/exit_handlers.py,sha256=Dke87CC6d6b6kqkC2mF0I4JsP4mHhlQTFxkS4sKKgyw,3308
|
128
130
|
flwr/common/grpc.py,sha256=GCdiTCppW-clhzOo7OIJbsKIWKnJ9pqNTsAKhj7y4So,9646
|
129
|
-
flwr/common/logger.py,sha256=
|
131
|
+
flwr/common/logger.py,sha256=xgS-oEN6U54vEYoWdX0U0ymqMfndhK0-ePiGtmjHdmU,12852
|
130
132
|
flwr/common/message.py,sha256=Zv4ID2BLQsbff0F03DI_MeFoHbSqVZAdDD9NcKYv6Zo,13832
|
131
133
|
flwr/common/object_ref.py,sha256=fIXf8aP5mG6Nuni7dvcKK5Di3zRfRWGs4ljvqIXplds,10115
|
132
134
|
flwr/common/parameter.py,sha256=-bFAUayToYDF50FZGrBC1hQYJCQDtB2bbr3ZuVLMtdE,2095
|
@@ -215,7 +217,7 @@ flwr/proto/transport_pb2_grpc.py,sha256=Nvn7oxzm1g1fPiGCGhyKxILDZHYG0CcgjySTzxq-
|
|
215
217
|
flwr/proto/transport_pb2_grpc.pyi,sha256=AGXf8RiIiW2J5IKMlm_3qT3AzcDa4F3P5IqUjve_esA,766
|
216
218
|
flwr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
217
219
|
flwr/server/__init__.py,sha256=cEg1oecBu4cKB69iJCqWEylC8b5XW47bl7rQiJsdTvM,1528
|
218
|
-
flwr/server/app.py,sha256=
|
220
|
+
flwr/server/app.py,sha256=ZHtoXbMzvTf82z96__WDv_ZaPqAYwVtCAZylwVXjOuE,30606
|
219
221
|
flwr/server/client_manager.py,sha256=7Ese0tgrH-i-ms363feYZJKwB8gWnXSmg_hYF2Bju4U,6227
|
220
222
|
flwr/server/client_proxy.py,sha256=4G-oTwhb45sfWLx2uZdcXD98IZwdTS6F88xe3akCdUg,2399
|
221
223
|
flwr/server/compat/__init__.py,sha256=VxnJtJyOjNFQXMNi9hIuzNlZM5n0Hj1p3aq_Pm2udw4,892
|
@@ -289,10 +291,10 @@ flwr/server/superlink/fleet/vce/backend/backend.py,sha256=LBAQxnbfPAphVOVIvYMj0Q
|
|
289
291
|
flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=jsUkFEVQTnrucK1jNQ_cUM8YwL7W4MQNA1GAf8ibRdg,7156
|
290
292
|
flwr/server/superlink/fleet/vce/vce_api.py,sha256=WTnUILr1OHS8LfjXQUA3FyWJYdJgdqpFAybyJUD-1Xo,13025
|
291
293
|
flwr/server/superlink/linkstate/__init__.py,sha256=v-2JyJlCB3qyhMNwMjmcNVOq4rkooqFU0LHH8Zo1jls,1064
|
292
|
-
flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=
|
293
|
-
flwr/server/superlink/linkstate/linkstate.py,sha256=
|
294
|
+
flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=4ahMj7TLe-GO2ctyeZ2BBRkviGv27s6x1NS_ETRJHP8,20514
|
295
|
+
flwr/server/superlink/linkstate/linkstate.py,sha256=LWA5zRwN829GDAeSo5kmkzsMu0SkXa9qg4aG_0QN0uk,12159
|
294
296
|
flwr/server/superlink/linkstate/linkstate_factory.py,sha256=ISSMjDlwuN7swxjOeYlTNpI_kuZ8PGkMcJnf1dbhUSE,2069
|
295
|
-
flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=
|
297
|
+
flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=E07w0_eCoOnZOBbdh3I3PN5k5tqt4LrEnYAuiMOF0I0,40398
|
296
298
|
flwr/server/superlink/linkstate/utils.py,sha256=EpRehwI4NeEW3oINICPWP9STK49N0aszd5s5jtle7DQ,13602
|
297
299
|
flwr/server/superlink/simulation/__init__.py,sha256=mg-oapC9dkzEfjXPQFior5lpWj4g9kwbLovptyYM_g0,718
|
298
300
|
flwr/server/superlink/simulation/simulationio_grpc.py,sha256=8aUrZZLdvprKUfLLqFID4aItus9beU6m1qLQYIPB7k0,2224
|
@@ -325,8 +327,8 @@ flwr/superexec/exec_servicer.py,sha256=X10ILT-AoGMrB3IgI2mBe9i-QcIVUAl9bucuqVOPY
|
|
325
327
|
flwr/superexec/exec_user_auth_interceptor.py,sha256=K06OU-l4LnYhTDg071hGJuOaQWEJbZsYi5qxUmmtiG0,3704
|
326
328
|
flwr/superexec/executor.py,sha256=_B55WW2TD1fBINpabSSDRenVHXYmvlfhv-k8hJKU4lQ,3115
|
327
329
|
flwr/superexec/simulation.py,sha256=WQDon15oqpMopAZnwRZoTICYCfHqtkvFSqiTQ2hLD_g,4088
|
328
|
-
flwr_nightly-1.15.0.
|
329
|
-
flwr_nightly-1.15.0.
|
330
|
-
flwr_nightly-1.15.0.
|
331
|
-
flwr_nightly-1.15.0.
|
332
|
-
flwr_nightly-1.15.0.
|
330
|
+
flwr_nightly-1.15.0.dev20250127.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
331
|
+
flwr_nightly-1.15.0.dev20250127.dist-info/METADATA,sha256=3hQe9KaV1UnCUP4MqYVnJR50-ki9SgS0a2m4Mv5E0oM,15864
|
332
|
+
flwr_nightly-1.15.0.dev20250127.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
333
|
+
flwr_nightly-1.15.0.dev20250127.dist-info/entry_points.txt,sha256=JlNxX3qhaV18_2yj5a3kJW1ESxm31cal9iS_N_pf1Rk,538
|
334
|
+
flwr_nightly-1.15.0.dev20250127.dist-info/RECORD,,
|
{flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/LICENSE
RENAMED
File without changes
|
{flwr_nightly-1.15.0.dev20250124.dist-info → flwr_nightly-1.15.0.dev20250127.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|