flwr 1.22.0__py3-none-any.whl → 1.24.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 +16 -5
- flwr/app/error.py +2 -2
- flwr/app/exception.py +3 -3
- flwr/cli/app.py +34 -1
- flwr/cli/app_cmd/__init__.py +23 -0
- flwr/cli/app_cmd/publish.py +285 -0
- flwr/cli/app_cmd/review.py +252 -0
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +94 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
- flwr/cli/build.py +166 -53
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
- flwr/cli/config_utils.py +101 -13
- flwr/cli/federation/__init__.py +24 -0
- flwr/cli/federation/ls.py +140 -0
- flwr/cli/federation/show.py +317 -0
- flwr/cli/install.py +91 -13
- flwr/cli/log.py +54 -11
- flwr/cli/login/login.py +41 -27
- flwr/cli/ls.py +177 -133
- flwr/cli/new/new.py +175 -40
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
- flwr/cli/pull.py +12 -7
- flwr/cli/run/run.py +82 -31
- flwr/cli/run_utils.py +130 -0
- flwr/cli/stop.py +27 -9
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +268 -0
- flwr/cli/supernode/register.py +190 -0
- flwr/cli/supernode/unregister.py +140 -0
- flwr/cli/utils.py +464 -81
- flwr/client/__init__.py +2 -1
- flwr/client/dpfedavg_numpy_client.py +4 -1
- flwr/client/grpc_adapter_client/connection.py +12 -15
- flwr/client/grpc_rere_client/connection.py +68 -41
- flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
- flwr/client/message_handler/message_handler.py +2 -2
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
- flwr/client/numpy_client.py +1 -1
- flwr/client/rest_client/connection.py +94 -51
- flwr/client/run_info_store.py +4 -5
- flwr/client/typing.py +1 -1
- flwr/clientapp/__init__.py +1 -2
- flwr/{client → clientapp}/client_app.py +9 -10
- flwr/clientapp/mod/centraldp_mods.py +16 -17
- flwr/clientapp/mod/localdp_mod.py +8 -9
- flwr/clientapp/typing.py +1 -1
- flwr/{client/clientapp → clientapp}/utils.py +4 -4
- flwr/common/address.py +1 -2
- flwr/common/args.py +3 -4
- flwr/common/config.py +13 -16
- flwr/common/constant.py +56 -13
- flwr/common/differential_privacy.py +3 -4
- flwr/common/event_log_plugin/event_log_plugin.py +3 -4
- flwr/common/exit/exit.py +15 -2
- flwr/common/exit/exit_code.py +39 -10
- flwr/common/exit/exit_handler.py +6 -2
- flwr/common/exit/signal_handler.py +5 -5
- flwr/common/grpc.py +6 -6
- flwr/common/inflatable_protobuf_utils.py +1 -1
- flwr/common/inflatable_utils.py +48 -31
- flwr/common/logger.py +19 -19
- flwr/common/message.py +4 -4
- flwr/common/object_ref.py +7 -7
- flwr/common/record/array.py +6 -6
- flwr/common/record/arrayrecord.py +18 -21
- flwr/common/record/configrecord.py +3 -3
- flwr/common/record/recorddict.py +5 -5
- flwr/common/record/typeddict.py +9 -2
- flwr/common/recorddict_compat.py +7 -10
- flwr/common/retry_invoker.py +20 -20
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
- flwr/common/serde.py +9 -6
- flwr/common/serde_utils.py +2 -2
- flwr/common/telemetry.py +9 -5
- flwr/common/typing.py +59 -43
- flwr/compat/client/app.py +39 -38
- flwr/compat/client/grpc_client/connection.py +13 -13
- flwr/compat/server/app.py +5 -6
- flwr/proto/appio_pb2.py +13 -3
- flwr/proto/appio_pb2.pyi +134 -65
- flwr/proto/appio_pb2_grpc.py +20 -0
- flwr/proto/appio_pb2_grpc.pyi +27 -0
- flwr/proto/clientappio_pb2.py +17 -7
- flwr/proto/clientappio_pb2.pyi +15 -0
- flwr/proto/clientappio_pb2_grpc.py +206 -40
- flwr/proto/clientappio_pb2_grpc.pyi +168 -53
- flwr/proto/control_pb2.py +72 -40
- flwr/proto/control_pb2.pyi +319 -87
- flwr/proto/control_pb2_grpc.py +339 -28
- flwr/proto/control_pb2_grpc.pyi +209 -37
- flwr/proto/error_pb2.py +13 -3
- flwr/proto/error_pb2.pyi +24 -6
- flwr/proto/error_pb2_grpc.py +20 -0
- flwr/proto/error_pb2_grpc.pyi +27 -0
- flwr/proto/fab_pb2.py +24 -10
- flwr/proto/fab_pb2.pyi +68 -20
- flwr/proto/fab_pb2_grpc.py +20 -0
- flwr/proto/fab_pb2_grpc.pyi +27 -0
- flwr/proto/federation_pb2.py +38 -0
- flwr/proto/federation_pb2.pyi +56 -0
- flwr/proto/federation_pb2_grpc.py +24 -0
- flwr/proto/federation_pb2_grpc.pyi +31 -0
- flwr/proto/fleet_pb2.py +45 -27
- flwr/proto/fleet_pb2.pyi +186 -70
- flwr/proto/fleet_pb2_grpc.py +277 -66
- flwr/proto/fleet_pb2_grpc.pyi +201 -55
- flwr/proto/grpcadapter_pb2.py +14 -4
- flwr/proto/grpcadapter_pb2.pyi +38 -16
- flwr/proto/grpcadapter_pb2_grpc.py +35 -4
- flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
- flwr/proto/heartbeat_pb2.py +17 -7
- flwr/proto/heartbeat_pb2.pyi +51 -22
- flwr/proto/heartbeat_pb2_grpc.py +20 -0
- flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
- flwr/proto/log_pb2.py +13 -3
- flwr/proto/log_pb2.pyi +34 -11
- flwr/proto/log_pb2_grpc.py +20 -0
- flwr/proto/log_pb2_grpc.pyi +27 -0
- flwr/proto/message_pb2.py +15 -5
- flwr/proto/message_pb2.pyi +154 -86
- flwr/proto/message_pb2_grpc.py +20 -0
- flwr/proto/message_pb2_grpc.pyi +27 -0
- flwr/proto/node_pb2.py +16 -4
- flwr/proto/node_pb2.pyi +77 -4
- flwr/proto/node_pb2_grpc.py +20 -0
- flwr/proto/node_pb2_grpc.pyi +27 -0
- flwr/proto/recorddict_pb2.py +13 -3
- flwr/proto/recorddict_pb2.pyi +184 -107
- flwr/proto/recorddict_pb2_grpc.py +20 -0
- flwr/proto/recorddict_pb2_grpc.pyi +27 -0
- flwr/proto/run_pb2.py +40 -31
- flwr/proto/run_pb2.pyi +149 -84
- flwr/proto/run_pb2_grpc.py +20 -0
- flwr/proto/run_pb2_grpc.pyi +27 -0
- flwr/proto/serverappio_pb2.py +13 -3
- flwr/proto/serverappio_pb2.pyi +32 -8
- flwr/proto/serverappio_pb2_grpc.py +246 -65
- flwr/proto/serverappio_pb2_grpc.pyi +221 -85
- flwr/proto/simulationio_pb2.py +16 -8
- flwr/proto/simulationio_pb2.pyi +15 -0
- flwr/proto/simulationio_pb2_grpc.py +162 -41
- flwr/proto/simulationio_pb2_grpc.pyi +149 -55
- flwr/proto/transport_pb2.py +20 -10
- flwr/proto/transport_pb2.pyi +249 -160
- flwr/proto/transport_pb2_grpc.py +35 -4
- flwr/proto/transport_pb2_grpc.pyi +38 -8
- flwr/server/app.py +173 -127
- flwr/server/client_manager.py +4 -5
- flwr/server/client_proxy.py +10 -11
- flwr/server/compat/app.py +4 -5
- flwr/server/compat/app_utils.py +2 -1
- flwr/server/compat/grid_client_proxy.py +10 -12
- flwr/server/compat/legacy_context.py +3 -4
- flwr/server/fleet_event_log_interceptor.py +2 -1
- flwr/server/grid/grid.py +2 -3
- flwr/server/grid/grpc_grid.py +10 -8
- flwr/server/grid/inmemory_grid.py +4 -4
- flwr/server/run_serverapp.py +2 -3
- flwr/server/server.py +34 -39
- flwr/server/server_app.py +7 -8
- flwr/server/server_config.py +1 -2
- flwr/server/serverapp/app.py +34 -28
- flwr/server/serverapp_components.py +4 -5
- flwr/server/strategy/aggregate.py +9 -8
- flwr/server/strategy/bulyan.py +13 -11
- flwr/server/strategy/dp_adaptive_clipping.py +16 -20
- flwr/server/strategy/dp_fixed_clipping.py +12 -17
- flwr/server/strategy/dpfedavg_adaptive.py +3 -4
- flwr/server/strategy/dpfedavg_fixed.py +6 -10
- flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
- flwr/server/strategy/fedadagrad.py +18 -14
- flwr/server/strategy/fedadam.py +16 -14
- flwr/server/strategy/fedavg.py +16 -17
- flwr/server/strategy/fedavg_android.py +15 -15
- flwr/server/strategy/fedavgm.py +21 -18
- flwr/server/strategy/fedmedian.py +2 -3
- flwr/server/strategy/fedopt.py +11 -10
- flwr/server/strategy/fedprox.py +10 -9
- flwr/server/strategy/fedtrimmedavg.py +12 -11
- flwr/server/strategy/fedxgb_bagging.py +13 -11
- flwr/server/strategy/fedxgb_cyclic.py +6 -6
- flwr/server/strategy/fedxgb_nn_avg.py +4 -4
- flwr/server/strategy/fedyogi.py +16 -14
- flwr/server/strategy/krum.py +12 -11
- flwr/server/strategy/qfedavg.py +16 -15
- flwr/server/strategy/strategy.py +6 -9
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +19 -8
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
- flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
- flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
- flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
- flwr/server/superlink/fleet/vce/vce_api.py +32 -13
- flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
- flwr/server/superlink/linkstate/linkstate.py +161 -62
- flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
- flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
- flwr/server/superlink/linkstate/utils.py +9 -60
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
- flwr/server/superlink/utils.py +4 -6
- flwr/server/typing.py +1 -1
- flwr/server/utils/tensorboard.py +15 -8
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/default_workflows.py +5 -5
- flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +12 -10
- flwr/serverapp/strategy/bulyan.py +16 -15
- flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
- flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
- flwr/serverapp/strategy/fedadagrad.py +10 -11
- flwr/serverapp/strategy/fedadam.py +10 -11
- flwr/serverapp/strategy/fedavg.py +9 -10
- flwr/serverapp/strategy/fedavgm.py +17 -16
- flwr/serverapp/strategy/fedmedian.py +2 -2
- flwr/serverapp/strategy/fedopt.py +10 -11
- flwr/serverapp/strategy/fedprox.py +7 -8
- flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
- flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
- flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
- flwr/serverapp/strategy/fedyogi.py +9 -11
- flwr/serverapp/strategy/krum.py +7 -7
- flwr/serverapp/strategy/multikrum.py +9 -9
- flwr/serverapp/strategy/qfedavg.py +17 -16
- flwr/serverapp/strategy/strategy.py +6 -9
- flwr/serverapp/strategy/strategy_utils.py +7 -8
- flwr/simulation/app.py +46 -42
- flwr/simulation/legacy_app.py +12 -12
- flwr/simulation/ray_transport/ray_actor.py +11 -12
- flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
- flwr/simulation/run_simulation.py +44 -43
- flwr/simulation/simulationio_connection.py +4 -4
- flwr/supercore/cli/flower_superexec.py +3 -4
- flwr/supercore/constant.py +52 -0
- flwr/supercore/corestate/corestate.py +24 -3
- flwr/supercore/corestate/in_memory_corestate.py +138 -0
- flwr/supercore/corestate/sqlite_corestate.py +157 -0
- flwr/supercore/ffs/disk_ffs.py +1 -2
- flwr/supercore/ffs/ffs.py +1 -2
- flwr/supercore/ffs/ffs_factory.py +1 -2
- flwr/{common → supercore}/heartbeat.py +20 -25
- flwr/supercore/object_store/in_memory_object_store.py +1 -6
- flwr/supercore/object_store/object_store.py +1 -2
- flwr/supercore/object_store/object_store_factory.py +27 -8
- flwr/supercore/object_store/sqlite_object_store.py +253 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
- flwr/supercore/sqlite_mixin.py +159 -0
- flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
- flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
- flwr/supercore/superexec/run_superexec.py +9 -13
- flwr/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/artifact_provider.py +1 -2
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +88 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
- flwr/superlink/federation/__init__.py +24 -0
- flwr/superlink/federation/federation_manager.py +64 -0
- flwr/superlink/federation/noop_federation_manager.py +71 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
- flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
- flwr/superlink/servicer/control/control_grpc.py +18 -17
- flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
- flwr/superlink/servicer/control/control_servicer.py +239 -63
- flwr/supernode/cli/flower_supernode.py +74 -26
- flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
- flwr/supernode/nodestate/nodestate.py +7 -8
- flwr/supernode/nodestate/nodestate_factory.py +7 -4
- flwr/supernode/runtime/run_clientapp.py +43 -24
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
- flwr/supernode/start_client_internal.py +175 -51
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
- flwr-1.24.0.dist-info/RECORD +454 -0
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/supercore/object_store/utils.py +0 -43
- flwr-1.22.0.dist-info/RECORD +0 -428
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
- {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""SQLite-based CoreState implementation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import secrets
|
|
19
|
+
import sqlite3
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
from flwr.common import now
|
|
23
|
+
from flwr.common.constant import (
|
|
24
|
+
FLWR_APP_TOKEN_LENGTH,
|
|
25
|
+
HEARTBEAT_DEFAULT_INTERVAL,
|
|
26
|
+
HEARTBEAT_PATIENCE,
|
|
27
|
+
)
|
|
28
|
+
from flwr.supercore.sqlite_mixin import SqliteMixin
|
|
29
|
+
from flwr.supercore.utils import int64_to_uint64, uint64_to_int64
|
|
30
|
+
|
|
31
|
+
from ..object_store import ObjectStore
|
|
32
|
+
from .corestate import CoreState
|
|
33
|
+
|
|
34
|
+
SQL_CREATE_TABLE_TOKEN_STORE = """
|
|
35
|
+
CREATE TABLE IF NOT EXISTS token_store (
|
|
36
|
+
run_id INTEGER PRIMARY KEY,
|
|
37
|
+
token TEXT UNIQUE NOT NULL,
|
|
38
|
+
active_until REAL
|
|
39
|
+
);
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SqliteCoreState(CoreState, SqliteMixin):
|
|
44
|
+
"""SQLite-based CoreState implementation."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, database_path: str, object_store: ObjectStore) -> None:
|
|
47
|
+
super().__init__(database_path)
|
|
48
|
+
self._object_store = object_store
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def object_store(self) -> ObjectStore:
|
|
52
|
+
"""Return the ObjectStore instance used by this CoreState."""
|
|
53
|
+
return self._object_store
|
|
54
|
+
|
|
55
|
+
def get_sql_statements(self) -> tuple[str, ...]:
|
|
56
|
+
"""Return SQL statements needed for CoreState tables."""
|
|
57
|
+
return (SQL_CREATE_TABLE_TOKEN_STORE,)
|
|
58
|
+
|
|
59
|
+
def create_token(self, run_id: int) -> str | None:
|
|
60
|
+
"""Create a token for the given run ID."""
|
|
61
|
+
token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
|
|
62
|
+
current = now().timestamp()
|
|
63
|
+
active_until = current + HEARTBEAT_DEFAULT_INTERVAL
|
|
64
|
+
query = """
|
|
65
|
+
INSERT INTO token_store (run_id, token, active_until)
|
|
66
|
+
VALUES (:run_id, :token, :active_until);
|
|
67
|
+
"""
|
|
68
|
+
data = {
|
|
69
|
+
"run_id": uint64_to_int64(run_id),
|
|
70
|
+
"token": token,
|
|
71
|
+
"active_until": active_until,
|
|
72
|
+
}
|
|
73
|
+
try:
|
|
74
|
+
self.query(query, data)
|
|
75
|
+
except sqlite3.IntegrityError:
|
|
76
|
+
return None # Token already created for this run ID
|
|
77
|
+
return token
|
|
78
|
+
|
|
79
|
+
def verify_token(self, run_id: int, token: str) -> bool:
|
|
80
|
+
"""Verify a token for the given run ID."""
|
|
81
|
+
self._cleanup_expired_tokens()
|
|
82
|
+
query = "SELECT token FROM token_store WHERE run_id = :run_id;"
|
|
83
|
+
data = {"run_id": uint64_to_int64(run_id)}
|
|
84
|
+
rows = self.query(query, data)
|
|
85
|
+
if not rows:
|
|
86
|
+
return False
|
|
87
|
+
return cast(str, rows[0]["token"]) == token
|
|
88
|
+
|
|
89
|
+
def delete_token(self, run_id: int) -> None:
|
|
90
|
+
"""Delete the token for the given run ID."""
|
|
91
|
+
query = "DELETE FROM token_store WHERE run_id = :run_id;"
|
|
92
|
+
data = {"run_id": uint64_to_int64(run_id)}
|
|
93
|
+
self.query(query, data)
|
|
94
|
+
|
|
95
|
+
def get_run_id_by_token(self, token: str) -> int | None:
|
|
96
|
+
"""Get the run ID associated with a given token."""
|
|
97
|
+
self._cleanup_expired_tokens()
|
|
98
|
+
query = "SELECT run_id FROM token_store WHERE token = :token;"
|
|
99
|
+
data = {"token": token}
|
|
100
|
+
rows = self.query(query, data)
|
|
101
|
+
if not rows:
|
|
102
|
+
return None
|
|
103
|
+
return int64_to_uint64(rows[0]["run_id"])
|
|
104
|
+
|
|
105
|
+
def acknowledge_app_heartbeat(self, token: str) -> bool:
|
|
106
|
+
"""Acknowledge an app heartbeat with the provided token."""
|
|
107
|
+
# Clean up expired tokens
|
|
108
|
+
self._cleanup_expired_tokens()
|
|
109
|
+
|
|
110
|
+
# Update the active_until field
|
|
111
|
+
current = now().timestamp()
|
|
112
|
+
active_until = current + HEARTBEAT_PATIENCE * HEARTBEAT_DEFAULT_INTERVAL
|
|
113
|
+
query = """
|
|
114
|
+
UPDATE token_store
|
|
115
|
+
SET active_until = :active_until
|
|
116
|
+
WHERE token = :token
|
|
117
|
+
RETURNING run_id;
|
|
118
|
+
"""
|
|
119
|
+
data = {"active_until": active_until, "token": token}
|
|
120
|
+
rows = self.query(query, data)
|
|
121
|
+
return len(rows) > 0
|
|
122
|
+
|
|
123
|
+
def _cleanup_expired_tokens(self) -> None:
|
|
124
|
+
"""Remove expired tokens and perform additional cleanup.
|
|
125
|
+
|
|
126
|
+
This method is called before token operations to ensure integrity.
|
|
127
|
+
Subclasses can override `_on_tokens_expired` to add custom cleanup logic.
|
|
128
|
+
"""
|
|
129
|
+
current = now().timestamp()
|
|
130
|
+
|
|
131
|
+
with self.conn:
|
|
132
|
+
# Delete expired tokens and get their run_ids and active_until timestamps
|
|
133
|
+
query = """
|
|
134
|
+
DELETE FROM token_store
|
|
135
|
+
WHERE active_until < :current
|
|
136
|
+
RETURNING run_id, active_until;
|
|
137
|
+
"""
|
|
138
|
+
rows = self.conn.execute(query, {"current": current}).fetchall()
|
|
139
|
+
expired_records = [
|
|
140
|
+
(int64_to_uint64(row["run_id"]), row["active_until"]) for row in rows
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
# Hook for subclasses
|
|
144
|
+
if expired_records:
|
|
145
|
+
self._on_tokens_expired(expired_records)
|
|
146
|
+
|
|
147
|
+
def _on_tokens_expired(self, expired_records: list[tuple[int, float]]) -> None:
|
|
148
|
+
"""Handle cleanup of expired tokens.
|
|
149
|
+
|
|
150
|
+
Override in subclasses to add custom cleanup logic.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
expired_records : list[tuple[int, float]]
|
|
155
|
+
List of tuples containing (run_id, active_until timestamp)
|
|
156
|
+
for expired tokens.
|
|
157
|
+
"""
|
flwr/supercore/ffs/disk_ffs.py
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import hashlib
|
|
19
19
|
import json
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Optional
|
|
22
21
|
|
|
23
22
|
from .ffs import Ffs
|
|
24
23
|
|
|
@@ -59,7 +58,7 @@ class DiskFfs(Ffs): # pylint: disable=R0904
|
|
|
59
58
|
|
|
60
59
|
return content_hash
|
|
61
60
|
|
|
62
|
-
def get(self, key: str) ->
|
|
61
|
+
def get(self, key: str) -> tuple[bytes, dict[str, str]] | None:
|
|
63
62
|
"""Return tuple containing the object content and metadata.
|
|
64
63
|
|
|
65
64
|
Parameters
|
flwr/supercore/ffs/ffs.py
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import abc
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class Ffs(abc.ABC): # pylint: disable=R0904
|
|
@@ -40,7 +39,7 @@ class Ffs(abc.ABC): # pylint: disable=R0904
|
|
|
40
39
|
"""
|
|
41
40
|
|
|
42
41
|
@abc.abstractmethod
|
|
43
|
-
def get(self, key: str) ->
|
|
42
|
+
def get(self, key: str) -> tuple[bytes, dict[str, str]] | None:
|
|
44
43
|
"""Return tuple containing the object content and metadata.
|
|
45
44
|
|
|
46
45
|
Parameters
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from logging import DEBUG
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from flwr.common.logger import log
|
|
22
21
|
|
|
@@ -35,7 +34,7 @@ class FfsFactory:
|
|
|
35
34
|
|
|
36
35
|
def __init__(self, base_dir: str) -> None:
|
|
37
36
|
self.base_dir = base_dir
|
|
38
|
-
self.ffs_instance:
|
|
37
|
+
self.ffs_instance: Ffs | None = None
|
|
39
38
|
|
|
40
39
|
def ffs(self) -> Ffs:
|
|
41
40
|
"""Return a Ffs instance and create it, if necessary."""
|
|
@@ -16,24 +16,27 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import random
|
|
19
|
+
import signal
|
|
19
20
|
import threading
|
|
20
|
-
from
|
|
21
|
+
from collections.abc import Callable
|
|
21
22
|
|
|
22
23
|
import grpc
|
|
23
24
|
|
|
25
|
+
from flwr.common.constant import (
|
|
26
|
+
HEARTBEAT_BASE_MULTIPLIER,
|
|
27
|
+
HEARTBEAT_CALL_TIMEOUT,
|
|
28
|
+
HEARTBEAT_DEFAULT_INTERVAL,
|
|
29
|
+
HEARTBEAT_RANDOM_RANGE,
|
|
30
|
+
)
|
|
31
|
+
from flwr.common.retry_invoker import RetryInvoker, exponential
|
|
32
|
+
from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
|
|
33
|
+
|
|
24
34
|
# pylint: disable=E0611
|
|
25
35
|
from flwr.proto.heartbeat_pb2 import SendAppHeartbeatRequest
|
|
26
36
|
from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub
|
|
27
37
|
from flwr.proto.simulationio_pb2_grpc import SimulationIoStub
|
|
28
38
|
|
|
29
39
|
# pylint: enable=E0611
|
|
30
|
-
from .constant import (
|
|
31
|
-
HEARTBEAT_BASE_MULTIPLIER,
|
|
32
|
-
HEARTBEAT_CALL_TIMEOUT,
|
|
33
|
-
HEARTBEAT_DEFAULT_INTERVAL,
|
|
34
|
-
HEARTBEAT_RANDOM_RANGE,
|
|
35
|
-
)
|
|
36
|
-
from .retry_invoker import RetryInvoker, exponential
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
class HeartbeatFailure(Exception):
|
|
@@ -116,24 +119,18 @@ class HeartbeatSender:
|
|
|
116
119
|
raise HeartbeatFailure
|
|
117
120
|
|
|
118
121
|
|
|
119
|
-
def
|
|
120
|
-
stub:
|
|
121
|
-
|
|
122
|
-
*,
|
|
123
|
-
failure_message: str,
|
|
122
|
+
def make_app_heartbeat_fn_grpc(
|
|
123
|
+
stub: ServerAppIoStub | SimulationIoStub | ClientAppIoStub,
|
|
124
|
+
token: str,
|
|
124
125
|
) -> Callable[[], bool]:
|
|
125
|
-
"""Get the function to send a heartbeat to gRPC endpoint.
|
|
126
|
-
|
|
127
|
-
This function is for app heartbeats only. It is not used for node heartbeats.
|
|
126
|
+
"""Get the function to send a heartbeat to gRPC endpoint from an app process.
|
|
128
127
|
|
|
129
128
|
Parameters
|
|
130
129
|
----------
|
|
131
130
|
stub : Union[ServerAppIoStub, SimulationIoStub]
|
|
132
131
|
gRPC stub to send the heartbeat.
|
|
133
|
-
|
|
134
|
-
The
|
|
135
|
-
failure_message : str
|
|
136
|
-
Error message to raise if the heartbeat fails.
|
|
132
|
+
token : str
|
|
133
|
+
The token to use in the heartbeat request.
|
|
137
134
|
|
|
138
135
|
Returns
|
|
139
136
|
-------
|
|
@@ -141,9 +138,7 @@ def get_grpc_app_heartbeat_fn(
|
|
|
141
138
|
Function that sends a heartbeat to the gRPC endpoint.
|
|
142
139
|
"""
|
|
143
140
|
# Construct the heartbeat request
|
|
144
|
-
req = SendAppHeartbeatRequest(
|
|
145
|
-
run_id=run_id, heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL
|
|
146
|
-
)
|
|
141
|
+
req = SendAppHeartbeatRequest(token=token)
|
|
147
142
|
|
|
148
143
|
def fn() -> bool:
|
|
149
144
|
# Call ServerAppIo API
|
|
@@ -157,9 +152,9 @@ def get_grpc_app_heartbeat_fn(
|
|
|
157
152
|
return False
|
|
158
153
|
raise
|
|
159
154
|
|
|
160
|
-
#
|
|
155
|
+
# Raise SIGINT to trigger graceful shutdown if heartbeat failed
|
|
161
156
|
if not res.success:
|
|
162
|
-
|
|
157
|
+
signal.raise_signal(signal.SIGINT)
|
|
163
158
|
return True
|
|
164
159
|
|
|
165
160
|
return fn
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
import threading
|
|
19
19
|
from dataclasses import dataclass
|
|
20
|
-
from typing import Optional
|
|
21
20
|
|
|
22
21
|
from flwr.common.inflatable import (
|
|
23
22
|
get_object_id,
|
|
@@ -48,9 +47,6 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
48
47
|
self.verify = verify
|
|
49
48
|
self.store: dict[str, ObjectEntry] = {}
|
|
50
49
|
self.lock_store = threading.RLock()
|
|
51
|
-
# Mapping the Object ID of a message to the list of descendant object IDs
|
|
52
|
-
self.msg_descendant_objects_mapping: dict[str, list[str]] = {}
|
|
53
|
-
self.lock_msg_mapping = threading.RLock()
|
|
54
50
|
# Mapping each run ID to a set of object IDs that are used in that run
|
|
55
51
|
self.run_objects_mapping: dict[int, set[str]] = {}
|
|
56
52
|
|
|
@@ -157,7 +153,7 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
157
153
|
self.store[object_id].content = object_content
|
|
158
154
|
self.store[object_id].is_available = True
|
|
159
155
|
|
|
160
|
-
def get(self, object_id: str) ->
|
|
156
|
+
def get(self, object_id: str) -> bytes | None:
|
|
161
157
|
"""Get an object from the store."""
|
|
162
158
|
with self.lock_store:
|
|
163
159
|
# Check if the object ID is pre-registered
|
|
@@ -215,7 +211,6 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
215
211
|
"""Clear the store."""
|
|
216
212
|
with self.lock_store:
|
|
217
213
|
self.store.clear()
|
|
218
|
-
self.msg_descendant_objects_mapping.clear()
|
|
219
214
|
self.run_objects_mapping.clear()
|
|
220
215
|
|
|
221
216
|
def __contains__(self, object_id: str) -> bool:
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import abc
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
22
21
|
|
|
@@ -89,7 +88,7 @@ class ObjectStore(abc.ABC):
|
|
|
89
88
|
"""
|
|
90
89
|
|
|
91
90
|
@abc.abstractmethod
|
|
92
|
-
def get(self, object_id: str) ->
|
|
91
|
+
def get(self, object_id: str) -> bytes | None:
|
|
93
92
|
"""Get an object from the store.
|
|
94
93
|
|
|
95
94
|
Parameters
|
|
@@ -16,19 +16,30 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from logging import DEBUG
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
from flwr.common.logger import log
|
|
21
|
+
from flwr.supercore.constant import FLWR_IN_MEMORY_DB_NAME
|
|
22
22
|
|
|
23
23
|
from .in_memory_object_store import InMemoryObjectStore
|
|
24
24
|
from .object_store import ObjectStore
|
|
25
|
+
from .sqlite_object_store import SqliteObjectStore
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class ObjectStoreFactory:
|
|
28
|
-
"""Factory class that creates ObjectStore instances.
|
|
29
|
+
"""Factory class that creates ObjectStore instances.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
database : str (default: FLWR_IN_MEMORY_DB_NAME)
|
|
34
|
+
A string representing the path to the database file that will be opened.
|
|
35
|
+
Note that passing ":memory:" will open a connection to a database that is
|
|
36
|
+
in RAM, instead of on disk. And FLWR_IN_MEMORY_DB_NAME will create an
|
|
37
|
+
Python-based in-memory ObjectStore.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, database: str = FLWR_IN_MEMORY_DB_NAME) -> None:
|
|
41
|
+
self.database = database
|
|
42
|
+
self.store_instance: ObjectStore | None = None
|
|
32
43
|
|
|
33
44
|
def store(self) -> ObjectStore:
|
|
34
45
|
"""Return an ObjectStore instance and create it, if necessary.
|
|
@@ -38,7 +49,15 @@ class ObjectStoreFactory:
|
|
|
38
49
|
ObjectStore
|
|
39
50
|
An ObjectStore instance for storing objects by object_id.
|
|
40
51
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
# InMemoryObjectStore
|
|
53
|
+
if self.database == FLWR_IN_MEMORY_DB_NAME:
|
|
54
|
+
if self.store_instance is None:
|
|
55
|
+
self.store_instance = InMemoryObjectStore()
|
|
56
|
+
log(DEBUG, "Using InMemoryObjectStore")
|
|
57
|
+
return self.store_instance
|
|
58
|
+
|
|
59
|
+
# SqliteObjectStore
|
|
60
|
+
store = SqliteObjectStore(self.database)
|
|
61
|
+
store.initialize()
|
|
62
|
+
log(DEBUG, "Using SqliteObjectStore")
|
|
63
|
+
return store
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Flower SQLite ObjectStore implementation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from typing import cast
|
|
19
|
+
|
|
20
|
+
from flwr.common.inflatable import (
|
|
21
|
+
get_object_id,
|
|
22
|
+
is_valid_sha256_hash,
|
|
23
|
+
iterate_object_tree,
|
|
24
|
+
)
|
|
25
|
+
from flwr.common.inflatable_utils import validate_object_content
|
|
26
|
+
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
27
|
+
from flwr.supercore.sqlite_mixin import SqliteMixin
|
|
28
|
+
from flwr.supercore.utils import uint64_to_int64
|
|
29
|
+
|
|
30
|
+
from .object_store import NoObjectInStoreError, ObjectStore
|
|
31
|
+
|
|
32
|
+
SQL_CREATE_OBJECTS = """
|
|
33
|
+
CREATE TABLE IF NOT EXISTS objects (
|
|
34
|
+
object_id TEXT PRIMARY KEY,
|
|
35
|
+
content BLOB,
|
|
36
|
+
is_available INTEGER NOT NULL CHECK (is_available IN (0,1)),
|
|
37
|
+
ref_count INTEGER NOT NULL
|
|
38
|
+
);
|
|
39
|
+
"""
|
|
40
|
+
SQL_CREATE_OBJECT_CHILDREN = """
|
|
41
|
+
CREATE TABLE IF NOT EXISTS object_children (
|
|
42
|
+
parent_id TEXT NOT NULL,
|
|
43
|
+
child_id TEXT NOT NULL,
|
|
44
|
+
FOREIGN KEY (parent_id) REFERENCES objects(object_id) ON DELETE CASCADE,
|
|
45
|
+
FOREIGN KEY (child_id) REFERENCES objects(object_id) ON DELETE CASCADE,
|
|
46
|
+
PRIMARY KEY (parent_id, child_id)
|
|
47
|
+
);
|
|
48
|
+
"""
|
|
49
|
+
SQL_CREATE_RUN_OBJECTS = """
|
|
50
|
+
CREATE TABLE IF NOT EXISTS run_objects (
|
|
51
|
+
run_id INTEGER NOT NULL,
|
|
52
|
+
object_id TEXT NOT NULL,
|
|
53
|
+
FOREIGN KEY (object_id) REFERENCES objects(object_id) ON DELETE CASCADE,
|
|
54
|
+
PRIMARY KEY (run_id, object_id)
|
|
55
|
+
);
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
60
|
+
"""SQLite-based implementation of the ObjectStore interface."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, database_path: str, verify: bool = True) -> None:
|
|
63
|
+
super().__init__(database_path)
|
|
64
|
+
self.verify = verify
|
|
65
|
+
|
|
66
|
+
def get_sql_statements(self) -> tuple[str, ...]:
|
|
67
|
+
"""Return SQL statements for ObjectStore tables."""
|
|
68
|
+
return (
|
|
69
|
+
SQL_CREATE_OBJECTS,
|
|
70
|
+
SQL_CREATE_OBJECT_CHILDREN,
|
|
71
|
+
SQL_CREATE_RUN_OBJECTS,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
75
|
+
"""Identify and preregister missing objects in the `ObjectStore`."""
|
|
76
|
+
new_objects = []
|
|
77
|
+
for tree_node in iterate_object_tree(object_tree):
|
|
78
|
+
obj_id = tree_node.object_id
|
|
79
|
+
if not is_valid_sha256_hash(obj_id):
|
|
80
|
+
raise ValueError(f"Invalid object ID format: {obj_id}")
|
|
81
|
+
|
|
82
|
+
child_ids = [child.object_id for child in tree_node.children]
|
|
83
|
+
with self.conn:
|
|
84
|
+
row = self.conn.execute(
|
|
85
|
+
"SELECT object_id, is_available FROM objects WHERE object_id=?",
|
|
86
|
+
(obj_id,),
|
|
87
|
+
).fetchone()
|
|
88
|
+
if row is None:
|
|
89
|
+
# Insert new object
|
|
90
|
+
self.conn.execute(
|
|
91
|
+
"INSERT INTO objects"
|
|
92
|
+
"(object_id, content, is_available, ref_count) "
|
|
93
|
+
"VALUES (?, ?, ?, ?)",
|
|
94
|
+
(obj_id, b"", 0, 0),
|
|
95
|
+
)
|
|
96
|
+
for cid in child_ids:
|
|
97
|
+
self.conn.execute(
|
|
98
|
+
"INSERT INTO object_children(parent_id, child_id) "
|
|
99
|
+
"VALUES (?, ?)",
|
|
100
|
+
(obj_id, cid),
|
|
101
|
+
)
|
|
102
|
+
self.conn.execute(
|
|
103
|
+
"UPDATE objects SET ref_count = ref_count + 1 "
|
|
104
|
+
"WHERE object_id = ?",
|
|
105
|
+
(cid,),
|
|
106
|
+
)
|
|
107
|
+
new_objects.append(obj_id)
|
|
108
|
+
else:
|
|
109
|
+
# Add to the list of new objects if not available
|
|
110
|
+
if not row["is_available"]:
|
|
111
|
+
new_objects.append(obj_id)
|
|
112
|
+
|
|
113
|
+
# Ensure run mapping
|
|
114
|
+
self.conn.execute(
|
|
115
|
+
"INSERT OR IGNORE INTO run_objects(run_id, object_id) "
|
|
116
|
+
"VALUES (?, ?)",
|
|
117
|
+
(uint64_to_int64(run_id), obj_id),
|
|
118
|
+
)
|
|
119
|
+
return new_objects
|
|
120
|
+
|
|
121
|
+
def get_object_tree(self, object_id: str) -> ObjectTree:
|
|
122
|
+
"""Get the object tree for a given object ID."""
|
|
123
|
+
with self.conn:
|
|
124
|
+
row = self.conn.execute(
|
|
125
|
+
"SELECT object_id FROM objects WHERE object_id=?", (object_id,)
|
|
126
|
+
).fetchone()
|
|
127
|
+
if not row:
|
|
128
|
+
raise NoObjectInStoreError(
|
|
129
|
+
f"Object {object_id} was not pre-registered."
|
|
130
|
+
)
|
|
131
|
+
children = self.query(
|
|
132
|
+
"SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Build the object trees of all children
|
|
136
|
+
try:
|
|
137
|
+
child_trees = [self.get_object_tree(ch["child_id"]) for ch in children]
|
|
138
|
+
except NoObjectInStoreError as e:
|
|
139
|
+
# Raise an error if any child object is missing
|
|
140
|
+
# This indicates an integrity issue
|
|
141
|
+
raise NoObjectInStoreError(
|
|
142
|
+
f"Object tree for object ID '{object_id}' contains missing "
|
|
143
|
+
"children. This may indicate a corrupted object store."
|
|
144
|
+
) from e
|
|
145
|
+
|
|
146
|
+
# Create and return the ObjectTree for the current object
|
|
147
|
+
return ObjectTree(object_id=object_id, children=child_trees)
|
|
148
|
+
|
|
149
|
+
def put(self, object_id: str, object_content: bytes) -> None:
|
|
150
|
+
"""Put an object into the store."""
|
|
151
|
+
if self.verify:
|
|
152
|
+
# Verify object_id and object_content match
|
|
153
|
+
object_id_from_content = get_object_id(object_content)
|
|
154
|
+
if object_id != object_id_from_content:
|
|
155
|
+
raise ValueError(f"Object ID {object_id} does not match content hash")
|
|
156
|
+
|
|
157
|
+
# Validate object content
|
|
158
|
+
validate_object_content(content=object_content)
|
|
159
|
+
|
|
160
|
+
with self.conn:
|
|
161
|
+
# Only allow adding the object if it has been preregistered
|
|
162
|
+
row = self.conn.execute(
|
|
163
|
+
"SELECT is_available FROM objects WHERE object_id=?", (object_id,)
|
|
164
|
+
).fetchone()
|
|
165
|
+
if row is None:
|
|
166
|
+
raise NoObjectInStoreError(
|
|
167
|
+
f"Object with ID '{object_id}' was not pre-registered."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Return if object is already present in the store
|
|
171
|
+
if row["is_available"]:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Update the object entry in the store
|
|
175
|
+
self.conn.execute(
|
|
176
|
+
"UPDATE objects SET content=?, is_available=1 WHERE object_id=?",
|
|
177
|
+
(object_content, object_id),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def get(self, object_id: str) -> bytes | None:
|
|
181
|
+
"""Get an object from the store."""
|
|
182
|
+
rows = self.query("SELECT content FROM objects WHERE object_id=?", (object_id,))
|
|
183
|
+
return rows[0]["content"] if rows else None
|
|
184
|
+
|
|
185
|
+
def delete(self, object_id: str) -> None:
|
|
186
|
+
"""Delete an object and its unreferenced descendants from the store."""
|
|
187
|
+
with self.conn:
|
|
188
|
+
row = self.conn.execute(
|
|
189
|
+
"SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
|
|
190
|
+
).fetchone()
|
|
191
|
+
|
|
192
|
+
# If the object is not in the store, nothing to delete
|
|
193
|
+
if row is None:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# Skip deletion if there are still references
|
|
197
|
+
if row["ref_count"] > 0:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Deleting will cascade via FK, but we need to decrement children first
|
|
201
|
+
children = self.conn.execute(
|
|
202
|
+
"SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
|
|
203
|
+
).fetchall()
|
|
204
|
+
child_ids = [child["child_id"] for child in children]
|
|
205
|
+
|
|
206
|
+
if child_ids:
|
|
207
|
+
placeholders = ", ".join("?" for _ in child_ids)
|
|
208
|
+
query = f"""
|
|
209
|
+
UPDATE objects SET ref_count = ref_count - 1
|
|
210
|
+
WHERE object_id IN ({placeholders})
|
|
211
|
+
"""
|
|
212
|
+
self.conn.execute(query, child_ids)
|
|
213
|
+
|
|
214
|
+
self.conn.execute("DELETE FROM objects WHERE object_id=?", (object_id,))
|
|
215
|
+
|
|
216
|
+
# Recursively clean children
|
|
217
|
+
for child_id in child_ids:
|
|
218
|
+
self.delete(child_id)
|
|
219
|
+
|
|
220
|
+
def delete_objects_in_run(self, run_id: int) -> None:
|
|
221
|
+
"""Delete all objects that were registered in a specific run."""
|
|
222
|
+
run_id_sint = uint64_to_int64(run_id)
|
|
223
|
+
with self.conn:
|
|
224
|
+
objs = self.conn.execute(
|
|
225
|
+
"SELECT object_id FROM run_objects WHERE run_id=?", (run_id_sint,)
|
|
226
|
+
).fetchall()
|
|
227
|
+
for obj in objs:
|
|
228
|
+
object_id = obj["object_id"]
|
|
229
|
+
row = self.conn.execute(
|
|
230
|
+
"SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
|
|
231
|
+
).fetchone()
|
|
232
|
+
if row and row["ref_count"] == 0:
|
|
233
|
+
self.delete(object_id)
|
|
234
|
+
self.conn.execute("DELETE FROM run_objects WHERE run_id=?", (run_id_sint,))
|
|
235
|
+
|
|
236
|
+
def clear(self) -> None:
|
|
237
|
+
"""Clear the store."""
|
|
238
|
+
with self.conn:
|
|
239
|
+
self.conn.execute("DELETE FROM object_children;")
|
|
240
|
+
self.conn.execute("DELETE FROM run_objects;")
|
|
241
|
+
self.conn.execute("DELETE FROM objects;")
|
|
242
|
+
|
|
243
|
+
def __contains__(self, object_id: str) -> bool:
|
|
244
|
+
"""Check if an object_id is in the store."""
|
|
245
|
+
row = self.conn.execute(
|
|
246
|
+
"SELECT 1 FROM objects WHERE object_id=?", (object_id,)
|
|
247
|
+
).fetchone()
|
|
248
|
+
return row is not None
|
|
249
|
+
|
|
250
|
+
def __len__(self) -> int:
|
|
251
|
+
"""Return the number of objects in the store."""
|
|
252
|
+
row = self.conn.execute("SELECT COUNT(*) AS cnt FROM objects;").fetchone()
|
|
253
|
+
return cast(int, row["cnt"])
|
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""Flower
|
|
15
|
+
"""Cryptographic primitives for the Flower infrastructure."""
|