flwr 1.25.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 +18 -69
- 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 +28 -58
- flwr/cli/new/new.py +9 -29
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +85 -93
- flwr/cli/run_utils.py +1 -1
- flwr/cli/stop.py +32 -73
- flwr/cli/supernode/ls.py +25 -57
- 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 -275
- flwr/client/grpc_rere_client/connection.py +3 -3
- 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/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 +7 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +4 -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/run_pb2.py +5 -13
- flwr/proto/run_pb2.pyi +0 -57
- 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 +6 -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 +6 -5
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
- flwr/server/superlink/linkstate/linkstate.py +2 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
- 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 +24 -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/supercore/state/__init__.py +15 -0
- flwr/supercore/state/alembic/__init__.py +15 -0
- 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/supercore/state/alembic/versions/__init__.py +15 -0
- 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/supercore/state/schema/__init__.py +15 -0
- 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 +36 -1
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_servicer.py +19 -17
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
- flwr/supernode/start_client_internal.py +10 -6
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
- flwr/cli/federation/show.py +0 -318
- flwr/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
"""Abstract base classes for credential store."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CredentialStore(ABC):
|
|
22
|
+
"""Abstract base class for credential store."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def set(self, key: str, value: bytes) -> None:
|
|
26
|
+
"""Set a credential in the store."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def get(self, key: str) -> bytes | None:
|
|
30
|
+
"""Get a credential from the store."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def delete(self, key: str) -> None:
|
|
34
|
+
"""Delete a credential from the store."""
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
"""File-based credential store implementation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
from ..utils import get_flwr_home
|
|
25
|
+
from .credential_store import CredentialStore
|
|
26
|
+
|
|
27
|
+
CREDENTIAL_FILE_PATH = get_flwr_home() / "credentials.yaml"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileCredentialStore(CredentialStore):
|
|
31
|
+
"""File-based credential store implementation."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, file_path: Path | None = None) -> None:
|
|
34
|
+
"""Initialize the file credential store.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
file_path : Path | None
|
|
39
|
+
Path to the credentials file. If None, uses default path.
|
|
40
|
+
"""
|
|
41
|
+
self.file_path = file_path or CREDENTIAL_FILE_PATH
|
|
42
|
+
|
|
43
|
+
def _load_credentials(self) -> dict[str, str]:
|
|
44
|
+
"""Load credentials from file."""
|
|
45
|
+
if not self.file_path.exists():
|
|
46
|
+
return {}
|
|
47
|
+
with self.file_path.open("r", encoding="utf-8") as f:
|
|
48
|
+
data = yaml.safe_load(f)
|
|
49
|
+
return cast(dict[str, str], data) if data else {}
|
|
50
|
+
|
|
51
|
+
def _save_credentials(self, credentials: dict[str, str]) -> None:
|
|
52
|
+
"""Save credentials to file."""
|
|
53
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
with self.file_path.open("w", encoding="utf-8") as f:
|
|
55
|
+
yaml.safe_dump(credentials, f)
|
|
56
|
+
|
|
57
|
+
def set(self, key: str, value: bytes) -> None:
|
|
58
|
+
"""Set a credential in the store."""
|
|
59
|
+
credentials = self._load_credentials()
|
|
60
|
+
credentials[key] = base64.b64encode(value).decode("utf-8")
|
|
61
|
+
self._save_credentials(credentials)
|
|
62
|
+
|
|
63
|
+
def get(self, key: str) -> bytes | None:
|
|
64
|
+
"""Get a credential from the store."""
|
|
65
|
+
credentials = self._load_credentials()
|
|
66
|
+
encoded_value = credentials.get(key)
|
|
67
|
+
if encoded_value is None:
|
|
68
|
+
return None
|
|
69
|
+
return base64.b64decode(encoded_value)
|
|
70
|
+
|
|
71
|
+
def delete(self, key: str) -> None:
|
|
72
|
+
"""Delete a credential from the store."""
|
|
73
|
+
credentials = self._load_credentials()
|
|
74
|
+
if key in credentials:
|
|
75
|
+
del credentials[key]
|
|
76
|
+
self._save_credentials(credentials)
|
|
@@ -23,17 +23,6 @@ def now() -> datetime.datetime:
|
|
|
23
23
|
return datetime.datetime.now(tz=datetime.timezone.utc)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def format_timedelta(td: datetime.timedelta) -> str:
|
|
27
|
-
"""Format a timedelta as a string."""
|
|
28
|
-
days = td.days
|
|
29
|
-
hours, remainder = divmod(td.seconds, 3600)
|
|
30
|
-
minutes, seconds = divmod(remainder, 60)
|
|
31
|
-
|
|
32
|
-
if days > 0:
|
|
33
|
-
return f"{days}d {hours:02}:{minutes:02}:{seconds:02}"
|
|
34
|
-
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
35
|
-
|
|
36
|
-
|
|
37
26
|
def isoformat8601_utc(dt: datetime.datetime) -> str:
|
|
38
27
|
"""Return the datetime formatted as an ISO 8601 string with a trailing 'Z'."""
|
|
39
28
|
if dt.tzinfo != datetime.timezone.utc:
|
flwr/supercore/ffs/disk_ffs.py
CHANGED
|
@@ -33,7 +33,7 @@ class DiskFfs(Ffs): # pylint: disable=R0904
|
|
|
33
33
|
base_dir : str
|
|
34
34
|
The base directory to store the objects.
|
|
35
35
|
"""
|
|
36
|
-
self.base_dir = Path(base_dir)
|
|
36
|
+
self.base_dir = Path(base_dir).expanduser()
|
|
37
37
|
|
|
38
38
|
def put(self, content: bytes, meta: dict[str, str]) -> str:
|
|
39
39
|
"""Store bytes and metadata and return key (hash of content).
|
|
@@ -22,7 +22,7 @@ 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 .
|
|
25
|
+
from .sql_object_store import SqlObjectStore
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class ObjectStoreFactory:
|
|
@@ -49,15 +49,23 @@ class ObjectStoreFactory:
|
|
|
49
49
|
ObjectStore
|
|
50
50
|
An ObjectStore instance for storing objects by object_id.
|
|
51
51
|
"""
|
|
52
|
+
# Return cached store if it exists
|
|
53
|
+
if self.store_instance is not None:
|
|
54
|
+
if self.database == FLWR_IN_MEMORY_DB_NAME:
|
|
55
|
+
log(DEBUG, "Using InMemoryObjectStore")
|
|
56
|
+
else:
|
|
57
|
+
log(DEBUG, "Using SqlObjectStore")
|
|
58
|
+
return self.store_instance
|
|
59
|
+
|
|
52
60
|
# InMemoryObjectStore
|
|
53
61
|
if self.database == FLWR_IN_MEMORY_DB_NAME:
|
|
54
|
-
|
|
55
|
-
self.store_instance = InMemoryObjectStore()
|
|
62
|
+
self.store_instance = InMemoryObjectStore()
|
|
56
63
|
log(DEBUG, "Using InMemoryObjectStore")
|
|
57
64
|
return self.store_instance
|
|
58
65
|
|
|
59
|
-
#
|
|
60
|
-
store =
|
|
66
|
+
# SqlObjectStore
|
|
67
|
+
store = SqlObjectStore(self.database)
|
|
61
68
|
store.initialize()
|
|
62
|
-
|
|
69
|
+
self.store_instance = store
|
|
70
|
+
log(DEBUG, "Using SqlObjectStore")
|
|
63
71
|
return store
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2026 Flower Labs GmbH. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""Flower
|
|
15
|
+
"""Flower SQLAlchemy-based ObjectStore implementation."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from
|
|
18
|
+
from sqlalchemy import MetaData
|
|
19
19
|
|
|
20
20
|
from flwr.common.inflatable import (
|
|
21
21
|
get_object_id,
|
|
@@ -24,52 +24,23 @@ from flwr.common.inflatable import (
|
|
|
24
24
|
)
|
|
25
25
|
from flwr.common.inflatable_utils import validate_object_content
|
|
26
26
|
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
27
|
-
from flwr.supercore.
|
|
27
|
+
from flwr.supercore.sql_mixin import SqlMixin
|
|
28
|
+
from flwr.supercore.state.schema.objectstore_tables import create_objectstore_metadata
|
|
28
29
|
from flwr.supercore.utils import uint64_to_int64
|
|
29
30
|
|
|
30
31
|
from .object_store import NoObjectInStoreError, ObjectStore
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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."""
|
|
33
|
+
|
|
34
|
+
class SqlObjectStore(ObjectStore, SqlMixin):
|
|
35
|
+
"""SQLAlchemy-based implementation of the ObjectStore interface."""
|
|
61
36
|
|
|
62
37
|
def __init__(self, database_path: str, verify: bool = True) -> None:
|
|
63
38
|
super().__init__(database_path)
|
|
64
39
|
self.verify = verify
|
|
65
40
|
|
|
66
|
-
def
|
|
67
|
-
"""Return
|
|
68
|
-
return (
|
|
69
|
-
SQL_CREATE_OBJECTS,
|
|
70
|
-
SQL_CREATE_OBJECT_CHILDREN,
|
|
71
|
-
SQL_CREATE_RUN_OBJECTS,
|
|
72
|
-
)
|
|
41
|
+
def get_metadata(self) -> MetaData:
|
|
42
|
+
"""Return SQLAlchemy MetaData for ObjectStore tables."""
|
|
43
|
+
return create_objectstore_metadata()
|
|
73
44
|
|
|
74
45
|
def preregister(self, run_id: int, object_tree: ObjectTree) -> list[str]:
|
|
75
46
|
"""Identify and preregister missing objects in the `ObjectStore`."""
|
|
@@ -80,56 +51,68 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
|
80
51
|
raise ValueError(f"Invalid object ID format: {obj_id}")
|
|
81
52
|
|
|
82
53
|
child_ids = [child.object_id for child in tree_node.children]
|
|
83
|
-
with self.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
54
|
+
with self.session():
|
|
55
|
+
# Insert new object if it doesn't exist (race-condition safe)
|
|
56
|
+
# RETURNING returns a row only if the insert succeeded
|
|
57
|
+
rows = self.query(
|
|
58
|
+
"INSERT INTO objects "
|
|
59
|
+
"(object_id, content, is_available, ref_count) "
|
|
60
|
+
"VALUES (:object_id, :content, :is_available, :ref_count) "
|
|
61
|
+
"ON CONFLICT (object_id) DO NOTHING "
|
|
62
|
+
"RETURNING object_id",
|
|
63
|
+
{
|
|
64
|
+
"object_id": obj_id,
|
|
65
|
+
"content": b"",
|
|
66
|
+
"is_available": 0,
|
|
67
|
+
"ref_count": 0,
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if rows:
|
|
72
|
+
# New object inserted: set up child relationships
|
|
96
73
|
for cid in child_ids:
|
|
97
|
-
self.
|
|
98
|
-
"INSERT INTO object_children(parent_id, child_id) "
|
|
99
|
-
"VALUES (
|
|
100
|
-
|
|
74
|
+
self.query(
|
|
75
|
+
"INSERT INTO object_children (parent_id, child_id) "
|
|
76
|
+
"VALUES (:parent_id, :child_id)",
|
|
77
|
+
{"parent_id": obj_id, "child_id": cid},
|
|
101
78
|
)
|
|
102
|
-
self.
|
|
79
|
+
self.query(
|
|
103
80
|
"UPDATE objects SET ref_count = ref_count + 1 "
|
|
104
|
-
"WHERE object_id =
|
|
105
|
-
|
|
81
|
+
"WHERE object_id = :object_id",
|
|
82
|
+
{"object_id": cid},
|
|
106
83
|
)
|
|
107
84
|
new_objects.append(obj_id)
|
|
108
85
|
else:
|
|
109
|
-
#
|
|
110
|
-
|
|
86
|
+
# Object exists: check if unavailable
|
|
87
|
+
rows = self.query(
|
|
88
|
+
"SELECT is_available FROM objects WHERE object_id = :object_id",
|
|
89
|
+
{"object_id": obj_id},
|
|
90
|
+
)
|
|
91
|
+
if rows and not rows[0]["is_available"]:
|
|
111
92
|
new_objects.append(obj_id)
|
|
112
93
|
|
|
113
94
|
# Ensure run mapping
|
|
114
|
-
self.
|
|
115
|
-
"INSERT
|
|
116
|
-
"VALUES (
|
|
117
|
-
|
|
95
|
+
self.query(
|
|
96
|
+
"INSERT INTO run_objects (run_id, object_id) "
|
|
97
|
+
"VALUES (:run_id, :object_id) ON CONFLICT DO NOTHING",
|
|
98
|
+
{"run_id": uint64_to_int64(run_id), "object_id": obj_id},
|
|
118
99
|
)
|
|
119
100
|
return new_objects
|
|
120
101
|
|
|
121
102
|
def get_object_tree(self, object_id: str) -> ObjectTree:
|
|
122
103
|
"""Get the object tree for a given object ID."""
|
|
123
|
-
with self.
|
|
124
|
-
|
|
125
|
-
"SELECT object_id FROM objects WHERE object_id
|
|
126
|
-
|
|
127
|
-
|
|
104
|
+
with self.session():
|
|
105
|
+
rows = self.query(
|
|
106
|
+
"SELECT object_id FROM objects WHERE object_id = :object_id",
|
|
107
|
+
{"object_id": object_id},
|
|
108
|
+
)
|
|
109
|
+
if not rows:
|
|
128
110
|
raise NoObjectInStoreError(
|
|
129
111
|
f"Object {object_id} was not pre-registered."
|
|
130
112
|
)
|
|
131
113
|
children = self.query(
|
|
132
|
-
"SELECT child_id FROM object_children WHERE parent_id
|
|
114
|
+
"SELECT child_id FROM object_children WHERE parent_id = :parent_id",
|
|
115
|
+
{"parent_id": object_id},
|
|
133
116
|
)
|
|
134
117
|
|
|
135
118
|
# Build the object trees of all children
|
|
@@ -157,61 +140,71 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
|
157
140
|
# Validate object content
|
|
158
141
|
validate_object_content(content=object_content)
|
|
159
142
|
|
|
160
|
-
with self.
|
|
143
|
+
with self.session():
|
|
161
144
|
# Only allow adding the object if it has been preregistered
|
|
162
|
-
|
|
163
|
-
"SELECT is_available FROM objects WHERE object_id
|
|
164
|
-
|
|
165
|
-
|
|
145
|
+
rows = self.query(
|
|
146
|
+
"SELECT is_available FROM objects WHERE object_id = :object_id",
|
|
147
|
+
{"object_id": object_id},
|
|
148
|
+
)
|
|
149
|
+
if not rows:
|
|
166
150
|
raise NoObjectInStoreError(
|
|
167
151
|
f"Object with ID '{object_id}' was not pre-registered."
|
|
168
152
|
)
|
|
169
153
|
|
|
170
154
|
# Return if object is already present in the store
|
|
171
|
-
if
|
|
155
|
+
if rows[0]["is_available"]:
|
|
172
156
|
return
|
|
173
157
|
|
|
174
158
|
# Update the object entry in the store
|
|
175
|
-
self.
|
|
176
|
-
"UPDATE objects SET content
|
|
177
|
-
|
|
159
|
+
self.query(
|
|
160
|
+
"UPDATE objects SET content = :content, is_available = 1 "
|
|
161
|
+
"WHERE object_id = :object_id",
|
|
162
|
+
{"content": object_content, "object_id": object_id},
|
|
178
163
|
)
|
|
179
164
|
|
|
180
165
|
def get(self, object_id: str) -> bytes | None:
|
|
181
166
|
"""Get an object from the store."""
|
|
182
|
-
rows = self.query(
|
|
167
|
+
rows = self.query(
|
|
168
|
+
"SELECT content FROM objects WHERE object_id = :oid", {"oid": object_id}
|
|
169
|
+
)
|
|
183
170
|
return rows[0]["content"] if rows else None
|
|
184
171
|
|
|
185
172
|
def delete(self, object_id: str) -> None:
|
|
186
173
|
"""Delete an object and its unreferenced descendants from the store."""
|
|
187
|
-
with self.
|
|
188
|
-
|
|
189
|
-
"SELECT ref_count FROM objects WHERE object_id
|
|
190
|
-
|
|
174
|
+
with self.session():
|
|
175
|
+
rows = self.query(
|
|
176
|
+
"SELECT ref_count FROM objects WHERE object_id = :object_id",
|
|
177
|
+
{"object_id": object_id},
|
|
178
|
+
)
|
|
191
179
|
|
|
192
180
|
# If the object is not in the store, nothing to delete
|
|
193
|
-
if
|
|
181
|
+
if not rows:
|
|
194
182
|
return
|
|
195
183
|
|
|
196
184
|
# Skip deletion if there are still references
|
|
197
|
-
if
|
|
185
|
+
if rows[0]["ref_count"] > 0:
|
|
198
186
|
return
|
|
199
187
|
|
|
200
188
|
# Deleting will cascade via FK, but we need to decrement children first
|
|
201
|
-
children = self.
|
|
202
|
-
"SELECT child_id FROM object_children WHERE parent_id
|
|
203
|
-
|
|
189
|
+
children = self.query(
|
|
190
|
+
"SELECT child_id FROM object_children WHERE parent_id = :parent_id",
|
|
191
|
+
{"parent_id": object_id},
|
|
192
|
+
)
|
|
204
193
|
child_ids = [child["child_id"] for child in children]
|
|
205
194
|
|
|
206
195
|
if child_ids:
|
|
207
|
-
placeholders = ", ".join("
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
196
|
+
placeholders = ", ".join(f":cid{i}" for i in range(len(child_ids)))
|
|
197
|
+
params = {f"cid{i}": cid for i, cid in enumerate(child_ids)}
|
|
198
|
+
self.query(
|
|
199
|
+
"UPDATE objects SET ref_count = ref_count - 1 "
|
|
200
|
+
f"WHERE object_id IN ({placeholders})",
|
|
201
|
+
params,
|
|
202
|
+
)
|
|
213
203
|
|
|
214
|
-
self.
|
|
204
|
+
self.query(
|
|
205
|
+
"DELETE FROM objects WHERE object_id = :object_id",
|
|
206
|
+
{"object_id": object_id},
|
|
207
|
+
)
|
|
215
208
|
|
|
216
209
|
# Recursively clean children
|
|
217
210
|
for child_id in child_ids:
|
|
@@ -220,34 +213,39 @@ class SqliteObjectStore(ObjectStore, SqliteMixin):
|
|
|
220
213
|
def delete_objects_in_run(self, run_id: int) -> None:
|
|
221
214
|
"""Delete all objects that were registered in a specific run."""
|
|
222
215
|
run_id_sint = uint64_to_int64(run_id)
|
|
223
|
-
with self.
|
|
224
|
-
objs = self.
|
|
225
|
-
"SELECT object_id FROM run_objects WHERE run_id
|
|
226
|
-
|
|
216
|
+
with self.session():
|
|
217
|
+
objs = self.query(
|
|
218
|
+
"SELECT object_id FROM run_objects WHERE run_id = :run_id",
|
|
219
|
+
{"run_id": run_id_sint},
|
|
220
|
+
)
|
|
227
221
|
for obj in objs:
|
|
228
222
|
object_id = obj["object_id"]
|
|
229
|
-
|
|
230
|
-
"SELECT ref_count FROM objects WHERE object_id
|
|
231
|
-
|
|
232
|
-
|
|
223
|
+
rows = self.query(
|
|
224
|
+
"SELECT ref_count FROM objects WHERE object_id=:object_id",
|
|
225
|
+
{"object_id": object_id},
|
|
226
|
+
)
|
|
227
|
+
if rows and rows[0]["ref_count"] == 0:
|
|
233
228
|
self.delete(object_id)
|
|
234
|
-
self.
|
|
229
|
+
self.query(
|
|
230
|
+
"DELETE FROM run_objects WHERE run_id = :run_id",
|
|
231
|
+
{"run_id": run_id_sint},
|
|
232
|
+
)
|
|
235
233
|
|
|
236
234
|
def clear(self) -> None:
|
|
237
235
|
"""Clear the store."""
|
|
238
|
-
with self.
|
|
239
|
-
self.
|
|
240
|
-
self.
|
|
241
|
-
self.
|
|
236
|
+
with self.session():
|
|
237
|
+
self.query("DELETE FROM object_children")
|
|
238
|
+
self.query("DELETE FROM run_objects")
|
|
239
|
+
self.query("DELETE FROM objects")
|
|
242
240
|
|
|
243
241
|
def __contains__(self, object_id: str) -> bool:
|
|
244
242
|
"""Check if an object_id is in the store."""
|
|
245
|
-
|
|
246
|
-
"SELECT 1 FROM objects WHERE object_id
|
|
247
|
-
)
|
|
248
|
-
return
|
|
243
|
+
rows = self.query(
|
|
244
|
+
"SELECT 1 FROM objects WHERE object_id = :oid", {"oid": object_id}
|
|
245
|
+
)
|
|
246
|
+
return len(rows) > 0
|
|
249
247
|
|
|
250
248
|
def __len__(self) -> int:
|
|
251
249
|
"""Return the number of objects in the store."""
|
|
252
|
-
|
|
253
|
-
return
|
|
250
|
+
rows = self.query("SELECT COUNT(*) AS cnt FROM objects")
|
|
251
|
+
return int(rows[0]["cnt"])
|