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
@@ -0,0 +1,152 @@
1
+ # Copyright 2026 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
+ """SQLAlchemy Core Table definitions for LinkState."""
16
+
17
+
18
+ from sqlalchemy import (
19
+ TIMESTAMP,
20
+ Column,
21
+ Float,
22
+ ForeignKey,
23
+ Index,
24
+ Integer,
25
+ LargeBinary,
26
+ MetaData,
27
+ String,
28
+ Table,
29
+ UniqueConstraint,
30
+ )
31
+
32
+
33
+ def create_linkstate_metadata() -> MetaData:
34
+ """Create and return MetaData with LinkState table definitions."""
35
+ metadata = MetaData()
36
+
37
+ # --------------------------------------------------------------------------
38
+ # Table: node
39
+ # --------------------------------------------------------------------------
40
+ Table(
41
+ "node",
42
+ metadata,
43
+ Column("node_id", Integer, unique=True),
44
+ Column("owner_aid", String),
45
+ Column("owner_name", String),
46
+ Column("status", String),
47
+ Column("registered_at", String),
48
+ Column("last_activated_at", String, nullable=True),
49
+ Column("last_deactivated_at", String, nullable=True),
50
+ Column("unregistered_at", String, nullable=True),
51
+ Column("online_until", TIMESTAMP, nullable=True),
52
+ Column("heartbeat_interval", Float),
53
+ Column("public_key", LargeBinary, unique=True),
54
+ # Indexes
55
+ # Used in delete_node and get_node_info (security/filtering)
56
+ Index("idx_node_owner_aid", "owner_aid"),
57
+ # Used in get_nodes and activation checks (frequent filtering)
58
+ Index("idx_node_status", "status"),
59
+ # Used in heartbeat checks to efficiently find expired nodes
60
+ Index("idx_online_until", "online_until"),
61
+ )
62
+
63
+ # --------------------------------------------------------------------------
64
+ # Table: run
65
+ # --------------------------------------------------------------------------
66
+ Table(
67
+ "run",
68
+ metadata,
69
+ Column("run_id", Integer, unique=True),
70
+ Column("fab_id", String),
71
+ Column("fab_version", String),
72
+ Column("fab_hash", String),
73
+ Column("override_config", String),
74
+ Column("pending_at", String),
75
+ Column("starting_at", String),
76
+ Column("running_at", String),
77
+ Column("finished_at", String),
78
+ Column("sub_status", String),
79
+ Column("details", String),
80
+ Column("federation", String),
81
+ Column("federation_options", LargeBinary),
82
+ Column("flwr_aid", String),
83
+ Column("bytes_sent", Integer, server_default="0"),
84
+ Column("bytes_recv", Integer, server_default="0"),
85
+ Column("clientapp_runtime", Float, server_default="0.0"),
86
+ )
87
+
88
+ # --------------------------------------------------------------------------
89
+ # Table: logs
90
+ # --------------------------------------------------------------------------
91
+ Table(
92
+ "logs",
93
+ metadata,
94
+ Column("timestamp", Float),
95
+ Column("run_id", Integer, ForeignKey("run.run_id")),
96
+ Column("node_id", Integer),
97
+ Column("log", String),
98
+ # Composite PK
99
+ UniqueConstraint("timestamp", "run_id", "node_id"),
100
+ )
101
+
102
+ # --------------------------------------------------------------------------
103
+ # Table: context
104
+ # --------------------------------------------------------------------------
105
+ Table(
106
+ "context",
107
+ metadata,
108
+ Column("run_id", Integer, ForeignKey("run.run_id"), unique=True),
109
+ Column("context", LargeBinary),
110
+ )
111
+
112
+ # --------------------------------------------------------------------------
113
+ # Table: message_ins
114
+ # --------------------------------------------------------------------------
115
+ Table(
116
+ "message_ins",
117
+ metadata,
118
+ Column("message_id", String, unique=True),
119
+ Column("group_id", String),
120
+ Column("run_id", Integer, ForeignKey("run.run_id")),
121
+ Column("src_node_id", Integer),
122
+ Column("dst_node_id", Integer),
123
+ Column("reply_to_message_id", String),
124
+ Column("created_at", Float),
125
+ Column("delivered_at", String),
126
+ Column("ttl", Float),
127
+ Column("message_type", String),
128
+ Column("content", LargeBinary, nullable=True),
129
+ Column("error", LargeBinary, nullable=True),
130
+ )
131
+
132
+ # --------------------------------------------------------------------------
133
+ # Table: message_res
134
+ # --------------------------------------------------------------------------
135
+ Table(
136
+ "message_res",
137
+ metadata,
138
+ Column("message_id", String, unique=True),
139
+ Column("group_id", String),
140
+ Column("run_id", Integer, ForeignKey("run.run_id")),
141
+ Column("src_node_id", Integer),
142
+ Column("dst_node_id", Integer),
143
+ Column("reply_to_message_id", String),
144
+ Column("created_at", Float),
145
+ Column("delivered_at", String),
146
+ Column("ttl", Float),
147
+ Column("message_type", String),
148
+ Column("content", LargeBinary, nullable=True),
149
+ Column("error", LargeBinary, nullable=True),
150
+ )
151
+
152
+ return metadata
@@ -0,0 +1,90 @@
1
+ # Copyright 2026 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
+ """SQLAlchemy Core Table definitions for ObjectStore."""
16
+
17
+
18
+ from sqlalchemy import (
19
+ CheckConstraint,
20
+ Column,
21
+ ForeignKey,
22
+ Integer,
23
+ LargeBinary,
24
+ MetaData,
25
+ PrimaryKeyConstraint,
26
+ String,
27
+ Table,
28
+ )
29
+
30
+
31
+ def create_objectstore_metadata() -> MetaData:
32
+ """Create and return MetaData with ObjectStore table definitions."""
33
+ metadata = MetaData()
34
+
35
+ # --------------------------------------------------------------------------
36
+ # Table: objects
37
+ # --------------------------------------------------------------------------
38
+ Table(
39
+ "objects",
40
+ metadata,
41
+ Column("object_id", String, primary_key=True, nullable=True),
42
+ Column("content", LargeBinary),
43
+ Column(
44
+ "is_available",
45
+ Integer,
46
+ nullable=False,
47
+ server_default="0",
48
+ ),
49
+ Column("ref_count", Integer, nullable=False, server_default="0"),
50
+ CheckConstraint("is_available IN (0, 1)", name="ck_objects_is_available"),
51
+ )
52
+
53
+ # --------------------------------------------------------------------------
54
+ # Table: object_children
55
+ # --------------------------------------------------------------------------
56
+ Table(
57
+ "object_children",
58
+ metadata,
59
+ Column(
60
+ "parent_id",
61
+ String,
62
+ ForeignKey("objects.object_id", ondelete="CASCADE"),
63
+ nullable=False,
64
+ ),
65
+ Column(
66
+ "child_id",
67
+ String,
68
+ ForeignKey("objects.object_id", ondelete="CASCADE"),
69
+ nullable=False,
70
+ ),
71
+ PrimaryKeyConstraint("parent_id", "child_id"),
72
+ )
73
+
74
+ # --------------------------------------------------------------------------
75
+ # Table: run_objects
76
+ # --------------------------------------------------------------------------
77
+ Table(
78
+ "run_objects",
79
+ metadata,
80
+ Column("run_id", Integer, nullable=False),
81
+ Column(
82
+ "object_id",
83
+ String,
84
+ ForeignKey("objects.object_id", ondelete="CASCADE"),
85
+ nullable=False,
86
+ ),
87
+ PrimaryKeyConstraint("run_id", "object_id"),
88
+ )
89
+
90
+ return metadata
@@ -23,7 +23,7 @@ from flwr.common.config import get_flwr_dir
23
23
  from flwr.common.exit import ExitCode, flwr_exit, register_signal_handlers
24
24
  from flwr.common.grpc import create_channel, on_channel_state_change
25
25
  from flwr.common.logger import log
26
- from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
26
+ from flwr.common.retry_invoker import make_simple_grpc_retry_invoker, wrap_stub
27
27
  from flwr.common.serde import run_from_proto
28
28
  from flwr.common.telemetry import EventType
29
29
  from flwr.common.typing import Run
@@ -101,7 +101,7 @@ def run_superexec( # pylint: disable=R0913,R0914,R0917
101
101
 
102
102
  # Create the gRPC stub for the AppIO API
103
103
  stub = stub_class(channel)
104
- _wrap_stub(stub, _make_simple_grpc_retry_invoker())
104
+ wrap_stub(stub, make_simple_grpc_retry_invoker())
105
105
 
106
106
  def get_run(run_id: int) -> Run:
107
107
  _req = GetRunRequest(run_id=run_id)
flwr/supercore/utils.py CHANGED
@@ -15,6 +15,19 @@
15
15
  """Utility functions for the infrastructure."""
16
16
 
17
17
 
18
+ import json
19
+ import os
20
+ import re
21
+ from pathlib import Path
22
+
23
+ import requests
24
+
25
+ from flwr.common.constant import FLWR_DIR, FLWR_HOME
26
+ from flwr.supercore.version import package_version as flwr_version
27
+
28
+ from .constant import APP_ID_PATTERN, APP_VERSION_PATTERN
29
+
30
+
18
31
  def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
19
32
  """Mask a string by preserving only the head and tail characters.
20
33
 
@@ -50,3 +63,215 @@ def int64_to_uint64(signed: int) -> int:
50
63
  if signed < 0:
51
64
  return signed + (1 << 64)
52
65
  return signed
66
+
67
+
68
+ def get_flwr_home() -> Path:
69
+ """Get the Flower home directory path.
70
+
71
+ Returns FLWR_HOME environment variable if set, otherwise returns a default
72
+ subdirectory in the user's home directory.
73
+ """
74
+ if flwr_home := os.getenv(FLWR_HOME):
75
+ return Path(flwr_home)
76
+ return Path.home() / FLWR_DIR
77
+
78
+
79
+ def parse_app_spec(app_spec: str) -> tuple[str, str | None]:
80
+ """Parse app specification string into app ID and version.
81
+
82
+ Parameters
83
+ ----------
84
+ app_spec : str
85
+ The app specification string in the format '@account/app' or
86
+ '@account/app==x.y.z' (digits only).
87
+
88
+ Returns
89
+ -------
90
+ tuple[str, str | None]
91
+ A tuple containing the app ID and optional version.
92
+
93
+ Raises
94
+ ------
95
+ ValueError
96
+ If the app specification format is invalid.
97
+ """
98
+ if "==" in app_spec:
99
+ app_id, app_version = app_spec.split("==", 1)
100
+
101
+ if not re.match(APP_VERSION_PATTERN, app_version):
102
+ raise ValueError(
103
+ "Invalid app version. Expected format: x.y.z (digits only)."
104
+ )
105
+ else:
106
+ app_id = app_spec
107
+ app_version = None
108
+
109
+ if not re.match(APP_ID_PATTERN, app_id):
110
+ raise ValueError(
111
+ "Invalid remote app ID. Expected format: '@account_name/app_name'."
112
+ )
113
+
114
+ return app_id, app_version
115
+
116
+
117
+ def request_download_link(
118
+ app_id: str, app_version: str | None, in_url: str, out_url: str
119
+ ) -> tuple[str, list[dict[str, str]] | None]:
120
+ """Request a download link for the given app from the Flower Platform API.
121
+
122
+ Parameters
123
+ ----------
124
+ app_id : str
125
+ The application identifier in the format '@account/app'.
126
+ app_version : str | None
127
+ The application version (e.g., '1.2.3'), or None to request the latest version.
128
+ in_url : str
129
+ The Platform API endpoint URL to query.
130
+ out_url : str
131
+ The key name in the response that contains the download URL.
132
+
133
+ Returns
134
+ -------
135
+ tuple[str, list[dict[str, str]] | None]
136
+ A tuple containing:
137
+ - The download URL for the application.
138
+ - A list of verification dictionaries if provided by the API, otherwise None.
139
+
140
+ Raises
141
+ ------
142
+ ValueError
143
+ If the API connection fails, the application or version is not found,
144
+ the API returns a non-200 response, or the response format is invalid.
145
+ """
146
+ headers = {
147
+ "Content-Type": "application/json",
148
+ "Accept": "application/json",
149
+ }
150
+ body = {
151
+ "app_id": app_id, # send raw string of app_id
152
+ "app_version": app_version,
153
+ "flwr_version": flwr_version,
154
+ }
155
+
156
+ try:
157
+ resp = requests.post(in_url, headers=headers, data=json.dumps(body), timeout=20)
158
+ except requests.RequestException as e:
159
+ raise ValueError(f"Unable to connect to Platform API: {e}") from e
160
+
161
+ if resp.status_code == 404:
162
+ # Expecting a JSON body with a "detail" field
163
+ try:
164
+ error_message = resp.json().get("detail")
165
+ except ValueError:
166
+ # JSON parsing failed
167
+ raise ValueError(f"{app_id} not found in Platform API.") from None
168
+
169
+ if isinstance(error_message, dict):
170
+ available_app_versions = error_message.get("available_app_versions", [])
171
+ available_versions_str = (
172
+ ", ".join(map(str, available_app_versions))
173
+ if available_app_versions
174
+ else "None"
175
+ )
176
+ raise ValueError(
177
+ f"{app_id}=={app_version} not found in Platform API. "
178
+ f"Available app versions for {app_id}: {available_versions_str}"
179
+ )
180
+
181
+ raise ValueError(f"{app_id} not found in Platform API.")
182
+
183
+ if not resp.ok:
184
+ raise ValueError(
185
+ f"Platform API request failed with status {resp.status_code}. "
186
+ f"Details: {resp.text}"
187
+ )
188
+
189
+ data = resp.json()
190
+ if out_url not in data:
191
+ raise ValueError("Invalid response from Platform API")
192
+
193
+ verifications = data["verifications"] if "verifications" in data else None
194
+
195
+ return str(data[out_url]), verifications
196
+
197
+
198
+ def humanize_duration(seconds: float) -> str:
199
+ """Convert a duration in seconds to a human-friendly string.
200
+
201
+ Rules:
202
+ - < 90 seconds: show seconds
203
+ - < 1 hour: show minutes + seconds
204
+ - < 1 day: show hours + minutes
205
+ - >= 1 day: show days + hours
206
+ """
207
+ seconds = int(seconds)
208
+
209
+ # Under 90 seconds → Seconds only
210
+ if seconds < 90:
211
+ return f"{seconds}s"
212
+
213
+ # Under 1 hour → Minutes and seconds
214
+ minutes, sec = divmod(seconds, 60)
215
+ if minutes < 60:
216
+ return f"{minutes}m {sec}s"
217
+
218
+ # Under 1 day → Hours and minutes
219
+ hours, minutes = divmod(minutes, 60)
220
+ if hours < 24:
221
+ return f"{hours}h {minutes}m"
222
+
223
+ # 1+ days → Days and hours
224
+ days, hours = divmod(hours, 24)
225
+ return f"{days}d {hours}h"
226
+
227
+
228
+ def humanize_bytes(num_bytes: int) -> str:
229
+ """Convert a number of bytes to a human-friendly string.
230
+
231
+ Uses 1024-based units and 0-1 decimal precision.
232
+ Rules:
233
+ - < 1 KB: bytes
234
+ - < 1 MB: KB
235
+ - < 1 GB: MB
236
+ - < 1 TB: GB
237
+ """
238
+ value = float(num_bytes)
239
+
240
+ for suffix in ["B", "KB", "MB", "GB", "TB"]:
241
+ if value < 1024 or suffix == "TB":
242
+ # Bytes → no decimals
243
+ if suffix == "B":
244
+ return f"{int(value)} B"
245
+
246
+ # Decide precision: 1 decimal for <10, otherwise no decimal
247
+ if value < 10:
248
+ formatted = f"{value:.1f}"
249
+ else:
250
+ formatted = f"{int(value)}"
251
+
252
+ return f"{formatted} {suffix}"
253
+
254
+ value /= 1024
255
+
256
+ raise RuntimeError("Unreachable code") # Make mypy happy
257
+
258
+
259
+ def check_federation_format(federation: str) -> None:
260
+ """Check if the federation string is valid.
261
+
262
+ Parameters
263
+ ----------
264
+ federation : str
265
+ The federation string to check.
266
+
267
+ Raises
268
+ ------
269
+ ValueError
270
+ If the federation string is not valid. The expected
271
+ format is '@<account-name>/<federation-name>'.
272
+ """
273
+ if not re.match(r"^@[a-zA-Z0-9\-_]+/[a-zA-Z0-9\-_]+$", federation):
274
+ raise ValueError(
275
+ f"Invalid federation format: {federation}. "
276
+ f"Expected format: '@<account-name>/<federation-name>'."
277
+ )
@@ -56,8 +56,8 @@ class FederationManager(ABC):
56
56
  """Given a node ID, check if it is in the federation."""
57
57
 
58
58
  @abstractmethod
59
- def get_federations(self, flwr_aid: str) -> list[str]:
60
- """Get federations of which the account is a member."""
59
+ def get_federations(self, flwr_aid: str) -> list[tuple[str, str]]:
60
+ """Get federations (name, description) of which the account is a member."""
61
61
 
62
62
  @abstractmethod
63
63
  def get_details(self, federation: str) -> Federation:
@@ -15,9 +15,10 @@
15
15
  """NoOp implementation of FederationManager."""
16
16
 
17
17
 
18
- from flwr.common.constant import NOOP_FLWR_AID
18
+ from flwr.common.constant import NOOP_ACCOUNT_NAME, NOOP_FLWR_AID
19
19
  from flwr.common.typing import Federation
20
- from flwr.supercore.constant import NOOP_FEDERATION
20
+ from flwr.proto.federation_pb2 import Account # pylint: disable=E0611
21
+ from flwr.supercore.constant import NOOP_FEDERATION, NOOP_FEDERATION_DESCRIPTION
21
22
 
22
23
  from .federation_manager import FederationManager
23
24
 
@@ -47,11 +48,11 @@ class NoOpFederationManager(FederationManager):
47
48
  raise ValueError(f"Federation '{federation}' does not exist.")
48
49
  return True
49
50
 
50
- def get_federations(self, flwr_aid: str) -> list[str]:
51
- """Get federations of which the account is a member."""
51
+ def get_federations(self, flwr_aid: str) -> list[tuple[str, str]]:
52
+ """Get federations (name, description) of which the account is a member."""
52
53
  if flwr_aid != NOOP_FLWR_AID:
53
54
  return []
54
- return [NOOP_FEDERATION]
55
+ return [(NOOP_FEDERATION, NOOP_FEDERATION_DESCRIPTION)]
55
56
 
56
57
  def get_details(self, federation: str) -> Federation:
57
58
  """Get details of the federation."""
@@ -65,7 +66,8 @@ class NoOpFederationManager(FederationManager):
65
66
  ]
66
67
  return Federation(
67
68
  name=NOOP_FEDERATION,
68
- member_aids=[NOOP_FLWR_AID],
69
+ description=NOOP_FEDERATION_DESCRIPTION,
70
+ accounts=[Account(id=NOOP_FLWR_AID, name=NOOP_ACCOUNT_NAME)],
69
71
  nodes=nodes,
70
72
  runs=runs,
71
73
  )
@@ -61,6 +61,7 @@ def run_control_api_grpc(
61
61
  authz_plugin: ControlAuthzPlugin,
62
62
  event_log_plugin: EventLogWriterPlugin | None = None,
63
63
  artifact_provider: ArtifactProvider | None = None,
64
+ fleet_api_type: str | None = None,
64
65
  ) -> grpc.Server:
65
66
  """Run Control API (gRPC, request-response)."""
66
67
  license_plugin: LicensePlugin | None = get_license_plugin()
@@ -74,6 +75,7 @@ def run_control_api_grpc(
74
75
  is_simulation=is_simulation,
75
76
  authn_plugin=authn_plugin,
76
77
  artifact_provider=artifact_provider,
78
+ fleet_api_type=fleet_api_type,
77
79
  )
78
80
  interceptors = [ControlAccountAuthInterceptor(authn_plugin, authz_plugin)]
79
81
  if license_plugin is not None: