flwr-nightly 1.20.0.dev20250617__py3-none-any.whl → 1.20.0.dev20250619__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/build.py CHANGED
@@ -25,7 +25,12 @@ import pathspec
25
25
  import tomli_w
26
26
  import typer
27
27
 
28
- from flwr.common.constant import FAB_ALLOWED_EXTENSIONS, FAB_DATE, FAB_HASH_TRUNCATION
28
+ from flwr.common.constant import (
29
+ FAB_ALLOWED_EXTENSIONS,
30
+ FAB_DATE,
31
+ FAB_HASH_TRUNCATION,
32
+ FAB_MAX_SIZE,
33
+ )
29
34
 
30
35
  from .config_utils import load as load_toml
31
36
  from .config_utils import load_and_validate
@@ -57,7 +62,7 @@ def build(
57
62
  Optional[Path],
58
63
  typer.Option(help="Path of the Flower App to bundle into a FAB"),
59
64
  ] = None,
60
- ) -> tuple[str, str]:
65
+ ) -> None:
61
66
  """Build a Flower App into a Flower App Bundle (FAB).
62
67
 
63
68
  You can run ``flwr build`` without any arguments to bundle the app located in the
@@ -119,8 +124,6 @@ def build(
119
124
  f"🎊 Successfully built {fab_filename}", fg=typer.colors.GREEN, bold=True
120
125
  )
121
126
 
122
- return fab_filename, fab_hash
123
-
124
127
 
125
128
  def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
126
129
  """Build a FAB in memory and return the bytes, hash, and config.
@@ -193,6 +196,13 @@ def build_fab(app: Path) -> tuple[bytes, str, dict[str, Any]]:
193
196
  write_to_zip(fab_file, ".info/CONTENT", list_file_content)
194
197
 
195
198
  fab_bytes = fab_buffer.getvalue()
199
+ if len(fab_bytes) > FAB_MAX_SIZE:
200
+ raise ValueError(
201
+ f"FAB size exceeds maximum allowed size of {FAB_MAX_SIZE:,} bytes."
202
+ "To reduce the package size, consider ignoring unnecessary files "
203
+ "via your `.gitignore` file or excluding them from the build."
204
+ )
205
+
196
206
  fab_hash = hashlib.sha256(fab_bytes).hexdigest()
197
207
 
198
208
  return fab_bytes, fab_hash, config
@@ -17,7 +17,6 @@
17
17
 
18
18
  from collections.abc import Iterator, Sequence
19
19
  from contextlib import contextmanager
20
- from copy import copy
21
20
  from logging import DEBUG, ERROR
22
21
  from pathlib import Path
23
22
  from typing import Callable, Optional, Union, cast
@@ -25,8 +24,6 @@ from typing import Callable, Optional, Union, cast
25
24
  import grpc
26
25
  from cryptography.hazmat.primitives.asymmetric import ec
27
26
 
28
- from flwr.app.metadata import Metadata
29
- from flwr.client.message_handler.message_handler import validate_out_message
30
27
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
31
28
  from flwr.common.constant import HEARTBEAT_CALL_TIMEOUT, HEARTBEAT_DEFAULT_INTERVAL
32
29
  from flwr.common.grpc import create_channel, on_channel_state_change
@@ -163,7 +160,6 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
163
160
  if adapter_cls is None:
164
161
  adapter_cls = FleetStub
165
162
  stub = adapter_cls(channel)
166
- metadata: Optional[Metadata] = None
167
163
  node: Optional[Node] = None
168
164
 
169
165
  def _should_giveup_fn(e: Exception) -> bool:
@@ -300,10 +296,6 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
300
296
  # Inject
301
297
  in_message.metadata.__dict__["_message_id"] = msg_id
302
298
 
303
- # Remember `metadata` of the in message
304
- nonlocal metadata
305
- metadata = copy(in_message.metadata) if in_message else None
306
-
307
299
  # Return the message if available
308
300
  return in_message
309
301
 
@@ -314,19 +306,6 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
314
306
  log(ERROR, "Node instance missing")
315
307
  return
316
308
 
317
- # Get the metadata of the incoming message
318
- nonlocal metadata
319
- if metadata is None:
320
- log(ERROR, "No current message")
321
- return
322
-
323
- # Set message_id
324
- message.metadata.__dict__["_message_id"] = message.object_id
325
- # Validate out message
326
- if not validate_out_message(message, metadata):
327
- log(ERROR, "Invalid out message")
328
- return
329
-
330
309
  with no_object_id_recompute():
331
310
  # Get all nested objects
332
311
  all_objects = get_all_nested_objects(message)
@@ -358,9 +337,6 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
358
337
  )
359
338
  log(DEBUG, "Pushed %s objects to servicer.", len(objs_to_push))
360
339
 
361
- # Cleanup
362
- metadata = None
363
-
364
340
  def get_run(run_id: int) -> Run:
365
341
  # Call FleetAPI
366
342
  get_run_request = GetRunRequest(node=node, run_id=run_id)
@@ -16,7 +16,6 @@
16
16
 
17
17
  from collections.abc import Iterator
18
18
  from contextlib import contextmanager
19
- from copy import copy
20
19
  from logging import DEBUG, ERROR, INFO, WARN
21
20
  from typing import Callable, Optional, TypeVar, Union, cast
22
21
 
@@ -24,8 +23,6 @@ from cryptography.hazmat.primitives.asymmetric import ec
24
23
  from google.protobuf.message import Message as GrpcMessage
25
24
  from requests.exceptions import ConnectionError as RequestsConnectionError
26
25
 
27
- from flwr.app.metadata import Metadata
28
- from flwr.client.message_handler.message_handler import validate_out_message
29
26
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH
30
27
  from flwr.common.constant import HEARTBEAT_DEFAULT_INTERVAL
31
28
  from flwr.common.exit import ExitCode, flwr_exit
@@ -178,7 +175,6 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
178
175
  log(ERROR, "Client authentication is not supported for this transport type.")
179
176
 
180
177
  # Shared variables for inner functions
181
- metadata: Optional[Metadata] = None
182
178
  node: Optional[Node] = None
183
179
 
184
180
  ###########################################################################
@@ -367,10 +363,6 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
367
363
  # Inject
368
364
  in_message.metadata.__dict__["_message_id"] = msg_id
369
365
 
370
- # Remember `metadata` of the in message
371
- nonlocal metadata
372
- metadata = copy(in_message.metadata) if in_message else None
373
-
374
366
  return in_message
375
367
 
376
368
  def send(message: Message) -> None:
@@ -380,19 +372,6 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
380
372
  log(ERROR, "Node instance missing")
381
373
  return
382
374
 
383
- # Get incoming message
384
- nonlocal metadata
385
- if metadata is None:
386
- log(ERROR, "No current message")
387
- return
388
-
389
- # Set message_id
390
- message.metadata.__dict__["_message_id"] = message.object_id
391
- # Validate out message
392
- if not validate_out_message(message, metadata):
393
- log(ERROR, "Invalid out message")
394
- return
395
-
396
375
  with no_object_id_recompute():
397
376
  # Get all nested objects
398
377
  all_objects = get_all_nested_objects(message)
@@ -450,9 +429,6 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
450
429
  )
451
430
  log(ERROR, str(e))
452
431
 
453
- # Cleanup
454
- metadata = None
455
-
456
432
  def get_run(run_id: int) -> Run:
457
433
  # Construct the request
458
434
  req = GetRunRequest(node=node, run_id=run_id)
flwr/common/constant.py CHANGED
@@ -74,6 +74,7 @@ FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
74
74
  FAB_CONFIG_FILE = "pyproject.toml"
75
75
  FAB_DATE = (2024, 10, 1, 0, 0, 0)
76
76
  FAB_HASH_TRUNCATION = 8
77
+ FAB_MAX_SIZE = 10 * 1024 * 1024 # 10 MB
77
78
  FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
78
79
  FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
79
80
 
@@ -17,29 +17,33 @@ from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
17
17
  from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2
18
18
 
19
19
 
20
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x11\n\x0fGetTokenRequest\"!\n\x10GetTokenResponse\x12\r\n\x05token\x18\x01 \x01(\x04\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x04\"\xa5\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\x12\x1c\n\x03\x66\x61\x62\x18\x04 \x01(\x0b\x32\x0f.flwr.proto.Fab\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\x04\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xad\x02\n\x0b\x43lientAppIo\x12G\n\x08GetToken\x12\x1b.flwr.proto.GetTokenRequest\x1a\x1c.flwr.proto.GetTokenResponse\"\x00\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3')
20
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"%\n#GetRunIdsWithPendingMessagesRequest\"7\n$GetRunIdsWithPendingMessagesResponse\x12\x0f\n\x07run_ids\x18\x01 \x03(\x04\"%\n\x13RequestTokenRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"%\n\x14RequestTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\t\"\xa5\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\x12\x1c\n\x03\x66\x61\x62\x18\x04 \x01(\x0b\x32\x0f.flwr.proto.Fab\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\t\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xbf\x03\n\x0b\x43lientAppIo\x12\x83\x01\n\x1cGetRunIdsWithPendingMessages\x12/.flwr.proto.GetRunIdsWithPendingMessagesRequest\x1a\x30.flwr.proto.GetRunIdsWithPendingMessagesResponse\"\x00\x12S\n\x0cRequestToken\x12\x1f.flwr.proto.RequestTokenRequest\x1a .flwr.proto.RequestTokenResponse\"\x00\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3')
21
21
 
22
22
  _globals = globals()
23
23
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
24
24
  _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.clientappio_pb2', _globals)
25
25
  if _descriptor._USE_C_DESCRIPTORS == False:
26
26
  DESCRIPTOR._options = None
27
- _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=675
28
- _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=751
29
- _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_start=114
30
- _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_end=201
31
- _globals['_GETTOKENREQUEST']._serialized_start=203
32
- _globals['_GETTOKENREQUEST']._serialized_end=220
33
- _globals['_GETTOKENRESPONSE']._serialized_start=222
34
- _globals['_GETTOKENRESPONSE']._serialized_end=255
35
- _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_start=257
36
- _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_end=300
37
- _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_start=303
38
- _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=468
39
- _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=470
40
- _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=590
41
- _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=592
42
- _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=673
43
- _globals['_CLIENTAPPIO']._serialized_start=754
44
- _globals['_CLIENTAPPIO']._serialized_end=1055
27
+ _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=795
28
+ _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=871
29
+ _globals['_GETRUNIDSWITHPENDINGMESSAGESREQUEST']._serialized_start=114
30
+ _globals['_GETRUNIDSWITHPENDINGMESSAGESREQUEST']._serialized_end=151
31
+ _globals['_GETRUNIDSWITHPENDINGMESSAGESRESPONSE']._serialized_start=153
32
+ _globals['_GETRUNIDSWITHPENDINGMESSAGESRESPONSE']._serialized_end=208
33
+ _globals['_REQUESTTOKENREQUEST']._serialized_start=210
34
+ _globals['_REQUESTTOKENREQUEST']._serialized_end=247
35
+ _globals['_REQUESTTOKENRESPONSE']._serialized_start=249
36
+ _globals['_REQUESTTOKENRESPONSE']._serialized_end=286
37
+ _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_start=288
38
+ _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_end=375
39
+ _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_start=377
40
+ _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_end=420
41
+ _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_start=423
42
+ _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=588
43
+ _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=590
44
+ _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=710
45
+ _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=712
46
+ _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=793
47
+ _globals['_CLIENTAPPIO']._serialized_start=874
48
+ _globals['_CLIENTAPPIO']._serialized_end=1321
45
49
  # @@protoc_insertion_point(module_scope)
@@ -7,6 +7,7 @@ import flwr.proto.fab_pb2
7
7
  import flwr.proto.message_pb2
8
8
  import flwr.proto.run_pb2
9
9
  import google.protobuf.descriptor
10
+ import google.protobuf.internal.containers
10
11
  import google.protobuf.internal.enum_type_wrapper
11
12
  import google.protobuf.message
12
13
  import typing
@@ -31,44 +32,69 @@ UNKNOWN_ERROR: ClientAppOutputCode.ValueType # 2
31
32
  global___ClientAppOutputCode = ClientAppOutputCode
32
33
 
33
34
 
34
- class ClientAppOutputStatus(google.protobuf.message.Message):
35
+ class GetRunIdsWithPendingMessagesRequest(google.protobuf.message.Message):
35
36
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
36
- CODE_FIELD_NUMBER: builtins.int
37
- MESSAGE_FIELD_NUMBER: builtins.int
38
- code: global___ClientAppOutputCode.ValueType
39
- message: typing.Text
37
+ def __init__(self,
38
+ ) -> None: ...
39
+ global___GetRunIdsWithPendingMessagesRequest = GetRunIdsWithPendingMessagesRequest
40
+
41
+ class GetRunIdsWithPendingMessagesResponse(google.protobuf.message.Message):
42
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
43
+ RUN_IDS_FIELD_NUMBER: builtins.int
44
+ @property
45
+ def run_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
46
+ """List of run IDs"""
47
+ pass
40
48
  def __init__(self,
41
49
  *,
42
- code: global___ClientAppOutputCode.ValueType = ...,
43
- message: typing.Text = ...,
50
+ run_ids: typing.Optional[typing.Iterable[builtins.int]] = ...,
44
51
  ) -> None: ...
45
- def ClearField(self, field_name: typing_extensions.Literal["code",b"code","message",b"message"]) -> None: ...
46
- global___ClientAppOutputStatus = ClientAppOutputStatus
52
+ def ClearField(self, field_name: typing_extensions.Literal["run_ids",b"run_ids"]) -> None: ...
53
+ global___GetRunIdsWithPendingMessagesResponse = GetRunIdsWithPendingMessagesResponse
47
54
 
48
- class GetTokenRequest(google.protobuf.message.Message):
55
+ class RequestTokenRequest(google.protobuf.message.Message):
49
56
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
57
+ RUN_ID_FIELD_NUMBER: builtins.int
58
+ run_id: builtins.int
50
59
  def __init__(self,
60
+ *,
61
+ run_id: builtins.int = ...,
51
62
  ) -> None: ...
52
- global___GetTokenRequest = GetTokenRequest
63
+ def ClearField(self, field_name: typing_extensions.Literal["run_id",b"run_id"]) -> None: ...
64
+ global___RequestTokenRequest = RequestTokenRequest
53
65
 
54
- class GetTokenResponse(google.protobuf.message.Message):
66
+ class RequestTokenResponse(google.protobuf.message.Message):
55
67
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
56
68
  TOKEN_FIELD_NUMBER: builtins.int
57
- token: builtins.int
69
+ token: typing.Text
58
70
  def __init__(self,
59
71
  *,
60
- token: builtins.int = ...,
72
+ token: typing.Text = ...,
61
73
  ) -> None: ...
62
74
  def ClearField(self, field_name: typing_extensions.Literal["token",b"token"]) -> None: ...
63
- global___GetTokenResponse = GetTokenResponse
75
+ global___RequestTokenResponse = RequestTokenResponse
76
+
77
+ class ClientAppOutputStatus(google.protobuf.message.Message):
78
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
79
+ CODE_FIELD_NUMBER: builtins.int
80
+ MESSAGE_FIELD_NUMBER: builtins.int
81
+ code: global___ClientAppOutputCode.ValueType
82
+ message: typing.Text
83
+ def __init__(self,
84
+ *,
85
+ code: global___ClientAppOutputCode.ValueType = ...,
86
+ message: typing.Text = ...,
87
+ ) -> None: ...
88
+ def ClearField(self, field_name: typing_extensions.Literal["code",b"code","message",b"message"]) -> None: ...
89
+ global___ClientAppOutputStatus = ClientAppOutputStatus
64
90
 
65
91
  class PullClientAppInputsRequest(google.protobuf.message.Message):
66
92
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
67
93
  TOKEN_FIELD_NUMBER: builtins.int
68
- token: builtins.int
94
+ token: typing.Text
69
95
  def __init__(self,
70
96
  *,
71
- token: builtins.int = ...,
97
+ token: typing.Text = ...,
72
98
  ) -> None: ...
73
99
  def ClearField(self, field_name: typing_extensions.Literal["token",b"token"]) -> None: ...
74
100
  global___PullClientAppInputsRequest = PullClientAppInputsRequest
@@ -103,14 +129,14 @@ class PushClientAppOutputsRequest(google.protobuf.message.Message):
103
129
  TOKEN_FIELD_NUMBER: builtins.int
104
130
  MESSAGE_FIELD_NUMBER: builtins.int
105
131
  CONTEXT_FIELD_NUMBER: builtins.int
106
- token: builtins.int
132
+ token: typing.Text
107
133
  @property
108
134
  def message(self) -> flwr.proto.message_pb2.Message: ...
109
135
  @property
110
136
  def context(self) -> flwr.proto.message_pb2.Context: ...
111
137
  def __init__(self,
112
138
  *,
113
- token: builtins.int = ...,
139
+ token: typing.Text = ...,
114
140
  message: typing.Optional[flwr.proto.message_pb2.Message] = ...,
115
141
  context: typing.Optional[flwr.proto.message_pb2.Context] = ...,
116
142
  ) -> None: ...
@@ -14,10 +14,15 @@ class ClientAppIoStub(object):
14
14
  Args:
15
15
  channel: A grpc.Channel.
16
16
  """
17
- self.GetToken = channel.unary_unary(
18
- '/flwr.proto.ClientAppIo/GetToken',
19
- request_serializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenRequest.SerializeToString,
20
- response_deserializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenResponse.FromString,
17
+ self.GetRunIdsWithPendingMessages = channel.unary_unary(
18
+ '/flwr.proto.ClientAppIo/GetRunIdsWithPendingMessages',
19
+ request_serializer=flwr_dot_proto_dot_clientappio__pb2.GetRunIdsWithPendingMessagesRequest.SerializeToString,
20
+ response_deserializer=flwr_dot_proto_dot_clientappio__pb2.GetRunIdsWithPendingMessagesResponse.FromString,
21
+ )
22
+ self.RequestToken = channel.unary_unary(
23
+ '/flwr.proto.ClientAppIo/RequestToken',
24
+ request_serializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.SerializeToString,
25
+ response_deserializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.FromString,
21
26
  )
22
27
  self.PullClientAppInputs = channel.unary_unary(
23
28
  '/flwr.proto.ClientAppIo/PullClientAppInputs',
@@ -34,22 +39,29 @@ class ClientAppIoStub(object):
34
39
  class ClientAppIoServicer(object):
35
40
  """Missing associated documentation comment in .proto file."""
36
41
 
37
- def GetToken(self, request, context):
38
- """Get token
42
+ def GetRunIdsWithPendingMessages(self, request, context):
43
+ """Get run IDs with pending messages
44
+ """
45
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
46
+ context.set_details('Method not implemented!')
47
+ raise NotImplementedError('Method not implemented!')
48
+
49
+ def RequestToken(self, request, context):
50
+ """Request token
39
51
  """
40
52
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
41
53
  context.set_details('Method not implemented!')
42
54
  raise NotImplementedError('Method not implemented!')
43
55
 
44
56
  def PullClientAppInputs(self, request, context):
45
- """Get Message, Context, and Run
57
+ """Pull client app inputs
46
58
  """
47
59
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
48
60
  context.set_details('Method not implemented!')
49
61
  raise NotImplementedError('Method not implemented!')
50
62
 
51
63
  def PushClientAppOutputs(self, request, context):
52
- """Send updated Message and Context
64
+ """Push client app outputs
53
65
  """
54
66
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
55
67
  context.set_details('Method not implemented!')
@@ -58,10 +70,15 @@ class ClientAppIoServicer(object):
58
70
 
59
71
  def add_ClientAppIoServicer_to_server(servicer, server):
60
72
  rpc_method_handlers = {
61
- 'GetToken': grpc.unary_unary_rpc_method_handler(
62
- servicer.GetToken,
63
- request_deserializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenRequest.FromString,
64
- response_serializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenResponse.SerializeToString,
73
+ 'GetRunIdsWithPendingMessages': grpc.unary_unary_rpc_method_handler(
74
+ servicer.GetRunIdsWithPendingMessages,
75
+ request_deserializer=flwr_dot_proto_dot_clientappio__pb2.GetRunIdsWithPendingMessagesRequest.FromString,
76
+ response_serializer=flwr_dot_proto_dot_clientappio__pb2.GetRunIdsWithPendingMessagesResponse.SerializeToString,
77
+ ),
78
+ 'RequestToken': grpc.unary_unary_rpc_method_handler(
79
+ servicer.RequestToken,
80
+ request_deserializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.FromString,
81
+ response_serializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.SerializeToString,
65
82
  ),
66
83
  'PullClientAppInputs': grpc.unary_unary_rpc_method_handler(
67
84
  servicer.PullClientAppInputs,
@@ -84,7 +101,24 @@ class ClientAppIo(object):
84
101
  """Missing associated documentation comment in .proto file."""
85
102
 
86
103
  @staticmethod
87
- def GetToken(request,
104
+ def GetRunIdsWithPendingMessages(request,
105
+ target,
106
+ options=(),
107
+ channel_credentials=None,
108
+ call_credentials=None,
109
+ insecure=False,
110
+ compression=None,
111
+ wait_for_ready=None,
112
+ timeout=None,
113
+ metadata=None):
114
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/GetRunIdsWithPendingMessages',
115
+ flwr_dot_proto_dot_clientappio__pb2.GetRunIdsWithPendingMessagesRequest.SerializeToString,
116
+ flwr_dot_proto_dot_clientappio__pb2.GetRunIdsWithPendingMessagesResponse.FromString,
117
+ options, channel_credentials,
118
+ insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
119
+
120
+ @staticmethod
121
+ def RequestToken(request,
88
122
  target,
89
123
  options=(),
90
124
  channel_credentials=None,
@@ -94,9 +128,9 @@ class ClientAppIo(object):
94
128
  wait_for_ready=None,
95
129
  timeout=None,
96
130
  metadata=None):
97
- return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/GetToken',
98
- flwr_dot_proto_dot_clientappio__pb2.GetTokenRequest.SerializeToString,
99
- flwr_dot_proto_dot_clientappio__pb2.GetTokenResponse.FromString,
131
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/RequestToken',
132
+ flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.SerializeToString,
133
+ flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.FromString,
100
134
  options, channel_credentials,
101
135
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
102
136
 
@@ -8,29 +8,42 @@ import grpc
8
8
 
9
9
  class ClientAppIoStub:
10
10
  def __init__(self, channel: grpc.Channel) -> None: ...
11
- GetToken: grpc.UnaryUnaryMultiCallable[
12
- flwr.proto.clientappio_pb2.GetTokenRequest,
13
- flwr.proto.clientappio_pb2.GetTokenResponse]
14
- """Get token"""
11
+ GetRunIdsWithPendingMessages: grpc.UnaryUnaryMultiCallable[
12
+ flwr.proto.clientappio_pb2.GetRunIdsWithPendingMessagesRequest,
13
+ flwr.proto.clientappio_pb2.GetRunIdsWithPendingMessagesResponse]
14
+ """Get run IDs with pending messages"""
15
+
16
+ RequestToken: grpc.UnaryUnaryMultiCallable[
17
+ flwr.proto.clientappio_pb2.RequestTokenRequest,
18
+ flwr.proto.clientappio_pb2.RequestTokenResponse]
19
+ """Request token"""
15
20
 
16
21
  PullClientAppInputs: grpc.UnaryUnaryMultiCallable[
17
22
  flwr.proto.clientappio_pb2.PullClientAppInputsRequest,
18
23
  flwr.proto.clientappio_pb2.PullClientAppInputsResponse]
19
- """Get Message, Context, and Run"""
24
+ """Pull client app inputs"""
20
25
 
21
26
  PushClientAppOutputs: grpc.UnaryUnaryMultiCallable[
22
27
  flwr.proto.clientappio_pb2.PushClientAppOutputsRequest,
23
28
  flwr.proto.clientappio_pb2.PushClientAppOutputsResponse]
24
- """Send updated Message and Context"""
29
+ """Push client app outputs"""
25
30
 
26
31
 
27
32
  class ClientAppIoServicer(metaclass=abc.ABCMeta):
28
33
  @abc.abstractmethod
29
- def GetToken(self,
30
- request: flwr.proto.clientappio_pb2.GetTokenRequest,
34
+ def GetRunIdsWithPendingMessages(self,
35
+ request: flwr.proto.clientappio_pb2.GetRunIdsWithPendingMessagesRequest,
36
+ context: grpc.ServicerContext,
37
+ ) -> flwr.proto.clientappio_pb2.GetRunIdsWithPendingMessagesResponse:
38
+ """Get run IDs with pending messages"""
39
+ pass
40
+
41
+ @abc.abstractmethod
42
+ def RequestToken(self,
43
+ request: flwr.proto.clientappio_pb2.RequestTokenRequest,
31
44
  context: grpc.ServicerContext,
32
- ) -> flwr.proto.clientappio_pb2.GetTokenResponse:
33
- """Get token"""
45
+ ) -> flwr.proto.clientappio_pb2.RequestTokenResponse:
46
+ """Request token"""
34
47
  pass
35
48
 
36
49
  @abc.abstractmethod
@@ -38,7 +51,7 @@ class ClientAppIoServicer(metaclass=abc.ABCMeta):
38
51
  request: flwr.proto.clientappio_pb2.PullClientAppInputsRequest,
39
52
  context: grpc.ServicerContext,
40
53
  ) -> flwr.proto.clientappio_pb2.PullClientAppInputsResponse:
41
- """Get Message, Context, and Run"""
54
+ """Pull client app inputs"""
42
55
  pass
43
56
 
44
57
  @abc.abstractmethod
@@ -46,7 +59,7 @@ class ClientAppIoServicer(metaclass=abc.ABCMeta):
46
59
  request: flwr.proto.clientappio_pb2.PushClientAppOutputsRequest,
47
60
  context: grpc.ServicerContext,
48
61
  ) -> flwr.proto.clientappio_pb2.PushClientAppOutputsResponse:
49
- """Send updated Message and Context"""
62
+ """Push client app outputs"""
50
63
  pass
51
64
 
52
65
 
@@ -0,0 +1,32 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Utility functions for the infrastructure."""
16
+
17
+
18
+ def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
19
+ """Mask a string by preserving only the head and tail characters.
20
+
21
+ Mask a string for safe display by preserving the head and tail characters,
22
+ and replacing the middle with '...'. Useful for logging tokens, secrets,
23
+ or IDs without exposing sensitive data.
24
+
25
+ Notes
26
+ -----
27
+ If the string is shorter than the combined length of `head` and `tail`,
28
+ the original string is returned unchanged.
29
+ """
30
+ if len(value) <= head + tail:
31
+ return value
32
+ return f"{value[:head]}...{value[-tail:]}"
@@ -25,6 +25,7 @@ import grpc
25
25
  from flwr.common import now
26
26
  from flwr.common.auth_plugin import ExecAuthPlugin
27
27
  from flwr.common.constant import (
28
+ FAB_MAX_SIZE,
28
29
  LOG_STREAM_INTERVAL,
29
30
  RUN_ID_NOT_FOUND_MESSAGE,
30
31
  Status,
@@ -84,6 +85,14 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
84
85
  """Create run ID."""
85
86
  log(INFO, "ExecServicer.StartRun")
86
87
 
88
+ if len(request.fab.content) > FAB_MAX_SIZE:
89
+ log(
90
+ ERROR,
91
+ "FAB size exceeds maximum allowed size of %d bytes.",
92
+ FAB_MAX_SIZE,
93
+ )
94
+ return StartRunResponse()
95
+
87
96
  flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
88
97
  run_id = self.executor.start_run(
89
98
  request.fab.content,
@@ -40,7 +40,6 @@ from flwr.common.constant import (
40
40
  TRANSPORT_TYPE_REST,
41
41
  )
42
42
  from flwr.common.exit import ExitCode, flwr_exit
43
- from flwr.common.exit_handlers import register_exit_handlers
44
43
  from flwr.common.logger import log
45
44
  from flwr.supernode.start_client_internal import start_client_internal
46
45
 
@@ -66,12 +65,6 @@ def flower_supernode() -> None:
66
65
 
67
66
  log(DEBUG, "Isolation mode: %s", args.isolation)
68
67
 
69
- # Register handlers for graceful shutdown
70
- register_exit_handlers(
71
- event_type=EventType.RUN_SUPERNODE_LEAVE,
72
- exit_message="SuperNode terminated gracefully.",
73
- )
74
-
75
68
  start_client_internal(
76
69
  server_address=args.superlink,
77
70
  transport=args.transport,
@@ -22,6 +22,7 @@ from flwr.common.args import add_args_flwr_app_common
22
22
  from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
23
23
  from flwr.common.exit import ExitCode, flwr_exit
24
24
  from flwr.common.logger import log
25
+ from flwr.supercore.utils import mask_string
25
26
  from flwr.supernode.runtime.run_clientapp import run_clientapp
26
27
 
27
28
 
@@ -40,11 +41,11 @@ def flwr_clientapp() -> None:
40
41
  "`flwr-clientapp` will attempt to connect to SuperNode's "
41
42
  "ClientAppIo API at %s with token %s",
42
43
  args.clientappio_api_address,
43
- args.token,
44
+ mask_string(args.token) if args.token else "None",
44
45
  )
45
46
  run_clientapp(
46
47
  clientappio_api_address=args.clientappio_api_address,
47
- run_once=(args.token is not None),
48
+ run_once=(args.token is not None) or args.run_once,
48
49
  token=args.token,
49
50
  flwr_dir=args.flwr_dir,
50
51
  certificates=None,
@@ -66,7 +67,7 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
66
67
  )
67
68
  parser.add_argument(
68
69
  "--token",
69
- type=int,
70
+ type=str,
70
71
  required=False,
71
72
  help="Unique token generated by SuperNode for each ClientApp execution",
72
73
  )
@@ -77,5 +78,11 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
77
78
  help="The PID of the parent process. When set, the process will terminate "
78
79
  "when the parent process exits.",
79
80
  )
81
+ parser.add_argument(
82
+ "--run-once",
83
+ action="store_true",
84
+ help="When set, this process will start a single ClientApp for a pending "
85
+ "message. If there is no pending message, the process will exit.",
86
+ )
80
87
  add_args_flwr_app_common(parser=parser)
81
88
  return parser