flwr-nightly 1.23.0.dev20251020__py3-none-any.whl → 1.23.0.dev20251022__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.
Potentially problematic release.
This version of flwr-nightly might be problematic. Click here for more details.
- flwr/cli/ls.py +5 -5
- flwr/cli/supernode/ls.py +8 -15
- flwr/common/constant.py +12 -4
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/proto/control_pb2.py +6 -6
- flwr/proto/control_pb2.pyi +0 -5
- flwr/server/app.py +4 -6
- flwr/server/superlink/fleet/vce/vce_api.py +2 -1
- flwr/server/superlink/linkstate/in_memory_linkstate.py +5 -1
- flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +64 -146
- flwr/server/superlink/linkstate/utils.py +3 -54
- flwr/simulation/run_simulation.py +2 -1
- flwr/supercore/constant.py +3 -0
- flwr/supercore/object_store/object_store_factory.py +26 -6
- flwr/supercore/object_store/sqlite_object_store.py +252 -0
- flwr/supercore/sqlite_mixin.py +156 -0
- flwr/supercore/utils.py +20 -0
- flwr/superlink/servicer/control/control_servicer.py +6 -70
- {flwr_nightly-1.23.0.dev20251020.dist-info → flwr_nightly-1.23.0.dev20251022.dist-info}/METADATA +1 -1
- {flwr_nightly-1.23.0.dev20251020.dist-info → flwr_nightly-1.23.0.dev20251022.dist-info}/RECORD +24 -22
- {flwr_nightly-1.23.0.dev20251020.dist-info → flwr_nightly-1.23.0.dev20251022.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.23.0.dev20251020.dist-info → flwr_nightly-1.23.0.dev20251022.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
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 SQLite ObjectStore implementation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from typing import Optional, cast
|
|
19
|
+
|
|
20
|
+
from flwr.common.inflatable import (
|
|
21
|
+
get_object_id,
|
|
22
|
+
is_valid_sha256_hash,
|
|
23
|
+
iterate_object_tree,
|
|
24
|
+
)
|
|
25
|
+
from flwr.common.inflatable_utils import validate_object_content
|
|
26
|
+
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
27
|
+
from flwr.supercore.sqlite_mixin import SqliteMixin
|
|
28
|
+
from flwr.supercore.utils import uint64_to_int64
|
|
29
|
+
|
|
30
|
+
from .object_store import NoObjectInStoreError, ObjectStore
|
|
31
|
+
|
|
32
|
+
SQL_CREATE_OBJECTS = """
|
|
33
|
+
CREATE TABLE IF NOT EXISTS objects (
|
|
34
|
+
object_id TEXT PRIMARY KEY,
|
|
35
|
+
content BLOB,
|
|
36
|
+
is_available INTEGER NOT NULL CHECK (is_available IN (0,1)),
|
|
37
|
+
ref_count INTEGER NOT NULL
|
|
38
|
+
);
|
|
39
|
+
"""
|
|
40
|
+
SQL_CREATE_OBJECT_CHILDREN = """
|
|
41
|
+
CREATE TABLE IF NOT EXISTS object_children (
|
|
42
|
+
parent_id TEXT NOT NULL,
|
|
43
|
+
child_id TEXT NOT NULL,
|
|
44
|
+
FOREIGN KEY (parent_id) REFERENCES objects(object_id) ON DELETE CASCADE,
|
|
45
|
+
FOREIGN KEY (child_id) REFERENCES objects(object_id) ON DELETE CASCADE,
|
|
46
|
+
PRIMARY KEY (parent_id, child_id)
|
|
47
|
+
);
|
|
48
|
+
"""
|
|
49
|
+
SQL_CREATE_RUN_OBJECTS = """
|
|
50
|
+
CREATE TABLE IF NOT EXISTS run_objects (
|
|
51
|
+
run_id INTEGER NOT NULL,
|
|
52
|
+
object_id TEXT NOT NULL,
|
|
53
|
+
FOREIGN KEY (object_id) REFERENCES objects(object_id) ON DELETE CASCADE,
|
|
54
|
+
PRIMARY KEY (run_id, object_id)
|
|
55
|
+
);
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
60
|
+
"""SQLite-based implementation of the ObjectStore interface."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, database_path: str, verify: bool = True) -> None:
|
|
63
|
+
super().__init__(database_path)
|
|
64
|
+
self.verify = verify
|
|
65
|
+
|
|
66
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
67
|
+
"""Connect to the DB, enable FK support, and create tables if needed."""
|
|
68
|
+
return self._ensure_initialized(
|
|
69
|
+
SQL_CREATE_OBJECTS,
|
|
70
|
+
SQL_CREATE_OBJECT_CHILDREN,
|
|
71
|
+
SQL_CREATE_RUN_OBJECTS,
|
|
72
|
+
log_queries=log_queries,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
76
|
+
"""Identify and preregister missing objects in the `ObjectStore`."""
|
|
77
|
+
new_objects = []
|
|
78
|
+
for tree_node in iterate_object_tree(object_tree):
|
|
79
|
+
obj_id = tree_node.object_id
|
|
80
|
+
if not is_valid_sha256_hash(obj_id):
|
|
81
|
+
raise ValueError(f"Invalid object ID format: {obj_id}")
|
|
82
|
+
|
|
83
|
+
child_ids = [child.object_id for child in tree_node.children]
|
|
84
|
+
with self.conn:
|
|
85
|
+
row = self.conn.execute(
|
|
86
|
+
"SELECT object_id, is_available FROM objects WHERE object_id=?",
|
|
87
|
+
(obj_id,),
|
|
88
|
+
).fetchone()
|
|
89
|
+
if row is None:
|
|
90
|
+
# Insert new object
|
|
91
|
+
self.conn.execute(
|
|
92
|
+
"INSERT INTO objects"
|
|
93
|
+
"(object_id, content, is_available, ref_count) "
|
|
94
|
+
"VALUES (?, ?, ?, ?)",
|
|
95
|
+
(obj_id, b"", 0, 0),
|
|
96
|
+
)
|
|
97
|
+
for cid in child_ids:
|
|
98
|
+
self.conn.execute(
|
|
99
|
+
"INSERT INTO object_children(parent_id, child_id) "
|
|
100
|
+
"VALUES (?, ?)",
|
|
101
|
+
(obj_id, cid),
|
|
102
|
+
)
|
|
103
|
+
self.conn.execute(
|
|
104
|
+
"UPDATE objects SET ref_count = ref_count + 1 "
|
|
105
|
+
"WHERE object_id = ?",
|
|
106
|
+
(cid,),
|
|
107
|
+
)
|
|
108
|
+
new_objects.append(obj_id)
|
|
109
|
+
else:
|
|
110
|
+
# Add to the list of new objects if not available
|
|
111
|
+
if not row["is_available"]:
|
|
112
|
+
new_objects.append(obj_id)
|
|
113
|
+
|
|
114
|
+
# Ensure run mapping
|
|
115
|
+
self.conn.execute(
|
|
116
|
+
"INSERT OR IGNORE INTO run_objects(run_id, object_id) "
|
|
117
|
+
"VALUES (?, ?)",
|
|
118
|
+
(uint64_to_int64(run_id), obj_id),
|
|
119
|
+
)
|
|
120
|
+
return new_objects
|
|
121
|
+
|
|
122
|
+
def get_object_tree(self, object_id: str) -> ObjectTree:
|
|
123
|
+
"""Get the object tree for a given object ID."""
|
|
124
|
+
with self.conn:
|
|
125
|
+
row = self.conn.execute(
|
|
126
|
+
"SELECT object_id FROM objects WHERE object_id=?", (object_id,)
|
|
127
|
+
).fetchone()
|
|
128
|
+
if not row:
|
|
129
|
+
raise NoObjectInStoreError(f"Object {object_id} not found.")
|
|
130
|
+
children = self.query(
|
|
131
|
+
"SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Build the object trees of all children
|
|
135
|
+
try:
|
|
136
|
+
child_trees = [self.get_object_tree(ch["child_id"]) for ch in children]
|
|
137
|
+
except NoObjectInStoreError as e:
|
|
138
|
+
# Raise an error if any child object is missing
|
|
139
|
+
# This indicates an integrity issue
|
|
140
|
+
raise NoObjectInStoreError(
|
|
141
|
+
f"Object tree for object ID '{object_id}' contains missing "
|
|
142
|
+
"children. This may indicate a corrupted object store."
|
|
143
|
+
) from e
|
|
144
|
+
|
|
145
|
+
# Create and return the ObjectTree for the current object
|
|
146
|
+
return ObjectTree(object_id=object_id, children=child_trees)
|
|
147
|
+
|
|
148
|
+
def put(self, object_id: str, object_content: bytes) -> None:
|
|
149
|
+
"""Put an object into the store."""
|
|
150
|
+
if self.verify:
|
|
151
|
+
# Verify object_id and object_content match
|
|
152
|
+
object_id_from_content = get_object_id(object_content)
|
|
153
|
+
if object_id != object_id_from_content:
|
|
154
|
+
raise ValueError(f"Object ID {object_id} does not match content hash")
|
|
155
|
+
|
|
156
|
+
# Validate object content
|
|
157
|
+
validate_object_content(content=object_content)
|
|
158
|
+
|
|
159
|
+
with self.conn:
|
|
160
|
+
# Only allow adding the object if it has been preregistered
|
|
161
|
+
row = self.conn.execute(
|
|
162
|
+
"SELECT is_available FROM objects WHERE object_id=?", (object_id,)
|
|
163
|
+
).fetchone()
|
|
164
|
+
if row is None:
|
|
165
|
+
raise NoObjectInStoreError(
|
|
166
|
+
f"Object with ID '{object_id}' was not pre-registered."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Return if object is already present in the store
|
|
170
|
+
if row["is_available"]:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Update the object entry in the store
|
|
174
|
+
self.conn.execute(
|
|
175
|
+
"UPDATE objects SET content=?, is_available=1 WHERE object_id=?",
|
|
176
|
+
(object_content, object_id),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def get(self, object_id: str) -> Optional[bytes]:
|
|
180
|
+
"""Get an object from the store."""
|
|
181
|
+
rows = self.query("SELECT content FROM objects WHERE object_id=?", (object_id,))
|
|
182
|
+
return rows[0]["content"] if rows else None
|
|
183
|
+
|
|
184
|
+
def delete(self, object_id: str) -> None:
|
|
185
|
+
"""Delete an object and its unreferenced descendants from the store."""
|
|
186
|
+
with self.conn:
|
|
187
|
+
row = self.conn.execute(
|
|
188
|
+
"SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
|
|
189
|
+
).fetchone()
|
|
190
|
+
|
|
191
|
+
# If the object is not in the store, nothing to delete
|
|
192
|
+
if row is None:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Skip deletion if there are still references
|
|
196
|
+
if row["ref_count"] > 0:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Deleting will cascade via FK, but we need to decrement children first
|
|
200
|
+
children = self.conn.execute(
|
|
201
|
+
"SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
|
|
202
|
+
).fetchall()
|
|
203
|
+
child_ids = [child["child_id"] for child in children]
|
|
204
|
+
|
|
205
|
+
if child_ids:
|
|
206
|
+
placeholders = ", ".join("?" for _ in child_ids)
|
|
207
|
+
query = f"""
|
|
208
|
+
UPDATE objects SET ref_count = ref_count - 1
|
|
209
|
+
WHERE object_id IN ({placeholders})
|
|
210
|
+
"""
|
|
211
|
+
self.conn.execute(query, child_ids)
|
|
212
|
+
|
|
213
|
+
self.conn.execute("DELETE FROM objects WHERE object_id=?", (object_id,))
|
|
214
|
+
|
|
215
|
+
# Recursively clean children
|
|
216
|
+
for child_id in child_ids:
|
|
217
|
+
self.delete(child_id)
|
|
218
|
+
|
|
219
|
+
def delete_objects_in_run(self, run_id: int) -> None:
|
|
220
|
+
"""Delete all objects that were registered in a specific run."""
|
|
221
|
+
run_id_sint = uint64_to_int64(run_id)
|
|
222
|
+
with self.conn:
|
|
223
|
+
objs = self.conn.execute(
|
|
224
|
+
"SELECT object_id FROM run_objects WHERE run_id=?", (run_id_sint,)
|
|
225
|
+
).fetchall()
|
|
226
|
+
for obj in objs:
|
|
227
|
+
object_id = obj["object_id"]
|
|
228
|
+
row = self.conn.execute(
|
|
229
|
+
"SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
|
|
230
|
+
).fetchone()
|
|
231
|
+
if row and row["ref_count"] == 0:
|
|
232
|
+
self.delete(object_id)
|
|
233
|
+
self.conn.execute("DELETE FROM run_objects WHERE run_id=?", (run_id_sint,))
|
|
234
|
+
|
|
235
|
+
def clear(self) -> None:
|
|
236
|
+
"""Clear the store."""
|
|
237
|
+
with self.conn:
|
|
238
|
+
self.conn.execute("DELETE FROM object_children;")
|
|
239
|
+
self.conn.execute("DELETE FROM run_objects;")
|
|
240
|
+
self.conn.execute("DELETE FROM objects;")
|
|
241
|
+
|
|
242
|
+
def __contains__(self, object_id: str) -> bool:
|
|
243
|
+
"""Check if an object_id is in the store."""
|
|
244
|
+
row = self.conn.execute(
|
|
245
|
+
"SELECT 1 FROM objects WHERE object_id=?", (object_id,)
|
|
246
|
+
).fetchone()
|
|
247
|
+
return row is not None
|
|
248
|
+
|
|
249
|
+
def __len__(self) -> int:
|
|
250
|
+
"""Return the number of objects in the store."""
|
|
251
|
+
row = self.conn.execute("SELECT COUNT(*) AS cnt FROM objects;").fetchone()
|
|
252
|
+
return cast(int, row["cnt"])
|
|
@@ -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))
|
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
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import hashlib
|
|
19
19
|
import time
|
|
20
20
|
from collections.abc import Generator, Sequence
|
|
21
|
-
from datetime import timedelta
|
|
22
21
|
from logging import ERROR, INFO
|
|
23
22
|
from typing import Any, Optional, cast
|
|
24
23
|
|
|
@@ -72,7 +71,6 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
72
71
|
)
|
|
73
72
|
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
|
74
73
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
75
|
-
from flwr.supercore.constant import NodeStatus
|
|
76
74
|
from flwr.supercore.ffs import FfsFactory
|
|
77
75
|
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
|
|
78
76
|
from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
|
|
@@ -466,79 +464,17 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
466
464
|
raise grpc.RpcError() # This line is unreachable
|
|
467
465
|
|
|
468
466
|
nodes_info: Sequence[NodeInfo] = []
|
|
469
|
-
#
|
|
470
|
-
|
|
471
|
-
nodes_info = _create_list_nodeif_for_dry_run()
|
|
472
|
-
|
|
473
|
-
else:
|
|
474
|
-
# Init link state
|
|
475
|
-
state = self.linkstate_factory.state()
|
|
467
|
+
# Init link state
|
|
468
|
+
state = self.linkstate_factory.state()
|
|
476
469
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
470
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
471
|
+
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
|
472
|
+
# Retrieve all nodes for the account
|
|
473
|
+
nodes_info = state.get_node_info(owner_aids=[flwr_aid])
|
|
481
474
|
|
|
482
475
|
return ListNodesResponse(nodes_info=nodes_info, now=now().isoformat())
|
|
483
476
|
|
|
484
477
|
|
|
485
|
-
def _create_list_nodeif_for_dry_run() -> Sequence[NodeInfo]:
|
|
486
|
-
"""Create a list of NodeInfo for dry run testing."""
|
|
487
|
-
nodes_info: list[NodeInfo] = []
|
|
488
|
-
# A node registered (but not connected)
|
|
489
|
-
nodes_info.append(
|
|
490
|
-
NodeInfo(
|
|
491
|
-
node_id=15390646978706312628,
|
|
492
|
-
owner_aid="owner_aid_1",
|
|
493
|
-
status=NodeStatus.REGISTERED,
|
|
494
|
-
registered_at=(now()).isoformat(),
|
|
495
|
-
last_activated_at="",
|
|
496
|
-
last_deactivated_at="",
|
|
497
|
-
unregistered_at="",
|
|
498
|
-
)
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
# A node registered and connected
|
|
502
|
-
nodes_info.append(
|
|
503
|
-
NodeInfo(
|
|
504
|
-
node_id=2941141058168602545,
|
|
505
|
-
owner_aid="owner_aid_2",
|
|
506
|
-
status=NodeStatus.ONLINE,
|
|
507
|
-
registered_at=(now()).isoformat(),
|
|
508
|
-
last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
|
|
509
|
-
last_deactivated_at="",
|
|
510
|
-
unregistered_at="",
|
|
511
|
-
)
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
# A node registered and unregistered (never connected)
|
|
515
|
-
nodes_info.append(
|
|
516
|
-
NodeInfo(
|
|
517
|
-
node_id=906971720890549292,
|
|
518
|
-
owner_aid="owner_aid_3",
|
|
519
|
-
status=NodeStatus.UNREGISTERED,
|
|
520
|
-
registered_at=(now()).isoformat(),
|
|
521
|
-
last_activated_at="",
|
|
522
|
-
last_deactivated_at="",
|
|
523
|
-
unregistered_at=(now() + timedelta(hours=1)).isoformat(),
|
|
524
|
-
)
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
# A node registered, deactivate and then unregistered
|
|
528
|
-
nodes_info.append(
|
|
529
|
-
NodeInfo(
|
|
530
|
-
node_id=1781174086018058152,
|
|
531
|
-
owner_aid="owner_aid_4",
|
|
532
|
-
status=NodeStatus.OFFLINE,
|
|
533
|
-
registered_at=(now()).isoformat(),
|
|
534
|
-
last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
|
|
535
|
-
last_deactivated_at=(now() + timedelta(hours=1)).isoformat(),
|
|
536
|
-
unregistered_at=(now() + timedelta(hours=1.5)).isoformat(),
|
|
537
|
-
)
|
|
538
|
-
)
|
|
539
|
-
return nodes_info
|
|
540
|
-
|
|
541
|
-
|
|
542
478
|
def _create_list_runs_response(
|
|
543
479
|
run_ids: set[int], state: LinkState, store: ObjectStore
|
|
544
480
|
) -> ListRunsResponse:
|
{flwr_nightly-1.23.0.dev20251020.dist-info → flwr_nightly-1.23.0.dev20251022.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: flwr-nightly
|
|
3
|
-
Version: 1.23.0.
|
|
3
|
+
Version: 1.23.0.dev20251022
|
|
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
|