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.
- flwr/__init__.py +1 -1
- flwr/app/__init__.py +4 -1
- flwr/app/message_type.py +29 -0
- flwr/app/metadata.py +5 -2
- flwr/app/user_config.py +19 -0
- flwr/cli/app.py +37 -19
- flwr/cli/app_cmd/publish.py +25 -75
- flwr/cli/app_cmd/review.py +25 -66
- flwr/cli/auth_plugin/auth_plugin.py +5 -10
- flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
- flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
- flwr/cli/build.py +15 -28
- flwr/cli/config/__init__.py +21 -0
- flwr/cli/config/ls.py +71 -0
- flwr/cli/config_migration.py +297 -0
- flwr/cli/config_utils.py +63 -156
- flwr/cli/constant.py +71 -0
- flwr/cli/federation/__init__.py +0 -2
- flwr/cli/federation/ls.py +256 -64
- flwr/cli/flower_config.py +429 -0
- flwr/cli/install.py +23 -62
- flwr/cli/log.py +23 -37
- flwr/cli/login/login.py +29 -63
- flwr/cli/ls.py +72 -61
- flwr/cli/new/new.py +98 -309
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +87 -100
- flwr/cli/run_utils.py +23 -5
- flwr/cli/stop.py +33 -74
- flwr/cli/supernode/ls.py +35 -62
- flwr/cli/supernode/register.py +31 -80
- flwr/cli/supernode/unregister.py +24 -70
- flwr/cli/typing.py +200 -0
- flwr/cli/utils.py +160 -412
- flwr/client/grpc_adapter_client/connection.py +2 -2
- flwr/client/grpc_rere_client/connection.py +9 -6
- flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
- flwr/client/message_handler/message_handler.py +2 -1
- flwr/client/mod/centraldp_mods.py +1 -1
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/rest_client/connection.py +6 -4
- flwr/client/run_info_store.py +2 -1
- flwr/clientapp/client_app.py +2 -1
- flwr/common/__init__.py +3 -2
- flwr/common/args.py +5 -5
- flwr/common/config.py +12 -17
- flwr/common/constant.py +3 -16
- flwr/common/context.py +2 -1
- flwr/common/exit/exit.py +4 -4
- flwr/common/exit/exit_code.py +6 -0
- flwr/common/grpc.py +2 -1
- flwr/common/logger.py +1 -1
- flwr/common/message.py +1 -1
- flwr/common/retry_invoker.py +13 -5
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
- flwr/common/serde.py +13 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +10 -3
- flwr/compat/client/app.py +6 -9
- flwr/compat/client/grpc_client/connection.py +2 -1
- flwr/compat/common/constant.py +29 -0
- flwr/compat/server/app.py +1 -1
- flwr/proto/clientappio_pb2.py +2 -2
- flwr/proto/clientappio_pb2_grpc.py +104 -88
- flwr/proto/clientappio_pb2_grpc.pyi +140 -80
- flwr/proto/federation_pb2.py +5 -3
- flwr/proto/federation_pb2.pyi +32 -2
- flwr/proto/fleet_pb2.py +10 -10
- flwr/proto/fleet_pb2.pyi +5 -1
- flwr/proto/run_pb2.py +18 -26
- flwr/proto/run_pb2.pyi +10 -58
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +138 -207
- flwr/proto/serverappio_pb2_grpc.pyi +189 -155
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +62 -90
- flwr/proto/simulationio_pb2_grpc.pyi +95 -55
- flwr/server/app.py +7 -13
- flwr/server/compat/grid_client_proxy.py +2 -1
- flwr/server/grid/grpc_grid.py +5 -5
- flwr/server/serverapp/app.py +11 -4
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
- flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
- flwr/server/superlink/linkstate/linkstate.py +34 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
- flwr/server/superlink/linkstate/utils.py +49 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
- flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
- flwr/server/utils/validator.py +1 -1
- flwr/server/workflow/default_workflows.py +2 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/strategy/bulyan.py +7 -1
- flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
- flwr/serverapp/strategy/fedavg.py +1 -1
- flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
- flwr/simulation/run_simulation.py +3 -12
- flwr/simulation/simulationio_connection.py +3 -3
- flwr/{common → supercore}/address.py +7 -33
- flwr/supercore/app_utils.py +2 -1
- flwr/supercore/constant.py +27 -2
- flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
- flwr/supercore/credential_store/__init__.py +33 -0
- flwr/supercore/credential_store/credential_store.py +34 -0
- flwr/supercore/credential_store/file_credential_store.py +76 -0
- flwr/{common → supercore}/date.py +0 -11
- flwr/supercore/ffs/disk_ffs.py +1 -1
- flwr/supercore/object_store/object_store_factory.py +14 -6
- flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
- flwr/supercore/sql_mixin.py +315 -0
- flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
- flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
- flwr/supercore/state/alembic/env.py +103 -0
- flwr/supercore/state/alembic/script.py.mako +43 -0
- flwr/supercore/state/alembic/utils.py +239 -0
- flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
- flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
- flwr/supercore/state/schema/README.md +121 -0
- flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
- flwr/supercore/state/schema/corestate_tables.py +36 -0
- flwr/supercore/state/schema/linkstate_tables.py +152 -0
- flwr/supercore/state/schema/objectstore_tables.py +90 -0
- flwr/supercore/superexec/run_superexec.py +2 -2
- flwr/supercore/utils.py +225 -0
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_grpc.py +2 -0
- flwr/superlink/servicer/control/control_servicer.py +106 -21
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
- flwr/supernode/nodestate/nodestate.py +45 -0
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
- flwr/supernode/start_client_internal.py +17 -10
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
- flwr/cli/federation/show.py +0 -317
- flwr/cli/new/templates/app/.gitignore.tpl +0 -163
- flwr/cli/new/templates/app/LICENSE.tpl +0 -202
- flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
- flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
- flwr/cli/new/templates/app/README.md.tpl +0 -37
- flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
- flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
- flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
- flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
- flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
- flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
- flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
- flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
- flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
- flwr/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|