flwr-nightly 1.15.0.dev20250107__py3-none-any.whl → 1.15.0.dev20250112__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.
Files changed (33) hide show
  1. flwr/cli/cli_user_auth_interceptor.py +6 -2
  2. flwr/cli/login/login.py +11 -4
  3. flwr/cli/utils.py +4 -4
  4. flwr/client/app.py +17 -9
  5. flwr/client/grpc_rere_client/client_interceptor.py +6 -0
  6. flwr/client/grpc_rere_client/grpc_adapter.py +16 -0
  7. flwr/common/auth_plugin/auth_plugin.py +33 -23
  8. flwr/common/constant.py +2 -0
  9. flwr/common/grpc.py +154 -3
  10. flwr/common/typing.py +20 -0
  11. flwr/proto/exec_pb2.py +12 -24
  12. flwr/proto/exec_pb2.pyi +27 -54
  13. flwr/proto/fleet_pb2.py +40 -27
  14. flwr/proto/fleet_pb2.pyi +84 -0
  15. flwr/proto/fleet_pb2_grpc.py +66 -0
  16. flwr/proto/fleet_pb2_grpc.pyi +20 -0
  17. flwr/server/app.py +53 -33
  18. flwr/server/superlink/driver/serverappio_grpc.py +1 -1
  19. flwr/server/superlink/driver/serverappio_servicer.py +22 -8
  20. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -165
  21. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +16 -0
  22. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -1
  23. flwr/server/superlink/linkstate/in_memory_linkstate.py +26 -22
  24. flwr/server/superlink/linkstate/linkstate.py +10 -4
  25. flwr/server/superlink/linkstate/sqlite_linkstate.py +50 -29
  26. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  27. flwr/superexec/exec_grpc.py +1 -1
  28. flwr/superexec/exec_servicer.py +23 -2
  29. {flwr_nightly-1.15.0.dev20250107.dist-info → flwr_nightly-1.15.0.dev20250112.dist-info}/METADATA +4 -4
  30. {flwr_nightly-1.15.0.dev20250107.dist-info → flwr_nightly-1.15.0.dev20250112.dist-info}/RECORD +33 -33
  31. {flwr_nightly-1.15.0.dev20250107.dist-info → flwr_nightly-1.15.0.dev20250112.dist-info}/LICENSE +0 -0
  32. {flwr_nightly-1.15.0.dev20250107.dist-info → flwr_nightly-1.15.0.dev20250112.dist-info}/WHEEL +0 -0
  33. {flwr_nightly-1.15.0.dev20250107.dist-info → flwr_nightly-1.15.0.dev20250112.dist-info}/entry_points.txt +0 -0
@@ -118,8 +118,9 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
118
118
  ffs: Ffs = self.ffs_factory.ffs()
119
119
  fab_hash = ffs.put(fab.content, {})
120
120
  _raise_if(
121
- fab_hash != fab.hash_str,
122
- f"FAB ({fab.hash_str}) hash from request doesn't match contents",
121
+ validation_error=fab_hash != fab.hash_str,
122
+ request_name="CreateRun",
123
+ detail=f"FAB ({fab.hash_str}) hash from request doesn't match contents",
123
124
  )
124
125
  else:
125
126
  fab_hash = ""
@@ -155,12 +156,22 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
155
156
  task_ins.task.pushed_at = pushed_at
156
157
 
157
158
  # Validate request
158
- _raise_if(len(request.task_ins_list) == 0, "`task_ins_list` must not be empty")
159
+ _raise_if(
160
+ validation_error=len(request.task_ins_list) == 0,
161
+ request_name="PushTaskIns",
162
+ detail="`task_ins_list` must not be empty",
163
+ )
159
164
  for task_ins in request.task_ins_list:
160
165
  validation_errors = validate_task_ins_or_res(task_ins)
161
- _raise_if(bool(validation_errors), ", ".join(validation_errors))
162
166
  _raise_if(
163
- request.run_id != task_ins.run_id, "`task_ins` has mismatched `run_id`"
167
+ validation_error=bool(validation_errors),
168
+ request_name="PushTaskIns",
169
+ detail=", ".join(validation_errors),
170
+ )
171
+ _raise_if(
172
+ validation_error=request.run_id != task_ins.run_id,
173
+ request_name="PushTaskIns",
174
+ detail="`task_ins` has mismatched `run_id`",
164
175
  )
165
176
 
166
177
  # Store each TaskIns
@@ -199,7 +210,9 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
199
210
  # Validate request
200
211
  for task_res in task_res_list:
201
212
  _raise_if(
202
- request.run_id != task_res.run_id, "`task_res` has mismatched `run_id`"
213
+ validation_error=request.run_id != task_res.run_id,
214
+ request_name="PullTaskRes",
215
+ detail="`task_res` has mismatched `run_id`",
203
216
  )
204
217
 
205
218
  # Delete the TaskIns/TaskRes pairs if TaskRes is found
@@ -344,6 +357,7 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
344
357
  return GetRunStatusResponse(run_status_dict=run_status_dict)
345
358
 
346
359
 
347
- def _raise_if(validation_error: bool, detail: str) -> None:
360
+ def _raise_if(validation_error: bool, request_name: str, detail: str) -> None:
361
+ """Raise a `ValueError` with a detailed message if a validation error occurs."""
348
362
  if validation_error:
349
- raise ValueError(f"Malformed PushTaskInsRequest: {detail}")
363
+ raise ValueError(f"Malformed {request_name}: {detail}")
@@ -15,49 +15,19 @@
15
15
  """Implements utility function to create a gRPC server."""
16
16
 
17
17
 
18
- import concurrent.futures
19
- import sys
20
- from collections.abc import Sequence
21
- from logging import ERROR
22
- from typing import Any, Callable, Optional, Union
18
+ from typing import Optional
23
19
 
24
20
  import grpc
25
21
 
26
22
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
27
- from flwr.common.address import is_port_in_use
28
- from flwr.common.logger import log
23
+ from flwr.common.grpc import generic_create_grpc_server
29
24
  from flwr.proto.transport_pb2_grpc import ( # pylint: disable=E0611
30
25
  add_FlowerServiceServicer_to_server,
31
26
  )
32
27
  from flwr.server.client_manager import ClientManager
33
- from flwr.server.superlink.driver.serverappio_servicer import ServerAppIoServicer
34
- from flwr.server.superlink.fleet.grpc_adapter.grpc_adapter_servicer import (
35
- GrpcAdapterServicer,
36
- )
37
28
  from flwr.server.superlink.fleet.grpc_bidi.flower_service_servicer import (
38
29
  FlowerServiceServicer,
39
30
  )
40
- from flwr.server.superlink.fleet.grpc_rere.fleet_servicer import FleetServicer
41
-
42
- INVALID_CERTIFICATES_ERR_MSG = """
43
- When setting any of root_certificate, certificate, or private_key,
44
- all of them need to be set.
45
- """
46
-
47
- AddServicerToServerFn = Callable[..., Any]
48
-
49
-
50
- def valid_certificates(certificates: tuple[bytes, bytes, bytes]) -> bool:
51
- """Validate certificates tuple."""
52
- is_valid = (
53
- all(isinstance(certificate, bytes) for certificate in certificates)
54
- and len(certificates) == 3
55
- )
56
-
57
- if not is_valid:
58
- log(ERROR, INVALID_CERTIFICATES_ERR_MSG)
59
-
60
- return is_valid
61
31
 
62
32
 
63
33
  def start_grpc_server( # pylint: disable=too-many-arguments,R0917
@@ -154,136 +124,3 @@ def start_grpc_server( # pylint: disable=too-many-arguments,R0917
154
124
  server.start()
155
125
 
156
126
  return server
157
-
158
-
159
- def generic_create_grpc_server( # pylint: disable=too-many-arguments,R0917
160
- servicer_and_add_fn: Union[
161
- tuple[FleetServicer, AddServicerToServerFn],
162
- tuple[GrpcAdapterServicer, AddServicerToServerFn],
163
- tuple[FlowerServiceServicer, AddServicerToServerFn],
164
- tuple[ServerAppIoServicer, AddServicerToServerFn],
165
- ],
166
- server_address: str,
167
- max_concurrent_workers: int = 1000,
168
- max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
169
- keepalive_time_ms: int = 210000,
170
- certificates: Optional[tuple[bytes, bytes, bytes]] = None,
171
- interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
172
- ) -> grpc.Server:
173
- """Create a gRPC server with a single servicer.
174
-
175
- Parameters
176
- ----------
177
- servicer_and_add_fn : tuple
178
- A tuple holding a servicer implementation and a matching
179
- add_Servicer_to_server function.
180
- server_address : str
181
- Server address in the form of HOST:PORT e.g. "[::]:8080"
182
- max_concurrent_workers : int
183
- Maximum number of clients the server can process before returning
184
- RESOURCE_EXHAUSTED status (default: 1000)
185
- max_message_length : int
186
- Maximum message length that the server can send or receive.
187
- Int valued in bytes. -1 means unlimited. (default: GRPC_MAX_MESSAGE_LENGTH)
188
- keepalive_time_ms : int
189
- Flower uses a default gRPC keepalive time of 210000ms (3 minutes 30 seconds)
190
- because some cloud providers (for example, Azure) agressively clean up idle
191
- TCP connections by terminating them after some time (4 minutes in the case
192
- of Azure). Flower does not use application-level keepalive signals and relies
193
- on the assumption that the transport layer will fail in cases where the
194
- connection is no longer active. `keepalive_time_ms` can be used to customize
195
- the keepalive interval for specific environments. The default Flower gRPC
196
- keepalive of 210000 ms (3 minutes 30 seconds) ensures that Flower can keep
197
- the long running streaming connection alive in most environments. The actual
198
- gRPC default of this setting is 7200000 (2 hours), which results in dropped
199
- connections in some cloud environments.
200
-
201
- These settings are related to the issue described here:
202
- - https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md
203
- - https://github.com/grpc/grpc/blob/master/doc/keepalive.md
204
- - https://grpc.io/docs/guides/performance/
205
-
206
- Mobile Flower clients may choose to increase this value if their server
207
- environment allows long-running idle TCP connections.
208
- (default: 210000)
209
- certificates : Tuple[bytes, bytes, bytes] (default: None)
210
- Tuple containing root certificate, server certificate, and private key to
211
- start a secure SSL-enabled server. The tuple is expected to have three bytes
212
- elements in the following order:
213
-
214
- * CA certificate.
215
- * server certificate.
216
- * server private key.
217
- interceptors : Optional[Sequence[grpc.ServerInterceptor]] (default: None)
218
- A list of gRPC interceptors.
219
-
220
- Returns
221
- -------
222
- server : grpc.Server
223
- A non-running instance of a gRPC server.
224
- """
225
- # Check if port is in use
226
- if is_port_in_use(server_address):
227
- sys.exit(f"Port in server address {server_address} is already in use.")
228
-
229
- # Deconstruct tuple into servicer and function
230
- servicer, add_servicer_to_server_fn = servicer_and_add_fn
231
-
232
- # Possible options:
233
- # https://github.com/grpc/grpc/blob/v1.43.x/include/grpc/impl/codegen/grpc_types.h
234
- options = [
235
- # Maximum number of concurrent incoming streams to allow on a http2
236
- # connection. Int valued.
237
- ("grpc.max_concurrent_streams", max(100, max_concurrent_workers)),
238
- # Maximum message length that the channel can send.
239
- # Int valued, bytes. -1 means unlimited.
240
- ("grpc.max_send_message_length", max_message_length),
241
- # Maximum message length that the channel can receive.
242
- # Int valued, bytes. -1 means unlimited.
243
- ("grpc.max_receive_message_length", max_message_length),
244
- # The gRPC default for this setting is 7200000 (2 hours). Flower uses a
245
- # customized default of 210000 (3 minutes and 30 seconds) to improve
246
- # compatibility with popular cloud providers. Mobile Flower clients may
247
- # choose to increase this value if their server environment allows
248
- # long-running idle TCP connections.
249
- ("grpc.keepalive_time_ms", keepalive_time_ms),
250
- # Setting this to zero will allow sending unlimited keepalive pings in between
251
- # sending actual data frames.
252
- ("grpc.http2.max_pings_without_data", 0),
253
- # Is it permissible to send keepalive pings from the client without
254
- # any outstanding streams. More explanation here:
255
- # https://github.com/adap/flower/pull/2197
256
- ("grpc.keepalive_permit_without_calls", 0),
257
- ]
258
-
259
- server = grpc.server(
260
- concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent_workers),
261
- # Set the maximum number of concurrent RPCs this server will service before
262
- # returning RESOURCE_EXHAUSTED status, or None to indicate no limit.
263
- maximum_concurrent_rpcs=max_concurrent_workers,
264
- options=options,
265
- interceptors=interceptors,
266
- )
267
- add_servicer_to_server_fn(servicer, server)
268
-
269
- if certificates is not None:
270
- if not valid_certificates(certificates):
271
- sys.exit(1)
272
-
273
- root_certificate_b, certificate_b, private_key_b = certificates
274
-
275
- server_credentials = grpc.ssl_server_credentials(
276
- ((private_key_b, certificate_b),),
277
- root_certificates=root_certificate_b,
278
- # A boolean indicating whether or not to require clients to be
279
- # authenticated. May only be True if root_certificates is not None.
280
- # We are explicitly setting the current gRPC default to document
281
- # the option. For further reference see:
282
- # https://grpc.github.io/grpc/python/grpc.html#create-server-credentials
283
- require_client_auth=False,
284
- )
285
- server.add_secure_port(server_address, server_credentials)
286
- else:
287
- server.add_insecure_port(server_address)
288
-
289
- return server
@@ -30,8 +30,12 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
30
30
  DeleteNodeResponse,
31
31
  PingRequest,
32
32
  PingResponse,
33
+ PullMessagesRequest,
34
+ PullMessagesResponse,
33
35
  PullTaskInsRequest,
34
36
  PullTaskInsResponse,
37
+ PushMessagesRequest,
38
+ PushMessagesResponse,
35
39
  PushTaskResRequest,
36
40
  PushTaskResResponse,
37
41
  )
@@ -95,6 +99,12 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
95
99
  state=self.state_factory.state(),
96
100
  )
97
101
 
102
+ def PullMessages(
103
+ self, request: PullMessagesRequest, context: grpc.ServicerContext
104
+ ) -> PullMessagesResponse:
105
+ """Pull Messages."""
106
+ return PullMessagesResponse()
107
+
98
108
  def PushTaskRes(
99
109
  self, request: PushTaskResRequest, context: grpc.ServicerContext
100
110
  ) -> PushTaskResResponse:
@@ -118,6 +128,12 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
118
128
 
119
129
  return res
120
130
 
131
+ def PushMessages(
132
+ self, request: PushMessagesRequest, context: grpc.ServicerContext
133
+ ) -> PushMessagesResponse:
134
+ """Push Messages."""
135
+ return PushMessagesResponse()
136
+
121
137
  def GetRun(
122
138
  self, request: GetRunRequest, context: grpc.ServicerContext
123
139
  ) -> GetRunResponse:
@@ -223,5 +223,6 @@ class AuthenticateServerInterceptor(grpc.ServerInterceptor): # type: ignore
223
223
  # No `node_id` exists for the provided `public_key`
224
224
  # Handle `CreateNode` here instead of calling the default method handler
225
225
  # Note: the innermost `CreateNode` method will never be called
226
- node_id = state.create_node(request.ping_interval, public_key_bytes)
226
+ node_id = state.create_node(request.ping_interval)
227
+ state.set_node_public_key(node_id, public_key_bytes)
227
228
  return CreateNodeResponse(node=Node(node_id=node_id, anonymous=False))
@@ -62,6 +62,7 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
62
62
  # Map node_id to (online_until, ping_interval)
63
63
  self.node_ids: dict[int, tuple[float, float]] = {}
64
64
  self.public_key_to_node_id: dict[bytes, int] = {}
65
+ self.node_id_to_public_key: dict[int, bytes] = {}
65
66
 
66
67
  # Map run_id to RunRecord
67
68
  self.run_ids: dict[int, RunRecord] = {}
@@ -306,9 +307,7 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
306
307
  """
307
308
  return len(self.task_res_store)
308
309
 
309
- def create_node(
310
- self, ping_interval: float, public_key: Optional[bytes] = None
311
- ) -> int:
310
+ def create_node(self, ping_interval: float) -> int:
312
311
  """Create, store in the link state, and return `node_id`."""
313
312
  # Sample a random int64 as node_id
314
313
  node_id = generate_rand_int_from_bytes(NODE_ID_NUM_BYTES)
@@ -318,33 +317,18 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
318
317
  log(ERROR, "Unexpected node registration failure.")
319
318
  return 0
320
319
 
321
- if public_key is not None:
322
- if (
323
- public_key in self.public_key_to_node_id
324
- or node_id in self.public_key_to_node_id.values()
325
- ):
326
- log(ERROR, "Unexpected node registration failure.")
327
- return 0
328
-
329
- self.public_key_to_node_id[public_key] = node_id
330
-
331
320
  self.node_ids[node_id] = (time.time() + ping_interval, ping_interval)
332
321
  return node_id
333
322
 
334
- def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None:
323
+ def delete_node(self, node_id: int) -> None:
335
324
  """Delete a node."""
336
325
  with self.lock:
337
326
  if node_id not in self.node_ids:
338
327
  raise ValueError(f"Node {node_id} not found")
339
328
 
340
- if public_key is not None:
341
- if (
342
- public_key not in self.public_key_to_node_id
343
- or node_id not in self.public_key_to_node_id.values()
344
- ):
345
- raise ValueError("Public key or node_id not found")
346
-
347
- del self.public_key_to_node_id[public_key]
329
+ # Remove node ID <> public key mappings
330
+ if pk := self.node_id_to_public_key.pop(node_id, None):
331
+ del self.public_key_to_node_id[pk]
348
332
 
349
333
  del self.node_ids[node_id]
350
334
 
@@ -366,6 +350,26 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
366
350
  if online_until > current_time
367
351
  }
368
352
 
353
+ def set_node_public_key(self, node_id: int, public_key: bytes) -> None:
354
+ """Set `public_key` for the specified `node_id`."""
355
+ with self.lock:
356
+ if node_id not in self.node_ids:
357
+ raise ValueError(f"Node {node_id} not found")
358
+
359
+ if public_key in self.public_key_to_node_id:
360
+ raise ValueError("Public key already in use")
361
+
362
+ self.public_key_to_node_id[public_key] = node_id
363
+ self.node_id_to_public_key[node_id] = public_key
364
+
365
+ def get_node_public_key(self, node_id: int) -> Optional[bytes]:
366
+ """Get `public_key` for the specified `node_id`."""
367
+ with self.lock:
368
+ if node_id not in self.node_ids:
369
+ raise ValueError(f"Node {node_id} not found")
370
+
371
+ return self.node_id_to_public_key.get(node_id)
372
+
369
373
  def get_node_id(self, node_public_key: bytes) -> Optional[int]:
370
374
  """Retrieve stored `node_id` filtered by `node_public_keys`."""
371
375
  return self.public_key_to_node_id.get(node_public_key)
@@ -154,13 +154,11 @@ class LinkState(abc.ABC): # pylint: disable=R0904
154
154
  """Get all TaskIns IDs for the given run_id."""
155
155
 
156
156
  @abc.abstractmethod
157
- def create_node(
158
- self, ping_interval: float, public_key: Optional[bytes] = None
159
- ) -> int:
157
+ def create_node(self, ping_interval: float) -> int:
160
158
  """Create, store in the link state, and return `node_id`."""
161
159
 
162
160
  @abc.abstractmethod
163
- def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None:
161
+ def delete_node(self, node_id: int) -> None:
164
162
  """Remove `node_id` from the link state."""
165
163
 
166
164
  @abc.abstractmethod
@@ -173,6 +171,14 @@ class LinkState(abc.ABC): # pylint: disable=R0904
173
171
  an empty `Set` MUST be returned.
174
172
  """
175
173
 
174
+ @abc.abstractmethod
175
+ def set_node_public_key(self, node_id: int, public_key: bytes) -> None:
176
+ """Set `public_key` for the specified `node_id`."""
177
+
178
+ @abc.abstractmethod
179
+ def get_node_public_key(self, node_id: int) -> Optional[bytes]:
180
+ """Get `public_key` for the specified `node_id`."""
181
+
176
182
  @abc.abstractmethod
177
183
  def get_node_id(self, node_public_key: bytes) -> Optional[int]:
178
184
  """Retrieve stored `node_id` filtered by `node_public_keys`."""
@@ -72,14 +72,14 @@ CREATE TABLE IF NOT EXISTS node(
72
72
 
73
73
  SQL_CREATE_TABLE_CREDENTIAL = """
74
74
  CREATE TABLE IF NOT EXISTS credential(
75
- private_key BLOB PRIMARY KEY,
76
- public_key BLOB
75
+ private_key BLOB PRIMARY KEY,
76
+ public_key BLOB
77
77
  );
78
78
  """
79
79
 
80
80
  SQL_CREATE_TABLE_PUBLIC_KEY = """
81
81
  CREATE TABLE IF NOT EXISTS public_key(
82
- public_key BLOB UNIQUE
82
+ public_key BLOB PRIMARY KEY
83
83
  );
84
84
  """
85
85
 
@@ -635,9 +635,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
635
635
 
636
636
  return {UUID(row["task_id"]) for row in rows}
637
637
 
638
- def create_node(
639
- self, ping_interval: float, public_key: Optional[bytes] = None
640
- ) -> int:
638
+ def create_node(self, ping_interval: float) -> int:
641
639
  """Create, store in the link state, and return `node_id`."""
642
640
  # Sample a random uint64 as node_id
643
641
  uint64_node_id = generate_rand_int_from_bytes(NODE_ID_NUM_BYTES)
@@ -645,13 +643,6 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
645
643
  # Convert the uint64 value to sint64 for SQLite
646
644
  sint64_node_id = convert_uint64_to_sint64(uint64_node_id)
647
645
 
648
- query = "SELECT node_id FROM node WHERE public_key = :public_key;"
649
- row = self.query(query, {"public_key": public_key})
650
-
651
- if len(row) > 0:
652
- log(ERROR, "Unexpected node registration failure.")
653
- return 0
654
-
655
646
  query = (
656
647
  "INSERT INTO node "
657
648
  "(node_id, online_until, ping_interval, public_key) "
@@ -665,7 +656,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
665
656
  sint64_node_id,
666
657
  time.time() + ping_interval,
667
658
  ping_interval,
668
- public_key,
659
+ b"", # Initialize with an empty public key
669
660
  ),
670
661
  )
671
662
  except sqlite3.IntegrityError:
@@ -675,7 +666,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
675
666
  # Note: we need to return the uint64 value of the node_id
676
667
  return uint64_node_id
677
668
 
678
- def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None:
669
+ def delete_node(self, node_id: int) -> None:
679
670
  """Delete a node."""
680
671
  # Convert the uint64 value to sint64 for SQLite
681
672
  sint64_node_id = convert_uint64_to_sint64(node_id)
@@ -683,10 +674,6 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
683
674
  query = "DELETE FROM node WHERE node_id = ?"
684
675
  params = (sint64_node_id,)
685
676
 
686
- if public_key is not None:
687
- query += " AND public_key = ?"
688
- params += (public_key,) # type: ignore
689
-
690
677
  if self.conn is None:
691
678
  raise AttributeError("LinkState is not initialized.")
692
679
 
@@ -694,7 +681,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
694
681
  with self.conn:
695
682
  rows = self.conn.execute(query, params)
696
683
  if rows.rowcount < 1:
697
- raise ValueError("Public key or node_id not found")
684
+ raise ValueError(f"Node {node_id} not found")
698
685
  except KeyError as exc:
699
686
  log(ERROR, {"query": query, "data": params, "exception": exc})
700
687
 
@@ -722,6 +709,41 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
722
709
  result: set[int] = {convert_sint64_to_uint64(row["node_id"]) for row in rows}
723
710
  return result
724
711
 
712
+ def set_node_public_key(self, node_id: int, public_key: bytes) -> None:
713
+ """Set `public_key` for the specified `node_id`."""
714
+ # Convert the uint64 value to sint64 for SQLite
715
+ sint64_node_id = convert_uint64_to_sint64(node_id)
716
+
717
+ # Check if the node exists in the `node` table
718
+ query = "SELECT 1 FROM node WHERE node_id = ?"
719
+ if not self.query(query, (sint64_node_id,)):
720
+ raise ValueError(f"Node {node_id} not found")
721
+
722
+ # Check if the public key is already in use in the `node` table
723
+ query = "SELECT 1 FROM node WHERE public_key = ?"
724
+ if self.query(query, (public_key,)):
725
+ raise ValueError("Public key already in use")
726
+
727
+ # Update the `node` table to set the public key for the given node ID
728
+ query = "UPDATE node SET public_key = ? WHERE node_id = ?"
729
+ self.query(query, (public_key, sint64_node_id))
730
+
731
+ def get_node_public_key(self, node_id: int) -> Optional[bytes]:
732
+ """Get `public_key` for the specified `node_id`."""
733
+ # Convert the uint64 value to sint64 for SQLite
734
+ sint64_node_id = convert_uint64_to_sint64(node_id)
735
+
736
+ # Query the public key for the given node_id
737
+ query = "SELECT public_key FROM node WHERE node_id = ?"
738
+ rows = self.query(query, (sint64_node_id,))
739
+
740
+ # If no result is found, return None
741
+ if not rows:
742
+ raise ValueError(f"Node {node_id} not found")
743
+
744
+ # Return the public key if it is not empty, otherwise return None
745
+ return rows[0]["public_key"] or None
746
+
725
747
  def get_node_id(self, node_public_key: bytes) -> Optional[int]:
726
748
  """Retrieve stored `node_id` filtered by `node_public_keys`."""
727
749
  query = "SELECT node_id FROM node WHERE public_key = :public_key;"
@@ -982,17 +1004,16 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
982
1004
  """Acknowledge a ping received from a node, serving as a heartbeat."""
983
1005
  sint64_node_id = convert_uint64_to_sint64(node_id)
984
1006
 
985
- # Update `online_until` and `ping_interval` for the given `node_id`
986
- query = "UPDATE node SET online_until = ?, ping_interval = ? WHERE node_id = ?;"
987
- try:
988
- self.query(
989
- query, (time.time() + ping_interval, ping_interval, sint64_node_id)
990
- )
991
- return True
992
- except sqlite3.IntegrityError:
993
- log(ERROR, "`node_id` does not exist.")
1007
+ # Check if the node exists in the `node` table
1008
+ query = "SELECT 1 FROM node WHERE node_id = ?"
1009
+ if not self.query(query, (sint64_node_id,)):
994
1010
  return False
995
1011
 
1012
+ # Update `online_until` and `ping_interval` for the given `node_id`
1013
+ query = "UPDATE node SET online_until = ?, ping_interval = ? WHERE node_id = ?"
1014
+ self.query(query, (time.time() + ping_interval, ping_interval, sint64_node_id))
1015
+ return True
1016
+
996
1017
  def get_serverapp_context(self, run_id: int) -> Optional[Context]:
997
1018
  """Get the context for the specified `run_id`."""
998
1019
  # Retrieve context if any
@@ -21,6 +21,7 @@ from typing import Optional
21
21
  import grpc
22
22
 
23
23
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
24
+ from flwr.common.grpc import generic_create_grpc_server
24
25
  from flwr.common.logger import log
25
26
  from flwr.proto.simulationio_pb2_grpc import ( # pylint: disable=E0611
26
27
  add_SimulationIoServicer_to_server,
@@ -28,7 +29,6 @@ from flwr.proto.simulationio_pb2_grpc import ( # pylint: disable=E0611
28
29
  from flwr.server.superlink.ffs.ffs_factory import FfsFactory
29
30
  from flwr.server.superlink.linkstate import LinkStateFactory
30
31
 
31
- from ..fleet.grpc_bidi.grpc_server import generic_create_grpc_server
32
32
  from .simulationio_servicer import SimulationIoServicer
33
33
 
34
34
 
@@ -23,11 +23,11 @@ import grpc
23
23
 
24
24
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
25
25
  from flwr.common.auth_plugin import ExecAuthPlugin
26
+ from flwr.common.grpc import generic_create_grpc_server
26
27
  from flwr.common.logger import log
27
28
  from flwr.common.typing import UserConfig
28
29
  from flwr.proto.exec_pb2_grpc import add_ExecServicer_to_server
29
30
  from flwr.server.superlink.ffs.ffs_factory import FfsFactory
30
- from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server
31
31
  from flwr.server.superlink.linkstate import LinkStateFactory
32
32
  from flwr.superexec.exec_user_auth_interceptor import ExecUserAuthInterceptor
33
33
 
@@ -181,8 +181,20 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
181
181
  "ExecServicer initialized without user authentication",
182
182
  )
183
183
  raise grpc.RpcError() # This line is unreachable
184
+
185
+ # Get login details
186
+ details = self.auth_plugin.get_login_details()
187
+
188
+ # Return empty response if details is None
189
+ if details is None:
190
+ return GetLoginDetailsResponse()
191
+
184
192
  return GetLoginDetailsResponse(
185
- login_details=self.auth_plugin.get_login_details()
193
+ auth_type=details.auth_type,
194
+ device_code=details.device_code,
195
+ verification_uri_complete=details.verification_uri_complete,
196
+ expires_in=details.expires_in,
197
+ interval=details.interval,
186
198
  )
187
199
 
188
200
  def GetAuthTokens(
@@ -196,8 +208,17 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
196
208
  "ExecServicer initialized without user authentication",
197
209
  )
198
210
  raise grpc.RpcError() # This line is unreachable
211
+
212
+ # Get auth tokens
213
+ credentials = self.auth_plugin.get_auth_tokens(request.device_code)
214
+
215
+ # Return empty response if credentials is None
216
+ if credentials is None:
217
+ return GetAuthTokensResponse()
218
+
199
219
  return GetAuthTokensResponse(
200
- auth_tokens=self.auth_plugin.get_auth_tokens(dict(request.auth_details))
220
+ access_token=credentials.access_token,
221
+ refresh_token=credentials.refresh_token,
201
222
  )
202
223
 
203
224
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.15.0.dev20250107
3
+ Version: 1.15.0.dev20250112
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -43,11 +43,11 @@ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
43
43
  Requires-Dist: ray (==2.10.0) ; (python_version >= "3.9" and python_version < "3.12") and (extra == "simulation")
44
44
  Requires-Dist: requests (>=2.31.0,<3.0.0)
45
45
  Requires-Dist: rich (>=13.5.0,<14.0.0)
46
- Requires-Dist: starlette (>=0.31.0,<0.32.0) ; extra == "rest"
46
+ Requires-Dist: starlette (>=0.45.2,<0.46.0) ; extra == "rest"
47
47
  Requires-Dist: tomli (>=2.0.1,<3.0.0)
48
48
  Requires-Dist: tomli-w (>=1.0.0,<2.0.0)
49
49
  Requires-Dist: typer (>=0.12.5,<0.13.0)
50
- Requires-Dist: uvicorn[standard] (>=0.23.0,<0.24.0) ; extra == "rest"
50
+ Requires-Dist: uvicorn[standard] (>=0.34.0,<0.35.0) ; extra == "rest"
51
51
  Project-URL: Documentation, https://flower.ai
52
52
  Project-URL: Repository, https://github.com/adap/flower
53
53
  Description-Content-Type: text/markdown
@@ -88,7 +88,7 @@ design of Flower is based on a few guiding principles:
88
88
 
89
89
  - **Framework-agnostic**: Different machine learning frameworks have different
90
90
  strengths. Flower can be used with any machine learning framework, for
91
- example, [PyTorch](https://pytorch.org), [TensorFlow](https://tensorflow.org), [Hugging Face Transformers](https://huggingface.co/), [PyTorch Lightning](https://pytorchlightning.ai/), [scikit-learn](https://scikit-learn.org/), [JAX](https://jax.readthedocs.io/), [TFLite](https://tensorflow.org/lite/), [MONAI](https://docs.monai.io/en/latest/index.html), [fastai](https://www.fast.ai/), [MLX](https://ml-explore.github.io/mlx/build/html/index.html), [XGBoost](https://xgboost.readthedocs.io/en/stable/), [Pandas](https://pandas.pydata.org/) for federated analytics, or even raw [NumPy](https://numpy.org/)
91
+ example, [PyTorch](https://pytorch.org), [TensorFlow](https://tensorflow.org), [Hugging Face Transformers](https://huggingface.co/), [PyTorch Lightning](https://pytorchlightning.ai/), [scikit-learn](https://scikit-learn.org/), [JAX](https://jax.readthedocs.io/), [TFLite](https://tensorflow.org/lite/), [MONAI](https://docs.monai.io/en/latest/index.html), [fastai](https://www.fast.ai/), [MLX](https://ml-explore.github.io/mlx/build/html/index.html), [XGBoost](https://xgboost.readthedocs.io/en/stable/), [LeRobot](https://github.com/huggingface/lerobot) for federated robots, [Pandas](https://pandas.pydata.org/) for federated analytics, or even raw [NumPy](https://numpy.org/)
92
92
  for users who enjoy computing gradients by hand.
93
93
 
94
94
  - **Understandable**: Flower is written with maintainability in mind. The