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.
Files changed (140) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +18 -69
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +28 -58
  25. flwr/cli/new/new.py +9 -29
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +85 -93
  28. flwr/cli/run_utils.py +1 -1
  29. flwr/cli/stop.py +32 -73
  30. flwr/cli/supernode/ls.py +25 -57
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -275
  35. flwr/client/grpc_rere_client/connection.py +3 -3
  36. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  37. flwr/client/message_handler/message_handler.py +2 -1
  38. flwr/client/mod/centraldp_mods.py +1 -1
  39. flwr/client/mod/localdp_mod.py +1 -1
  40. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  41. flwr/client/run_info_store.py +2 -1
  42. flwr/clientapp/client_app.py +2 -1
  43. flwr/common/__init__.py +3 -2
  44. flwr/common/args.py +5 -5
  45. flwr/common/config.py +12 -17
  46. flwr/common/constant.py +3 -16
  47. flwr/common/context.py +2 -1
  48. flwr/common/exit/exit.py +4 -4
  49. flwr/common/exit/exit_code.py +6 -0
  50. flwr/common/grpc.py +2 -1
  51. flwr/common/logger.py +1 -1
  52. flwr/common/message.py +1 -1
  53. flwr/common/retry_invoker.py +13 -5
  54. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  55. flwr/common/serde.py +7 -5
  56. flwr/common/telemetry.py +1 -1
  57. flwr/common/typing.py +4 -3
  58. flwr/compat/client/app.py +6 -9
  59. flwr/compat/client/grpc_client/connection.py +2 -1
  60. flwr/compat/common/constant.py +29 -0
  61. flwr/compat/server/app.py +1 -1
  62. flwr/proto/clientappio_pb2.py +2 -2
  63. flwr/proto/clientappio_pb2_grpc.py +104 -88
  64. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  65. flwr/proto/federation_pb2.py +5 -3
  66. flwr/proto/federation_pb2.pyi +32 -2
  67. flwr/proto/run_pb2.py +5 -13
  68. flwr/proto/run_pb2.pyi +0 -57
  69. flwr/proto/serverappio_pb2.py +2 -2
  70. flwr/proto/serverappio_pb2_grpc.py +138 -207
  71. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  72. flwr/proto/simulationio_pb2.py +2 -2
  73. flwr/proto/simulationio_pb2_grpc.py +62 -90
  74. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  75. flwr/server/app.py +6 -13
  76. flwr/server/compat/grid_client_proxy.py +2 -1
  77. flwr/server/grid/grpc_grid.py +5 -5
  78. flwr/server/serverapp/app.py +11 -4
  79. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  80. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  81. flwr/server/superlink/fleet/message_handler/message_handler.py +6 -5
  82. flwr/server/superlink/linkstate/__init__.py +2 -2
  83. flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
  84. flwr/server/superlink/linkstate/linkstate.py +2 -21
  85. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  86. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
  87. flwr/server/superlink/linkstate/utils.py +49 -2
  88. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  89. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  90. flwr/server/utils/validator.py +1 -1
  91. flwr/server/workflow/default_workflows.py +2 -1
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  93. flwr/serverapp/strategy/bulyan.py +7 -1
  94. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  95. flwr/serverapp/strategy/fedavg.py +1 -1
  96. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  97. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  98. flwr/simulation/run_simulation.py +3 -12
  99. flwr/simulation/simulationio_connection.py +3 -3
  100. flwr/{common → supercore}/address.py +7 -33
  101. flwr/supercore/app_utils.py +2 -1
  102. flwr/supercore/constant.py +24 -2
  103. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  104. flwr/supercore/credential_store/__init__.py +33 -0
  105. flwr/supercore/credential_store/credential_store.py +34 -0
  106. flwr/supercore/credential_store/file_credential_store.py +76 -0
  107. flwr/{common → supercore}/date.py +0 -11
  108. flwr/supercore/ffs/disk_ffs.py +1 -1
  109. flwr/supercore/object_store/object_store_factory.py +14 -6
  110. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  111. flwr/supercore/sql_mixin.py +315 -0
  112. flwr/supercore/state/__init__.py +15 -0
  113. flwr/supercore/state/alembic/__init__.py +15 -0
  114. flwr/supercore/state/alembic/env.py +103 -0
  115. flwr/supercore/state/alembic/script.py.mako +43 -0
  116. flwr/supercore/state/alembic/utils.py +239 -0
  117. flwr/supercore/state/alembic/versions/__init__.py +15 -0
  118. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  119. flwr/supercore/state/schema/README.md +121 -0
  120. flwr/supercore/state/schema/__init__.py +15 -0
  121. flwr/supercore/state/schema/corestate_tables.py +36 -0
  122. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  123. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  124. flwr/supercore/superexec/run_superexec.py +2 -2
  125. flwr/supercore/utils.py +36 -1
  126. flwr/superlink/federation/federation_manager.py +2 -2
  127. flwr/superlink/federation/noop_federation_manager.py +8 -6
  128. flwr/superlink/servicer/control/control_servicer.py +19 -17
  129. flwr/supernode/cli/flower_supernode.py +2 -1
  130. flwr/supernode/runtime/run_clientapp.py +14 -14
  131. flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
  132. flwr/supernode/start_client_internal.py +10 -6
  133. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
  134. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
  135. flwr/cli/federation/show.py +0 -318
  136. flwr/common/pyproject.py +0 -42
  137. flwr/supercore/sqlite_mixin.py +0 -159
  138. /flwr/{common → supercore}/version.py +0 -0
  139. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  140. {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:
@@ -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 .sqlite_object_store import SqliteObjectStore
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
- if self.store_instance is None:
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
- # SqliteObjectStore
60
- store = SqliteObjectStore(self.database)
66
+ # SqlObjectStore
67
+ store = SqlObjectStore(self.database)
61
68
  store.initialize()
62
- log(DEBUG, "Using SqliteObjectStore")
69
+ self.store_instance = store
70
+ log(DEBUG, "Using SqlObjectStore")
63
71
  return store
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
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 SQLite ObjectStore implementation."""
15
+ """Flower SQLAlchemy-based ObjectStore implementation."""
16
16
 
17
17
 
18
- from typing import cast
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.sqlite_mixin import SqliteMixin
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
- 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."""
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 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
- )
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.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
- )
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.conn.execute(
98
- "INSERT INTO object_children(parent_id, child_id) "
99
- "VALUES (?, ?)",
100
- (obj_id, cid),
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.conn.execute(
79
+ self.query(
103
80
  "UPDATE objects SET ref_count = ref_count + 1 "
104
- "WHERE object_id = ?",
105
- (cid,),
81
+ "WHERE object_id = :object_id",
82
+ {"object_id": cid},
106
83
  )
107
84
  new_objects.append(obj_id)
108
85
  else:
109
- # Add to the list of new objects if not available
110
- if not row["is_available"]:
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.conn.execute(
115
- "INSERT OR IGNORE INTO run_objects(run_id, object_id) "
116
- "VALUES (?, ?)",
117
- (uint64_to_int64(run_id), obj_id),
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.conn:
124
- row = self.conn.execute(
125
- "SELECT object_id FROM objects WHERE object_id=?", (object_id,)
126
- ).fetchone()
127
- if not row:
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=?", (object_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.conn:
143
+ with self.session():
161
144
  # 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:
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 row["is_available"]:
155
+ if rows[0]["is_available"]:
172
156
  return
173
157
 
174
158
  # 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),
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("SELECT content FROM objects WHERE object_id=?", (object_id,))
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.conn:
188
- row = self.conn.execute(
189
- "SELECT ref_count FROM objects WHERE object_id=?", (object_id,)
190
- ).fetchone()
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 row is None:
181
+ if not rows:
194
182
  return
195
183
 
196
184
  # Skip deletion if there are still references
197
- if row["ref_count"] > 0:
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.conn.execute(
202
- "SELECT child_id FROM object_children WHERE parent_id=?", (object_id,)
203
- ).fetchall()
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("?" 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)
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.conn.execute("DELETE FROM objects WHERE object_id=?", (object_id,))
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.conn:
224
- objs = self.conn.execute(
225
- "SELECT object_id FROM run_objects WHERE run_id=?", (run_id_sint,)
226
- ).fetchall()
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
- 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:
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.conn.execute("DELETE FROM run_objects WHERE run_id=?", (run_id_sint,))
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.conn:
239
- self.conn.execute("DELETE FROM object_children;")
240
- self.conn.execute("DELETE FROM run_objects;")
241
- self.conn.execute("DELETE FROM objects;")
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
- row = self.conn.execute(
246
- "SELECT 1 FROM objects WHERE object_id=?", (object_id,)
247
- ).fetchone()
248
- return row is not None
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
- row = self.conn.execute("SELECT COUNT(*) AS cnt FROM objects;").fetchone()
253
- return cast(int, row["cnt"])
250
+ rows = self.query("SELECT COUNT(*) AS cnt FROM objects")
251
+ return int(rows[0]["cnt"])