flwr 1.15.2__py3-none-any.whl → 1.16.0__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 (68) hide show
  1. flwr/cli/build.py +2 -0
  2. flwr/cli/log.py +20 -21
  3. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  4. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  5. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  11. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  12. flwr/client/client_app.py +147 -36
  13. flwr/client/clientapp/app.py +4 -0
  14. flwr/client/message_handler/message_handler.py +1 -1
  15. flwr/client/rest_client/connection.py +4 -6
  16. flwr/client/supernode/__init__.py +0 -2
  17. flwr/client/supernode/app.py +1 -11
  18. flwr/common/address.py +35 -0
  19. flwr/common/args.py +8 -2
  20. flwr/common/auth_plugin/auth_plugin.py +2 -1
  21. flwr/common/constant.py +16 -0
  22. flwr/common/event_log_plugin/__init__.py +22 -0
  23. flwr/common/event_log_plugin/event_log_plugin.py +60 -0
  24. flwr/common/grpc.py +1 -1
  25. flwr/common/message.py +18 -7
  26. flwr/common/object_ref.py +0 -10
  27. flwr/common/record/conversion_utils.py +8 -17
  28. flwr/common/record/parametersrecord.py +151 -16
  29. flwr/common/record/recordset.py +95 -88
  30. flwr/common/secure_aggregation/quantization.py +5 -1
  31. flwr/common/serde.py +8 -126
  32. flwr/common/telemetry.py +0 -10
  33. flwr/common/typing.py +36 -0
  34. flwr/server/app.py +18 -2
  35. flwr/server/compat/app.py +4 -1
  36. flwr/server/compat/app_utils.py +10 -2
  37. flwr/server/compat/driver_client_proxy.py +2 -2
  38. flwr/server/driver/driver.py +1 -1
  39. flwr/server/driver/grpc_driver.py +10 -1
  40. flwr/server/driver/inmemory_driver.py +17 -20
  41. flwr/server/run_serverapp.py +2 -13
  42. flwr/server/server_app.py +93 -20
  43. flwr/server/superlink/driver/serverappio_servicer.py +25 -27
  44. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +2 -2
  45. flwr/server/superlink/fleet/message_handler/message_handler.py +8 -12
  46. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  47. flwr/server/superlink/fleet/vce/vce_api.py +32 -35
  48. flwr/server/superlink/linkstate/in_memory_linkstate.py +140 -126
  49. flwr/server/superlink/linkstate/linkstate.py +47 -60
  50. flwr/server/superlink/linkstate/sqlite_linkstate.py +210 -276
  51. flwr/server/superlink/linkstate/utils.py +91 -119
  52. flwr/server/utils/__init__.py +2 -2
  53. flwr/server/utils/validator.py +53 -68
  54. flwr/server/workflow/default_workflows.py +4 -1
  55. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +3 -3
  56. flwr/superexec/app.py +0 -14
  57. flwr/superexec/exec_servicer.py +4 -4
  58. flwr/superexec/exec_user_auth_interceptor.py +5 -3
  59. {flwr-1.15.2.dist-info → flwr-1.16.0.dist-info}/METADATA +4 -4
  60. {flwr-1.15.2.dist-info → flwr-1.16.0.dist-info}/RECORD +63 -66
  61. {flwr-1.15.2.dist-info → flwr-1.16.0.dist-info}/entry_points.txt +0 -3
  62. flwr/client/message_handler/task_handler.py +0 -37
  63. flwr/proto/task_pb2.py +0 -33
  64. flwr/proto/task_pb2.pyi +0 -100
  65. flwr/proto/task_pb2_grpc.py +0 -4
  66. flwr/proto/task_pb2_grpc.pyi +0 -4
  67. {flwr-1.15.2.dist-info → flwr-1.16.0.dist-info}/LICENSE +0 -0
  68. {flwr-1.15.2.dist-info → flwr-1.16.0.dist-info}/WHEEL +0 -0
flwr/cli/build.py CHANGED
@@ -138,6 +138,8 @@ def build(
138
138
  and f.name != "pyproject.toml" # Exclude the original pyproject.toml
139
139
  ]
140
140
 
141
+ all_files.sort()
142
+
141
143
  for file_path in all_files:
142
144
  # Read the file content manually
143
145
  with open(file_path, "rb") as f:
flwr/cli/log.py CHANGED
@@ -38,6 +38,10 @@ from flwr.proto.exec_pb2_grpc import ExecStub
38
38
  from .utils import init_channel, try_obtain_cli_auth_plugin, unauthenticated_exc_handler
39
39
 
40
40
 
41
+ class AllLogsRetrieved(BaseException):
42
+ """Raised when all logs are retrieved."""
43
+
44
+
41
45
  def start_stream(
42
46
  run_id: int, channel: grpc.Channel, refresh_period: int = CONN_REFRESH_PERIOD
43
47
  ) -> None:
@@ -56,10 +60,10 @@ def start_stream(
56
60
  # pylint: disable=E1101
57
61
  if e.code() == grpc.StatusCode.NOT_FOUND:
58
62
  logger(ERROR, "Invalid run_id `%s`, exiting", run_id)
59
- if e.code() == grpc.StatusCode.CANCELLED:
60
- pass
61
63
  else:
62
64
  raise e
65
+ except AllLogsRetrieved:
66
+ pass
63
67
  finally:
64
68
  channel.close()
65
69
 
@@ -94,6 +98,7 @@ def stream_logs(
94
98
  with unauthenticated_exc_handler():
95
99
  for res in stub.StreamLogs(req, timeout=duration):
96
100
  print(res.log_output, end="")
101
+ raise AllLogsRetrieved()
97
102
  except grpc.RpcError as e:
98
103
  # pylint: disable=E1101
99
104
  if e.code() != grpc.StatusCode.DEADLINE_EXCEEDED:
@@ -108,27 +113,21 @@ def stream_logs(
108
113
  def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
109
114
  """Print logs from the beginning of a run."""
110
115
  stub = ExecStub(channel)
111
- req = StreamLogsRequest(run_id=run_id)
116
+ req = StreamLogsRequest(run_id=run_id, after_timestamp=0.0)
112
117
 
113
118
  try:
114
- while True:
115
- try:
116
- with unauthenticated_exc_handler():
117
- # Enforce timeout for graceful exit
118
- for res in stub.StreamLogs(req, timeout=timeout):
119
- print(res.log_output)
120
- except grpc.RpcError as e:
121
- # pylint: disable=E1101
122
- if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
123
- break
124
- if e.code() == grpc.StatusCode.NOT_FOUND:
125
- logger(ERROR, "Invalid run_id `%s`, exiting", run_id)
126
- break
127
- if e.code() == grpc.StatusCode.CANCELLED:
128
- break
129
- raise e
130
- except KeyboardInterrupt:
131
- logger(DEBUG, "Stream interrupted by user")
119
+ with unauthenticated_exc_handler():
120
+ # Enforce timeout for graceful exit
121
+ for res in stub.StreamLogs(req, timeout=timeout):
122
+ print(res.log_output)
123
+ break
124
+ except grpc.RpcError as e:
125
+ if e.code() == grpc.StatusCode.NOT_FOUND: # pylint: disable=E1101
126
+ logger(ERROR, "Invalid run_id `%s`, exiting", run_id)
127
+ elif e.code() == grpc.StatusCode.DEADLINE_EXCEEDED: # pylint: disable=E1101
128
+ pass
129
+ else:
130
+ raise e
132
131
  finally:
133
132
  channel.close()
134
133
  logger(DEBUG, "Channel closed")
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "torch==2.5.1",
14
14
  "torchvision==0.20.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets>=0.5.0",
13
13
  "torch==2.3.1",
14
14
  "trl==0.8.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets>=0.5.0",
13
13
  "torch==2.5.1",
14
14
  "transformers>=4.30.0,<5.0",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "jax==0.4.30",
13
13
  "jaxlib==0.4.30",
14
14
  "scikit-learn==1.6.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "mlx==0.21.1",
14
14
  ]
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "numpy>=2.0.2",
13
13
  ]
14
14
 
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "torch==2.5.1",
14
14
  "torchvision==0.20.1",
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "scikit-learn>=1.6.1",
14
14
  ]
@@ -8,7 +8,7 @@ version = "1.0.0"
8
8
  description = ""
9
9
  license = "Apache-2.0"
10
10
  dependencies = [
11
- "flwr[simulation]>=1.15.2",
11
+ "flwr[simulation]>=1.16.0",
12
12
  "flwr-datasets[vision]>=0.5.0",
13
13
  "tensorflow>=2.11.1,<2.18.0",
14
14
  ]
flwr/client/client_app.py CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
 
18
18
  import inspect
19
+ from collections.abc import Iterator
20
+ from contextlib import contextmanager
19
21
  from typing import Callable, Optional
20
22
 
21
23
  from flwr.client.client import Client
@@ -71,6 +73,11 @@ def _inspect_maybe_adapt_client_fn_signature(client_fn: ClientFnExt) -> ClientFn
71
73
  return client_fn
72
74
 
73
75
 
76
+ @contextmanager
77
+ def _empty_lifespan(_: Context) -> Iterator[None]:
78
+ yield
79
+
80
+
74
81
  class ClientAppException(Exception):
75
82
  """Exception raised when an exception is raised while executing a ClientApp."""
76
83
 
@@ -95,15 +102,6 @@ class ClientApp:
95
102
  >>> return FlowerClient().to_client()
96
103
  >>>
97
104
  >>> app = ClientApp(client_fn)
98
-
99
- If the above code is in a Python module called `client`, it can be started as
100
- follows:
101
-
102
- >>> flower-client-app client:app --insecure
103
-
104
- In this `client:app` example, `client` refers to the Python module `client.py` in
105
- which the previous code lives in and `app` refers to the global attribute `app` that
106
- points to an object of type `ClientApp`.
107
105
  """
108
106
 
109
107
  def __init__(
@@ -135,35 +133,41 @@ class ClientApp:
135
133
  self._train: Optional[ClientAppCallable] = None
136
134
  self._evaluate: Optional[ClientAppCallable] = None
137
135
  self._query: Optional[ClientAppCallable] = None
136
+ self._lifespan = _empty_lifespan
138
137
 
139
138
  def __call__(self, message: Message, context: Context) -> Message:
140
139
  """Execute `ClientApp`."""
141
- # Execute message using `client_fn`
142
- if self._call:
143
- return self._call(message, context)
144
-
145
- # Execute message using a new
146
- if message.metadata.message_type == MessageType.TRAIN:
147
- if self._train:
148
- return self._train(message, context)
149
- raise ValueError("No `train` function registered")
150
- if message.metadata.message_type == MessageType.EVALUATE:
151
- if self._evaluate:
152
- return self._evaluate(message, context)
153
- raise ValueError("No `evaluate` function registered")
154
- if message.metadata.message_type == MessageType.QUERY:
155
- if self._query:
156
- return self._query(message, context)
157
- raise ValueError("No `query` function registered")
158
-
159
- # Message type did not match one of the known message types abvoe
160
- raise ValueError(f"Unknown message_type: {message.metadata.message_type}")
161
-
162
- def train(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
140
+ with self._lifespan(context):
141
+ # Execute message using `client_fn`
142
+ if self._call:
143
+ return self._call(message, context)
144
+
145
+ # Execute message using a new
146
+ if message.metadata.message_type == MessageType.TRAIN:
147
+ if self._train:
148
+ return self._train(message, context)
149
+ raise ValueError("No `train` function registered")
150
+ if message.metadata.message_type == MessageType.EVALUATE:
151
+ if self._evaluate:
152
+ return self._evaluate(message, context)
153
+ raise ValueError("No `evaluate` function registered")
154
+ if message.metadata.message_type == MessageType.QUERY:
155
+ if self._query:
156
+ return self._query(message, context)
157
+ raise ValueError("No `query` function registered")
158
+
159
+ # Message type did not match one of the known message types abvoe
160
+ raise ValueError(f"Unknown message_type: {message.metadata.message_type}")
161
+
162
+ def train(
163
+ self, mods: Optional[list[Mod]] = None
164
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
163
165
  """Return a decorator that registers the train fn with the client app.
164
166
 
165
167
  Examples
166
168
  --------
169
+ Registering a train function:
170
+
167
171
  >>> app = ClientApp()
168
172
  >>>
169
173
  >>> @app.train()
@@ -171,6 +175,17 @@ class ClientApp:
171
175
  >>> print("ClientApp training running")
172
176
  >>> # Create and return an echo reply message
173
177
  >>> return message.create_reply(content=message.content())
178
+
179
+ Registering a train function with a function-specific modifier:
180
+
181
+ >>> from flwr.client.mod import message_size_mod
182
+ >>>
183
+ >>> app = ClientApp()
184
+ >>>
185
+ >>> @app.train(mods=[message_size_mod])
186
+ >>> def train(message: Message, context: Context) -> Message:
187
+ >>> print("ClientApp training running with message size mod")
188
+ >>> return message.create_reply(content=message.content())
174
189
  """
175
190
 
176
191
  def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:
@@ -182,18 +197,22 @@ class ClientApp:
182
197
 
183
198
  # Register provided function with the ClientApp object
184
199
  # Wrap mods around the wrapped step function
185
- self._train = make_ffn(train_fn, self._mods)
200
+ self._train = make_ffn(train_fn, self._mods + (mods or []))
186
201
 
187
202
  # Return provided function unmodified
188
203
  return train_fn
189
204
 
190
205
  return train_decorator
191
206
 
192
- def evaluate(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
207
+ def evaluate(
208
+ self, mods: Optional[list[Mod]] = None
209
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
193
210
  """Return a decorator that registers the evaluate fn with the client app.
194
211
 
195
212
  Examples
196
213
  --------
214
+ Registering an evaluate function:
215
+
197
216
  >>> app = ClientApp()
198
217
  >>>
199
218
  >>> @app.evaluate()
@@ -201,6 +220,18 @@ class ClientApp:
201
220
  >>> print("ClientApp evaluation running")
202
221
  >>> # Create and return an echo reply message
203
222
  >>> return message.create_reply(content=message.content())
223
+
224
+ Registering an evaluate function with a function-specific modifier:
225
+
226
+ >>> from flwr.client.mod import message_size_mod
227
+ >>>
228
+ >>> app = ClientApp()
229
+ >>>
230
+ >>> @app.evaluate(mods=[message_size_mod])
231
+ >>> def evaluate(message: Message, context: Context) -> Message:
232
+ >>> print("ClientApp evaluation running with message size mod")
233
+ >>> # Create and return an echo reply message
234
+ >>> return message.create_reply(content=message.content())
204
235
  """
205
236
 
206
237
  def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:
@@ -212,18 +243,22 @@ class ClientApp:
212
243
 
213
244
  # Register provided function with the ClientApp object
214
245
  # Wrap mods around the wrapped step function
215
- self._evaluate = make_ffn(evaluate_fn, self._mods)
246
+ self._evaluate = make_ffn(evaluate_fn, self._mods + (mods or []))
216
247
 
217
248
  # Return provided function unmodified
218
249
  return evaluate_fn
219
250
 
220
251
  return evaluate_decorator
221
252
 
222
- def query(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
253
+ def query(
254
+ self, mods: Optional[list[Mod]] = None
255
+ ) -> Callable[[ClientAppCallable], ClientAppCallable]:
223
256
  """Return a decorator that registers the query fn with the client app.
224
257
 
225
258
  Examples
226
259
  --------
260
+ Registering a query function:
261
+
227
262
  >>> app = ClientApp()
228
263
  >>>
229
264
  >>> @app.query()
@@ -231,6 +266,18 @@ class ClientApp:
231
266
  >>> print("ClientApp query running")
232
267
  >>> # Create and return an echo reply message
233
268
  >>> return message.create_reply(content=message.content())
269
+
270
+ Registering a query function with a function-specific modifier:
271
+
272
+ >>> from flwr.client.mod import message_size_mod
273
+ >>>
274
+ >>> app = ClientApp()
275
+ >>>
276
+ >>> @app.query(mods=[message_size_mod])
277
+ >>> def query(message: Message, context: Context) -> Message:
278
+ >>> print("ClientApp query running with message size mod")
279
+ >>> # Create and return an echo reply message
280
+ >>> return message.create_reply(content=message.content())
234
281
  """
235
282
 
236
283
  def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:
@@ -242,13 +289,77 @@ class ClientApp:
242
289
 
243
290
  # Register provided function with the ClientApp object
244
291
  # Wrap mods around the wrapped step function
245
- self._query = make_ffn(query_fn, self._mods)
292
+ self._query = make_ffn(query_fn, self._mods + (mods or []))
246
293
 
247
294
  # Return provided function unmodified
248
295
  return query_fn
249
296
 
250
297
  return query_decorator
251
298
 
299
+ def lifespan(
300
+ self,
301
+ ) -> Callable[
302
+ [Callable[[Context], Iterator[None]]], Callable[[Context], Iterator[None]]
303
+ ]:
304
+ """Return a decorator that registers the lifespan fn with the client app.
305
+
306
+ The decorated function should accept a `Context` object and use `yield`
307
+ to define enter and exit behavior.
308
+
309
+ Examples
310
+ --------
311
+ >>> app = ClientApp()
312
+ >>>
313
+ >>> @app.lifespan()
314
+ >>> def lifespan(context: Context) -> None:
315
+ >>> # Perform initialization tasks before the app starts
316
+ >>> print("Initializing ClientApp")
317
+ >>>
318
+ >>> yield # ClientApp is running
319
+ >>>
320
+ >>> # Perform cleanup tasks after the app stops
321
+ >>> print("Cleaning up ClientApp")
322
+ """
323
+
324
+ def lifespan_decorator(
325
+ lifespan_fn: Callable[[Context], Iterator[None]]
326
+ ) -> Callable[[Context], Iterator[None]]:
327
+ """Register the lifespan fn with the ServerApp object."""
328
+ warn_preview_feature("ClientApp-register-lifespan-function")
329
+
330
+ @contextmanager
331
+ def decorated_lifespan(context: Context) -> Iterator[None]:
332
+ # Execute the code before `yield` in lifespan_fn
333
+ try:
334
+ if not isinstance(it := lifespan_fn(context), Iterator):
335
+ raise StopIteration
336
+ next(it)
337
+ except StopIteration:
338
+ raise RuntimeError(
339
+ "lifespan function should yield at least once."
340
+ ) from None
341
+
342
+ try:
343
+ # Enter the context
344
+ yield
345
+ finally:
346
+ try:
347
+ # Execute the code after `yield` in lifespan_fn
348
+ next(it)
349
+ except StopIteration:
350
+ pass
351
+ else:
352
+ raise RuntimeError("lifespan function should only yield once.")
353
+
354
+ # Register provided function with the ClientApp object
355
+ # Ignore mypy error because of different argument names (`_` vs `context`)
356
+ self._lifespan = decorated_lifespan # type: ignore
357
+
358
+ # Return provided function unmodified
359
+ return lifespan_fn
360
+
361
+ return lifespan_decorator
362
+
252
363
 
253
364
  class LoadClientAppError(Exception):
254
365
  """Error when trying to load `ClientApp`."""
@@ -16,6 +16,7 @@
16
16
 
17
17
 
18
18
  import argparse
19
+ import gc
19
20
  import time
20
21
  from logging import DEBUG, ERROR, INFO
21
22
  from typing import Optional
@@ -160,6 +161,9 @@ def run_clientapp( # pylint: disable=R0914
160
161
  stub=stub, token=token, message=reply_message, context=context
161
162
  )
162
163
 
164
+ del client_app, message, context, run, fab, reply_message
165
+ gc.collect()
166
+
163
167
  # Reset token to `None` to prevent flwr-clientapp from trying to pull the
164
168
  # same inputs again
165
169
  token = None
@@ -82,7 +82,7 @@ def handle_control_message(message: Message) -> tuple[Optional[Message], int]:
82
82
  recordset = RecordSet()
83
83
  recordset.configs_records["config"] = ConfigsRecord({"reason": reason})
84
84
  out_message = message.create_reply(recordset)
85
- # Return TaskRes and sleep duration
85
+ # Return Message and sleep duration
86
86
  return out_message, sleep_duration
87
87
 
88
88
  # Any other message
@@ -66,9 +66,7 @@ except ModuleNotFoundError:
66
66
 
67
67
  PATH_CREATE_NODE: str = "api/v0/fleet/create-node"
68
68
  PATH_DELETE_NODE: str = "api/v0/fleet/delete-node"
69
- PATH_PULL_TASK_INS: str = "api/v0/fleet/pull-task-ins"
70
69
  PATH_PULL_MESSAGES: str = "/api/v0/fleet/pull-messages"
71
- PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res"
72
70
  PATH_PUSH_MESSAGES: str = "/api/v0/fleet/push-messages"
73
71
  PATH_PING: str = "api/v0/fleet/ping"
74
72
  PATH_GET_RUN: str = "/api/v0/fleet/get-run"
@@ -280,7 +278,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
280
278
  node = None
281
279
 
282
280
  def receive() -> Optional[Message]:
283
- """Receive next task from server."""
281
+ """Receive next Message from server."""
284
282
  # Get Node
285
283
  if node is None:
286
284
  log(ERROR, "Node instance missing")
@@ -309,11 +307,11 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
309
307
  if message_proto is not None:
310
308
  message = message_from_proto(message_proto)
311
309
  metadata = copy(message.metadata)
312
- log(INFO, "[Node] POST /%s: success", PATH_PULL_TASK_INS)
310
+ log(INFO, "[Node] POST /%s: success", PATH_PULL_MESSAGES)
313
311
  return message
314
312
 
315
313
  def send(message: Message) -> None:
316
- """Send task result back to server."""
314
+ """Send Message result back to server."""
317
315
  # Get Node
318
316
  if node is None:
319
317
  log(ERROR, "Node instance missing")
@@ -345,7 +343,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
345
343
  log(
346
344
  INFO,
347
345
  "[Node] POST /%s: success, created result %s",
348
- PATH_PUSH_TASK_RES,
346
+ PATH_PUSH_MESSAGES,
349
347
  res.results, # pylint: disable=no-member
350
348
  )
351
349
 
@@ -15,10 +15,8 @@
15
15
  """Flower SuperNode."""
16
16
 
17
17
 
18
- from .app import run_client_app as run_client_app
19
18
  from .app import run_supernode as run_supernode
20
19
 
21
20
  __all__ = [
22
- "run_client_app",
23
21
  "run_supernode",
24
22
  ]
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  import argparse
19
- from logging import DEBUG, ERROR, INFO, WARN
19
+ from logging import DEBUG, INFO, WARN
20
20
  from pathlib import Path
21
21
  from typing import Optional
22
22
 
@@ -98,16 +98,6 @@ def run_supernode() -> None:
98
98
  )
99
99
 
100
100
 
101
- def run_client_app() -> None:
102
- """Run Flower client app."""
103
- event(EventType.RUN_CLIENT_APP_ENTER)
104
- log(
105
- ERROR,
106
- "The command `flower-client-app` has been replaced by `flwr run`.",
107
- )
108
- register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
109
-
110
-
111
101
  def _parse_args_run_supernode() -> argparse.ArgumentParser:
112
102
  """Parse flower-supernode command line arguments."""
113
103
  parser = argparse.ArgumentParser(
flwr/common/address.py CHANGED
@@ -15,10 +15,13 @@
15
15
  """Flower IP address utils."""
16
16
 
17
17
 
18
+ import re
18
19
  import socket
19
20
  from ipaddress import ip_address
20
21
  from typing import Optional
21
22
 
23
+ import grpc
24
+
22
25
  IPV6: int = 6
23
26
 
24
27
 
@@ -101,3 +104,35 @@ def is_port_in_use(address: str) -> bool:
101
104
  return True
102
105
 
103
106
  return False
107
+
108
+
109
+ def get_ip_address_from_servicer_context(context: grpc.ServicerContext) -> str:
110
+ """Extract the client's IPv4 or IPv6 address from the gRPC ServicerContext.
111
+
112
+ Parameters
113
+ ----------
114
+ context : grpc.ServicerContext
115
+ The gRPC ServicerContext object. The context.peer() returns a string like
116
+ "ipv4:127.0.0.1:56789" for IPv4 and "ipv6:[2001:db8::1]:54321" for IPv6.
117
+
118
+ Returns
119
+ -------
120
+ str
121
+ If one of the format matches, the function will return the client's IP address,
122
+ otherwise, it will raise a ValueError.
123
+ """
124
+ peer: str = context.peer()
125
+ # Match IPv4: "ipv4:IP:port"
126
+ ipv4_match = re.match(r"^ipv4:(?P<ip>[^:]+):", peer)
127
+ if ipv4_match:
128
+ return ipv4_match.group("ip")
129
+
130
+ # Match IPv6: "ipv6:[IP]:port"
131
+ ipv6_match = re.match(r"^ipv6:\[(?P<ip>[^\]]+)\]:", peer)
132
+ if ipv6_match:
133
+ return ipv6_match.group("ip")
134
+
135
+ raise ValueError(
136
+ f"Unsupported peer address format: {peer} for the transport protocol. "
137
+ "The supported formats are ipv4:IP:port and ipv6:[IP]:port."
138
+ )
flwr/common/args.py CHANGED
@@ -43,7 +43,8 @@ def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
43
43
  "--insecure",
44
44
  action="store_true",
45
45
  help="Run the server without HTTPS, regardless of whether certificate "
46
- "paths are provided. By default, the server runs with HTTPS enabled. "
46
+ "paths are provided. Data transmitted between the gRPC client and server "
47
+ "is not encrypted. By default, the server runs with HTTPS enabled. "
47
48
  "Use this flag only if you understand the risks.",
48
49
  )
49
50
 
@@ -99,7 +100,12 @@ def try_obtain_server_certificates(
99
100
  ) -> Optional[tuple[bytes, bytes, bytes]]:
100
101
  """Validate and return the CA cert, server cert, and server private key."""
101
102
  if args.insecure:
102
- log(WARN, "Option `--insecure` was set. Starting insecure HTTP server.")
103
+ log(
104
+ WARN,
105
+ "Option `--insecure` was set. Starting insecure HTTP server with "
106
+ "unencrypted communication (TLS disabled). Proceed only if you understand "
107
+ "the risks.",
108
+ )
103
109
  return None
104
110
  # Check if certificates are provided
105
111
  if args.ssl_certfile and args.ssl_keyfile and args.ssl_ca_certfile:
@@ -20,6 +20,7 @@ from collections.abc import Sequence
20
20
  from pathlib import Path
21
21
  from typing import Optional, Union
22
22
 
23
+ from flwr.common.typing import UserInfo
23
24
  from flwr.proto.exec_pb2_grpc import ExecStub
24
25
 
25
26
  from ..typing import UserAuthCredentials, UserAuthLoginDetails
@@ -49,7 +50,7 @@ class ExecAuthPlugin(ABC):
49
50
  @abstractmethod
50
51
  def validate_tokens_in_metadata(
51
52
  self, metadata: Sequence[tuple[str, Union[str, bytes]]]
52
- ) -> bool:
53
+ ) -> tuple[bool, Optional[UserInfo]]:
53
54
  """Validate authentication tokens in the provided metadata."""
54
55
 
55
56
  @abstractmethod