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.

@@ -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 DEBUG, ERROR, WARNING
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
- created_at TEXT,
79
+ registered_at TEXT,
80
80
  last_activated_at TEXT NULL,
81
81
  last_deactivated_at TEXT NULL,
82
- deleted_at TEXT NULL,
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
- """Create tables if they don't exist yet.
209
-
210
- Parameters
211
- ----------
212
- log_queries : bool
213
- Log each query which is executed.
214
-
215
- Returns
216
- -------
217
- list[tuple[str]]
218
- The list of all tables in the DB.
219
- """
220
- self.conn = sqlite3.connect(self.database_path)
221
- self.conn.execute("PRAGMA foreign_keys = ON;")
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
- SELECT node_id, online_until
494
- FROM node
495
- WHERE node_id IN ({",".join(["?"] * len(dst_node_ids))});
496
- """
497
- rows = self.query(query, tuple(dst_node_ids))
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, created_at, last_activated_at,
627
- last_deactivated_at, deleted_at, online_until, heartbeat_interval,
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.CREATED, # status
640
- now().isoformat(), # created_at
570
+ NodeStatus.REGISTERED, # status
571
+ now().isoformat(), # registered_at
641
572
  None, # last_activated_at
642
573
  None, # last_deactivated_at
643
- None, # deleted_at
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 = ?, deleted_at = ?
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.DELETED,
671
- now().isoformat(),
603
+ NodeStatus.UNREGISTERED,
604
+ current.isoformat(),
605
+ current.timestamp(),
606
+ current.timestamp(),
672
607
  sint64_node_id,
673
- NodeStatus.DELETED,
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.DELETED))
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.DELETED))
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.DELETED)
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 = {
@@ -24,10 +24,10 @@ EXEC_PLUGIN_SECTION = "exec_plugin"
24
24
  class NodeStatus:
25
25
  """Event log writer types."""
26
26
 
27
- CREATED = "created"
27
+ REGISTERED = "registered"
28
28
  ONLINE = "online"
29
29
  OFFLINE = "offline"
30
- DELETED = "deleted"
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
- ListNodesCliRequest,
61
- ListNodesCliResponse,
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 CreateNodeCli(
393
- self, request: CreateNodeCliRequest, context: grpc.ServicerContext
394
- ) -> CreateNodeCliResponse:
391
+ def RegisterNode(
392
+ self, request: RegisterNodeRequest, context: grpc.ServicerContext
393
+ ) -> RegisterNodeResponse:
395
394
  """Add a SuperNode."""
396
- log(INFO, "ControlServicer.CreateNodeCli")
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.CreateNodeCli] Created node_id=%s", node_id)
429
+ log(INFO, "[ControlServicer.RegisterNode] Created node_id=%s", node_id)
431
430
 
432
- return CreateNodeCliResponse(node_id=node_id)
431
+ return RegisterNodeResponse(node_id=node_id)
433
432
 
434
- def DeleteNodeCli(
435
- self, request: DeleteNodeCliRequest, context: grpc.ServicerContext
436
- ) -> DeleteNodeCliResponse:
433
+ def UnregisterNode(
434
+ self, request: UnregisterNodeRequest, context: grpc.ServicerContext
435
+ ) -> UnregisterNodeResponse:
437
436
  """Remove a SuperNode."""
438
- log(INFO, "ControlServicer.RemoveNode")
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 DeleteNodeCliResponse()
450
+ return UnregisterNodeResponse()
452
451
 
453
- def ListNodesCli(
454
- self, request: ListNodesCliRequest, context: grpc.ServicerContext
455
- ) -> ListNodesCliResponse:
452
+ def ListNodes(
453
+ self, request: ListNodesRequest, context: grpc.ServicerContext
454
+ ) -> ListNodesResponse:
456
455
  """List all SuperNodes."""
457
- log(INFO, "ControlServicer.ListNodesCli")
456
+ log(INFO, "ControlServicer.ListNodes")
458
457
 
459
458
  if self.is_simulation:
460
- log(ERROR, "ListNodesCli is not available in simulation mode.")
459
+ log(ERROR, "ListNodes is not available in simulation mode.")
461
460
  context.abort(
462
461
  grpc.StatusCode.UNIMPLEMENTED,
463
- "ListNodesCli is not available in simulation mode.",
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
- # If dry run is enabled, create dummy NodeInfo data
469
- if request.dry_run:
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
- # A node created and deleted (never connected)
514
- nodes_info.append(
515
- NodeInfo(
516
- node_id=906971720890549292,
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
- # A node created, deactivate and then deleted
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.23.0.dev20251017
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