flwr-nightly 1.19.0.dev20250510__py3-none-any.whl → 1.19.0.dev20250512__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/cli/build.py +82 -57
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +13 -16
- flwr/cli/run/run.py +8 -12
- flwr/common/constant.py +1 -0
- flwr/server/superlink/linkstate/in_memory_linkstate.py +84 -4
- flwr/server/superlink/linkstate/linkstate.py +23 -0
- flwr/server/superlink/linkstate/sqlite_linkstate.py +98 -8
- {flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/METADATA +2 -1
- {flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/RECORD +14 -14
- {flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/entry_points.txt +0 -0
flwr/cli/build.py
CHANGED
@@ -16,10 +16,8 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
import hashlib
|
19
|
-
import os
|
20
|
-
import shutil
|
21
|
-
import tempfile
|
22
19
|
import zipfile
|
20
|
+
from io import BytesIO
|
23
21
|
from pathlib import Path
|
24
22
|
from typing import Annotated, Any, Optional, Union
|
25
23
|
|
@@ -29,6 +27,7 @@ import typer
|
|
29
27
|
|
30
28
|
from flwr.common.constant import FAB_ALLOWED_EXTENSIONS, FAB_DATE, FAB_HASH_TRUNCATION
|
31
29
|
|
30
|
+
from .config_utils import load as load_toml
|
32
31
|
from .config_utils import load_and_validate
|
33
32
|
from .utils import is_valid_project_name
|
34
33
|
|
@@ -43,11 +42,11 @@ def write_to_zip(
|
|
43
42
|
return zipfile_obj
|
44
43
|
|
45
44
|
|
46
|
-
def get_fab_filename(
|
45
|
+
def get_fab_filename(config: dict[str, Any], fab_hash: str) -> str:
|
47
46
|
"""Get the FAB filename based on the given config and FAB hash."""
|
48
|
-
publisher =
|
49
|
-
name =
|
50
|
-
version =
|
47
|
+
publisher = config["tool"]["flwr"]["app"]["publisher"]
|
48
|
+
name = config["project"]["name"]
|
49
|
+
version = config["project"]["version"].replace(".", "-")
|
51
50
|
fab_hash_truncated = fab_hash[:FAB_HASH_TRUNCATION]
|
52
51
|
return f"{publisher}.{name}.{version}.{fab_hash_truncated}.fab"
|
53
52
|
|
@@ -89,8 +88,8 @@ def build(
|
|
89
88
|
)
|
90
89
|
raise typer.Exit(code=1)
|
91
90
|
|
92
|
-
|
93
|
-
if
|
91
|
+
config, errors, warnings = load_and_validate(app / "pyproject.toml")
|
92
|
+
if config is None:
|
94
93
|
typer.secho(
|
95
94
|
"Project configuration could not be loaded.\npyproject.toml is invalid:\n"
|
96
95
|
+ "\n".join([f"- {line}" for line in errors]),
|
@@ -107,70 +106,96 @@ def build(
|
|
107
106
|
bold=True,
|
108
107
|
)
|
109
108
|
|
110
|
-
#
|
111
|
-
|
109
|
+
# Build FAB
|
110
|
+
fab_bytes, fab_hash, _ = build_fab(app)
|
112
111
|
|
113
|
-
|
112
|
+
# Get the name of the zip file
|
113
|
+
fab_filename = get_fab_filename(config, fab_hash)
|
114
|
+
|
115
|
+
# Write the FAB
|
116
|
+
Path(fab_filename).write_bytes(fab_bytes)
|
117
|
+
|
118
|
+
typer.secho(
|
119
|
+
f"🎊 Successfully built {fab_filename}", fg=typer.colors.GREEN, bold=True
|
120
|
+
)
|
121
|
+
|
122
|
+
return fab_filename, fab_hash
|
114
123
|
|
115
|
-
# Remove the 'federations' field from 'tool.flwr' if it exists
|
116
|
-
if (
|
117
|
-
"tool" in conf
|
118
|
-
and "flwr" in conf["tool"]
|
119
|
-
and "federations" in conf["tool"]["flwr"]
|
120
|
-
):
|
121
|
-
del conf["tool"]["flwr"]["federations"]
|
122
124
|
|
123
|
-
|
125
|
+
def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
|
126
|
+
"""Build a FAB in memory and return the bytes, hash, and config.
|
124
127
|
|
125
|
-
|
126
|
-
|
128
|
+
This function assumes that the provided path points to a valid Flower app and
|
129
|
+
bundles it into a FAB without performing additional validation.
|
127
130
|
|
128
|
-
|
129
|
-
|
131
|
+
Parameters
|
132
|
+
----------
|
133
|
+
app : Path
|
134
|
+
Path to the Flower app to bundle into a FAB.
|
130
135
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
136
|
+
Returns
|
137
|
+
-------
|
138
|
+
tuple[bytes, str, dict[str, Any]]
|
139
|
+
A tuple containing:
|
140
|
+
- the FAB as bytes
|
141
|
+
- the SHA256 hash of the FAB
|
142
|
+
- the project configuration (with the 'federations' field removed)
|
143
|
+
"""
|
144
|
+
app = app.resolve()
|
145
|
+
|
146
|
+
# Load the pyproject.toml file
|
147
|
+
config = load_toml(app / "pyproject.toml")
|
148
|
+
if config is None:
|
149
|
+
raise ValueError("Project configuration could not be loaded.")
|
140
150
|
|
141
|
-
|
151
|
+
# Remove the 'federations' field if it exists
|
152
|
+
if (
|
153
|
+
"tool" in config
|
154
|
+
and "flwr" in config["tool"]
|
155
|
+
and "federations" in config["tool"]["flwr"]
|
156
|
+
):
|
157
|
+
del config["tool"]["flwr"]["federations"]
|
142
158
|
|
143
|
-
|
144
|
-
|
145
|
-
with open(file_path, "rb") as f:
|
146
|
-
file_contents = f.read()
|
159
|
+
# Load .gitignore rules if present
|
160
|
+
ignore_spec = _load_gitignore(app)
|
147
161
|
|
148
|
-
|
149
|
-
|
162
|
+
# Search for all files in the app directory
|
163
|
+
all_files = [
|
164
|
+
f
|
165
|
+
for f in app.rglob("*")
|
166
|
+
if not ignore_spec.match_file(f)
|
167
|
+
and f.suffix in FAB_ALLOWED_EXTENSIONS
|
168
|
+
and f.name != "pyproject.toml" # Exclude the original pyproject.toml
|
169
|
+
]
|
170
|
+
all_files.sort()
|
171
|
+
|
172
|
+
# Create a zip file in memory
|
173
|
+
list_file_content = ""
|
150
174
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
175
|
+
fab_buffer = BytesIO()
|
176
|
+
with zipfile.ZipFile(fab_buffer, "w", zipfile.ZIP_DEFLATED) as fab_file:
|
177
|
+
# Add pyproject.toml
|
178
|
+
write_to_zip(fab_file, "pyproject.toml", tomli_w.dumps(config))
|
155
179
|
|
156
|
-
|
157
|
-
|
180
|
+
for file_path in all_files:
|
181
|
+
# Read the file content manually
|
182
|
+
file_contents = file_path.read_bytes()
|
158
183
|
|
159
|
-
|
160
|
-
|
161
|
-
fab_hash = hashlib.sha256(content).hexdigest()
|
184
|
+
archive_path = str(file_path.relative_to(app))
|
185
|
+
write_to_zip(fab_file, archive_path, file_contents)
|
162
186
|
|
163
|
-
|
164
|
-
|
187
|
+
# Calculate file info
|
188
|
+
sha256_hash = hashlib.sha256(file_contents).hexdigest()
|
189
|
+
file_size_bits = len(file_contents) * 8 # size in bits
|
190
|
+
list_file_content += f"{archive_path},{sha256_hash},{file_size_bits}\n"
|
165
191
|
|
166
|
-
|
167
|
-
|
192
|
+
# Add CONTENT and CONTENT.jwt to the zip file
|
193
|
+
write_to_zip(fab_file, ".info/CONTENT", list_file_content)
|
168
194
|
|
169
|
-
|
170
|
-
|
171
|
-
)
|
195
|
+
fab_bytes = fab_buffer.getvalue()
|
196
|
+
fab_hash = hashlib.sha256(fab_bytes).hexdigest()
|
172
197
|
|
173
|
-
return
|
198
|
+
return fab_bytes, fab_hash, config
|
174
199
|
|
175
200
|
|
176
201
|
def _load_gitignore(app: Path) -> pathspec.PathSpec:
|
@@ -1,9 +1,9 @@
|
|
1
1
|
"""$project_name: A Flower Baseline."""
|
2
2
|
|
3
3
|
import torch
|
4
|
-
|
5
4
|
from flwr.client import ClientApp, NumPyClient
|
6
5
|
from flwr.common import Context
|
6
|
+
|
7
7
|
from $import_name.dataset import load_data
|
8
8
|
from $import_name.model import Net, get_weights, set_weights, test, train
|
9
9
|
|
@@ -76,5 +76,5 @@ def get_weights(net):
|
|
76
76
|
def set_weights(net, parameters):
|
77
77
|
"""Apply parameters to an existing model."""
|
78
78
|
params_dict = zip(net.state_dict().keys(), parameters)
|
79
|
-
state_dict = OrderedDict({k: torch.
|
79
|
+
state_dict = OrderedDict({k: torch.from_numpy(v) for k, v in params_dict})
|
80
80
|
net.load_state_dict(state_dict, strict=True)
|
@@ -1,15 +1,14 @@
|
|
1
1
|
"""$project_name: A Flower Baseline."""
|
2
2
|
|
3
|
-
from typing import List, Tuple
|
4
|
-
|
5
3
|
from flwr.common import Context, Metrics, ndarrays_to_parameters
|
6
4
|
from flwr.server import ServerApp, ServerAppComponents, ServerConfig
|
7
5
|
from flwr.server.strategy import FedAvg
|
6
|
+
|
8
7
|
from $import_name.model import Net, get_weights
|
9
8
|
|
10
9
|
|
11
10
|
# Define metric aggregation function
|
12
|
-
def weighted_average(metrics:
|
11
|
+
def weighted_average(metrics: list[tuple[int, Metrics]]) -> Metrics:
|
13
12
|
"""Do weighted average of accuracy metric."""
|
14
13
|
# Multiply accuracy of each client by number of examples used
|
15
14
|
accuracies = [num_examples * float(m["accuracy"]) for num_examples, m in metrics]
|
@@ -10,8 +10,8 @@ license = "Apache-2.0"
|
|
10
10
|
dependencies = [
|
11
11
|
"flwr[simulation]>=1.19.0",
|
12
12
|
"flwr-datasets[vision]>=0.5.0",
|
13
|
-
"torch==2.
|
14
|
-
"torchvision==0.
|
13
|
+
"torch==2.6.0",
|
14
|
+
"torchvision==0.21.0",
|
15
15
|
]
|
16
16
|
|
17
17
|
[tool.hatch.metadata]
|
@@ -23,28 +23,23 @@ dev = [
|
|
23
23
|
"black==24.2.0",
|
24
24
|
"docformatter==1.7.5",
|
25
25
|
"mypy==1.8.0",
|
26
|
-
"pylint==3.
|
27
|
-
"
|
28
|
-
"pytest==6.2.4",
|
26
|
+
"pylint==3.3.1",
|
27
|
+
"pytest==7.4.4",
|
29
28
|
"pytest-watch==4.2.0",
|
30
|
-
"ruff==0.
|
29
|
+
"ruff==0.4.5",
|
31
30
|
"types-requests==2.31.0.20240125",
|
32
31
|
]
|
33
32
|
|
34
33
|
[tool.isort]
|
35
34
|
profile = "black"
|
36
|
-
known_first_party = ["flwr"]
|
37
35
|
|
38
36
|
[tool.black]
|
39
37
|
line-length = 88
|
40
|
-
target-version = ["
|
38
|
+
target-version = ["py310", "py311", "py312"]
|
41
39
|
|
42
40
|
[tool.pytest.ini_options]
|
43
41
|
minversion = "6.2"
|
44
42
|
addopts = "-qq"
|
45
|
-
testpaths = [
|
46
|
-
"flwr_baselines",
|
47
|
-
]
|
48
43
|
|
49
44
|
[tool.mypy]
|
50
45
|
ignore_missing_imports = true
|
@@ -82,11 +77,8 @@ wrap-summaries = 88
|
|
82
77
|
wrap-descriptions = 88
|
83
78
|
|
84
79
|
[tool.ruff]
|
85
|
-
target-version = "
|
80
|
+
target-version = "py310"
|
86
81
|
line-length = 88
|
87
|
-
select = ["D", "E", "F", "W", "B", "ISC", "C4"]
|
88
|
-
fixable = ["D", "E", "F", "W", "B", "ISC", "C4"]
|
89
|
-
ignore = ["B024", "B027"]
|
90
82
|
exclude = [
|
91
83
|
".bzr",
|
92
84
|
".direnv",
|
@@ -111,7 +103,12 @@ exclude = [
|
|
111
103
|
"proto",
|
112
104
|
]
|
113
105
|
|
114
|
-
[tool.ruff.
|
106
|
+
[tool.ruff.lint]
|
107
|
+
select = ["D", "E", "F", "W", "B", "ISC", "C4", "UP"]
|
108
|
+
fixable = ["D", "E", "F", "W", "B", "ISC", "C4", "UP"]
|
109
|
+
ignore = ["B024", "B027", "D205", "D209"]
|
110
|
+
|
111
|
+
[tool.ruff.lint.pydocstyle]
|
115
112
|
convention = "numpy"
|
116
113
|
|
117
114
|
[tool.hatch.build.targets.wheel]
|
flwr/cli/run/run.py
CHANGED
@@ -24,9 +24,8 @@ from typing import Annotated, Any, Optional
|
|
24
24
|
import typer
|
25
25
|
from rich.console import Console
|
26
26
|
|
27
|
-
from flwr.cli.build import
|
27
|
+
from flwr.cli.build import build_fab, get_fab_filename
|
28
28
|
from flwr.cli.config_utils import (
|
29
|
-
get_fab_metadata,
|
30
29
|
load_and_validate,
|
31
30
|
process_loaded_project_config,
|
32
31
|
validate_federation_in_project_config,
|
@@ -34,6 +33,7 @@ from flwr.cli.config_utils import (
|
|
34
33
|
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
35
34
|
from flwr.common.config import (
|
36
35
|
flatten_dict,
|
36
|
+
get_metadata_from_config,
|
37
37
|
parse_config_args,
|
38
38
|
user_config_to_configrecord,
|
39
39
|
)
|
@@ -158,18 +158,14 @@ def _run_with_exec_api(
|
|
158
158
|
channel = init_channel(app, federation_config, auth_plugin)
|
159
159
|
stub = ExecStub(channel)
|
160
160
|
|
161
|
-
|
162
|
-
|
163
|
-
fab_id, fab_version = get_fab_metadata(Path(fab_path))
|
161
|
+
fab_bytes, fab_hash, config = build_fab(app)
|
162
|
+
fab_id, fab_version = get_metadata_from_config(config)
|
164
163
|
|
165
|
-
|
166
|
-
Path(fab_path).unlink()
|
167
|
-
|
168
|
-
fab = Fab(fab_hash, content)
|
164
|
+
fab = Fab(fab_hash, fab_bytes)
|
169
165
|
|
170
166
|
# Construct a `ConfigRecord` out of a flattened `UserConfig`
|
171
|
-
|
172
|
-
c_record = user_config_to_configrecord(
|
167
|
+
fed_config = flatten_dict(federation_config.get("options", {}))
|
168
|
+
c_record = user_config_to_configrecord(fed_config)
|
173
169
|
|
174
170
|
req = StartRunRequest(
|
175
171
|
fab=fab_to_proto(fab),
|
@@ -194,7 +190,7 @@ def _run_with_exec_api(
|
|
194
190
|
"fab-name": fab_id.rsplit("/", maxsplit=1)[-1],
|
195
191
|
"fab-version": fab_version,
|
196
192
|
"fab-hash": fab_hash[:8],
|
197
|
-
"fab-filename":
|
193
|
+
"fab-filename": get_fab_filename(config, fab_hash),
|
198
194
|
}
|
199
195
|
)
|
200
196
|
restore_output()
|
flwr/common/constant.py
CHANGED
@@ -25,12 +25,15 @@ from uuid import UUID, uuid4
|
|
25
25
|
|
26
26
|
from flwr.common import Context, Message, log, now
|
27
27
|
from flwr.common.constant import (
|
28
|
+
HEARTBEAT_MAX_INTERVAL,
|
28
29
|
HEARTBEAT_PATIENCE,
|
29
30
|
MESSAGE_TTL_TOLERANCE,
|
30
31
|
NODE_ID_NUM_BYTES,
|
32
|
+
RUN_FAILURE_DETAILS_NO_HEARTBEAT,
|
31
33
|
RUN_ID_NUM_BYTES,
|
32
34
|
SUPERLINK_NODE_ID,
|
33
35
|
Status,
|
36
|
+
SubStatus,
|
34
37
|
)
|
35
38
|
from flwr.common.record import ConfigRecord
|
36
39
|
from flwr.common.typing import Run, RunStatus, UserConfig
|
@@ -52,8 +55,11 @@ class RunRecord: # pylint: disable=R0902
|
|
52
55
|
"""The record of a specific run, including its status and timestamps."""
|
53
56
|
|
54
57
|
run: Run
|
58
|
+
active_until: float = 0.0
|
59
|
+
heartbeat_interval: float = 0.0
|
55
60
|
logs: list[tuple[float, str]] = field(default_factory=list)
|
56
61
|
log_lock: threading.Lock = field(default_factory=threading.Lock)
|
62
|
+
lock: threading.RLock = field(default_factory=threading.RLock)
|
57
63
|
|
58
64
|
|
59
65
|
class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
@@ -461,8 +467,29 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
461
467
|
with self.lock:
|
462
468
|
return set(self.run_ids.keys())
|
463
469
|
|
470
|
+
def _check_and_tag_inactive_run(self, run_ids: set[int]) -> None:
|
471
|
+
"""Check if any runs are no longer active.
|
472
|
+
|
473
|
+
Marks runs with status 'starting' or 'running' as failed
|
474
|
+
if they have not sent a heartbeat before `active_until`.
|
475
|
+
"""
|
476
|
+
current = now()
|
477
|
+
for record in [self.run_ids[run_id] for run_id in run_ids]:
|
478
|
+
with record.lock:
|
479
|
+
if record.run.status.status in (Status.STARTING, Status.RUNNING):
|
480
|
+
if record.active_until < current.timestamp():
|
481
|
+
record.run.status = RunStatus(
|
482
|
+
status=Status.FINISHED,
|
483
|
+
sub_status=SubStatus.FAILED,
|
484
|
+
details=RUN_FAILURE_DETAILS_NO_HEARTBEAT,
|
485
|
+
)
|
486
|
+
record.run.finished_at = now().isoformat()
|
487
|
+
|
464
488
|
def get_run(self, run_id: int) -> Optional[Run]:
|
465
489
|
"""Retrieve information about the run with the specified `run_id`."""
|
490
|
+
# Check if runs are still active
|
491
|
+
self._check_and_tag_inactive_run(run_ids={run_id})
|
492
|
+
|
466
493
|
with self.lock:
|
467
494
|
if run_id not in self.run_ids:
|
468
495
|
log(ERROR, "`run_id` is invalid")
|
@@ -471,6 +498,9 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
471
498
|
|
472
499
|
def get_run_status(self, run_ids: set[int]) -> dict[int, RunStatus]:
|
473
500
|
"""Retrieve the statuses for the specified runs."""
|
501
|
+
# Check if runs are still active
|
502
|
+
self._check_and_tag_inactive_run(run_ids=run_ids)
|
503
|
+
|
474
504
|
with self.lock:
|
475
505
|
return {
|
476
506
|
run_id: self.run_ids[run_id].run.status
|
@@ -480,12 +510,16 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
480
510
|
|
481
511
|
def update_run_status(self, run_id: int, new_status: RunStatus) -> bool:
|
482
512
|
"""Update the status of the run with the specified `run_id`."""
|
513
|
+
# Check if runs are still active
|
514
|
+
self._check_and_tag_inactive_run(run_ids={run_id})
|
515
|
+
|
483
516
|
with self.lock:
|
484
517
|
# Check if the run_id exists
|
485
518
|
if run_id not in self.run_ids:
|
486
519
|
log(ERROR, "`run_id` is invalid")
|
487
520
|
return False
|
488
521
|
|
522
|
+
with self.run_ids[run_id].lock:
|
489
523
|
# Check if the status transition is valid
|
490
524
|
current_status = self.run_ids[run_id].run.status
|
491
525
|
if not is_valid_transition(current_status, new_status):
|
@@ -507,14 +541,23 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
507
541
|
)
|
508
542
|
return False
|
509
543
|
|
510
|
-
#
|
544
|
+
# Initialize heartbeat_interval and active_until
|
545
|
+
# when switching to starting or running
|
546
|
+
current = now()
|
511
547
|
run_record = self.run_ids[run_id]
|
548
|
+
if new_status.status in (Status.STARTING, Status.RUNNING):
|
549
|
+
run_record.heartbeat_interval = HEARTBEAT_MAX_INTERVAL
|
550
|
+
run_record.active_until = (
|
551
|
+
current.timestamp() + run_record.heartbeat_interval
|
552
|
+
)
|
553
|
+
|
554
|
+
# Update the run status
|
512
555
|
if new_status.status == Status.STARTING:
|
513
|
-
run_record.run.starting_at =
|
556
|
+
run_record.run.starting_at = current.isoformat()
|
514
557
|
elif new_status.status == Status.RUNNING:
|
515
|
-
run_record.run.running_at =
|
558
|
+
run_record.run.running_at = current.isoformat()
|
516
559
|
elif new_status.status == Status.FINISHED:
|
517
|
-
run_record.run.finished_at =
|
560
|
+
run_record.run.finished_at = current.isoformat()
|
518
561
|
run_record.run.status = new_status
|
519
562
|
return True
|
520
563
|
|
@@ -558,6 +601,43 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
558
601
|
return True
|
559
602
|
return False
|
560
603
|
|
604
|
+
def acknowledge_app_heartbeat(self, run_id: int, heartbeat_interval: float) -> bool:
|
605
|
+
"""Acknowledge a heartbeat received from a ServerApp for a given run.
|
606
|
+
|
607
|
+
A run with status `"running"` is considered alive as long as it sends heartbeats
|
608
|
+
within the tolerated interval: HEARTBEAT_PATIENCE × heartbeat_interval.
|
609
|
+
HEARTBEAT_PATIENCE = N allows for N-1 missed heartbeat before the run is
|
610
|
+
marked as `"completed:failed"`.
|
611
|
+
"""
|
612
|
+
with self.lock:
|
613
|
+
# Search for the run
|
614
|
+
record = self.run_ids.get(run_id)
|
615
|
+
|
616
|
+
# Check if the run_id exists
|
617
|
+
if record is None:
|
618
|
+
log(ERROR, "`run_id` is invalid")
|
619
|
+
return False
|
620
|
+
|
621
|
+
with record.lock:
|
622
|
+
# Check if runs are still active
|
623
|
+
self._check_and_tag_inactive_run(run_ids={run_id})
|
624
|
+
|
625
|
+
# Check if the run is of status "running"/"starting"
|
626
|
+
current_status = record.run.status
|
627
|
+
if current_status.status not in (Status.RUNNING, Status.STARTING):
|
628
|
+
log(
|
629
|
+
ERROR,
|
630
|
+
'Cannot acknowledge heartbeat for run with status "%s"',
|
631
|
+
current_status.status,
|
632
|
+
)
|
633
|
+
return False
|
634
|
+
|
635
|
+
# Update the `active_until` and `heartbeat_interval` for the given run
|
636
|
+
current = now().timestamp()
|
637
|
+
record.active_until = current + HEARTBEAT_PATIENCE * heartbeat_interval
|
638
|
+
record.heartbeat_interval = heartbeat_interval
|
639
|
+
return True
|
640
|
+
|
561
641
|
def get_serverapp_context(self, run_id: int) -> Optional[Context]:
|
562
642
|
"""Get the context for the specified `run_id`."""
|
563
643
|
return self.contexts.get(run_id)
|
@@ -292,6 +292,29 @@ class LinkState(abc.ABC): # pylint: disable=R0904
|
|
292
292
|
True if the heartbeat is successfully acknowledged; otherwise, False.
|
293
293
|
"""
|
294
294
|
|
295
|
+
@abc.abstractmethod
|
296
|
+
def acknowledge_app_heartbeat(self, run_id: int, heartbeat_interval: float) -> bool:
|
297
|
+
"""Acknowledge a heartbeat received from a ServerApp for a given run.
|
298
|
+
|
299
|
+
A run with status `"running"` is considered alive as long as it sends heartbeats
|
300
|
+
within the tolerated interval: HEARTBEAT_PATIENCE × heartbeat_interval.
|
301
|
+
HEARTBEAT_PATIENCE = N allows for N-1 missed heartbeat before the run is
|
302
|
+
marked as `"completed:failed"`.
|
303
|
+
|
304
|
+
Parameters
|
305
|
+
----------
|
306
|
+
run_id : int
|
307
|
+
The `run_id` from which the heartbeat was received.
|
308
|
+
heartbeat_interval : float
|
309
|
+
The interval (in seconds) from the current timestamp within which the next
|
310
|
+
heartbeat from the ServerApp for this run must be received.
|
311
|
+
|
312
|
+
Returns
|
313
|
+
-------
|
314
|
+
is_acknowledged : bool
|
315
|
+
True if the heartbeat is successfully acknowledged; otherwise, False.
|
316
|
+
"""
|
317
|
+
|
295
318
|
@abc.abstractmethod
|
296
319
|
def get_serverapp_context(self, run_id: int) -> Optional[Context]:
|
297
320
|
"""Get the context for the specified `run_id`.
|
@@ -28,12 +28,15 @@ from uuid import UUID, uuid4
|
|
28
28
|
|
29
29
|
from flwr.common import Context, Message, Metadata, log, now
|
30
30
|
from flwr.common.constant import (
|
31
|
+
HEARTBEAT_MAX_INTERVAL,
|
31
32
|
HEARTBEAT_PATIENCE,
|
32
33
|
MESSAGE_TTL_TOLERANCE,
|
33
34
|
NODE_ID_NUM_BYTES,
|
35
|
+
RUN_FAILURE_DETAILS_NO_HEARTBEAT,
|
34
36
|
RUN_ID_NUM_BYTES,
|
35
37
|
SUPERLINK_NODE_ID,
|
36
38
|
Status,
|
39
|
+
SubStatus,
|
37
40
|
)
|
38
41
|
from flwr.common.message import make_message
|
39
42
|
from flwr.common.record import ConfigRecord
|
@@ -92,6 +95,8 @@ CREATE INDEX IF NOT EXISTS idx_online_until ON node (online_until);
|
|
92
95
|
SQL_CREATE_TABLE_RUN = """
|
93
96
|
CREATE TABLE IF NOT EXISTS run(
|
94
97
|
run_id INTEGER UNIQUE,
|
98
|
+
active_until REAL,
|
99
|
+
heartbeat_interval REAL,
|
95
100
|
fab_id TEXT,
|
96
101
|
fab_version TEXT,
|
97
102
|
fab_hash TEXT,
|
@@ -742,20 +747,21 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
742
747
|
if self.query(query, (sint64_run_id,))[0]["COUNT(*)"] == 0:
|
743
748
|
query = (
|
744
749
|
"INSERT INTO run "
|
745
|
-
"(run_id,
|
746
|
-
"
|
747
|
-
"sub_status, details)
|
750
|
+
"(run_id, active_until, heartbeat_interval, fab_id, fab_version, "
|
751
|
+
"fab_hash, override_config, federation_options, pending_at, "
|
752
|
+
"starting_at, running_at, finished_at, sub_status, details) "
|
753
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
|
748
754
|
)
|
749
755
|
override_config_json = json.dumps(override_config)
|
750
756
|
data = [
|
751
757
|
sint64_run_id,
|
758
|
+
0, # The `active_until` is not used until the run is started
|
759
|
+
0, # This `heartbeat_interval` is not used until the run is started
|
752
760
|
fab_id,
|
753
761
|
fab_version,
|
754
762
|
fab_hash,
|
755
763
|
override_config_json,
|
756
764
|
configrecord_to_bytes(federation_options),
|
757
|
-
]
|
758
|
-
data += [
|
759
765
|
now().isoformat(),
|
760
766
|
"",
|
761
767
|
"",
|
@@ -796,8 +802,33 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
796
802
|
rows = self.query(query)
|
797
803
|
return {convert_sint64_to_uint64(row["run_id"]) for row in rows}
|
798
804
|
|
805
|
+
def _check_and_tag_inactive_run(self, run_ids: set[int]) -> None:
|
806
|
+
"""Check if any runs are no longer active.
|
807
|
+
|
808
|
+
Marks runs with status 'starting' or 'running' as failed
|
809
|
+
if they have not sent a heartbeat before `active_until`.
|
810
|
+
"""
|
811
|
+
sint_run_ids = [convert_uint64_to_sint64(run_id) for run_id in run_ids]
|
812
|
+
query = "UPDATE run SET finished_at = ?, sub_status = ?, details = ? "
|
813
|
+
query += "WHERE starting_at != '' AND finished_at = '' AND active_until < ?"
|
814
|
+
query += f" AND run_id IN ({','.join(['?'] * len(run_ids))});"
|
815
|
+
current = now()
|
816
|
+
self.query(
|
817
|
+
query,
|
818
|
+
(
|
819
|
+
current.isoformat(),
|
820
|
+
SubStatus.FAILED,
|
821
|
+
RUN_FAILURE_DETAILS_NO_HEARTBEAT,
|
822
|
+
current.timestamp(),
|
823
|
+
*sint_run_ids,
|
824
|
+
),
|
825
|
+
)
|
826
|
+
|
799
827
|
def get_run(self, run_id: int) -> Optional[Run]:
|
800
828
|
"""Retrieve information about the run with the specified `run_id`."""
|
829
|
+
# Check if runs are still active
|
830
|
+
self._check_and_tag_inactive_run(run_ids={run_id})
|
831
|
+
|
801
832
|
# Convert the uint64 value to sint64 for SQLite
|
802
833
|
sint64_run_id = convert_uint64_to_sint64(run_id)
|
803
834
|
query = "SELECT * FROM run WHERE run_id = ?;"
|
@@ -825,6 +856,9 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
825
856
|
|
826
857
|
def get_run_status(self, run_ids: set[int]) -> dict[int, RunStatus]:
|
827
858
|
"""Retrieve the statuses for the specified runs."""
|
859
|
+
# Check if runs are still active
|
860
|
+
self._check_and_tag_inactive_run(run_ids=run_ids)
|
861
|
+
|
828
862
|
# Convert the uint64 value to sint64 for SQLite
|
829
863
|
sint64_run_ids = (convert_uint64_to_sint64(run_id) for run_id in set(run_ids))
|
830
864
|
query = f"SELECT * FROM run WHERE run_id IN ({','.join(['?'] * len(run_ids))});"
|
@@ -842,6 +876,9 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
842
876
|
|
843
877
|
def update_run_status(self, run_id: int, new_status: RunStatus) -> bool:
|
844
878
|
"""Update the status of the run with the specified `run_id`."""
|
879
|
+
# Check if runs are still active
|
880
|
+
self._check_and_tag_inactive_run(run_ids={run_id})
|
881
|
+
|
845
882
|
# Convert the uint64 value to sint64 for SQLite
|
846
883
|
sint64_run_id = convert_uint64_to_sint64(run_id)
|
847
884
|
query = "SELECT * FROM run WHERE run_id = ?;"
|
@@ -879,9 +916,22 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
879
916
|
return False
|
880
917
|
|
881
918
|
# Update the status
|
882
|
-
query = "UPDATE run SET %s= ?, sub_status = ?, details =
|
919
|
+
query = "UPDATE run SET %s= ?, sub_status = ?, details = ?, "
|
920
|
+
query += "active_until = ?, heartbeat_interval = ? "
|
883
921
|
query += "WHERE run_id = ?;"
|
884
922
|
|
923
|
+
# Prepare data for query
|
924
|
+
# Initialize heartbeat_interval and active_until
|
925
|
+
# when switching to starting or running
|
926
|
+
current = now()
|
927
|
+
if new_status.status in (Status.STARTING, Status.RUNNING):
|
928
|
+
heartbeat_interval = HEARTBEAT_MAX_INTERVAL
|
929
|
+
active_until = current.timestamp() + heartbeat_interval
|
930
|
+
else:
|
931
|
+
heartbeat_interval = 0
|
932
|
+
active_until = 0
|
933
|
+
|
934
|
+
# Determine the timestamp field based on the new status
|
885
935
|
timestamp_fld = ""
|
886
936
|
if new_status.status == Status.STARTING:
|
887
937
|
timestamp_fld = "starting_at"
|
@@ -891,10 +941,12 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
891
941
|
timestamp_fld = "finished_at"
|
892
942
|
|
893
943
|
data = (
|
894
|
-
|
944
|
+
current.isoformat(),
|
895
945
|
new_status.sub_status,
|
896
946
|
new_status.details,
|
897
|
-
|
947
|
+
active_until,
|
948
|
+
heartbeat_interval,
|
949
|
+
convert_uint64_to_sint64(run_id),
|
898
950
|
)
|
899
951
|
self.query(query % timestamp_fld, data)
|
900
952
|
return True
|
@@ -957,6 +1009,44 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
957
1009
|
)
|
958
1010
|
return True
|
959
1011
|
|
1012
|
+
def acknowledge_app_heartbeat(self, run_id: int, heartbeat_interval: float) -> bool:
|
1013
|
+
"""Acknowledge a heartbeat received from a ServerApp for a given run.
|
1014
|
+
|
1015
|
+
A run with status `"running"` is considered alive as long as it sends heartbeats
|
1016
|
+
within the tolerated interval: HEARTBEAT_PATIENCE × heartbeat_interval.
|
1017
|
+
HEARTBEAT_PATIENCE = N allows for N-1 missed heartbeat before the run is
|
1018
|
+
marked as `"completed:failed"`.
|
1019
|
+
"""
|
1020
|
+
# Check if runs are still active
|
1021
|
+
self._check_and_tag_inactive_run(run_ids={run_id})
|
1022
|
+
|
1023
|
+
# Search for the run
|
1024
|
+
sint_run_id = convert_uint64_to_sint64(run_id)
|
1025
|
+
query = "SELECT * FROM run WHERE run_id = ?;"
|
1026
|
+
rows = self.query(query, (sint_run_id,))
|
1027
|
+
|
1028
|
+
if not rows:
|
1029
|
+
log(ERROR, "`run_id` is invalid")
|
1030
|
+
return False
|
1031
|
+
|
1032
|
+
# Check if the run is of status "running"/"starting"
|
1033
|
+
row = rows[0]
|
1034
|
+
status = determine_run_status(row)
|
1035
|
+
if status not in (Status.RUNNING, Status.STARTING):
|
1036
|
+
log(
|
1037
|
+
ERROR,
|
1038
|
+
'Cannot acknowledge heartbeat for run with status "%s"',
|
1039
|
+
status,
|
1040
|
+
)
|
1041
|
+
return False
|
1042
|
+
|
1043
|
+
# Update the `active_until` and `heartbeat_interval` for the given run
|
1044
|
+
active_until = now().timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval
|
1045
|
+
query = "UPDATE run SET active_until = ?, heartbeat_interval = ? "
|
1046
|
+
query += "WHERE run_id = ?"
|
1047
|
+
self.query(query, (active_until, heartbeat_interval, sint_run_id))
|
1048
|
+
return True
|
1049
|
+
|
960
1050
|
def get_serverapp_context(self, run_id: int) -> Optional[Context]:
|
961
1051
|
"""Get the context for the specified `run_id`."""
|
962
1052
|
# Retrieve context if any
|
{flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: flwr-nightly
|
3
|
-
Version: 1.19.0.
|
3
|
+
Version: 1.19.0.dev20250512
|
4
4
|
Summary: Flower: A Friendly Federated AI Framework
|
5
5
|
License: Apache-2.0
|
6
6
|
Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
|
@@ -31,6 +31,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
31
31
|
Classifier: Typing :: Typed
|
32
32
|
Provides-Extra: rest
|
33
33
|
Provides-Extra: simulation
|
34
|
+
Requires-Dist: click (<8.2.0)
|
34
35
|
Requires-Dist: cryptography (>=44.0.1,<45.0.0)
|
35
36
|
Requires-Dist: grpcio (>=1.62.3,<2.0.0,!=1.65.0)
|
36
37
|
Requires-Dist: iterators (>=0.0.2,<0.0.3)
|
{flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/RECORD
RENAMED
@@ -3,7 +3,7 @@ flwr/cli/__init__.py,sha256=EfMGmHoobET6P2blBt_eOByXL8299MgFfB7XNdaPQ6I,720
|
|
3
3
|
flwr/cli/app.py,sha256=AKCP45Dkbpvdil_4Ir9S93L3HP3iUOnHmcZjscoM8uU,1856
|
4
4
|
flwr/cli/auth_plugin/__init__.py,sha256=FyaoqPzcxlBTFfJ2sBRC5USwQLmAhFr5KuBwfMO4bmo,1052
|
5
5
|
flwr/cli/auth_plugin/oidc_cli_plugin.py,sha256=gIhW6Jg9QAo-jL43LYPpw_kn7pdUZZae0s0H8dEgjLM,5384
|
6
|
-
flwr/cli/build.py,sha256=
|
6
|
+
flwr/cli/build.py,sha256=bzvXbA0MdNMni3uu8KUl32y3VOTJpsUToxNQl_0qE6U,6967
|
7
7
|
flwr/cli/cli_user_auth_interceptor.py,sha256=-JqDXpeZNQVwoSG7hMKsiS5qY5k5oklNSlQOVpM0-aY,3126
|
8
8
|
flwr/cli/config_utils.py,sha256=IAVn2uWTXpN72YYt7raLtwp8ziwZugUKSURpc471VzU,9123
|
9
9
|
flwr/cli/constant.py,sha256=g7Ad7o3DJDkJNrWS0T3SSJETWSTkkVJWGpLM8zlbpcY,1289
|
@@ -25,7 +25,7 @@ flwr/cli/new/templates/app/__init__.py,sha256=LbR0ksGiF566JcHM_H5m1Tc4-oYUEilWFl
|
|
25
25
|
flwr/cli/new/templates/app/code/__init__.baseline.py.tpl,sha256=YkHAgppUeD2BnBoGfVB6dEvBfjuIPGsU1gw4CiUi3qA,40
|
26
26
|
flwr/cli/new/templates/app/code/__init__.py,sha256=zXa2YU1swzHxOKDQbwlDMEwVPOUswVeosjkiXNMTgFo,736
|
27
27
|
flwr/cli/new/templates/app/code/__init__.py.tpl,sha256=J0Gn74E7khpLyKJVNqOPu7ev93vkcu1PZugsbxtABMw,52
|
28
|
-
flwr/cli/new/templates/app/code/client.baseline.py.tpl,sha256=
|
28
|
+
flwr/cli/new/templates/app/code/client.baseline.py.tpl,sha256=IYlCZqnaxT2ucP1ReffRNohOkYwNrhtrnDoQBBcrThY,1901
|
29
29
|
flwr/cli/new/templates/app/code/client.huggingface.py.tpl,sha256=ifD08KwjdoGieV26hFCgf3PQB6rMhj_NZLo5iUUndm8,1846
|
30
30
|
flwr/cli/new/templates/app/code/client.jax.py.tpl,sha256=4EkcGGmbPAa6dgw8GYII-GfrGsu8VU6amRHpJvF0WuA,1319
|
31
31
|
flwr/cli/new/templates/app/code/client.mlx.py.tpl,sha256=gOxt_QUTfGFpofdNaxdwTSLZlkTWHPYGix2OGHC1hYE,2376
|
@@ -40,8 +40,8 @@ flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl,sha256=1NA2Sf-EviNtOaYN
|
|
40
40
|
flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl,sha256=ONJw_BgBWEofVNGRDu8KAIThb8saRQlUEK4uS2u_6To,2449
|
41
41
|
flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl,sha256=xkmmBKr0oGmewP56SP3s_6FG6JOVlGlquhg3a9nYMis,3270
|
42
42
|
flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl,sha256=BhiqRg9w1MGuU5h2_vrLhRc0oHItYzE69qX_JI411k8,2754
|
43
|
-
flwr/cli/new/templates/app/code/model.baseline.py.tpl,sha256=
|
44
|
-
flwr/cli/new/templates/app/code/server.baseline.py.tpl,sha256=
|
43
|
+
flwr/cli/new/templates/app/code/model.baseline.py.tpl,sha256=zJklLwH4vPx7rruzhzbAGdInxjjJw-djHCuCx5wshVA,2575
|
44
|
+
flwr/cli/new/templates/app/code/server.baseline.py.tpl,sha256=rBB-DKEuA2SG1DGLW8uSHUg-GydEgb-7NHEclsC2X2g,1539
|
45
45
|
flwr/cli/new/templates/app/code/server.huggingface.py.tpl,sha256=0PJmnZvR9_VPLSak1yVfkOx3dmqo6cynhY1l2s4AZrE,1158
|
46
46
|
flwr/cli/new/templates/app/code/server.jax.py.tpl,sha256=IHk57syZhvO4nWVHGxE9S8f5DTxRKIrTitDufF4RhMY,828
|
47
47
|
flwr/cli/new/templates/app/code/server.mlx.py.tpl,sha256=GAqalaI-U2uRdttNeRn75k1FzdEW3rmgT-ywuKkFdK4,988
|
@@ -58,7 +58,7 @@ flwr/cli/new/templates/app/code/task.pytorch.py.tpl,sha256=XlJqA4Ix_PloO_zJLhjiN
|
|
58
58
|
flwr/cli/new/templates/app/code/task.sklearn.py.tpl,sha256=vHdhtMp0FHxbYafXyhDT9aKmmmA0Jvpx5Oum1Yu9lWY,1850
|
59
59
|
flwr/cli/new/templates/app/code/task.tensorflow.py.tpl,sha256=SKXAZdgBnPpbAbJ90Rb7oQ5ilnopBx_j_JNFoUDeEAI,1732
|
60
60
|
flwr/cli/new/templates/app/code/utils.baseline.py.tpl,sha256=YkHAgppUeD2BnBoGfVB6dEvBfjuIPGsU1gw4CiUi3qA,40
|
61
|
-
flwr/cli/new/templates/app/pyproject.baseline.toml.tpl,sha256=
|
61
|
+
flwr/cli/new/templates/app/pyproject.baseline.toml.tpl,sha256=_hhn9kMIaiQDKe2qlx3flQVHVvqtckKG3WbuXw-ZUzc,2623
|
62
62
|
flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl,sha256=4WfSNMTbhJ-_CQ71gphHVocGBrPxOe33VThGXKmsiHY,1873
|
63
63
|
flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl,sha256=f7xUVuqemFliGqqmwJ_vDgIlBtJM71yTJsXdSzkPDDA,1143
|
64
64
|
flwr/cli/new/templates/app/pyproject.jax.toml.tpl,sha256=35AUGbM1hMlJ4plYsFkydiYvG3XWIOLNnlcemWtIgn4,673
|
@@ -68,7 +68,7 @@ flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl,sha256=dckyBDmvBHbPNB5LQhX
|
|
68
68
|
flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl,sha256=F93FdohqSBzcdFanew33V8bBeC3s9r3IaV8tfd4zw-E,686
|
69
69
|
flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl,sha256=7aOtbvAAnVXyTEYLsg_LtxDRQD16XUjRmnENgAWyiMs,710
|
70
70
|
flwr/cli/run/__init__.py,sha256=RPyB7KbYTFl6YRiilCch6oezxrLQrl1kijV7BMGkLbA,790
|
71
|
-
flwr/cli/run/run.py,sha256=
|
71
|
+
flwr/cli/run/run.py,sha256=mbyf46Tm3qrL8NW02JyDjs6BI49m9UMzXsGK8-Af1r4,8232
|
72
72
|
flwr/cli/stop.py,sha256=iLbh1dq8XMdcIlh0Lh8ufG6h0VvrP1kyp_mGO-kimt0,4976
|
73
73
|
flwr/cli/utils.py,sha256=FjRYfzTw75qh5YHmrg9XzBA6o73T6xWt9WQYIxq-iHY,11207
|
74
74
|
flwr/client/__init__.py,sha256=FslaZOoCGPIzlK-NhL7bFMVVnmFDOh_PhW4AfGzno68,1192
|
@@ -115,7 +115,7 @@ flwr/common/args.py,sha256=-aX_jVnSaDrJR2KZ8Wq0Y3dQHII4R4MJtJOIXzVUA0c,5417
|
|
115
115
|
flwr/common/auth_plugin/__init__.py,sha256=m271m9YjK2QfKDOuIIhcTvGmv1GWh1PL97QB05NTSHs,887
|
116
116
|
flwr/common/auth_plugin/auth_plugin.py,sha256=GaXw4IiU2DkVNkp5S9ue821sbkU9zWSu6HSVZetEdjs,3938
|
117
117
|
flwr/common/config.py,sha256=glcZDjco-amw1YfQcYTFJ4S1pt9APoexT-mf1QscuHs,13960
|
118
|
-
flwr/common/constant.py,sha256=
|
118
|
+
flwr/common/constant.py,sha256=Q8N-up1TvL_vllV_QA8mQlKjqVJ6Kdoze3iem6nSF9E,7375
|
119
119
|
flwr/common/context.py,sha256=Be8obQR_OvEDy1OmshuUKxGRQ7Qx89mf5F4xlhkR10s,2407
|
120
120
|
flwr/common/date.py,sha256=1ZT2cRSpC2DJqprOVTLXYCR_O2_OZR0zXO_brJ3LqWc,1554
|
121
121
|
flwr/common/differential_privacy.py,sha256=FdlpdpPl_H_2HJa8CQM1iCUGBBQ5Dc8CzxmHERM-EoE,6148
|
@@ -293,10 +293,10 @@ flwr/server/superlink/fleet/vce/backend/backend.py,sha256=-wDHjgAy5mrfEgXj0GxkJI
|
|
293
293
|
flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=Hx9hxL7lju1_VJoAwkhBOGerZ3628u0P1zgkPhGWRPY,7154
|
294
294
|
flwr/server/superlink/fleet/vce/vce_api.py,sha256=m7WUiHRl-jTqzjH3cqNCj3RXe3ohT6V6I0JIR6zWZj8,12780
|
295
295
|
flwr/server/superlink/linkstate/__init__.py,sha256=OtsgvDTnZLU3k0sUbkHbqoVwW6ql2FDmb6uT6DbNkZo,1064
|
296
|
-
flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=
|
297
|
-
flwr/server/superlink/linkstate/linkstate.py,sha256=
|
296
|
+
flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=vvoOWjYlmOlbakH7AzpMh0jB70Qxx7UTlAGqjcA8ctM,25926
|
297
|
+
flwr/server/superlink/linkstate/linkstate.py,sha256=j6nW351t07VrBhFqjO34z8tf2PuKOE9aCX9SqpW96pQ,13100
|
298
298
|
flwr/server/superlink/linkstate/linkstate_factory.py,sha256=8RlosqSpKOoD_vhUUQPY0jtE3A84GeF96Z7sWNkRRcA,2069
|
299
|
-
flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=
|
299
|
+
flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=E43YO88vdnG9GW6Rwh9Fb7oWGgEABS9RXDRg3OR3T4Q,43573
|
300
300
|
flwr/server/superlink/linkstate/utils.py,sha256=AJs9jTAEK7JnjF2AODXnOfy0pKAKpe6oUWPCanAP57s,15382
|
301
301
|
flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
|
302
302
|
flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=opJ6SYwIAbu4NWEo3K-VxFO-tMSFmE4H3i2HwHIVRzw,2173
|
@@ -333,7 +333,7 @@ flwr/superexec/exec_servicer.py,sha256=Z0YYfs6eNPhqn8rY0x_R04XgR2mKFpggt07IH0EhU
|
|
333
333
|
flwr/superexec/exec_user_auth_interceptor.py,sha256=iqygALkOMBUu_s_R9G0mFThZA7HTUzuXCLgxLCefiwI,4440
|
334
334
|
flwr/superexec/executor.py,sha256=M5ucqSE53jfRtuCNf59WFLqQvA1Mln4741TySeZE7qQ,3112
|
335
335
|
flwr/superexec/simulation.py,sha256=j6YwUvBN7EQ09ID7MYOCVZ70PGbuyBy8f9bXU0EszEM,4088
|
336
|
-
flwr_nightly-1.19.0.
|
337
|
-
flwr_nightly-1.19.0.
|
338
|
-
flwr_nightly-1.19.0.
|
339
|
-
flwr_nightly-1.19.0.
|
336
|
+
flwr_nightly-1.19.0.dev20250512.dist-info/METADATA,sha256=Sc_-zl1MmehvY10Jr4aE7QBaKxEY_ZXfqbsGdnCAEAc,15910
|
337
|
+
flwr_nightly-1.19.0.dev20250512.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
338
|
+
flwr_nightly-1.19.0.dev20250512.dist-info/entry_points.txt,sha256=2-1L-GNKhwGw2_7_RoH55vHw2SIHjdAQy3HAVAWl9PY,374
|
339
|
+
flwr_nightly-1.19.0.dev20250512.dist-info/RECORD,,
|
{flwr_nightly-1.19.0.dev20250510.dist-info → flwr_nightly-1.19.0.dev20250512.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|