flwr-nightly 1.23.0.dev20251017__py3-none-any.whl → 1.23.0.dev20251021__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/app.py +4 -4
- flwr/cli/ls.py +5 -5
- flwr/cli/supernode/__init__.py +4 -4
- flwr/cli/supernode/ls.py +19 -26
- flwr/cli/supernode/{create.py → register.py} +13 -12
- flwr/cli/supernode/{delete.py → unregister.py} +12 -10
- flwr/common/constant.py +12 -4
- flwr/common/exit/exit_code.py +5 -0
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/proto/control_pb2.py +15 -15
- flwr/proto/control_pb2.pyi +12 -17
- flwr/proto/control_pb2_grpc.py +41 -41
- flwr/proto/control_pb2_grpc.pyi +24 -24
- flwr/proto/node_pb2.py +2 -2
- flwr/proto/node_pb2.pyi +10 -10
- flwr/server/app.py +11 -0
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +28 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +18 -12
- flwr/server/superlink/linkstate/sqlite_linkstate.py +41 -118
- flwr/supercore/constant.py +2 -2
- flwr/supercore/sqlite_mixin.py +188 -0
- flwr/superlink/servicer/control/control_servicer.py +30 -93
- flwr/supernode/start_client_internal.py +14 -0
- {flwr_nightly-1.23.0.dev20251017.dist-info → flwr_nightly-1.23.0.dev20251021.dist-info}/METADATA +1 -1
- {flwr_nightly-1.23.0.dev20251017.dist-info → flwr_nightly-1.23.0.dev20251021.dist-info}/RECORD +28 -27
- {flwr_nightly-1.23.0.dev20251017.dist-info → flwr_nightly-1.23.0.dev20251021.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.23.0.dev20251017.dist-info → flwr_nightly-1.23.0.dev20251021.dist-info}/entry_points.txt +0 -0
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
# pylint: disable=too-many-lines
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
|
-
import re
|
|
22
21
|
import secrets
|
|
23
22
|
import sqlite3
|
|
24
23
|
from collections.abc import Sequence
|
|
25
|
-
from logging import
|
|
24
|
+
from logging import ERROR, WARNING
|
|
26
25
|
from typing import Any, Optional, Union, cast
|
|
27
26
|
|
|
28
27
|
from flwr.common import Context, Message, Metadata, log, now
|
|
@@ -52,6 +51,7 @@ from flwr.proto.recorddict_pb2 import RecordDict as ProtoRecordDict
|
|
|
52
51
|
# pylint: enable=E0611
|
|
53
52
|
from flwr.server.utils.validator import validate_message
|
|
54
53
|
from flwr.supercore.constant import NodeStatus
|
|
54
|
+
from flwr.supercore.sqlite_mixin import SqliteMixin
|
|
55
55
|
|
|
56
56
|
from .linkstate import LinkState
|
|
57
57
|
from .utils import (
|
|
@@ -76,10 +76,10 @@ CREATE TABLE IF NOT EXISTS node(
|
|
|
76
76
|
node_id INTEGER UNIQUE,
|
|
77
77
|
owner_aid TEXT,
|
|
78
78
|
status TEXT,
|
|
79
|
-
|
|
79
|
+
registered_at TEXT,
|
|
80
80
|
last_activated_at TEXT NULL,
|
|
81
81
|
last_deactivated_at TEXT NULL,
|
|
82
|
-
|
|
82
|
+
unregistered_at TEXT NULL,
|
|
83
83
|
online_until TIMESTAMP NULL,
|
|
84
84
|
heartbeat_interval REAL,
|
|
85
85
|
public_key BLOB UNIQUE
|
|
@@ -183,95 +183,25 @@ CREATE TABLE IF NOT EXISTS token_store (
|
|
|
183
183
|
);
|
|
184
184
|
"""
|
|
185
185
|
|
|
186
|
-
DictOrTuple = Union[tuple[Any, ...], dict[str, Any]]
|
|
187
186
|
|
|
188
|
-
|
|
189
|
-
class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
187
|
+
class SqliteLinkState(LinkState, SqliteMixin): # pylint: disable=R0904
|
|
190
188
|
"""SQLite-based LinkState implementation."""
|
|
191
189
|
|
|
192
|
-
def __init__(
|
|
193
|
-
self,
|
|
194
|
-
database_path: str,
|
|
195
|
-
) -> None:
|
|
196
|
-
"""Initialize an SqliteLinkState.
|
|
197
|
-
|
|
198
|
-
Parameters
|
|
199
|
-
----------
|
|
200
|
-
database : (path-like object)
|
|
201
|
-
The path to the database file to be opened. Pass ":memory:" to open
|
|
202
|
-
a connection to a database that is in RAM, instead of on disk.
|
|
203
|
-
"""
|
|
204
|
-
self.database_path = database_path
|
|
205
|
-
self.conn: Optional[sqlite3.Connection] = None
|
|
206
|
-
|
|
207
190
|
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
208
|
-
"""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
self.conn.row_factory = dict_factory
|
|
223
|
-
if log_queries:
|
|
224
|
-
self.conn.set_trace_callback(lambda query: log(DEBUG, query))
|
|
225
|
-
cur = self.conn.cursor()
|
|
226
|
-
|
|
227
|
-
# Create each table if not exists queries
|
|
228
|
-
cur.execute(SQL_CREATE_TABLE_RUN)
|
|
229
|
-
cur.execute(SQL_CREATE_TABLE_LOGS)
|
|
230
|
-
cur.execute(SQL_CREATE_TABLE_CONTEXT)
|
|
231
|
-
cur.execute(SQL_CREATE_TABLE_MESSAGE_INS)
|
|
232
|
-
cur.execute(SQL_CREATE_TABLE_MESSAGE_RES)
|
|
233
|
-
cur.execute(SQL_CREATE_TABLE_NODE)
|
|
234
|
-
cur.execute(SQL_CREATE_TABLE_PUBLIC_KEY)
|
|
235
|
-
cur.execute(SQL_CREATE_TABLE_TOKEN_STORE)
|
|
236
|
-
cur.execute(SQL_CREATE_INDEX_ONLINE_UNTIL)
|
|
237
|
-
cur.execute(SQL_CREATE_INDEX_OWNER_AID)
|
|
238
|
-
res = cur.execute("SELECT name FROM sqlite_schema;")
|
|
239
|
-
return res.fetchall()
|
|
240
|
-
|
|
241
|
-
def query(
|
|
242
|
-
self,
|
|
243
|
-
query: str,
|
|
244
|
-
data: Optional[Union[Sequence[DictOrTuple], DictOrTuple]] = None,
|
|
245
|
-
) -> list[dict[str, Any]]:
|
|
246
|
-
"""Execute a SQL query."""
|
|
247
|
-
if self.conn is None:
|
|
248
|
-
raise AttributeError("LinkState is not initialized.")
|
|
249
|
-
|
|
250
|
-
if data is None:
|
|
251
|
-
data = []
|
|
252
|
-
|
|
253
|
-
# Clean up whitespace to make the logs nicer
|
|
254
|
-
query = re.sub(r"\s+", " ", query)
|
|
255
|
-
|
|
256
|
-
try:
|
|
257
|
-
with self.conn:
|
|
258
|
-
if (
|
|
259
|
-
len(data) > 0
|
|
260
|
-
and isinstance(data, (tuple, list))
|
|
261
|
-
and isinstance(data[0], (tuple, dict))
|
|
262
|
-
):
|
|
263
|
-
rows = self.conn.executemany(query, data)
|
|
264
|
-
else:
|
|
265
|
-
rows = self.conn.execute(query, data)
|
|
266
|
-
|
|
267
|
-
# Extract results before committing to support
|
|
268
|
-
# INSERT/UPDATE ... RETURNING
|
|
269
|
-
# style queries
|
|
270
|
-
result = rows.fetchall()
|
|
271
|
-
except KeyError as exc:
|
|
272
|
-
log(ERROR, {"query": query, "data": data, "exception": exc})
|
|
273
|
-
|
|
274
|
-
return result
|
|
191
|
+
"""Connect to the DB, enable FK support, and create tables if needed."""
|
|
192
|
+
return self._ensure_initialized(
|
|
193
|
+
SQL_CREATE_TABLE_RUN,
|
|
194
|
+
SQL_CREATE_TABLE_LOGS,
|
|
195
|
+
SQL_CREATE_TABLE_CONTEXT,
|
|
196
|
+
SQL_CREATE_TABLE_MESSAGE_INS,
|
|
197
|
+
SQL_CREATE_TABLE_MESSAGE_RES,
|
|
198
|
+
SQL_CREATE_TABLE_NODE,
|
|
199
|
+
SQL_CREATE_TABLE_PUBLIC_KEY,
|
|
200
|
+
SQL_CREATE_TABLE_TOKEN_STORE,
|
|
201
|
+
SQL_CREATE_INDEX_ONLINE_UNTIL,
|
|
202
|
+
SQL_CREATE_INDEX_OWNER_AID,
|
|
203
|
+
log_queries=log_queries,
|
|
204
|
+
)
|
|
275
205
|
|
|
276
206
|
def store_message_ins(self, message: Message) -> Optional[str]:
|
|
277
207
|
"""Store one Message."""
|
|
@@ -490,11 +420,12 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
490
420
|
sint_node_id = convert_uint64_to_sint64(in_message.metadata.dst_node_id)
|
|
491
421
|
dst_node_ids.add(sint_node_id)
|
|
492
422
|
query = f"""
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
423
|
+
SELECT node_id, online_until
|
|
424
|
+
FROM node
|
|
425
|
+
WHERE node_id IN ({",".join(["?"] * len(dst_node_ids))})
|
|
426
|
+
AND status != ?
|
|
427
|
+
"""
|
|
428
|
+
rows = self.query(query, tuple(dst_node_ids) + (NodeStatus.UNREGISTERED,))
|
|
498
429
|
tmp_ret_dict = check_node_availability_for_in_message(
|
|
499
430
|
inquired_in_message_ids=message_ids,
|
|
500
431
|
found_in_message_dict=found_message_ins_dict,
|
|
@@ -623,8 +554,8 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
623
554
|
|
|
624
555
|
query = """
|
|
625
556
|
INSERT INTO node
|
|
626
|
-
(node_id, owner_aid, status,
|
|
627
|
-
last_deactivated_at,
|
|
557
|
+
(node_id, owner_aid, status, registered_at, last_activated_at,
|
|
558
|
+
last_deactivated_at, unregistered_at, online_until, heartbeat_interval,
|
|
628
559
|
public_key)
|
|
629
560
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
630
561
|
"""
|
|
@@ -636,11 +567,11 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
636
567
|
(
|
|
637
568
|
sint64_node_id, # node_id
|
|
638
569
|
owner_aid, # owner_aid
|
|
639
|
-
NodeStatus.
|
|
640
|
-
now().isoformat(), #
|
|
570
|
+
NodeStatus.REGISTERED, # status
|
|
571
|
+
now().isoformat(), # registered_at
|
|
641
572
|
None, # last_activated_at
|
|
642
573
|
None, # last_deactivated_at
|
|
643
|
-
None, #
|
|
574
|
+
None, # unregistered_at
|
|
644
575
|
None, # online_until, initialized with offline status
|
|
645
576
|
heartbeat_interval, # heartbeat_interval
|
|
646
577
|
public_key, # public_key
|
|
@@ -662,15 +593,19 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
662
593
|
|
|
663
594
|
query = """
|
|
664
595
|
UPDATE node
|
|
665
|
-
SET status = ?,
|
|
596
|
+
SET status = ?, unregistered_at = ?,
|
|
597
|
+
online_until = IIF(online_until > ?, ?, online_until)
|
|
666
598
|
WHERE node_id = ? AND status != ? AND owner_aid = ?
|
|
667
599
|
RETURNING node_id
|
|
668
600
|
"""
|
|
601
|
+
current = now()
|
|
669
602
|
params = (
|
|
670
|
-
NodeStatus.
|
|
671
|
-
|
|
603
|
+
NodeStatus.UNREGISTERED,
|
|
604
|
+
current.isoformat(),
|
|
605
|
+
current.timestamp(),
|
|
606
|
+
current.timestamp(),
|
|
672
607
|
sint64_node_id,
|
|
673
|
-
NodeStatus.
|
|
608
|
+
NodeStatus.UNREGISTERED,
|
|
674
609
|
owner_aid,
|
|
675
610
|
)
|
|
676
611
|
|
|
@@ -775,7 +710,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
775
710
|
|
|
776
711
|
# Query the public key for the given node_id
|
|
777
712
|
query = "SELECT public_key FROM node WHERE node_id = ? AND status != ?;"
|
|
778
|
-
rows = self.query(query, (sint64_node_id, NodeStatus.
|
|
713
|
+
rows = self.query(query, (sint64_node_id, NodeStatus.UNREGISTERED))
|
|
779
714
|
|
|
780
715
|
# If no result is found, return None
|
|
781
716
|
if not rows:
|
|
@@ -788,7 +723,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
788
723
|
"""Get `node_id` for the specified `public_key` if it exists and is not
|
|
789
724
|
deleted."""
|
|
790
725
|
query = "SELECT node_id FROM node WHERE public_key = ? AND status != ?;"
|
|
791
|
-
rows = self.query(query, (public_key, NodeStatus.
|
|
726
|
+
rows = self.query(query, (public_key, NodeStatus.UNREGISTERED))
|
|
792
727
|
|
|
793
728
|
# If no result is found, return None
|
|
794
729
|
if not rows:
|
|
@@ -1059,7 +994,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
1059
994
|
# Check if node exists and not deleted
|
|
1060
995
|
query = "SELECT status FROM node WHERE node_id = ? AND status != ?"
|
|
1061
996
|
row = self.conn.execute(
|
|
1062
|
-
query, (sint64_node_id, NodeStatus.
|
|
997
|
+
query, (sint64_node_id, NodeStatus.UNREGISTERED)
|
|
1063
998
|
).fetchone()
|
|
1064
999
|
if row is None:
|
|
1065
1000
|
return False
|
|
@@ -1250,18 +1185,6 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
|
1250
1185
|
return convert_sint64_to_uint64(rows[0]["run_id"])
|
|
1251
1186
|
|
|
1252
1187
|
|
|
1253
|
-
def dict_factory(
|
|
1254
|
-
cursor: sqlite3.Cursor,
|
|
1255
|
-
row: sqlite3.Row,
|
|
1256
|
-
) -> dict[str, Any]:
|
|
1257
|
-
"""Turn SQLite results into dicts.
|
|
1258
|
-
|
|
1259
|
-
Less efficent for retrival of large amounts of data but easier to use.
|
|
1260
|
-
"""
|
|
1261
|
-
fields = [column[0] for column in cursor.description]
|
|
1262
|
-
return dict(zip(fields, row))
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
1188
|
def message_to_dict(message: Message) -> dict[str, Any]:
|
|
1266
1189
|
"""Transform Message to dict."""
|
|
1267
1190
|
result = {
|
flwr/supercore/constant.py
CHANGED
|
@@ -24,10 +24,10 @@ EXEC_PLUGIN_SECTION = "exec_plugin"
|
|
|
24
24
|
class NodeStatus:
|
|
25
25
|
"""Event log writer types."""
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
REGISTERED = "registered"
|
|
28
28
|
ONLINE = "online"
|
|
29
29
|
OFFLINE = "offline"
|
|
30
|
-
|
|
30
|
+
UNREGISTERED = "unregistered"
|
|
31
31
|
|
|
32
32
|
def __new__(cls) -> NodeStatus:
|
|
33
33
|
"""Prevent instantiation."""
|
|
@@ -0,0 +1,188 @@
|
|
|
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 contextlib
|
|
19
|
+
import re
|
|
20
|
+
import sqlite3
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from collections.abc import Iterator, Sequence
|
|
23
|
+
from logging import DEBUG, ERROR
|
|
24
|
+
from typing import Any, Optional, Union
|
|
25
|
+
|
|
26
|
+
from flwr.common.logger import log
|
|
27
|
+
|
|
28
|
+
DictOrTuple = Union[tuple[Any, ...], dict[str, Any]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SqliteMixin(ABC):
|
|
32
|
+
"""Mixin providing common SQLite connection and initialization logic."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, database_path: str) -> None:
|
|
35
|
+
self.database_path = database_path
|
|
36
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def conn(self) -> sqlite3.Connection:
|
|
40
|
+
"""Get the SQLite connection."""
|
|
41
|
+
if self._conn is None:
|
|
42
|
+
raise AttributeError("Database not initialized. Call initialize() first.")
|
|
43
|
+
return self._conn
|
|
44
|
+
|
|
45
|
+
@contextlib.contextmanager
|
|
46
|
+
def transaction(self) -> Iterator[None]:
|
|
47
|
+
"""Context manager for a transaction.
|
|
48
|
+
|
|
49
|
+
This allows nesting of transactions by checking if a transaction is
|
|
50
|
+
already in progress.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
::
|
|
55
|
+
|
|
56
|
+
with self.transaction():
|
|
57
|
+
# Do some DB operations here
|
|
58
|
+
...
|
|
59
|
+
with self.transaction():
|
|
60
|
+
# Do some more DB operations here
|
|
61
|
+
...
|
|
62
|
+
"""
|
|
63
|
+
if self._conn is None:
|
|
64
|
+
raise AttributeError("Database not initialized. Call initialize() first.")
|
|
65
|
+
|
|
66
|
+
# Start a transaction if not already in one
|
|
67
|
+
if not self._conn.in_transaction:
|
|
68
|
+
self._conn.execute("BEGIN")
|
|
69
|
+
try:
|
|
70
|
+
yield
|
|
71
|
+
self._conn.commit()
|
|
72
|
+
except Exception:
|
|
73
|
+
self._conn.rollback()
|
|
74
|
+
raise
|
|
75
|
+
# Do nothing if already in a transaction
|
|
76
|
+
else:
|
|
77
|
+
yield
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
81
|
+
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
log_queries : bool
|
|
86
|
+
Log each query which is executed.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
list[tuple[str]]
|
|
91
|
+
The list of all tables in the DB.
|
|
92
|
+
|
|
93
|
+
Examples
|
|
94
|
+
--------
|
|
95
|
+
Implement in subclass:
|
|
96
|
+
|
|
97
|
+
.. code:: python
|
|
98
|
+
|
|
99
|
+
def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
|
|
100
|
+
return self._ensure_initialized(
|
|
101
|
+
SQL_CREATE_TABLE_FOO,
|
|
102
|
+
SQL_CREATE_TABLE_BAR,
|
|
103
|
+
log_queries=log_queries
|
|
104
|
+
)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def _ensure_initialized(
|
|
108
|
+
self,
|
|
109
|
+
*create_statements: str,
|
|
110
|
+
log_queries: bool = False,
|
|
111
|
+
) -> list[tuple[str]]:
|
|
112
|
+
"""Connect to the DB, enable FK support, and create tables if needed.
|
|
113
|
+
|
|
114
|
+
Subclasses should call this with their own CREATE TABLE/INDEX statements in
|
|
115
|
+
their `.initialize()` methods.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
create_statements : str
|
|
120
|
+
SQL statements to create tables and indexes.
|
|
121
|
+
log_queries : bool
|
|
122
|
+
Log each query which is executed.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
list[tuple[str]]
|
|
127
|
+
The list of all tables in the DB.
|
|
128
|
+
"""
|
|
129
|
+
self._conn = sqlite3.connect(self.database_path)
|
|
130
|
+
self._conn.execute("PRAGMA foreign_keys = ON;")
|
|
131
|
+
self._conn.row_factory = dict_factory
|
|
132
|
+
|
|
133
|
+
if log_queries:
|
|
134
|
+
self._conn.set_trace_callback(lambda q: log(DEBUG, q))
|
|
135
|
+
|
|
136
|
+
# Create tables and indexes
|
|
137
|
+
cur = self._conn.cursor()
|
|
138
|
+
for sql in create_statements:
|
|
139
|
+
cur.execute(sql)
|
|
140
|
+
res = cur.execute("SELECT name FROM sqlite_schema;")
|
|
141
|
+
return res.fetchall()
|
|
142
|
+
|
|
143
|
+
def query(
|
|
144
|
+
self,
|
|
145
|
+
query: str,
|
|
146
|
+
data: Optional[Union[Sequence[DictOrTuple], DictOrTuple]] = None,
|
|
147
|
+
) -> list[dict[str, Any]]:
|
|
148
|
+
"""Execute a SQL query and return the results as list of dicts."""
|
|
149
|
+
if self._conn is None:
|
|
150
|
+
raise AttributeError("LinkState is not initialized.")
|
|
151
|
+
|
|
152
|
+
if data is None:
|
|
153
|
+
data = []
|
|
154
|
+
|
|
155
|
+
# Clean up whitespace to make the logs nicer
|
|
156
|
+
query = re.sub(r"\s+", " ", query)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
with self.transaction():
|
|
160
|
+
if (
|
|
161
|
+
len(data) > 0
|
|
162
|
+
and isinstance(data, (tuple, list))
|
|
163
|
+
and isinstance(data[0], (tuple, dict))
|
|
164
|
+
):
|
|
165
|
+
rows = self._conn.executemany(query, data)
|
|
166
|
+
else:
|
|
167
|
+
rows = self._conn.execute(query, data)
|
|
168
|
+
|
|
169
|
+
# Extract results before committing to support
|
|
170
|
+
# INSERT/UPDATE ... RETURNING
|
|
171
|
+
# style queries
|
|
172
|
+
result = rows.fetchall()
|
|
173
|
+
except KeyError as exc:
|
|
174
|
+
log(ERROR, {"query": query, "data": data, "exception": exc})
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def dict_factory(
|
|
180
|
+
cursor: sqlite3.Cursor,
|
|
181
|
+
row: sqlite3.Row,
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
"""Turn SQLite results into dicts.
|
|
184
|
+
|
|
185
|
+
Less efficent for retrival of large amounts of data but easier to use.
|
|
186
|
+
"""
|
|
187
|
+
fields = [column[0] for column in cursor.description]
|
|
188
|
+
return dict(zip(fields, row))
|
|
@@ -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
|
|
|
@@ -49,26 +48,26 @@ from flwr.common.serde import (
|
|
|
49
48
|
from flwr.common.typing import Fab, Run, RunStatus
|
|
50
49
|
from flwr.proto import control_pb2_grpc # pylint: disable=E0611
|
|
51
50
|
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
52
|
-
CreateNodeCliRequest,
|
|
53
|
-
CreateNodeCliResponse,
|
|
54
|
-
DeleteNodeCliRequest,
|
|
55
|
-
DeleteNodeCliResponse,
|
|
56
51
|
GetAuthTokensRequest,
|
|
57
52
|
GetAuthTokensResponse,
|
|
58
53
|
GetLoginDetailsRequest,
|
|
59
54
|
GetLoginDetailsResponse,
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
ListNodesRequest,
|
|
56
|
+
ListNodesResponse,
|
|
62
57
|
ListRunsRequest,
|
|
63
58
|
ListRunsResponse,
|
|
64
59
|
PullArtifactsRequest,
|
|
65
60
|
PullArtifactsResponse,
|
|
61
|
+
RegisterNodeRequest,
|
|
62
|
+
RegisterNodeResponse,
|
|
66
63
|
StartRunRequest,
|
|
67
64
|
StartRunResponse,
|
|
68
65
|
StopRunRequest,
|
|
69
66
|
StopRunResponse,
|
|
70
67
|
StreamLogsRequest,
|
|
71
68
|
StreamLogsResponse,
|
|
69
|
+
UnregisterNodeRequest,
|
|
70
|
+
UnregisterNodeResponse,
|
|
72
71
|
)
|
|
73
72
|
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
|
74
73
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
@@ -389,11 +388,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
389
388
|
download_url = self.artifact_provider.get_url(run_id)
|
|
390
389
|
return PullArtifactsResponse(url=download_url)
|
|
391
390
|
|
|
392
|
-
def
|
|
393
|
-
self, request:
|
|
394
|
-
) ->
|
|
391
|
+
def RegisterNode(
|
|
392
|
+
self, request: RegisterNodeRequest, context: grpc.ServicerContext
|
|
393
|
+
) -> RegisterNodeResponse:
|
|
395
394
|
"""Add a SuperNode."""
|
|
396
|
-
log(INFO, "ControlServicer.
|
|
395
|
+
log(INFO, "ControlServicer.RegisterNode")
|
|
397
396
|
|
|
398
397
|
# Verify public key
|
|
399
398
|
try:
|
|
@@ -427,15 +426,15 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
427
426
|
context.abort(
|
|
428
427
|
grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
|
|
429
428
|
)
|
|
430
|
-
log(INFO, "[ControlServicer.
|
|
429
|
+
log(INFO, "[ControlServicer.RegisterNode] Created node_id=%s", node_id)
|
|
431
430
|
|
|
432
|
-
return
|
|
431
|
+
return RegisterNodeResponse(node_id=node_id)
|
|
433
432
|
|
|
434
|
-
def
|
|
435
|
-
self, request:
|
|
436
|
-
) ->
|
|
433
|
+
def UnregisterNode(
|
|
434
|
+
self, request: UnregisterNodeRequest, context: grpc.ServicerContext
|
|
435
|
+
) -> UnregisterNodeResponse:
|
|
437
436
|
"""Remove a SuperNode."""
|
|
438
|
-
log(INFO, "ControlServicer.
|
|
437
|
+
log(INFO, "ControlServicer.UnregisterNode")
|
|
439
438
|
|
|
440
439
|
# Init link state
|
|
441
440
|
state = self.linkstate_factory.state()
|
|
@@ -448,94 +447,32 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
448
447
|
log(ERROR, NODE_NOT_FOUND_MESSAGE)
|
|
449
448
|
context.abort(grpc.StatusCode.NOT_FOUND, NODE_NOT_FOUND_MESSAGE)
|
|
450
449
|
|
|
451
|
-
return
|
|
450
|
+
return UnregisterNodeResponse()
|
|
452
451
|
|
|
453
|
-
def
|
|
454
|
-
self, request:
|
|
455
|
-
) ->
|
|
452
|
+
def ListNodes(
|
|
453
|
+
self, request: ListNodesRequest, context: grpc.ServicerContext
|
|
454
|
+
) -> ListNodesResponse:
|
|
456
455
|
"""List all SuperNodes."""
|
|
457
|
-
log(INFO, "ControlServicer.
|
|
456
|
+
log(INFO, "ControlServicer.ListNodes")
|
|
458
457
|
|
|
459
458
|
if self.is_simulation:
|
|
460
|
-
log(ERROR, "
|
|
459
|
+
log(ERROR, "ListNodes is not available in simulation mode.")
|
|
461
460
|
context.abort(
|
|
462
461
|
grpc.StatusCode.UNIMPLEMENTED,
|
|
463
|
-
"
|
|
462
|
+
"ListNodesis not available in simulation mode.",
|
|
464
463
|
)
|
|
465
464
|
raise grpc.RpcError() # This line is unreachable
|
|
466
465
|
|
|
467
466
|
nodes_info: Sequence[NodeInfo] = []
|
|
468
|
-
#
|
|
469
|
-
|
|
470
|
-
nodes_info = _create_list_nodeif_for_dry_run()
|
|
471
|
-
|
|
472
|
-
else:
|
|
473
|
-
# Init link state
|
|
474
|
-
state = self.linkstate_factory.state()
|
|
475
|
-
|
|
476
|
-
flwr_aid = shared_account_info.get().flwr_aid
|
|
477
|
-
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
|
478
|
-
# Retrieve all nodes for the account
|
|
479
|
-
nodes_info = state.get_node_info(owner_aids=[flwr_aid])
|
|
480
|
-
|
|
481
|
-
return ListNodesCliResponse(nodes_info=nodes_info, now=now().isoformat())
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
def _create_list_nodeif_for_dry_run() -> Sequence[NodeInfo]:
|
|
485
|
-
"""Create a list of NodeInfo for dry run testing."""
|
|
486
|
-
nodes_info: list[NodeInfo] = []
|
|
487
|
-
# A node created (but not connected)
|
|
488
|
-
nodes_info.append(
|
|
489
|
-
NodeInfo(
|
|
490
|
-
node_id=15390646978706312628,
|
|
491
|
-
owner_aid="owner_aid_1",
|
|
492
|
-
status="created",
|
|
493
|
-
created_at=(now()).isoformat(),
|
|
494
|
-
last_activated_at="",
|
|
495
|
-
last_deactivated_at="",
|
|
496
|
-
deleted_at="",
|
|
497
|
-
)
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
# A node created and connected
|
|
501
|
-
nodes_info.append(
|
|
502
|
-
NodeInfo(
|
|
503
|
-
node_id=2941141058168602545,
|
|
504
|
-
owner_aid="owner_aid_2",
|
|
505
|
-
status="online",
|
|
506
|
-
created_at=(now()).isoformat(),
|
|
507
|
-
last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
|
|
508
|
-
last_deactivated_at="",
|
|
509
|
-
deleted_at="",
|
|
510
|
-
)
|
|
511
|
-
)
|
|
467
|
+
# Init link state
|
|
468
|
+
state = self.linkstate_factory.state()
|
|
512
469
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
owner_aid="owner_aid_3",
|
|
518
|
-
status="deleted",
|
|
519
|
-
created_at=(now()).isoformat(),
|
|
520
|
-
last_activated_at="",
|
|
521
|
-
last_deactivated_at="",
|
|
522
|
-
deleted_at=(now() + timedelta(hours=1)).isoformat(),
|
|
523
|
-
)
|
|
524
|
-
)
|
|
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])
|
|
525
474
|
|
|
526
|
-
|
|
527
|
-
nodes_info.append(
|
|
528
|
-
NodeInfo(
|
|
529
|
-
node_id=1781174086018058152,
|
|
530
|
-
owner_aid="owner_aid_4",
|
|
531
|
-
status="offline",
|
|
532
|
-
created_at=(now()).isoformat(),
|
|
533
|
-
last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
|
|
534
|
-
last_deactivated_at=(now() + timedelta(hours=1)).isoformat(),
|
|
535
|
-
deleted_at=(now() + timedelta(hours=1.5)).isoformat(),
|
|
536
|
-
)
|
|
537
|
-
)
|
|
538
|
-
return nodes_info
|
|
475
|
+
return ListNodesResponse(nodes_info=nodes_info, now=now().isoformat())
|
|
539
476
|
|
|
540
477
|
|
|
541
478
|
def _create_list_runs_response(
|
|
@@ -54,6 +54,7 @@ from flwr.common.logger import log
|
|
|
54
54
|
from flwr.common.retry_invoker import RetryInvoker, _make_simple_grpc_retry_invoker
|
|
55
55
|
from flwr.common.telemetry import EventType
|
|
56
56
|
from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
|
|
57
|
+
from flwr.common.version import package_version
|
|
57
58
|
from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
|
|
58
59
|
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
59
60
|
from flwr.supercore.ffs import Ffs, FfsFactory
|
|
@@ -141,6 +142,19 @@ def start_client_internal(
|
|
|
141
142
|
if insecure is None:
|
|
142
143
|
insecure = root_certificates is None
|
|
143
144
|
|
|
145
|
+
# Insecure HTTP is incompatible with authentication
|
|
146
|
+
if insecure and authentication_keys is not None:
|
|
147
|
+
url_v = f"https://flower.ai/docs/framework/v{package_version}/en/"
|
|
148
|
+
page = "how-to-authenticate-supernodes.html"
|
|
149
|
+
flwr_exit(
|
|
150
|
+
ExitCode.SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED,
|
|
151
|
+
"Insecure connection is enabled, but the SuperNode's private key is "
|
|
152
|
+
"provided for authentication. SuperNode authentication requires a "
|
|
153
|
+
"secure TLS connection with the SuperLink. Please enable TLS by "
|
|
154
|
+
"providing the certificate via `--root-certificates`. Please refer "
|
|
155
|
+
f"to the Flower documentation for more information: {url_v}{page}",
|
|
156
|
+
)
|
|
157
|
+
|
|
144
158
|
# Initialize factories
|
|
145
159
|
state_factory = NodeStateFactory()
|
|
146
160
|
ffs_factory = FfsFactory(get_flwr_dir(flwr_path) / "supernode" / "ffs") # type: ignore
|
{flwr_nightly-1.23.0.dev20251017.dist-info → flwr_nightly-1.23.0.dev20251021.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.dev20251021
|
|
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
|