flwr-nightly 1.23.0.dev20251010__py3-none-any.whl → 1.23.0.dev20251013__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/supernode/ls.py CHANGED
@@ -47,7 +47,7 @@ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugi
47
47
  _NodeListType = tuple[int, str, str, str, str, str, str, str]
48
48
 
49
49
 
50
- def ls( # pylint: disable=R0914
50
+ def ls( # pylint: disable=R0914, R0913, R0917
51
51
  ctx: typer.Context,
52
52
  app: Annotated[
53
53
  Path,
@@ -73,6 +73,13 @@ def ls( # pylint: disable=R0914
73
73
  help="Enable verbose output",
74
74
  ),
75
75
  ] = False,
76
+ dry_run: Annotated[
77
+ bool,
78
+ typer.Option(
79
+ "--dry-run",
80
+ help="Simulate the command without contacting any SuperNodes",
81
+ ),
82
+ ] = False,
76
83
  ) -> None:
77
84
  """List SuperNodes in the federation."""
78
85
  # Resolve command used (list or ls)
@@ -98,7 +105,7 @@ def ls( # pylint: disable=R0914
98
105
  channel = init_channel(app, federation_config, auth_plugin)
99
106
  stub = ControlStub(channel)
100
107
  typer.echo("📄 Listing all nodes...")
101
- formatted_nodes = _list_nodes(stub)
108
+ formatted_nodes = _list_nodes(stub, dry_run=dry_run)
102
109
  restore_output()
103
110
  if output_format == CliOutputFormat.JSON:
104
111
  Console().print_json(_to_json(formatted_nodes, verbose=verbose))
@@ -125,10 +132,12 @@ def ls( # pylint: disable=R0914
125
132
  captured_output.close()
126
133
 
127
134
 
128
- def _list_nodes(stub: ControlStub) -> list[_NodeListType]:
135
+ def _list_nodes(stub: ControlStub, dry_run: bool) -> list[_NodeListType]:
129
136
  """List all nodes."""
130
137
  with flwr_cli_grpc_exc_handler():
131
- res: ListNodesCliResponse = stub.ListNodesCli(ListNodesCliRequest())
138
+ res: ListNodesCliResponse = stub.ListNodesCli(
139
+ ListNodesCliRequest(dry_run=dry_run)
140
+ )
132
141
 
133
142
  return _format_nodes(list(res.nodes_info), res.now)
134
143
 
flwr/proto/control_pb2.py CHANGED
@@ -19,7 +19,7 @@ from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
19
19
  from flwr.proto import node_pb2 as flwr_dot_proto_dot_node__pb2
20
20
 
21
21
 
22
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/control.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1b\x66lwr/proto/recorddict.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x15\x66lwr/proto/node.proto\"\xfa\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x34\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x18.flwr.proto.ConfigRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\"\x18\n\x16GetLoginDetailsRequest\"\x8b\x01\n\x17GetLoginDetailsResponse\x12\x12\n\nauthn_type\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_code\x18\x02 \x01(\t\x12!\n\x19verification_uri_complete\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x03\x12\x10\n\x08interval\x18\x05 \x01(\x03\"+\n\x14GetAuthTokensRequest\x12\x13\n\x0b\x64\x65vice_code\x18\x01 \x01(\t\"D\n\x15GetAuthTokensResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"&\n\x14PullArtifactsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"1\n\x15PullArtifactsResponse\x12\x10\n\x03url\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_url\"*\n\x14\x43reateNodeCliRequest\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"9\n\x15\x43reateNodeCliResponse\x12\x14\n\x07node_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_node_id\"\'\n\x14\x44\x65leteNodeCliRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\x17\n\x15\x44\x65leteNodeCliResponse\"\x15\n\x13ListNodesCliRequest\"M\n\x14ListNodesCliResponse\x12(\n\nnodes_info\x18\x01 \x03(\x0b\x32\x14.flwr.proto.NodeInfo\x12\x0b\n\x03now\x18\x02 \x01(\t2\xc5\x06\n\x07\x43ontrol\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12\x44\n\x07StopRun\x12\x1a.flwr.proto.StopRunRequest\x1a\x1b.flwr.proto.StopRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x12G\n\x08ListRuns\x12\x1b.flwr.proto.ListRunsRequest\x1a\x1c.flwr.proto.ListRunsResponse\"\x00\x12\\\n\x0fGetLoginDetails\x12\".flwr.proto.GetLoginDetailsRequest\x1a#.flwr.proto.GetLoginDetailsResponse\"\x00\x12V\n\rGetAuthTokens\x12 .flwr.proto.GetAuthTokensRequest\x1a!.flwr.proto.GetAuthTokensResponse\"\x00\x12V\n\rPullArtifacts\x12 .flwr.proto.PullArtifactsRequest\x1a!.flwr.proto.PullArtifactsResponse\"\x00\x12V\n\rCreateNodeCli\x12 .flwr.proto.CreateNodeCliRequest\x1a!.flwr.proto.CreateNodeCliResponse\"\x00\x12V\n\rDeleteNodeCli\x12 .flwr.proto.DeleteNodeCliRequest\x1a!.flwr.proto.DeleteNodeCliResponse\"\x00\x12S\n\x0cListNodesCli\x12\x1f.flwr.proto.ListNodesCliRequest\x1a .flwr.proto.ListNodesCliResponse\"\x00\x62\x06proto3')
22
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/control.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1b\x66lwr/proto/recorddict.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x15\x66lwr/proto/node.proto\"\xfa\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x34\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x18.flwr.proto.ConfigRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\"\x18\n\x16GetLoginDetailsRequest\"\x8b\x01\n\x17GetLoginDetailsResponse\x12\x12\n\nauthn_type\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_code\x18\x02 \x01(\t\x12!\n\x19verification_uri_complete\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x03\x12\x10\n\x08interval\x18\x05 \x01(\x03\"+\n\x14GetAuthTokensRequest\x12\x13\n\x0b\x64\x65vice_code\x18\x01 \x01(\t\"D\n\x15GetAuthTokensResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"&\n\x14PullArtifactsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"1\n\x15PullArtifactsResponse\x12\x10\n\x03url\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_url\"*\n\x14\x43reateNodeCliRequest\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"9\n\x15\x43reateNodeCliResponse\x12\x14\n\x07node_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_node_id\"\'\n\x14\x44\x65leteNodeCliRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\x17\n\x15\x44\x65leteNodeCliResponse\"&\n\x13ListNodesCliRequest\x12\x0f\n\x07\x64ry_run\x18\x01 \x01(\x08\"M\n\x14ListNodesCliResponse\x12(\n\nnodes_info\x18\x01 \x03(\x0b\x32\x14.flwr.proto.NodeInfo\x12\x0b\n\x03now\x18\x02 \x01(\t2\xc5\x06\n\x07\x43ontrol\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12\x44\n\x07StopRun\x12\x1a.flwr.proto.StopRunRequest\x1a\x1b.flwr.proto.StopRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x12G\n\x08ListRuns\x12\x1b.flwr.proto.ListRunsRequest\x1a\x1c.flwr.proto.ListRunsResponse\"\x00\x12\\\n\x0fGetLoginDetails\x12\".flwr.proto.GetLoginDetailsRequest\x1a#.flwr.proto.GetLoginDetailsResponse\"\x00\x12V\n\rGetAuthTokens\x12 .flwr.proto.GetAuthTokensRequest\x1a!.flwr.proto.GetAuthTokensResponse\"\x00\x12V\n\rPullArtifacts\x12 .flwr.proto.PullArtifactsRequest\x1a!.flwr.proto.PullArtifactsResponse\"\x00\x12V\n\rCreateNodeCli\x12 .flwr.proto.CreateNodeCliRequest\x1a!.flwr.proto.CreateNodeCliResponse\"\x00\x12V\n\rDeleteNodeCli\x12 .flwr.proto.DeleteNodeCliRequest\x1a!.flwr.proto.DeleteNodeCliResponse\"\x00\x12S\n\x0cListNodesCli\x12\x1f.flwr.proto.ListNodesCliRequest\x1a .flwr.proto.ListNodesCliResponse\"\x00\x62\x06proto3')
23
23
 
24
24
  _globals = globals()
25
25
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -71,9 +71,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
71
71
  _globals['_DELETENODECLIRESPONSE']._serialized_start=1398
72
72
  _globals['_DELETENODECLIRESPONSE']._serialized_end=1421
73
73
  _globals['_LISTNODESCLIREQUEST']._serialized_start=1423
74
- _globals['_LISTNODESCLIREQUEST']._serialized_end=1444
75
- _globals['_LISTNODESCLIRESPONSE']._serialized_start=1446
76
- _globals['_LISTNODESCLIRESPONSE']._serialized_end=1523
77
- _globals['_CONTROL']._serialized_start=1526
78
- _globals['_CONTROL']._serialized_end=2363
74
+ _globals['_LISTNODESCLIREQUEST']._serialized_end=1461
75
+ _globals['_LISTNODESCLIRESPONSE']._serialized_start=1463
76
+ _globals['_LISTNODESCLIRESPONSE']._serialized_end=1540
77
+ _globals['_CONTROL']._serialized_start=1543
78
+ _globals['_CONTROL']._serialized_end=2380
79
79
  # @@protoc_insertion_point(module_scope)
@@ -279,8 +279,13 @@ global___DeleteNodeCliResponse = DeleteNodeCliResponse
279
279
 
280
280
  class ListNodesCliRequest(google.protobuf.message.Message):
281
281
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
282
+ DRY_RUN_FIELD_NUMBER: builtins.int
283
+ dry_run: builtins.bool
282
284
  def __init__(self,
285
+ *,
286
+ dry_run: builtins.bool = ...,
283
287
  ) -> None: ...
288
+ def ClearField(self, field_name: typing_extensions.Literal["dry_run",b"dry_run"]) -> None: ...
284
289
  global___ListNodesCliRequest = ListNodesCliRequest
285
290
 
286
291
  class ListNodesCliResponse(google.protobuf.message.Message):
flwr/proto/node_pb2.py CHANGED
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
14
14
 
15
15
 
16
16
 
17
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\xe4\x01\n\x08NodeInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\towner_aid\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\ncreated_at\x18\x04 \x01(\t\x12\x19\n\x11last_activated_at\x18\x05 \x01(\t\x12\x1b\n\x13last_deactivated_at\x18\x06 \x01(\t\x12\x12\n\ndeleted_at\x18\x07 \x01(\t\x12\x14\n\x0conline_until\x18\x08 \x01(\x01\x12\x1a\n\x12heartbeat_interval\x18\t \x01(\x01\x12\x12\n\npublic_key\x18\n \x01(\x0c\x62\x06proto3')
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\xc6\x02\n\x08NodeInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\towner_aid\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\ncreated_at\x18\x04 \x01(\t\x12\x1e\n\x11last_activated_at\x18\x05 \x01(\tH\x00\x88\x01\x01\x12 \n\x13last_deactivated_at\x18\x06 \x01(\tH\x01\x88\x01\x01\x12\x17\n\ndeleted_at\x18\x07 \x01(\tH\x02\x88\x01\x01\x12\x19\n\x0conline_until\x18\x08 \x01(\x01H\x03\x88\x01\x01\x12\x1a\n\x12heartbeat_interval\x18\t \x01(\x01\x12\x12\n\npublic_key\x18\n \x01(\x0c\x42\x14\n\x12_last_activated_atB\x16\n\x14_last_deactivated_atB\r\n\x0b_deleted_atB\x0f\n\r_online_untilb\x06proto3')
18
18
 
19
19
  _globals = globals()
20
20
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -24,5 +24,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
24
24
  _globals['_NODE']._serialized_start=37
25
25
  _globals['_NODE']._serialized_end=60
26
26
  _globals['_NODEINFO']._serialized_start=63
27
- _globals['_NODEINFO']._serialized_end=291
27
+ _globals['_NODEINFO']._serialized_end=389
28
28
  # @@protoc_insertion_point(module_scope)
flwr/proto/node_pb2.pyi CHANGED
@@ -49,12 +49,21 @@ class NodeInfo(google.protobuf.message.Message):
49
49
  owner_aid: typing.Text = ...,
50
50
  status: typing.Text = ...,
51
51
  created_at: typing.Text = ...,
52
- last_activated_at: typing.Text = ...,
53
- last_deactivated_at: typing.Text = ...,
54
- deleted_at: typing.Text = ...,
55
- online_until: builtins.float = ...,
52
+ last_activated_at: typing.Optional[typing.Text] = ...,
53
+ last_deactivated_at: typing.Optional[typing.Text] = ...,
54
+ deleted_at: typing.Optional[typing.Text] = ...,
55
+ online_until: typing.Optional[builtins.float] = ...,
56
56
  heartbeat_interval: builtins.float = ...,
57
57
  public_key: builtins.bytes = ...,
58
58
  ) -> None: ...
59
- def ClearField(self, field_name: typing_extensions.Literal["created_at",b"created_at","deleted_at",b"deleted_at","heartbeat_interval",b"heartbeat_interval","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","node_id",b"node_id","online_until",b"online_until","owner_aid",b"owner_aid","public_key",b"public_key","status",b"status"]) -> None: ...
59
+ def HasField(self, field_name: typing_extensions.Literal["_deleted_at",b"_deleted_at","_last_activated_at",b"_last_activated_at","_last_deactivated_at",b"_last_deactivated_at","_online_until",b"_online_until","deleted_at",b"deleted_at","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","online_until",b"online_until"]) -> builtins.bool: ...
60
+ def ClearField(self, field_name: typing_extensions.Literal["_deleted_at",b"_deleted_at","_last_activated_at",b"_last_activated_at","_last_deactivated_at",b"_last_deactivated_at","_online_until",b"_online_until","created_at",b"created_at","deleted_at",b"deleted_at","heartbeat_interval",b"heartbeat_interval","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","node_id",b"node_id","online_until",b"online_until","owner_aid",b"owner_aid","public_key",b"public_key","status",b"status"]) -> None: ...
61
+ @typing.overload
62
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["_deleted_at",b"_deleted_at"]) -> typing.Optional[typing_extensions.Literal["deleted_at"]]: ...
63
+ @typing.overload
64
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["_last_activated_at",b"_last_activated_at"]) -> typing.Optional[typing_extensions.Literal["last_activated_at"]]: ...
65
+ @typing.overload
66
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["_last_deactivated_at",b"_last_deactivated_at"]) -> typing.Optional[typing_extensions.Literal["last_deactivated_at"]]: ...
67
+ @typing.overload
68
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["_online_until",b"_online_until"]) -> typing.Optional[typing_extensions.Literal["online_until"]]: ...
60
69
  global___NodeInfo = NodeInfo
@@ -63,6 +63,9 @@ def _register_nodes(
63
63
  secrets.token_bytes(32),
64
64
  heartbeat_interval=HEARTBEAT_MAX_INTERVAL,
65
65
  )
66
+ state.acknowledge_node_heartbeat(
67
+ node_id=node_id, heartbeat_interval=HEARTBEAT_MAX_INTERVAL
68
+ )
66
69
  nodes_mapping[node_id] = i
67
70
  log(DEBUG, "Registered %i nodes", len(nodes_mapping))
68
71
  return nodes_mapping
@@ -19,7 +19,9 @@ import secrets
19
19
  import threading
20
20
  from bisect import bisect_right
21
21
  from collections import defaultdict
22
+ from collections.abc import Sequence
22
23
  from dataclasses import dataclass, field
24
+ from datetime import datetime
23
25
  from logging import ERROR, WARNING
24
26
  from typing import Optional
25
27
 
@@ -41,6 +43,7 @@ from flwr.common.typing import Run, RunStatus, UserConfig
41
43
  from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
42
44
  from flwr.server.superlink.linkstate.linkstate import LinkState
43
45
  from flwr.server.utils import validate_message
46
+ from flwr.supercore.constant import NodeStatus
44
47
 
45
48
  from .utils import (
46
49
  check_node_availability_for_in_message,
@@ -346,17 +349,16 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
346
349
  if public_key in self.registered_node_public_keys:
347
350
  raise ValueError("Public key already in use")
348
351
 
349
- # Mark the node online until now().timestamp() + heartbeat_interval
350
- current = now()
352
+ # The node is not activated upon creation
351
353
  self.nodes[node_id] = NodeInfo(
352
354
  node_id=node_id,
353
- owner_aid=owner_aid, # Unused for now
354
- status="created", # Unused for now
355
- created_at=current.isoformat(), # Unused for now
356
- last_activated_at=current.isoformat(), # Unused for now
357
- last_deactivated_at="", # Unused for now
358
- deleted_at="", # Unused for now
359
- online_until=current.timestamp() + heartbeat_interval,
355
+ owner_aid=owner_aid,
356
+ status=NodeStatus.CREATED,
357
+ created_at=now().isoformat(),
358
+ last_activated_at=None,
359
+ last_deactivated_at=None,
360
+ deleted_at=None,
361
+ online_until=None,
360
362
  heartbeat_interval=heartbeat_interval,
361
363
  public_key=public_key,
362
364
  )
@@ -367,13 +369,18 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
367
369
  def delete_node(self, owner_aid: str, node_id: int) -> None:
368
370
  """Delete a node."""
369
371
  with self.lock:
370
- if node_id not in self.nodes or owner_aid != self.nodes[node_id].owner_aid:
372
+ if (
373
+ not (node := self.nodes.get(node_id))
374
+ or node.status == NodeStatus.DELETED
375
+ or owner_aid != self.nodes[node_id].owner_aid
376
+ ):
371
377
  raise ValueError(
372
- f"Node ID {node_id} not found or unauthorized deletion attempt."
378
+ f"Node ID {node_id} already deleted, not found or unauthorized "
379
+ "deletion attempt."
373
380
  )
374
381
 
375
- node = self.nodes.pop(node_id)
376
- self.registered_node_public_keys.discard(node.public_key)
382
+ node.status = NodeStatus.DELETED
383
+ node.deleted_at = now().isoformat()
377
384
 
378
385
  def get_nodes(self, run_id: int) -> set[int]:
379
386
  """Return all available nodes.
@@ -386,19 +393,51 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
386
393
  with self.lock:
387
394
  if run_id not in self.run_ids:
388
395
  return set()
389
- current_time = now().timestamp()
390
396
  return {
391
- info.node_id
392
- for info in self.nodes.values()
393
- if info.online_until > current_time
397
+ node.node_id
398
+ for node in self.get_node_info(statuses=[NodeStatus.ONLINE])
394
399
  }
395
400
 
401
+ def get_node_info(
402
+ self,
403
+ *,
404
+ node_ids: Optional[Sequence[int]] = None,
405
+ owner_aids: Optional[Sequence[str]] = None,
406
+ statuses: Optional[Sequence[str]] = None,
407
+ ) -> Sequence[NodeInfo]:
408
+ """Retrieve information about nodes based on the specified filters."""
409
+ with self.lock:
410
+ self._check_and_tag_deactivated_nodes()
411
+ result = []
412
+ for node in self.nodes.values():
413
+ if node_ids is not None and node.node_id not in node_ids:
414
+ continue
415
+ if owner_aids is not None and node.owner_aid not in owner_aids:
416
+ continue
417
+ if statuses is not None and node.status not in statuses:
418
+ continue
419
+ result.append(node)
420
+ return result
421
+
422
+ def _check_and_tag_deactivated_nodes(self) -> None:
423
+ with self.lock:
424
+ # Set all nodes of "online" status to "offline" if they've offline
425
+ current_ts = now().timestamp()
426
+ for node in self.nodes.values():
427
+ if node.status == NodeStatus.ONLINE:
428
+ if node.online_until <= current_ts:
429
+ node.status = NodeStatus.OFFLINE
430
+ node.last_deactivated_at = datetime.fromtimestamp(
431
+ node.online_until
432
+ ).isoformat()
433
+
396
434
  def get_node_public_key(self, node_id: int) -> bytes:
397
435
  """Get `public_key` for the specified `node_id`."""
398
436
  with self.lock:
399
- if (node := self.nodes.get(node_id)) is None:
437
+ if (
438
+ node := self.nodes.get(node_id)
439
+ ) is None or node.status == NodeStatus.DELETED:
400
440
  raise ValueError(f"Node ID {node_id} not found")
401
-
402
441
  return node.public_key
403
442
 
404
443
  # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -606,11 +645,19 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
606
645
  the node is marked as offline.
607
646
  """
608
647
  with self.lock:
609
- if info := self.nodes.get(node_id):
610
- info.online_until = (
611
- now().timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval
648
+ if (node := self.nodes.get(node_id)) and node.status != NodeStatus.DELETED:
649
+ current_dt = now()
650
+
651
+ # Set timestamp if the status changes
652
+ if node.status != NodeStatus.ONLINE: # offline or created
653
+ node.status = NodeStatus.ONLINE
654
+ node.last_activated_at = current_dt.isoformat()
655
+
656
+ # Refresh `online_until` and `heartbeat_interval`
657
+ node.online_until = (
658
+ current_dt.timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval
612
659
  )
613
- info.heartbeat_interval = heartbeat_interval
660
+ node.heartbeat_interval = heartbeat_interval
614
661
  return True
615
662
  return False
616
663
 
@@ -16,11 +16,13 @@
16
16
 
17
17
 
18
18
  import abc
19
+ from collections.abc import Sequence
19
20
  from typing import Optional
20
21
 
21
22
  from flwr.common import Context, Message
22
23
  from flwr.common.record import ConfigRecord
23
24
  from flwr.common.typing import Run, RunStatus, UserConfig
25
+ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
24
26
  from flwr.supercore.corestate import CoreState
25
27
 
26
28
 
@@ -147,6 +149,38 @@ class LinkState(CoreState): # pylint: disable=R0904
147
149
  an empty `Set` MUST be returned.
148
150
  """
149
151
 
152
+ @abc.abstractmethod
153
+ def get_node_info(
154
+ self,
155
+ *,
156
+ node_ids: Optional[Sequence[int]] = None,
157
+ owner_aids: Optional[Sequence[str]] = None,
158
+ statuses: Optional[Sequence[str]] = None,
159
+ ) -> Sequence[NodeInfo]:
160
+ """Retrieve information about nodes based on the specified filters.
161
+
162
+ If a filter is set to None, it is ignored.
163
+ If multiple filters are provided, they are combined using AND logic.
164
+
165
+ Parameters
166
+ ----------
167
+ node_ids : Optional[Sequence[int]] (default: None)
168
+ Sequence of node IDs to filter by. If a sequence is provided,
169
+ it is treated as an OR condition.
170
+ owner_aids : Optional[Sequence[str]] (default: None)
171
+ Sequence of owner account IDs to filter by. If a sequence is provided,
172
+ it is treated as an OR condition.
173
+ statuses : Optional[Sequence[str]] (default: None)
174
+ Sequence of node status values (e.g., "created", "activated")
175
+ to filter by. If a sequence is provided, it is treated as an OR condition.
176
+
177
+ Returns
178
+ -------
179
+ Sequence[NodeInfo]
180
+ A sequence of NodeInfo objects representing the nodes matching
181
+ the specified filters.
182
+ """
183
+
150
184
  @abc.abstractmethod
151
185
  def get_node_public_key(self, node_id: int) -> bytes:
152
186
  """Get `public_key` for the specified `node_id`.
@@ -46,10 +46,12 @@ from flwr.common.typing import Run, RunStatus, UserConfig
46
46
 
47
47
  # pylint: disable=E0611
48
48
  from flwr.proto.error_pb2 import Error as ProtoError
49
+ from flwr.proto.node_pb2 import NodeInfo
49
50
  from flwr.proto.recorddict_pb2 import RecordDict as ProtoRecordDict
50
51
 
51
52
  # pylint: enable=E0611
52
53
  from flwr.server.utils.validator import validate_message
54
+ from flwr.supercore.constant import NodeStatus
53
55
 
54
56
  from .linkstate import LinkState
55
57
  from .utils import (
@@ -75,10 +77,10 @@ CREATE TABLE IF NOT EXISTS node(
75
77
  owner_aid TEXT,
76
78
  status TEXT,
77
79
  created_at TEXT,
78
- last_activated_at TEXT,
79
- last_deactivated_at TEXT,
80
- deleted_at TEXT,
81
- online_until REAL,
80
+ last_activated_at TEXT NULL,
81
+ last_deactivated_at TEXT NULL,
82
+ deleted_at TEXT NULL,
83
+ online_until TIMESTAMP NULL,
82
84
  heartbeat_interval REAL,
83
85
  public_key BLOB UNIQUE
84
86
  );
@@ -633,13 +635,13 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
633
635
  query,
634
636
  (
635
637
  sint64_node_id, # node_id
636
- owner_aid, # owner_aid, unused for now
637
- "created", # status, unused for now
638
- now().isoformat(), # created_at, unused for now
639
- now().isoformat(), # last_activated_at, unused for now
640
- "", # last_deactivated_at, unused for now
641
- "", # deleted_at, unused for now
642
- now().timestamp() + heartbeat_interval, # online_until
638
+ owner_aid, # owner_aid
639
+ NodeStatus.CREATED, # status
640
+ now().isoformat(), # created_at
641
+ None, # last_activated_at
642
+ None, # last_deactivated_at
643
+ None, # deleted_at
644
+ None, # online_until, initialized with offline status
643
645
  heartbeat_interval, # heartbeat_interval
644
646
  public_key, # public_key
645
647
  ),
@@ -656,24 +658,28 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
656
658
 
657
659
  def delete_node(self, owner_aid: str, node_id: int) -> None:
658
660
  """Delete a node."""
659
- # Convert the uint64 value to sint64 for SQLite
660
661
  sint64_node_id = convert_uint64_to_sint64(node_id)
661
662
 
662
- query = "DELETE FROM node WHERE node_id = ? AND owner_aid = ?"
663
- params = (sint64_node_id, owner_aid)
664
-
665
- if self.conn is None:
666
- raise AttributeError("LinkState is not initialized.")
663
+ query = """
664
+ UPDATE node
665
+ SET status = ?, deleted_at = ?
666
+ WHERE node_id = ? AND status != ? AND owner_aid = ?
667
+ RETURNING node_id
668
+ """
669
+ params = (
670
+ NodeStatus.DELETED,
671
+ now().isoformat(),
672
+ sint64_node_id,
673
+ NodeStatus.DELETED,
674
+ owner_aid,
675
+ )
667
676
 
668
- try:
669
- with self.conn:
670
- rows = self.conn.execute(query, params)
671
- if rows.rowcount < 1:
672
- raise ValueError(
673
- f"Node ID {node_id} not found or unauthorized deletion attempt."
674
- )
675
- except KeyError as exc:
676
- log(ERROR, {"query": query, "data": params, "exception": exc})
677
+ rows = self.query(query, params)
678
+ if not rows:
679
+ raise ValueError(
680
+ f"Node {node_id} already deleted, not found or unauthorized "
681
+ "deletion attempt."
682
+ )
677
683
 
678
684
  def get_nodes(self, run_id: int) -> set[int]:
679
685
  """Retrieve all currently stored node IDs as a set.
@@ -683,21 +689,84 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
683
689
  If the provided `run_id` does not exist or has no matching nodes,
684
690
  an empty `Set` MUST be returned.
685
691
  """
692
+ if self.conn is None:
693
+ raise AttributeError("LinkState not initialized")
694
+
686
695
  # Convert the uint64 value to sint64 for SQLite
687
696
  sint64_run_id = convert_uint64_to_sint64(run_id)
688
697
 
689
698
  # Validate run ID
690
- query = "SELECT COUNT(*) FROM run WHERE run_id = ?;"
691
- if self.query(query, (sint64_run_id,))[0]["COUNT(*)"] == 0:
699
+ query = "SELECT COUNT(*) FROM run WHERE run_id = ?"
700
+ rows = self.query(query, (sint64_run_id,))
701
+ if rows[0]["COUNT(*)"] == 0:
692
702
  return set()
693
703
 
694
- # Get nodes
695
- query = "SELECT node_id FROM node WHERE online_until > ?;"
696
- rows = self.query(query, (now().timestamp(),))
704
+ # Retrieve all online nodes
705
+ return {
706
+ node.node_id for node in self.get_node_info(statuses=[NodeStatus.ONLINE])
707
+ }
697
708
 
698
- # Convert sint64 node_ids to uint64
699
- result: set[int] = {convert_sint64_to_uint64(row["node_id"]) for row in rows}
700
- return result
709
+ def get_node_info(
710
+ self,
711
+ *,
712
+ node_ids: Optional[Sequence[int]] = None,
713
+ owner_aids: Optional[Sequence[str]] = None,
714
+ statuses: Optional[Sequence[str]] = None,
715
+ ) -> Sequence[NodeInfo]:
716
+ """Retrieve information about nodes based on the specified filters."""
717
+ if self.conn is None:
718
+ raise AttributeError("LinkState is not initialized.")
719
+
720
+ with self.conn:
721
+ # Check and tag offline nodes
722
+ current_dt = now()
723
+ # strftime will convert POSIX timestamp to ISO format
724
+ query = """
725
+ UPDATE node SET status = ?,
726
+ last_deactivated_at =
727
+ strftime("%Y-%m-%dT%H:%M:%f+00:00", online_until, "unixepoch")
728
+ WHERE online_until <= ? AND status == ?
729
+ """
730
+ params: list[Any] = [
731
+ NodeStatus.OFFLINE,
732
+ current_dt.timestamp(),
733
+ NodeStatus.ONLINE,
734
+ ]
735
+ self.conn.execute(query, params)
736
+
737
+ # Build the WHERE clause based on provided filters
738
+ conditions = []
739
+ params = []
740
+ if node_ids is not None:
741
+ sint64_node_ids = [
742
+ convert_uint64_to_sint64(node_id) for node_id in node_ids
743
+ ]
744
+ placeholders = ",".join(["?"] * len(sint64_node_ids))
745
+ conditions.append(f"node_id IN ({placeholders})")
746
+ params.extend(sint64_node_ids)
747
+ if owner_aids is not None:
748
+ placeholders = ",".join(["?"] * len(owner_aids))
749
+ conditions.append(f"owner_aid IN ({placeholders})")
750
+ params.extend(owner_aids)
751
+ if statuses is not None:
752
+ placeholders = ",".join(["?"] * len(statuses))
753
+ conditions.append(f"status IN ({placeholders})")
754
+ params.extend(statuses)
755
+
756
+ # Construct the final query
757
+ query = "SELECT * FROM node"
758
+ if conditions:
759
+ query += " WHERE " + " AND ".join(conditions)
760
+
761
+ rows = self.conn.execute(query, params).fetchall()
762
+
763
+ result: list[NodeInfo] = []
764
+ for row in rows:
765
+ # Convert sint64 node_id to uint64
766
+ row["node_id"] = convert_sint64_to_uint64(row["node_id"])
767
+ result.append(NodeInfo(**row))
768
+
769
+ return result
701
770
 
702
771
  def get_node_public_key(self, node_id: int) -> bytes:
703
772
  """Get `public_key` for the specified `node_id`."""
@@ -705,8 +774,8 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
705
774
  sint64_node_id = convert_uint64_to_sint64(node_id)
706
775
 
707
776
  # Query the public key for the given node_id
708
- query = "SELECT public_key FROM node WHERE node_id = ?"
709
- rows = self.query(query, (sint64_node_id,))
777
+ query = "SELECT public_key FROM node WHERE node_id = ? AND status != ?;"
778
+ rows = self.query(query, (sint64_node_id, NodeStatus.DELETED))
710
779
 
711
780
  # If no result is found, return None
712
781
  if not rows:
@@ -989,26 +1058,38 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
989
1058
  HEARTBEAT_PATIENCE = N allows for N-1 missed heartbeat before
990
1059
  the node is marked as offline.
991
1060
  """
992
- sint64_node_id = convert_uint64_to_sint64(node_id)
1061
+ if self.conn is None:
1062
+ raise AttributeError("LinkState not initialized")
993
1063
 
994
- # Check if the node exists in the `node` table
995
- query = "SELECT 1 FROM node WHERE node_id = ?"
996
- if not self.query(query, (sint64_node_id,)):
997
- return False
1064
+ sint64_node_id = convert_uint64_to_sint64(node_id)
998
1065
 
999
- # Update `online_until` and `heartbeat_interval` for the given `node_id`
1000
- query = (
1001
- "UPDATE node SET online_until = ?, heartbeat_interval = ? WHERE node_id = ?"
1002
- )
1003
- self.query(
1004
- query,
1005
- (
1006
- now().timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval,
1066
+ with self.conn:
1067
+ # Check if node exists and not deleted
1068
+ query = "SELECT status FROM node WHERE node_id = ? AND status != ?"
1069
+ row = self.conn.execute(
1070
+ query, (sint64_node_id, NodeStatus.DELETED)
1071
+ ).fetchone()
1072
+ if row is None:
1073
+ return False
1074
+
1075
+ # Construct query and params
1076
+ current_dt = now()
1077
+ query = "UPDATE node SET online_until = ?, heartbeat_interval = ?"
1078
+ params: list[Any] = [
1079
+ current_dt.timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval,
1007
1080
  heartbeat_interval,
1008
- sint64_node_id,
1009
- ),
1010
- )
1011
- return True
1081
+ ]
1082
+
1083
+ # Set timestamp if the status changes
1084
+ if row["status"] != NodeStatus.ONLINE:
1085
+ query += ", status = ?, last_activated_at = ?"
1086
+ params += [NodeStatus.ONLINE, current_dt.isoformat()]
1087
+
1088
+ # Execute the query, refreshing `online_until` and `heartbeat_interval`
1089
+ query += " WHERE node_id = ?"
1090
+ params += [sint64_node_id]
1091
+ self.conn.execute(query, params)
1092
+ return True
1012
1093
 
1013
1094
  def acknowledge_app_heartbeat(self, run_id: int, heartbeat_interval: float) -> bool:
1014
1095
  """Acknowledge a heartbeat received from a ServerApp for a given run.
@@ -15,5 +15,20 @@
15
15
  """Constants for Flower infrastructure."""
16
16
 
17
17
 
18
+ from __future__ import annotations
19
+
18
20
  # Top-level key in YAML config for exec plugin settings
19
21
  EXEC_PLUGIN_SECTION = "exec_plugin"
22
+
23
+
24
+ class NodeStatus:
25
+ """Event log writer types."""
26
+
27
+ CREATED = "created"
28
+ ONLINE = "online"
29
+ OFFLINE = "offline"
30
+ DELETED = "deleted"
31
+
32
+ def __new__(cls) -> NodeStatus:
33
+ """Prevent instantiation."""
34
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
@@ -17,7 +17,7 @@
17
17
 
18
18
  import hashlib
19
19
  import time
20
- from collections.abc import Generator
20
+ from collections.abc import Generator, Sequence
21
21
  from datetime import timedelta
22
22
  from logging import ERROR, INFO
23
23
  from typing import Any, Optional, cast
@@ -456,60 +456,86 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
456
456
  """List all SuperNodes."""
457
457
  log(INFO, "ControlServicer.ListNodesCli")
458
458
 
459
- nodes_info = []
460
- # A node created (but not connected)
461
- nodes_info.append(
462
- NodeInfo(
463
- node_id=15390646978706312628,
464
- owner_aid="owner_aid_1",
465
- status="created",
466
- created_at=(now()).isoformat(),
467
- last_activated_at="",
468
- last_deactivated_at="",
469
- deleted_at="",
459
+ if self.is_simulation:
460
+ log(ERROR, "ListNodesCli is not available in simulation mode.")
461
+ context.abort(
462
+ grpc.StatusCode.UNIMPLEMENTED,
463
+ "ListNodesCli is not available in simulation mode.",
470
464
  )
471
- )
465
+ raise grpc.RpcError() # This line is unreachable
472
466
 
473
- # A node created and connected
474
- nodes_info.append(
475
- NodeInfo(
476
- node_id=2941141058168602545,
477
- owner_aid="owner_aid_2",
478
- status="online",
479
- created_at=(now()).isoformat(),
480
- last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
481
- last_deactivated_at="",
482
- deleted_at="",
483
- )
467
+ 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="",
484
497
  )
498
+ )
485
499
 
486
- # A node created and deleted (never connected)
487
- nodes_info.append(
488
- NodeInfo(
489
- node_id=906971720890549292,
490
- owner_aid="owner_aid_3",
491
- status="deleted",
492
- created_at=(now()).isoformat(),
493
- last_activated_at="",
494
- last_deactivated_at="",
495
- deleted_at=(now() + timedelta(hours=1)).isoformat(),
496
- )
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="",
497
510
  )
511
+ )
498
512
 
499
- # A node created, deactivate and then deleted
500
- nodes_info.append(
501
- NodeInfo(
502
- node_id=1781174086018058152,
503
- owner_aid="owner_aid_4",
504
- status="offline",
505
- created_at=(now()).isoformat(),
506
- last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
507
- last_deactivated_at=(now() + timedelta(hours=1)).isoformat(),
508
- deleted_at=(now() + timedelta(hours=1.5)).isoformat(),
509
- )
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(),
510
523
  )
524
+ )
511
525
 
512
- return ListNodesCliResponse(nodes_info=nodes_info, now=now().isoformat())
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
513
539
 
514
540
 
515
541
  def _create_list_runs_response(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.23.0.dev20251010
3
+ Version: 1.23.0.dev20251013
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
@@ -89,7 +89,7 @@ flwr/cli/stop.py,sha256=W7ynTYLm0-_1nC5Il4IaZTji6A3GCCm_0R-HQUudAsI,4988
89
89
  flwr/cli/supernode/__init__.py,sha256=DVrTcyCg9NFll6glPLAAA6WPi7boxu6pFY_PRqIyHMk,893
90
90
  flwr/cli/supernode/create.py,sha256=9KvRO0IrZa4jw0sypAYxFlzzpjmnzf1KW71b-ySxeuI,6383
91
91
  flwr/cli/supernode/delete.py,sha256=SmeKpvVtly8iisNpzJ-MNY-fe0gH2jwwilJs_NMo7do,4528
92
- flwr/cli/supernode/ls.py,sha256=exeu-9fpkh27k2oyYNNT7uDhHgf8dlX0TR6WwMV8KIQ,8538
92
+ flwr/cli/supernode/ls.py,sha256=WEjP-_-5LHzTFuEFIhvnEeFRSaIAndEOrYpX4Ds-ENM,8807
93
93
  flwr/cli/utils.py,sha256=66RqZdF0DKsqmR4ZqE9zZ1bX6opdxD2U50GV41WQDoY,14864
94
94
  flwr/client/__init__.py,sha256=Q0MIF442vLIGSkcwHKq_sIfECQynLARJrumAscq2Q6E,1241
95
95
  flwr/client/client.py,sha256=3HAchxvknKG9jYbB7swNyDj-e5vUWDuMKoLvbT7jCVM,7895
@@ -191,8 +191,8 @@ flwr/proto/clientappio_pb2.py,sha256=vJjzwWydhg7LruK8cvRAeVQeHPsJztgdIW9nyiPBZF0
191
191
  flwr/proto/clientappio_pb2.pyi,sha256=XbFvpZvvrS7QcH5AFXfpRGl4hQvhd3QdKO6x0oTlCCU,165
192
192
  flwr/proto/clientappio_pb2_grpc.py,sha256=iobNROP0qvn5zddx7k-uIi_dJWP3T_BRp_kbKq086i8,17550
193
193
  flwr/proto/clientappio_pb2_grpc.pyi,sha256=Ytf1O1ktKB0Vsuc3AWLIErGjIJYokzKYzi2uA7mdMeg,4785
194
- flwr/proto/control_pb2.py,sha256=zPejyt6qE3bzHHpSs_RJkTiOJI6YbtRVuNmmNrQgazc,7744
195
- flwr/proto/control_pb2.pyi,sha256=iBpjs4mVgpa_QeW-01hUk179VyxNa6_vWlDFXApISCU,13386
194
+ flwr/proto/control_pb2.py,sha256=5dPV4DfEVhAinofsDctelFSl2TTrjhf6nANU2j329IA,7783
195
+ flwr/proto/control_pb2.pyi,sha256=Vv3TssjFMGbq_LEECxrkOO-aY5xoPCXvO4Lu6ljYKqY,13600
196
196
  flwr/proto/control_pb2_grpc.py,sha256=Of6tnetwnzvJfJWEXhuTFszz4T_IJbdLJD9uRXBdG04,17182
197
197
  flwr/proto/control_pb2_grpc.pyi,sha256=9z_MRrexz0o3_owST8SvhCjQIIAn-SwMWKIihPHzqdM,4801
198
198
  flwr/proto/error_pb2.py,sha256=PQVWrfjVPo88ql_KgV9nCxyQNCcV9PVfmcw7sOzTMro,1084
@@ -223,8 +223,8 @@ flwr/proto/message_pb2.py,sha256=giymevXYEUdpIO-3A0XKsmRabXW1xSz0sIo5oOlbQ8Y,519
223
223
  flwr/proto/message_pb2.pyi,sha256=EzXZHy2mtabofrd_ZgKSI6M4QH-soIaRZIZBPwBGPv0,11260
224
224
  flwr/proto/message_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
225
225
  flwr/proto/message_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
226
- flwr/proto/node_pb2.py,sha256=C8Pj-6rGP0t44eb4OHkze8wHDf_b_5g8S906tyuqfUI,1572
227
- flwr/proto/node_pb2.pyi,sha256=hxMkWNE2NjA4j9RVGz8qrfotd4B_1sgdcO4flv-CjHY,2368
226
+ flwr/proto/node_pb2.py,sha256=LBIBkiR_TIgAz2RrGn6-wJKGGN8YxStQ3oIwsFB_qCQ,1739
227
+ flwr/proto/node_pb2.pyi,sha256=IYSwKID4QM80PK_UqkHf2zVH2pBJyfCeOBO4EBLTM74,3747
228
228
  flwr/proto/node_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
229
229
  flwr/proto/node_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
230
230
  flwr/proto/recorddict_pb2.py,sha256=eVkcnxMTFa3rvknRNiFuJ8z8xxPqgw7bV04aFiTe1j4,5290
@@ -315,12 +315,12 @@ flwr/server/superlink/fleet/vce/__init__.py,sha256=XOKbAWOzlCqEOQ3M2cBYkH7HKA7Px
315
315
  flwr/server/superlink/fleet/vce/backend/__init__.py,sha256=PPH89Yqd1XKm-sRJN6R0WQlKT_b4v54Kzl2yzHAFzM8,1437
316
316
  flwr/server/superlink/fleet/vce/backend/backend.py,sha256=cSrHZ5SjCCvy4vI0pgsyjtx3cDMuMQve8KcKkK-dWWo,2196
317
317
  flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=cBZYTmfiAsb1HmVUmOQXYLU-UJmJTFWkj1wW4RYRDuc,7218
318
- flwr/server/superlink/fleet/vce/vce_api.py,sha256=cWlRkgn7G_cS4jw6g1ua6ZcGsyF-1YwP9IgSQ6Dk62Q,13209
318
+ flwr/server/superlink/fleet/vce/vce_api.py,sha256=EU0DLt4njtKelOpOWfQ7zWW45bSVC6K7pPYfHSyOJwM,13332
319
319
  flwr/server/superlink/linkstate/__init__.py,sha256=OtsgvDTnZLU3k0sUbkHbqoVwW6ql2FDmb6uT6DbNkZo,1064
320
- flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=oga9vECdDFSFs6egfOS9iXMQmgc5pWgDSfGwi76XOp8,28082
321
- flwr/server/superlink/linkstate/linkstate.py,sha256=08C1v7dAMOo5346tKNDJ7Y0OCxhpdPdRU11xPwmOoDw,13400
320
+ flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=C3Y0eQC0A8-B9z5h7q-c-bVXOs5umTkNTuXSdsgDKzM,29925
321
+ flwr/server/superlink/linkstate/linkstate.py,sha256=iL_mRzmdG4BA0q02BxBRf9ljKR80y3gwNR_Cum9aAdo,14772
322
322
  flwr/server/superlink/linkstate/linkstate_factory.py,sha256=8RlosqSpKOoD_vhUUQPY0jtE3A84GeF96Z7sWNkRRcA,2069
323
- flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=2a3EP-PFUtg-LUJlct0PQloQ4kuKb0xe18CBsYZHb6U,45361
323
+ flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=kHGSBIumGmMJ750RnZw8awQ8kfFsCNTw8ko5k-aLq4c,48313
324
324
  flwr/server/superlink/linkstate/utils.py,sha256=IeLh7iGRCHU5MEWOl7iriaSE4L__8GWOa2OleXadK5M,15444
325
325
  flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
326
326
  flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=-I7kBbr4w4ZVYwBZoAIle-xHKthFnZrsVfxa6WR8uxA,2310
@@ -375,7 +375,7 @@ flwr/supercore/__init__.py,sha256=pqkFoow_E6UhbBlhmoD1gmTH-33yJRhBsIZqxRPFZ7U,75
375
375
  flwr/supercore/app_utils.py,sha256=K76Zt6R670b1hUmxOsNc1WUCVYvF7lejXPcCO9K0Q0g,1753
376
376
  flwr/supercore/cli/__init__.py,sha256=EDl2aO-fuQfxSbL-T1W9RAfA2N0hpWHmqX_GSwblJbQ,845
377
377
  flwr/supercore/cli/flower_superexec.py,sha256=JtqYrEWVu3BxLkjavsdohTOwvMwzuFqWP5j4Mo9dqsk,6155
378
- flwr/supercore/constant.py,sha256=F9kRjisedaZcoyGvUITSDmIG12QDSCpo2LlM_l-q6jM,820
378
+ flwr/supercore/constant.py,sha256=tSZ741Q4siF5LPANvkakwIMh7wd5x1VjqHj0VH0wN1Y,1146
379
379
  flwr/supercore/corestate/__init__.py,sha256=Vau6-L_JG5QzNqtCTa9xCKGGljc09wY8avZmIjSJemg,774
380
380
  flwr/supercore/corestate/corestate.py,sha256=rDAWWeG5DcpCyQso9Z3RhwL4zr2IroPlRMcDzqoSu8s,2328
381
381
  flwr/supercore/ffs/__init__.py,sha256=U3KXwG_SplEvchat27K0LYPoPHzh-cwwT_NHsGlYMt8,908
@@ -415,7 +415,7 @@ flwr/superlink/servicer/control/control_account_auth_interceptor.py,sha256=Tbi4W
415
415
  flwr/superlink/servicer/control/control_event_log_interceptor.py,sha256=5uBl6VcJlUOgCF0d4kmsmJc1Rs1qxyouaZv0-uH2axs,5969
416
416
  flwr/superlink/servicer/control/control_grpc.py,sha256=MRCaX4I2a5ogjKmhtFs6Mj-VdWemxL2h3gU9QbQmvCA,4183
417
417
  flwr/superlink/servicer/control/control_license_interceptor.py,sha256=T3AzmRt-PPwyTq3hrdpmZHQd5_CpPOk7TtnFZrB-JRY,3349
418
- flwr/superlink/servicer/control/control_servicer.py,sha256=qQHmhKjn2iIssO63dHSnrK6dZsh_jZrxpSAYH_qV5gA,20550
418
+ flwr/superlink/servicer/control/control_servicer.py,sha256=8nMbXnO9eUnm-H9nikIbyzMQUx7-qZ74PE0X2if_mDE,21377
419
419
  flwr/supernode/__init__.py,sha256=KgeCaVvXWrU3rptNR1y0oBp4YtXbAcrnCcJAiOoWkI4,707
420
420
  flwr/supernode/cli/__init__.py,sha256=JuEMr0-s9zv-PEWKuLB9tj1ocNfroSyNJ-oyv7ati9A,887
421
421
  flwr/supernode/cli/flower_supernode.py,sha256=7aBm0z03OU-npVd1onLCvUotyhSvlZLxAnFkGVMhZcw,8670
@@ -430,7 +430,7 @@ flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca
430
430
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
431
431
  flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=nIHRu38EWK-rpNOkcgBRAAKwYQQWFeCwu0lkO7OPZGQ,10239
432
432
  flwr/supernode/start_client_internal.py,sha256=Y9S1-QlO2WP6eo4JvWzIpfaCoh2aoE7bjEYyxNNnlyg,20777
433
- flwr_nightly-1.23.0.dev20251010.dist-info/METADATA,sha256=HtYyoP2UzTQPv3_ixhl6ZPE6VLR2UhzoiWLCspZ9qCM,14559
434
- flwr_nightly-1.23.0.dev20251010.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
435
- flwr_nightly-1.23.0.dev20251010.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
436
- flwr_nightly-1.23.0.dev20251010.dist-info/RECORD,,
433
+ flwr_nightly-1.23.0.dev20251013.dist-info/METADATA,sha256=KeMnP5CEzO8pPocuBzKVTkoEiKwMhK_5RQjZjIjvq-k,14559
434
+ flwr_nightly-1.23.0.dev20251013.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
435
+ flwr_nightly-1.23.0.dev20251013.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
436
+ flwr_nightly-1.23.0.dev20251013.dist-info/RECORD,,