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.
@@ -46,20 +46,23 @@ from flwr.common.typing import Fab, Run
46
46
 
47
47
  # pylint: disable=E0611
48
48
  from flwr.proto.clientappio_pb2 import (
49
- GetTokenRequest,
50
- GetTokenResponse,
49
+ GetRunIdsWithPendingMessagesRequest,
50
+ GetRunIdsWithPendingMessagesResponse,
51
51
  PullClientAppInputsRequest,
52
52
  PullClientAppInputsResponse,
53
53
  PushClientAppOutputsRequest,
54
54
  PushClientAppOutputsResponse,
55
+ RequestTokenRequest,
56
+ RequestTokenResponse,
55
57
  )
56
58
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
59
+ from flwr.supercore.utils import mask_string
57
60
 
58
61
 
59
62
  def run_clientapp( # pylint: disable=R0913, R0914, R0917
60
63
  clientappio_api_address: str,
61
64
  run_once: bool,
62
- token: Optional[int] = None,
65
+ token: Optional[str] = None,
63
66
  flwr_dir: Optional[str] = None,
64
67
  certificates: Optional[bytes] = None,
65
68
  parent_pid: Optional[int] = None,
@@ -84,9 +87,8 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
84
87
 
85
88
  while True:
86
89
  # If token is not set, loop until token is received from SuperNode
87
- while token is None:
90
+ if token is None:
88
91
  token = get_token(stub)
89
- time.sleep(1)
90
92
 
91
93
  # Pull Message, Context, Run and (optional) FAB from SuperNode
92
94
  message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
@@ -172,26 +174,30 @@ def start_parent_process_monitor(
172
174
  threading.Thread(target=monitor, daemon=True).start()
173
175
 
174
176
 
175
- def get_token(stub: grpc.Channel) -> Optional[int]:
177
+ def get_token(stub: ClientAppIoStub) -> str:
176
178
  """Get a token from SuperNode."""
177
179
  log(DEBUG, "[flwr-clientapp] Request token")
178
- try:
179
- res: GetTokenResponse = stub.GetToken(GetTokenRequest())
180
- log(DEBUG, "[GetToken] Received token: %s", res.token)
181
- return res.token
182
- except grpc.RpcError as e:
183
- if e.code() == grpc.StatusCode.FAILED_PRECONDITION: # pylint: disable=no-member
184
- log(DEBUG, "[GetToken] No token available yet")
185
- else:
186
- log(ERROR, "[GetToken] gRPC error occurred: %s", str(e))
187
- return None
180
+ while True:
181
+ res: GetRunIdsWithPendingMessagesResponse = stub.GetRunIdsWithPendingMessages(
182
+ GetRunIdsWithPendingMessagesRequest()
183
+ )
184
+
185
+ for run_id in res.run_ids:
186
+ tk_res: RequestTokenResponse = stub.RequestToken(
187
+ RequestTokenRequest(run_id=run_id)
188
+ )
189
+ if tk_res.token:
190
+ return tk_res.token
191
+
192
+ time.sleep(1) # Wait before retrying to get run IDs
188
193
 
189
194
 
190
195
  def pull_clientappinputs(
191
- stub: grpc.Channel, token: int
196
+ stub: ClientAppIoStub, token: str
192
197
  ) -> tuple[Message, Context, Run, Optional[Fab]]:
193
198
  """Pull ClientAppInputs from SuperNode."""
194
- log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", token)
199
+ masked_token = mask_string(token)
200
+ log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", masked_token)
195
201
  try:
196
202
  res: PullClientAppInputsResponse = stub.PullClientAppInputs(
197
203
  PullClientAppInputsRequest(token=token)
@@ -207,10 +213,13 @@ def pull_clientappinputs(
207
213
 
208
214
 
209
215
  def push_clientappoutputs(
210
- stub: grpc.Channel, token: int, message: Message, context: Context
216
+ stub: ClientAppIoStub, token: str, message: Message, context: Context
211
217
  ) -> PushClientAppOutputsResponse:
212
218
  """Push ClientAppOutputs to SuperNode."""
213
- log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", token)
219
+ masked_token = mask_string(token)
220
+ log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", masked_token)
221
+ # Set message ID
222
+ message.metadata.__dict__["_message_id"] = message.object_id
214
223
  proto_message = message_to_proto(message)
215
224
  proto_context = context_to_proto(context)
216
225
 
@@ -15,10 +15,8 @@
15
15
  """ClientAppIo API Servicer."""
16
16
 
17
17
 
18
- from .clientappio_servicer import ClientAppInputs, ClientAppIoServicer, ClientAppOutputs
18
+ from .clientappio_servicer import ClientAppIoServicer
19
19
 
20
20
  __all__ = [
21
- "ClientAppInputs",
22
21
  "ClientAppIoServicer",
23
- "ClientAppOutputs",
24
22
  ]
@@ -15,16 +15,14 @@
15
15
  """ClientAppIo API servicer."""
16
16
 
17
17
 
18
- from dataclasses import dataclass
19
- from logging import DEBUG, ERROR
20
- from typing import Optional, cast
18
+ from logging import DEBUG
19
+ from typing import cast
21
20
 
22
21
  import grpc
23
22
 
24
- from flwr.common import Context, Message, typing
23
+ from flwr.common import Context
25
24
  from flwr.common.logger import log
26
25
  from flwr.common.serde import (
27
- clientappstatus_to_proto,
28
26
  context_from_proto,
29
27
  context_to_proto,
30
28
  fab_to_proto,
@@ -37,71 +35,70 @@ from flwr.common.typing import Fab, Run
37
35
  # pylint: disable=E0611
38
36
  from flwr.proto import clientappio_pb2_grpc
39
37
  from flwr.proto.clientappio_pb2 import ( # pylint: disable=E0401
40
- GetTokenRequest,
41
- GetTokenResponse,
38
+ GetRunIdsWithPendingMessagesRequest,
39
+ GetRunIdsWithPendingMessagesResponse,
42
40
  PullClientAppInputsRequest,
43
41
  PullClientAppInputsResponse,
44
42
  PushClientAppOutputsRequest,
45
43
  PushClientAppOutputsResponse,
44
+ RequestTokenRequest,
45
+ RequestTokenResponse,
46
46
  )
47
+ from flwr.supercore.ffs import FfsFactory
48
+ from flwr.supercore.object_store import ObjectStoreFactory
49
+ from flwr.supernode.nodestate import NodeStateFactory
47
50
 
48
51
 
49
- @dataclass
50
- class ClientAppInputs:
51
- """Specify the inputs to the ClientApp."""
52
-
53
- message: Message
54
- context: Context
55
- run: Run
56
- fab: Optional[Fab]
57
- token: int
58
-
52
+ # pylint: disable=C0103,W0613,W0201
53
+ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
54
+ """ClientAppIo API servicer."""
59
55
 
60
- @dataclass
61
- class ClientAppOutputs:
62
- """Specify the outputs from the ClientApp."""
56
+ def __init__(
57
+ self,
58
+ state_factory: NodeStateFactory,
59
+ ffs_factory: FfsFactory,
60
+ objectstore_factory: ObjectStoreFactory,
61
+ ) -> None:
62
+ self.state_factory = state_factory
63
+ self.ffs_factory = ffs_factory
64
+ self.objectstore_factory = objectstore_factory
63
65
 
64
- message: Message
65
- context: Context
66
+ def GetRunIdsWithPendingMessages(
67
+ self,
68
+ request: GetRunIdsWithPendingMessagesRequest,
69
+ context: grpc.ServicerContext,
70
+ ) -> GetRunIdsWithPendingMessagesResponse:
71
+ """Get run IDs with pending messages."""
72
+ log(DEBUG, "ClientAppIo.GetRunIdsWithPendingMessages")
66
73
 
74
+ # Initialize state connection
75
+ state = self.state_factory.state()
67
76
 
68
- # pylint: disable=C0103,W0613,W0201
69
- class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
70
- """ClientAppIo API servicer."""
77
+ # Get run IDs with pending messages
78
+ run_ids = state.get_run_ids_with_pending_messages()
71
79
 
72
- def __init__(self) -> None:
73
- self.clientapp_input: Optional[ClientAppInputs] = None
74
- self.clientapp_output: Optional[ClientAppOutputs] = None
75
- self.token_returned: bool = False
76
- self.inputs_returned: bool = False
80
+ # Return run IDs
81
+ return GetRunIdsWithPendingMessagesResponse(run_ids=run_ids)
77
82
 
78
- def GetToken(
79
- self, request: GetTokenRequest, context: grpc.ServicerContext
80
- ) -> GetTokenResponse:
81
- """Get token."""
82
- log(DEBUG, "ClientAppIo.GetToken")
83
+ def RequestToken(
84
+ self, request: RequestTokenRequest, context: grpc.ServicerContext
85
+ ) -> RequestTokenResponse:
86
+ """Request token."""
87
+ log(DEBUG, "ClientAppIo.RequestToken")
83
88
 
84
- # Fail if no ClientAppInputs are available
85
- if self.clientapp_input is None:
86
- context.abort(
87
- grpc.StatusCode.FAILED_PRECONDITION,
88
- "No inputs available.",
89
- )
90
- clientapp_input = cast(ClientAppInputs, self.clientapp_input)
89
+ # Initialize state connection
90
+ state = self.state_factory.state()
91
91
 
92
- # Fail if token was already returned in a previous call
93
- if self.token_returned:
94
- context.abort(
95
- grpc.StatusCode.FAILED_PRECONDITION,
96
- "Token already returned. A token can be returned only once.",
97
- )
92
+ # Attempt to create a token for the provided run ID
93
+ try:
94
+ token = state.create_token(request.run_id)
95
+ except ValueError:
96
+ # Return an empty token if A token already exists for this run ID,
97
+ # indicating the run is in progress
98
+ return RequestTokenResponse(token="")
98
99
 
99
- # If
100
- # - ClientAppInputs is set, and
101
- # - token hasn't been returned before,
102
- # return token
103
- self.token_returned = True
104
- return GetTokenResponse(token=clientapp_input.token)
100
+ # Return the token
101
+ return RequestTokenResponse(token=token)
105
102
 
106
103
  def PullClientAppInputs(
107
104
  self, request: PullClientAppInputsRequest, context: grpc.ServicerContext
@@ -109,36 +106,30 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
109
106
  """Pull Message, Context, and Run."""
110
107
  log(DEBUG, "ClientAppIo.PullClientAppInputs")
111
108
 
112
- # Fail if no ClientAppInputs are available
113
- if self.clientapp_input is None:
114
- context.abort(
115
- grpc.StatusCode.FAILED_PRECONDITION,
116
- "No inputs available.",
117
- )
118
- clientapp_input = cast(ClientAppInputs, self.clientapp_input)
109
+ # Initialize state and ffs connection
110
+ state = self.state_factory.state()
111
+ ffs = self.ffs_factory.ffs()
119
112
 
120
- # Fail if token wasn't returned in a previous call
121
- if not self.token_returned:
113
+ # Validate the token
114
+ run_id = state.get_run_id_by_token(request.token)
115
+ if run_id is None or not state.verify_token(run_id, request.token):
122
116
  context.abort(
123
- grpc.StatusCode.FAILED_PRECONDITION,
124
- "Token hasn't been returned."
125
- "Token must be returned before can be returned only once.",
117
+ grpc.StatusCode.PERMISSION_DENIED,
118
+ "Invalid token.",
126
119
  )
120
+ raise RuntimeError("This line should never be reached.")
127
121
 
128
- # Fail if token isn't matching
129
- if request.token != clientapp_input.token:
130
- context.abort(
131
- grpc.StatusCode.INVALID_ARGUMENT,
132
- "Mismatch between ClientApp and SuperNode token",
133
- )
122
+ # Retrieve message, context, run and fab for this run
123
+ message = state.get_messages(run_ids=[run_id], is_reply=False)[0]
124
+ context = cast(Context, state.get_context(run_id))
125
+ run = cast(Run, state.get_run(run_id))
126
+ fab = Fab(run.fab_hash, ffs.get(run.fab_hash)[0]) # type: ignore
134
127
 
135
- # Success
136
- self.inputs_returned = True
137
128
  return PullClientAppInputsResponse(
138
- message=message_to_proto(clientapp_input.message),
139
- context=context_to_proto(clientapp_input.context),
140
- run=run_to_proto(clientapp_input.run),
141
- fab=fab_to_proto(clientapp_input.fab) if clientapp_input.fab else None,
129
+ message=message_to_proto(message),
130
+ context=context_to_proto(context),
131
+ run=run_to_proto(run),
132
+ fab=fab_to_proto(fab),
142
133
  )
143
134
 
144
135
  def PushClientAppOutputs(
@@ -147,98 +138,24 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
147
138
  """Push Message and Context."""
148
139
  log(DEBUG, "ClientAppIo.PushClientAppOutputs")
149
140
 
150
- # Fail if no ClientAppInputs are available
151
- if not self.clientapp_input:
152
- context.abort(
153
- grpc.StatusCode.FAILED_PRECONDITION,
154
- "No inputs available.",
155
- )
156
- clientapp_input = cast(ClientAppInputs, self.clientapp_input)
141
+ # Initialize state connection
142
+ state = self.state_factory.state()
157
143
 
158
- # Fail if token wasn't returned in a previous call
159
- if not self.token_returned:
144
+ # Validate the token
145
+ run_id = state.get_run_id_by_token(request.token)
146
+ if run_id is None or not state.verify_token(run_id, request.token):
160
147
  context.abort(
161
- grpc.StatusCode.FAILED_PRECONDITION,
162
- "Token hasn't been returned."
163
- "Token must be returned before can be returned only once.",
148
+ grpc.StatusCode.PERMISSION_DENIED,
149
+ "Invalid token.",
164
150
  )
151
+ raise RuntimeError("This line should never be reached.")
165
152
 
166
- # Fail if inputs weren't delivered in a previous call
167
- if not self.inputs_returned:
168
- context.abort(
169
- grpc.StatusCode.FAILED_PRECONDITION,
170
- "Inputs haven't been delivered."
171
- "Inputs must be delivered before can be returned only once.",
172
- )
153
+ # Save the message and context to the state
154
+ state.store_message(message_from_proto(request.message))
155
+ state.store_context(context_from_proto(request.context))
173
156
 
174
- # Fail if token isn't matching
175
- if request.token != clientapp_input.token:
176
- context.abort(
177
- grpc.StatusCode.INVALID_ARGUMENT,
178
- "Mismatch between ClientApp and SuperNode token",
179
- )
157
+ # Remove the token to make the run eligible for processing
158
+ # A run associated with a token cannot be handled until its token is cleared
159
+ state.delete_token(run_id)
180
160
 
181
- # Preconditions met
182
- try:
183
- # Update Message and Context
184
- self.clientapp_output = ClientAppOutputs(
185
- message=message_from_proto(request.message),
186
- context=context_from_proto(request.context),
187
- )
188
-
189
- # Set status
190
- code = typing.ClientAppOutputCode.SUCCESS
191
- status = typing.ClientAppOutputStatus(code=code, message="Success")
192
- except Exception as e: # pylint: disable=broad-exception-caught
193
- log(ERROR, "ClientApp failed to push message to SuperNode, %s", e)
194
- code = typing.ClientAppOutputCode.UNKNOWN_ERROR
195
- status = typing.ClientAppOutputStatus(code=code, message="Unkonwn error")
196
-
197
- # Return status to ClientApp process
198
- proto_status = clientappstatus_to_proto(status=status)
199
- return PushClientAppOutputsResponse(status=proto_status)
200
-
201
- def set_inputs(
202
- self, clientapp_input: ClientAppInputs, token_returned: bool
203
- ) -> None:
204
- """Set ClientApp inputs.
205
-
206
- Parameters
207
- ----------
208
- clientapp_input : ClientAppInputs
209
- The inputs to the ClientApp.
210
- token_returned : bool
211
- A boolean indicating if the token has been returned.
212
- Set to `True` when passing the token to `flwr-clientap`
213
- and `False` otherwise.
214
- """
215
- if (
216
- self.clientapp_input is not None
217
- or self.clientapp_output is not None
218
- or self.token_returned
219
- ):
220
- raise ValueError(
221
- "ClientAppInputs and ClientAppOutputs must not be set before "
222
- "calling `set_inputs`."
223
- )
224
- log(DEBUG, "ClientAppInputs set (token: %s)", clientapp_input.token)
225
- self.clientapp_input = clientapp_input
226
- self.token_returned = token_returned
227
-
228
- def has_outputs(self) -> bool:
229
- """Check if ClientAppOutputs are available."""
230
- return self.clientapp_output is not None
231
-
232
- def get_outputs(self) -> ClientAppOutputs:
233
- """Get ClientApp outputs."""
234
- if self.clientapp_output is None:
235
- raise ValueError("ClientAppOutputs not set before calling `get_outputs`.")
236
-
237
- # Set outputs to a local variable and clear state
238
- output: ClientAppOutputs = self.clientapp_output
239
- self.clientapp_input = None
240
- self.clientapp_output = None
241
- self.token_returned = False
242
- self.inputs_returned = False
243
-
244
- return output
161
+ return PushClientAppOutputsResponse()