flwr 1.24.0__py3-none-any.whl → 1.26.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 (204) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +25 -66
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +72 -61
  25. flwr/cli/new/new.py +98 -309
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +87 -100
  28. flwr/cli/run_utils.py +23 -5
  29. flwr/cli/stop.py +33 -74
  30. flwr/cli/supernode/ls.py +35 -62
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -412
  35. flwr/client/grpc_adapter_client/connection.py +2 -2
  36. flwr/client/grpc_rere_client/connection.py +9 -6
  37. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  38. flwr/client/message_handler/message_handler.py +2 -1
  39. flwr/client/mod/centraldp_mods.py +1 -1
  40. flwr/client/mod/localdp_mod.py +1 -1
  41. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  42. flwr/client/rest_client/connection.py +6 -4
  43. flwr/client/run_info_store.py +2 -1
  44. flwr/clientapp/client_app.py +2 -1
  45. flwr/common/__init__.py +3 -2
  46. flwr/common/args.py +5 -5
  47. flwr/common/config.py +12 -17
  48. flwr/common/constant.py +3 -16
  49. flwr/common/context.py +2 -1
  50. flwr/common/exit/exit.py +4 -4
  51. flwr/common/exit/exit_code.py +6 -0
  52. flwr/common/grpc.py +2 -1
  53. flwr/common/logger.py +1 -1
  54. flwr/common/message.py +1 -1
  55. flwr/common/retry_invoker.py +13 -5
  56. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  57. flwr/common/serde.py +13 -5
  58. flwr/common/telemetry.py +1 -1
  59. flwr/common/typing.py +10 -3
  60. flwr/compat/client/app.py +6 -9
  61. flwr/compat/client/grpc_client/connection.py +2 -1
  62. flwr/compat/common/constant.py +29 -0
  63. flwr/compat/server/app.py +1 -1
  64. flwr/proto/clientappio_pb2.py +2 -2
  65. flwr/proto/clientappio_pb2_grpc.py +104 -88
  66. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  67. flwr/proto/federation_pb2.py +5 -3
  68. flwr/proto/federation_pb2.pyi +32 -2
  69. flwr/proto/fleet_pb2.py +10 -10
  70. flwr/proto/fleet_pb2.pyi +5 -1
  71. flwr/proto/run_pb2.py +18 -26
  72. flwr/proto/run_pb2.pyi +10 -58
  73. flwr/proto/serverappio_pb2.py +2 -2
  74. flwr/proto/serverappio_pb2_grpc.py +138 -207
  75. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  76. flwr/proto/simulationio_pb2.py +2 -2
  77. flwr/proto/simulationio_pb2_grpc.py +62 -90
  78. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  79. flwr/server/app.py +7 -13
  80. flwr/server/compat/grid_client_proxy.py +2 -1
  81. flwr/server/grid/grpc_grid.py +5 -5
  82. flwr/server/serverapp/app.py +11 -4
  83. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  84. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  85. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  86. flwr/server/superlink/linkstate/__init__.py +2 -2
  87. flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
  88. flwr/server/superlink/linkstate/linkstate.py +34 -21
  89. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  90. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
  91. flwr/server/superlink/linkstate/utils.py +49 -2
  92. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  93. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  94. flwr/server/utils/validator.py +1 -1
  95. flwr/server/workflow/default_workflows.py +2 -1
  96. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  97. flwr/serverapp/strategy/bulyan.py +7 -1
  98. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  99. flwr/serverapp/strategy/fedavg.py +1 -1
  100. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  101. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  102. flwr/simulation/run_simulation.py +3 -12
  103. flwr/simulation/simulationio_connection.py +3 -3
  104. flwr/{common → supercore}/address.py +7 -33
  105. flwr/supercore/app_utils.py +2 -1
  106. flwr/supercore/constant.py +27 -2
  107. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  108. flwr/supercore/credential_store/__init__.py +33 -0
  109. flwr/supercore/credential_store/credential_store.py +34 -0
  110. flwr/supercore/credential_store/file_credential_store.py +76 -0
  111. flwr/{common → supercore}/date.py +0 -11
  112. flwr/supercore/ffs/disk_ffs.py +1 -1
  113. flwr/supercore/object_store/object_store_factory.py +14 -6
  114. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  115. flwr/supercore/sql_mixin.py +315 -0
  116. flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
  117. flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
  118. flwr/supercore/state/alembic/env.py +103 -0
  119. flwr/supercore/state/alembic/script.py.mako +43 -0
  120. flwr/supercore/state/alembic/utils.py +239 -0
  121. flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
  122. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  123. flwr/supercore/state/schema/README.md +121 -0
  124. flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
  125. flwr/supercore/state/schema/corestate_tables.py +36 -0
  126. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  127. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  128. flwr/supercore/superexec/run_superexec.py +2 -2
  129. flwr/supercore/utils.py +225 -0
  130. flwr/superlink/federation/federation_manager.py +2 -2
  131. flwr/superlink/federation/noop_federation_manager.py +8 -6
  132. flwr/superlink/servicer/control/control_grpc.py +2 -0
  133. flwr/superlink/servicer/control/control_servicer.py +106 -21
  134. flwr/supernode/cli/flower_supernode.py +2 -1
  135. flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
  136. flwr/supernode/nodestate/nodestate.py +45 -0
  137. flwr/supernode/runtime/run_clientapp.py +14 -14
  138. flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
  139. flwr/supernode/start_client_internal.py +17 -10
  140. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
  141. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
  142. flwr/cli/federation/show.py +0 -317
  143. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  144. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  145. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  146. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  147. flwr/cli/new/templates/app/README.md.tpl +0 -37
  148. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  149. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  150. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  151. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  152. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  153. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  154. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  155. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  156. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  157. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  158. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  159. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  160. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  161. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  162. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  163. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  164. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  165. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  166. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  167. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  168. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  169. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  170. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  171. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  172. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  173. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  174. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  175. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  176. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  177. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  178. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  179. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  180. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  181. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  182. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  183. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
  184. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  185. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  186. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  187. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  188. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  189. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  190. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  191. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  192. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  193. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  194. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  195. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  196. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  197. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  198. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  199. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  200. flwr/common/pyproject.py +0 -42
  201. flwr/supercore/sqlite_mixin.py +0 -159
  202. /flwr/{common → supercore}/version.py +0 -0
  203. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  204. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
@@ -16,17 +16,21 @@
16
16
 
17
17
 
18
18
  import hashlib
19
+ import json
19
20
  import time
20
21
  from collections.abc import Generator, Sequence
21
22
  from logging import ERROR, INFO
22
23
  from typing import Any, cast
23
24
 
24
25
  import grpc
26
+ import requests
25
27
 
26
28
  from flwr.cli.config_utils import get_fab_metadata
27
29
  from flwr.common import Context, RecordDict, now
28
30
  from flwr.common.constant import (
29
31
  FAB_MAX_SIZE,
32
+ FEDERATION_NOT_FOUND_MESSAGE,
33
+ FEDERATION_NOT_SPECIFIED_MESSAGE,
30
34
  HEARTBEAT_DEFAULT_INTERVAL,
31
35
  LOG_STREAM_INTERVAL,
32
36
  NO_ACCOUNT_AUTH_MESSAGE,
@@ -36,6 +40,8 @@ from flwr.common.constant import (
36
40
  PUBLIC_KEY_NOT_VALID,
37
41
  PULL_UNFINISHED_RUN_MESSAGE,
38
42
  RUN_ID_NOT_FOUND_MESSAGE,
43
+ SUPERLINK_NODE_ID,
44
+ TRANSPORT_TYPE_GRPC_ADAPTER,
39
45
  Status,
40
46
  SubStatus,
41
47
  )
@@ -76,9 +82,11 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
76
82
  from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
77
83
  from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
78
84
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
85
+ from flwr.supercore.constant import NOOP_FEDERATION, PLATFORM_API_URL
79
86
  from flwr.supercore.ffs import FfsFactory
80
87
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
81
88
  from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
89
+ from flwr.supercore.utils import parse_app_spec, request_download_link
82
90
  from flwr.superlink.artifact_provider import ArtifactProvider
83
91
  from flwr.superlink.auth_plugin import ControlAuthnPlugin
84
92
 
@@ -96,6 +104,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
96
104
  is_simulation: bool,
97
105
  authn_plugin: ControlAuthnPlugin,
98
106
  artifact_provider: ArtifactProvider | None = None,
107
+ fleet_api_type: str | None = None,
99
108
  ) -> None:
100
109
  self.linkstate_factory = linkstate_factory
101
110
  self.ffs_factory = ffs_factory
@@ -103,8 +112,9 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
103
112
  self.is_simulation = is_simulation
104
113
  self.authn_plugin = authn_plugin
105
114
  self.artifact_provider = artifact_provider
115
+ self.fleet_api_type = fleet_api_type
106
116
 
107
- def StartRun( # pylint: disable=too-many-locals
117
+ def StartRun( # pylint: disable=too-many-locals, too-many-branches, too-many-statements
108
118
  self, request: StartRunRequest, context: grpc.ServicerContext
109
119
  ) -> StartRunResponse:
110
120
  """Create run ID."""
@@ -112,7 +122,15 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
112
122
  state = self.linkstate_factory.state()
113
123
  ffs = self.ffs_factory.ffs()
114
124
 
115
- if len(request.fab.content) > FAB_MAX_SIZE:
125
+ verification_dict: dict[str, str] = {}
126
+ if request.app_spec:
127
+ fab_file, verification_dict = _get_remote_fab(
128
+ self.fleet_api_type, request.app_spec, context
129
+ )
130
+ else:
131
+ fab_file = request.fab.content
132
+
133
+ if len(fab_file) > FAB_MAX_SIZE:
116
134
  log(
117
135
  ERROR,
118
136
  "FAB size exceeds maximum allowed size of %d bytes.",
@@ -124,7 +142,6 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
124
142
  flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
125
143
  override_config = user_config_from_proto(request.override_config)
126
144
  federation_options = config_record_from_proto(request.federation_options)
127
- fab_file = request.fab.content
128
145
 
129
146
  try:
130
147
  # Check that num-supernodes is set
@@ -134,10 +151,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
134
151
  )
135
152
 
136
153
  # Check (1) federation exists and (2) the flwr_aid is a member
137
- federation = request.federation
138
-
154
+ federation = request.federation or NOOP_FEDERATION
139
155
  if not state.federation_manager.exists(federation):
140
- raise ValueError(f"Federation '{federation}' does not exist.")
156
+ if request.federation:
157
+ raise ValueError(FEDERATION_NOT_FOUND_MESSAGE % federation)
158
+ raise ValueError(FEDERATION_NOT_SPECIFIED_MESSAGE)
141
159
 
142
160
  if not state.federation_manager.has_member(flwr_aid, federation):
143
161
  raise ValueError(
@@ -150,11 +168,12 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
150
168
  fab = Fab(
151
169
  hashlib.sha256(fab_file).hexdigest(),
152
170
  fab_file,
153
- dict(request.fab.verifications),
171
+ verification_dict,
154
172
  )
155
- fab_hash = ffs.put(fab.content, {})
173
+ fab_hash = ffs.put(fab.content, fab.verifications)
174
+
156
175
  if fab_hash != fab.hash_str:
157
- raise RuntimeError(
176
+ raise ValueError(
158
177
  f"FAB ({fab.hash_str}) hash from request doesn't match contents"
159
178
  )
160
179
  fab_id, fab_version = get_fab_metadata(fab.content)
@@ -164,7 +183,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
164
183
  fab_version,
165
184
  fab_hash,
166
185
  override_config,
167
- request.federation,
186
+ federation,
168
187
  federation_options,
169
188
  flwr_aid,
170
189
  )
@@ -180,7 +199,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
180
199
  # Create an empty context for the Run
181
200
  context = Context(
182
201
  run_id=run_id,
183
- node_id=0,
202
+ node_id=SUPERLINK_NODE_ID,
184
203
  # Dict is invariant in mypy
185
204
  node_config=node_config, # type: ignore[arg-type]
186
205
  state=RecordDict(),
@@ -190,13 +209,9 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
190
209
  # Register the context at the LinkState
191
210
  state.set_serverapp_context(run_id=run_id, context=context)
192
211
 
193
- # pylint: disable-next=broad-except
194
- except Exception as e:
212
+ except ValueError as e:
195
213
  log(ERROR, "Could not start run: %s", str(e))
196
- context.abort(
197
- grpc.StatusCode.FAILED_PRECONDITION,
198
- str(e),
199
- )
214
+ context.abort(grpc.StatusCode.FAILED_PRECONDITION, str(e))
200
215
 
201
216
  log(INFO, "Created run %s", str(run_id))
202
217
  return StartRunResponse(run_id=run_id)
@@ -521,9 +536,10 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
521
536
 
522
537
  # Get federations the account is a member of
523
538
  federations = state.federation_manager.get_federations(flwr_aid=flwr_aid)
524
-
525
539
  return ListFederationsResponse(
526
- federations=[Federation(name=fed) for fed in federations]
540
+ federations=[
541
+ Federation(name=fed[0], description=fed[1]) for fed in federations
542
+ ]
527
543
  )
528
544
 
529
545
  def ShowFederation(
@@ -543,7 +559,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
543
559
 
544
560
  # Ensure flwr_aid is a member of the requested federation
545
561
  federation = request.federation_name
546
- if federation not in federations:
562
+ if federation not in [fed[0] for fed in federations]:
547
563
  context.abort(
548
564
  grpc.StatusCode.FAILED_PRECONDITION,
549
565
  f"Federation '{federation}' does not exist or you are "
@@ -556,7 +572,8 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
556
572
  # Build Federation proto object
557
573
  federation_proto = Federation(
558
574
  name=federation,
559
- member_aids=details.member_aids,
575
+ member_aids=[acc.id for acc in details.accounts], # Deprecated in v1.26.0
576
+ accounts=details.accounts,
560
577
  nodes=details.nodes,
561
578
  runs=[run_to_proto(run) for run in details.runs],
562
579
  )
@@ -612,3 +629,71 @@ def _check_flwr_aid_in_run(
612
629
  grpc.StatusCode.PERMISSION_DENIED,
613
630
  "⛔️ Run ID does not belong to the account",
614
631
  )
632
+
633
+
634
+ def _format_verification(verifications: list[dict[str, str]]) -> dict[str, str]:
635
+ """Format verification information for FAB."""
636
+ # Convert verifications to dict[str, str] type
637
+ verification_dict = {
638
+ item["public_key_id"]: json.dumps(
639
+ {k: v for k, v in item.items() if k != "public_key_id"}
640
+ )
641
+ for item in verifications
642
+ }
643
+ verification_dict.update({"valid_license": "Valid"})
644
+
645
+ return verification_dict
646
+
647
+
648
+ def _get_remote_fab(
649
+ fleet_api_type: str | None,
650
+ app_spec: str,
651
+ context: grpc.ServicerContext,
652
+ ) -> tuple[bytes, dict[str, str]]:
653
+ """Get remote FAB from Flower platform API."""
654
+ if fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER:
655
+ context.abort(
656
+ grpc.StatusCode.FAILED_PRECONDITION,
657
+ "The selected SuperLink transport type is not "
658
+ "supported for connecting to Flower Platform.",
659
+ )
660
+
661
+ # Parse and validate app specification
662
+ try:
663
+ app_id, app_version = parse_app_spec(app_spec)
664
+ except ValueError as e:
665
+ context.abort(
666
+ grpc.StatusCode.FAILED_PRECONDITION,
667
+ f"{e}",
668
+ )
669
+
670
+ # Request download link and verification information
671
+ url = f"{PLATFORM_API_URL}/hub/fetch-fab"
672
+ try:
673
+ presigned_url, verifications = request_download_link(
674
+ app_id, app_version, url, "fab_url"
675
+ )
676
+ except ValueError as e:
677
+ context.abort(
678
+ grpc.StatusCode.FAILED_PRECONDITION,
679
+ f"{e}",
680
+ )
681
+
682
+ # Format verification information
683
+ verification_dict = (
684
+ _format_verification(verifications)
685
+ if verifications is not None
686
+ else {"valid_license": ""}
687
+ )
688
+
689
+ # Download FAB from Flower platform API
690
+ try:
691
+ r = requests.get(presigned_url, timeout=60)
692
+ r.raise_for_status()
693
+ except requests.RequestException as e:
694
+ context.abort(
695
+ grpc.StatusCode.FAILED_PRECONDITION,
696
+ f"FAB download failed: {str(e)}",
697
+ )
698
+ fab_file = r.content
699
+ return fab_file, verification_dict
@@ -233,7 +233,7 @@ def _try_setup_client_authentication(
233
233
 
234
234
  try:
235
235
  ssh_private_key = load_ssh_private_key(
236
- Path(args.auth_supernode_private_key).read_bytes(),
236
+ Path(args.auth_supernode_private_key).expanduser().read_bytes(),
237
237
  None,
238
238
  )
239
239
  if not isinstance(ssh_private_key, ec.EllipticCurvePrivateKey):
@@ -260,6 +260,7 @@ def _try_obtain_trusted_entities(
260
260
  """Validate and return the trust entities."""
261
261
  if not trusted_entities_path:
262
262
  return None
263
+ trusted_entities_path = trusted_entities_path.expanduser()
263
264
  if not trusted_entities_path.is_file():
264
265
  flwr_exit(
265
266
  ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
@@ -19,7 +19,7 @@ from collections.abc import Sequence
19
19
  from dataclasses import dataclass
20
20
  from threading import Lock, RLock
21
21
 
22
- from flwr.common import Context, Error, Message
22
+ from flwr.common import Context, Error, Message, now
23
23
  from flwr.common.constant import ErrorCode
24
24
  from flwr.common.inflatable import (
25
25
  get_all_nested_objects,
@@ -27,6 +27,7 @@ from flwr.common.inflatable import (
27
27
  no_object_id_recompute,
28
28
  )
29
29
  from flwr.common.typing import Run
30
+ from flwr.supercore.constant import MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS
30
31
  from flwr.supercore.corestate.in_memory_corestate import InMemoryCoreState
31
32
  from flwr.supercore.object_store import ObjectStore
32
33
 
@@ -45,6 +46,14 @@ class MessageEntry:
45
46
  is_retrieved: bool = False
46
47
 
47
48
 
49
+ @dataclass
50
+ class TimeEntry:
51
+ """Data class to represent a time entry."""
52
+
53
+ starting_at: float
54
+ finished_at: float | None = None
55
+
56
+
48
57
  class InMemoryNodeState(
49
58
  NodeState, InMemoryCoreState
50
59
  ): # pylint: disable=too-many-instance-attributes
@@ -63,6 +72,9 @@ class InMemoryNodeState(
63
72
  # Store run ID to Context mapping
64
73
  self.ctx_store: dict[int, Context] = {}
65
74
  self.lock_ctx_store = Lock()
75
+ # Store msg ID to TimeEntry mapping
76
+ self.time_store: dict[str, TimeEntry] = {}
77
+ self.lock_time_store = Lock()
66
78
 
67
79
  def set_node_id(self, node_id: int | None) -> None:
68
80
  """Set the node ID."""
@@ -208,3 +220,52 @@ class InMemoryNodeState(
208
220
 
209
221
  # Store the error reply message
210
222
  self.store_message(error_reply)
223
+
224
+ def record_message_processing_start(self, message_id: str) -> None:
225
+ """Record the start time of message processing based on the message ID."""
226
+ with self.lock_time_store:
227
+ self.time_store[message_id] = TimeEntry(starting_at=now().timestamp())
228
+
229
+ def record_message_processing_end(self, message_id: str) -> None:
230
+ """Record the end time of message processing based on the message ID."""
231
+ with self.lock_time_store:
232
+ if message_id not in self.time_store:
233
+ raise ValueError(
234
+ f"Cannot record end time: Message ID {message_id} not found."
235
+ )
236
+ entry = self.time_store[message_id]
237
+ entry.finished_at = now().timestamp()
238
+
239
+ def get_message_processing_duration(self, message_id: str) -> float:
240
+ """Get the message processing duration based on the message ID."""
241
+ # Cleanup old message processing times
242
+ self._cleanup_old_message_times()
243
+ with self.lock_time_store:
244
+ if message_id not in self.time_store:
245
+ raise ValueError(f"Message ID {message_id} not found.")
246
+
247
+ entry = self.time_store[message_id]
248
+ if entry.starting_at is None or entry.finished_at is None:
249
+ raise ValueError(
250
+ f"Start time or end time for message ID {message_id} is missing."
251
+ )
252
+
253
+ duration = entry.finished_at - entry.starting_at
254
+ return duration
255
+
256
+ def _cleanup_old_message_times(self) -> None:
257
+ """Remove time entries older than MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS."""
258
+ with self.lock_time_store:
259
+ cutoff = now().timestamp() - MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS
260
+ # Find message IDs for entries that have a finishing_at time
261
+ # before the cutoff, and those that don't exist in msg_store
262
+ to_delete = [
263
+ msg_id
264
+ for msg_id, entry in self.time_store.items()
265
+ if (entry.finished_at and entry.finished_at < cutoff)
266
+ or msg_id not in self.msg_store
267
+ ]
268
+
269
+ # Delete the identified entries
270
+ for msg_id in to_delete:
271
+ del self.time_store[msg_id]
@@ -168,3 +168,48 @@ class NodeState(CoreState):
168
168
  Sequence[int]
169
169
  Sequence of run IDs with pending messages.
170
170
  """
171
+
172
+ @abstractmethod
173
+ def record_message_processing_start(self, message_id: str) -> None:
174
+ """Record the start time of message processing based on the message ID.
175
+
176
+ Parameters
177
+ ----------
178
+ message_id : str
179
+ The ID of the message associated with the start time.
180
+ """
181
+
182
+ @abstractmethod
183
+ def record_message_processing_end(self, message_id: str) -> None:
184
+ """Record the end time of message processing based on the message ID.
185
+
186
+ Parameters
187
+ ----------
188
+ message_id : str
189
+ The ID of the message associated with the end time.
190
+
191
+ Raises
192
+ ------
193
+ ValueError
194
+ If the message ID is not found.
195
+ """
196
+
197
+ @abstractmethod
198
+ def get_message_processing_duration(self, message_id: str) -> float:
199
+ """Get the message processing duration based on the message ID.
200
+
201
+ Parameters
202
+ ----------
203
+ message_id : str
204
+ The ID of the message.
205
+
206
+ Returns
207
+ -------
208
+ float
209
+ The processing duration in seconds.
210
+
211
+ Raises
212
+ ------
213
+ ValueError
214
+ If the message ID is not found, or if start/end times are missing.
215
+ """
@@ -41,7 +41,7 @@ from flwr.common.inflatable_protobuf_utils import (
41
41
  from flwr.common.inflatable_utils import pull_and_inflate_object_from_tree, push_objects
42
42
  from flwr.common.logger import log
43
43
  from flwr.common.message import remove_content_from_message
44
- from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
44
+ from flwr.common.retry_invoker import make_simple_grpc_retry_invoker, wrap_stub
45
45
  from flwr.common.serde import (
46
46
  context_from_proto,
47
47
  context_to_proto,
@@ -103,14 +103,14 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
103
103
  flwr_dir_ = get_flwr_dir(flwr_dir)
104
104
  try:
105
105
  stub = ClientAppIoStub(channel)
106
- _wrap_stub(stub, _make_simple_grpc_retry_invoker())
106
+ wrap_stub(stub, make_simple_grpc_retry_invoker())
107
107
 
108
108
  # Start app heartbeat
109
109
  heartbeat_sender = HeartbeatSender(make_app_heartbeat_fn_grpc(stub, token))
110
110
  heartbeat_sender.start()
111
111
 
112
112
  # Pull Message, Context, Run and (optional) FAB from SuperNode
113
- message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
113
+ message, context, run, fab = pull_appinputs(stub=stub, token=token)
114
114
 
115
115
  try:
116
116
 
@@ -152,7 +152,7 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
152
152
  reply_message = Message(Error(code=e_code, reason=reason), reply_to=message)
153
153
 
154
154
  # Push Message and Context to SuperNode
155
- _ = push_clientappoutputs(
155
+ _ = push_appoutputs(
156
156
  stub=stub, token=token, message=reply_message, context=context
157
157
  )
158
158
 
@@ -165,15 +165,15 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
165
165
  )
166
166
 
167
167
 
168
- def pull_clientappinputs(
168
+ def pull_appinputs(
169
169
  stub: ClientAppIoStub, token: str
170
170
  ) -> tuple[Message, Context, Run, Fab | None]:
171
- """Pull ClientAppInputs from SuperNode."""
171
+ """Pull AppInputs from SuperNode."""
172
172
  masked_token = mask_string(token)
173
- log(INFO, "[flwr-clientapp] Pull `ClientAppInputs` for token %s", masked_token)
173
+ log(INFO, "[flwr-clientapp] Pull `AppInputs` for token %s", masked_token)
174
174
  try:
175
175
  # Pull Context, Run and (optional) FAB
176
- res: PullAppInputsResponse = stub.PullClientAppInputs(
176
+ res: PullAppInputsResponse = stub.PullAppInputs(
177
177
  PullAppInputsRequest(token=token)
178
178
  )
179
179
  context = context_from_proto(res.context)
@@ -201,16 +201,16 @@ def pull_clientappinputs(
201
201
  message.metadata.__dict__["_message_id"] = object_tree.object_id
202
202
  return message, context, run, fab
203
203
  except grpc.RpcError as e:
204
- log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e))
204
+ log(ERROR, "[PullAppInputs] gRPC error occurred: %s", str(e))
205
205
  raise e
206
206
 
207
207
 
208
- def push_clientappoutputs(
208
+ def push_appoutputs(
209
209
  stub: ClientAppIoStub, token: str, message: Message, context: Context
210
210
  ) -> PushAppOutputsResponse:
211
- """Push ClientAppOutputs to SuperNode."""
211
+ """Push AppOutputs to SuperNode."""
212
212
  masked_token = mask_string(token)
213
- log(INFO, "[flwr-clientapp] Push `ClientAppOutputs` for token %s", masked_token)
213
+ log(INFO, "[flwr-clientapp] Push `AppOutputs` for token %s", masked_token)
214
214
  # Set message ID
215
215
  message.metadata.__dict__["_message_id"] = message.object_id
216
216
  proto_message = message_to_proto(remove_content_from_message(message))
@@ -250,10 +250,10 @@ def push_clientappoutputs(
250
250
  )
251
251
 
252
252
  # Push Context
253
- res: PushAppOutputsResponse = stub.PushClientAppOutputs(
253
+ res: PushAppOutputsResponse = stub.PushAppOutputs(
254
254
  PushAppOutputsRequest(token=token, context=proto_context)
255
255
  )
256
256
  return res
257
257
  except grpc.RpcError as e:
258
- log(ERROR, "[PushClientAppOutputs] gRPC error occurred: %s", str(e))
258
+ log(ERROR, "[PushAppOutputs] gRPC error occurred: %s", str(e))
259
259
  raise e
@@ -129,11 +129,11 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
129
129
 
130
130
  return GetRunResponse(run=run_to_proto(run))
131
131
 
132
- def PullClientAppInputs(
132
+ def PullAppInputs(
133
133
  self, request: PullAppInputsRequest, context: grpc.ServicerContext
134
134
  ) -> PullAppInputsResponse:
135
135
  """Pull Message, Context, and Run."""
136
- log(DEBUG, "ClientAppIo.PullClientAppInputs")
136
+ log(DEBUG, "ClientAppIo.PullAppInputs")
137
137
 
138
138
  # Initialize state and ffs connection
139
139
  state = self.state_factory.state()
@@ -176,11 +176,11 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
176
176
  fab=fab_to_proto(fab),
177
177
  )
178
178
 
179
- def PushClientAppOutputs(
179
+ def PushAppOutputs(
180
180
  self, request: PushAppOutputsRequest, context: grpc.ServicerContext
181
181
  ) -> PushAppOutputsResponse:
182
182
  """Push Message and Context."""
183
- log(DEBUG, "ClientAppIo.PushClientAppOutputs")
183
+ log(DEBUG, "ClientAppIo.PushAppOutputs")
184
184
 
185
185
  # Initialize state connection
186
186
  state = self.state_factory.state()
@@ -223,6 +223,9 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
223
223
  # Retrieve message for this run
224
224
  message = state.get_messages(run_ids=[run_id], is_reply=False)[0]
225
225
 
226
+ # Record message processing start time
227
+ state.record_message_processing_start(message_id=message.metadata.message_id)
228
+
226
229
  # Retrieve the object tree for the message
227
230
  object_tree = store.get_object_tree(message.metadata.message_id)
228
231
 
@@ -248,13 +251,18 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
248
251
  )
249
252
  raise RuntimeError("This line should never be reached.")
250
253
 
254
+ # Record message processing end time
255
+ state.record_message_processing_end(
256
+ message_id=request.messages_list[0].metadata.reply_to_message_id
257
+ )
258
+
251
259
  # Store Message object to descendants mapping and preregister objects
252
260
  objects_to_push: set[str] = set()
253
261
  for object_tree in request.message_object_trees:
254
262
  objects_to_push |= set(store.preregister(run_id, object_tree))
263
+
255
264
  # Save the message to the state
256
265
  state.store_message(message_from_proto(request.messages_list[0]))
257
-
258
266
  return PushAppMessagesResponse(objects_to_push=objects_to_push)
259
267
 
260
268
  def SendAppHeartbeat(
@@ -32,10 +32,10 @@ from cryptography.hazmat.primitives.asymmetric import ec, ed25519
32
32
  from cryptography.hazmat.primitives.serialization.ssh import load_ssh_public_key
33
33
  from grpc import RpcError
34
34
 
35
+ from flwr.app.user_config import UserConfig
35
36
  from flwr.client.grpc_adapter_client.connection import grpc_adapter
36
37
  from flwr.client.grpc_rere_client.connection import grpc_request_response
37
38
  from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, Error, Message, RecordDict
38
- from flwr.common.address import parse_address
39
39
  from flwr.common.config import get_flwr_dir, get_fused_config_from_fab
40
40
  from flwr.common.constant import (
41
41
  CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
@@ -60,12 +60,12 @@ from flwr.common.inflatable_utils import (
60
60
  push_object_contents_from_iterable,
61
61
  )
62
62
  from flwr.common.logger import log
63
- from flwr.common.retry_invoker import RetryInvoker, _make_simple_grpc_retry_invoker
63
+ from flwr.common.retry_invoker import RetryInvoker, make_simple_grpc_retry_invoker
64
64
  from flwr.common.telemetry import EventType
65
- from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
66
- from flwr.common.version import package_version
65
+ from flwr.common.typing import Fab, Run, RunNotRunningException
67
66
  from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
68
67
  from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
68
+ from flwr.supercore.address import parse_address, resolve_bind_address
69
69
  from flwr.supercore.ffs import Ffs, FfsFactory
70
70
  from flwr.supercore.grpc_health import run_health_server_grpc_no_tls
71
71
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
@@ -74,6 +74,7 @@ from flwr.supercore.primitives.asymmetric_ed25519 import (
74
74
  decode_base64url,
75
75
  verify_signature,
76
76
  )
77
+ from flwr.supercore.version import package_version
77
78
  from flwr.supernode.nodestate import NodeState, NodeStateFactory
78
79
  from flwr.supernode.servicer.clientappio import ClientAppIoServicer
79
80
 
@@ -212,7 +213,10 @@ def start_client_internal(
212
213
  # Launch the SuperExec if the isolation mode is `subprocess`
213
214
  if isolation == ISOLATION_MODE_SUBPROCESS:
214
215
  command = ["flower-superexec", "--insecure"]
215
- command += ["--appio-api-address", clientappio_api_address]
216
+ command += [
217
+ "--appio-api-address",
218
+ resolve_bind_address(clientappio_api_address),
219
+ ]
216
220
  command += ["--plugin-type", ExecPluginType.CLIENT_APP]
217
221
  command += ["--parent-pid", str(os.getpid())]
218
222
  # pylint: disable-next=consider-using-with
@@ -432,7 +436,7 @@ def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
432
436
  def _push_messages(
433
437
  state: NodeState,
434
438
  object_store: ObjectStore,
435
- send: Callable[[Message, ObjectTree], set[str]],
439
+ send: Callable[[Message, ObjectTree, float], set[str]],
436
440
  push_object: Callable[[int, str, bytes], None],
437
441
  ) -> None:
438
442
  """Push reply messages to the SuperLink."""
@@ -480,9 +484,12 @@ def _push_messages(
480
484
 
481
485
  # Send the message
482
486
  try:
483
- # Send the reply message with its ObjectTree
487
+ clientapp_runtime = state.get_message_processing_duration(
488
+ message_id=message.metadata.reply_to_message_id,
489
+ )
490
+ # Send the reply message with its ObjectTree and ClientApp runtime
484
491
  # Get the IDs of objects to send
485
- ids_obj_to_send = send(message, object_tree)
492
+ ids_obj_to_send = send(message, object_tree, clientapp_runtime)
486
493
 
487
494
  # Push object contents from the ObjectStore
488
495
  run_id = message.metadata.run_id
@@ -539,7 +546,7 @@ def _init_connection( # pylint: disable=too-many-positional-arguments
539
546
  tuple[
540
547
  int,
541
548
  Callable[[], tuple[Message, ObjectTree] | None],
542
- Callable[[Message, ObjectTree], set[str]],
549
+ Callable[[Message, ObjectTree, float], set[str]],
543
550
  Callable[[int], Run],
544
551
  Callable[[str, int], Fab],
545
552
  Callable[[int, str], bytes],
@@ -603,7 +610,7 @@ def _make_fleet_connection_retry_invoker(
603
610
  connection_error_type: type[Exception] = RpcError,
604
611
  ) -> RetryInvoker:
605
612
  """Create a retry invoker for fleet connection."""
606
- retry_invoker = _make_simple_grpc_retry_invoker()
613
+ retry_invoker = make_simple_grpc_retry_invoker()
607
614
  retry_invoker.recoverable_exceptions = connection_error_type
608
615
  if max_retries is not None:
609
616
  retry_invoker.max_tries = max_retries + 1