flwr-nightly 1.26.0.dev20260122__py3-none-any.whl → 1.26.0.dev20260126__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 (35) hide show
  1. flwr/cli/app_cmd/publish.py +18 -44
  2. flwr/cli/app_cmd/review.py +8 -25
  3. flwr/cli/auth_plugin/oidc_cli_plugin.py +3 -6
  4. flwr/cli/build.py +8 -19
  5. flwr/cli/config/ls.py +8 -13
  6. flwr/cli/config_utils.py +19 -171
  7. flwr/cli/federation/ls.py +3 -7
  8. flwr/cli/flower_config.py +28 -47
  9. flwr/cli/install.py +18 -57
  10. flwr/cli/log.py +2 -2
  11. flwr/cli/login/login.py +8 -21
  12. flwr/cli/ls.py +3 -7
  13. flwr/cli/new/new.py +9 -29
  14. flwr/cli/pull.py +3 -7
  15. flwr/cli/run/run.py +6 -15
  16. flwr/cli/stop.py +5 -17
  17. flwr/cli/supernode/register.py +6 -22
  18. flwr/cli/supernode/unregister.py +3 -13
  19. flwr/cli/utils.py +66 -169
  20. flwr/common/config.py +5 -9
  21. flwr/common/constant.py +2 -0
  22. flwr/server/superlink/fleet/message_handler/message_handler.py +4 -4
  23. flwr/server/superlink/linkstate/__init__.py +0 -2
  24. flwr/server/superlink/linkstate/sql_linkstate.py +38 -10
  25. flwr/supercore/object_store/object_store_factory.py +4 -4
  26. flwr/supercore/object_store/sql_object_store.py +171 -6
  27. flwr/superlink/servicer/control/control_servicer.py +11 -12
  28. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/METADATA +2 -2
  29. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/RECORD +31 -35
  30. flwr/server/superlink/linkstate/sqlite_linkstate.py +0 -1302
  31. flwr/supercore/corestate/sqlite_corestate.py +0 -157
  32. flwr/supercore/object_store/sqlite_object_store.py +0 -253
  33. flwr/supercore/sqlite_mixin.py +0 -156
  34. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/WHEEL +0 -0
  35. {flwr_nightly-1.26.0.dev20260122.dist-info → flwr_nightly-1.26.0.dev20260126.dist-info}/entry_points.txt +0 -0
@@ -1,157 +0,0 @@
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
- """
@@ -1,253 +0,0 @@
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"])
@@ -1,156 +0,0 @@
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
- """Mixin providing common SQLite connection and initialization logic."""
16
-
17
-
18
- import re
19
- import sqlite3
20
- from abc import ABC
21
- from collections.abc import Sequence
22
- from logging import DEBUG, ERROR
23
- from typing import Any
24
-
25
- from flwr.common.logger import log
26
- from flwr.supercore.constant import SQLITE_PRAGMAS
27
-
28
- DictOrTuple = tuple[Any, ...] | dict[str, Any]
29
-
30
-
31
- class SqliteMixin(ABC):
32
- """Mixin providing common SQLite connection and initialization logic."""
33
-
34
- def __init__(self, database_path: str) -> None:
35
- self.database_path = database_path
36
- self._conn: sqlite3.Connection | None = None
37
-
38
- @property
39
- def conn(self) -> sqlite3.Connection:
40
- """Get the SQLite connection."""
41
- if self._conn is None:
42
- raise AttributeError("Database not initialized. Call initialize() first.")
43
- return self._conn
44
-
45
- def get_sql_statements(self) -> tuple[str, ...]:
46
- """Return SQL statements for this class.
47
-
48
- Subclasses can override this to provide their SQL CREATE statements.
49
- The base implementation returns an empty tuple.
50
-
51
- Returns
52
- -------
53
- tuple[str, ...]
54
- SQL CREATE TABLE/INDEX statements for this class.
55
- """
56
- return ()
57
-
58
- def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
59
- """Connect to the DB, enable FK support, and create tables if needed.
60
-
61
- This method executes SQL statements returned by `get_sql_statements()`.
62
-
63
- Parameters
64
- ----------
65
- log_queries : bool
66
- Log each query which is executed.
67
-
68
- Returns
69
- -------
70
- list[tuple[str]]
71
- The list of all tables in the DB.
72
-
73
- Examples
74
- --------
75
- Override `get_sql_statements()` in your subclass:
76
-
77
- .. code:: python
78
-
79
- def get_sql_statements(self) -> tuple[str, ...]:
80
- return (
81
- SQL_CREATE_TABLE_FOO,
82
- SQL_CREATE_TABLE_BAR,
83
- )
84
-
85
- To include parent SQL statements, call super():
86
-
87
- .. code:: python
88
-
89
- def get_sql_statements(self) -> tuple[str, ...]:
90
- return super().get_sql_statements() + (
91
- SQL_CREATE_TABLE_FOO,
92
- SQL_CREATE_TABLE_BAR,
93
- )
94
- """
95
- self._conn = sqlite3.connect(self.database_path)
96
- # Set SQLite pragmas for optimal performance and correctness
97
- for pragma, value in SQLITE_PRAGMAS:
98
- self._conn.execute(f"PRAGMA {pragma} = {value};")
99
- self._conn.row_factory = dict_factory
100
-
101
- if log_queries:
102
- self._conn.set_trace_callback(lambda q: log(DEBUG, q))
103
-
104
- # Create tables and indexes
105
- cur = self._conn.cursor()
106
- for sql in self.get_sql_statements():
107
- cur.execute(sql)
108
- res = cur.execute("SELECT name FROM sqlite_schema;")
109
- return res.fetchall()
110
-
111
- def query(
112
- self,
113
- query: str,
114
- data: Sequence[DictOrTuple] | DictOrTuple | None = None,
115
- ) -> list[dict[str, Any]]:
116
- """Execute a SQL query and return the results as list of dicts."""
117
- if self._conn is None:
118
- raise AttributeError("LinkState is not initialized.")
119
-
120
- if data is None:
121
- data = []
122
-
123
- # Clean up whitespace to make the logs nicer
124
- query = re.sub(r"\s+", " ", query)
125
-
126
- try:
127
- with self._conn:
128
- if (
129
- len(data) > 0
130
- and isinstance(data, (tuple | list))
131
- and isinstance(data[0], (tuple | dict))
132
- ):
133
- rows = self._conn.executemany(query, data)
134
- else:
135
- rows = self._conn.execute(query, data)
136
-
137
- # Extract results before committing to support
138
- # INSERT/UPDATE ... RETURNING
139
- # style queries
140
- result = rows.fetchall()
141
- except KeyError as exc:
142
- log(ERROR, {"query": query, "data": data, "exception": exc})
143
-
144
- return result
145
-
146
-
147
- def dict_factory(
148
- cursor: sqlite3.Cursor,
149
- row: sqlite3.Row,
150
- ) -> dict[str, Any]:
151
- """Turn SQLite results into dicts.
152
-
153
- Less efficent for retrival of large amounts of data but easier to use.
154
- """
155
- fields = [column[0] for column in cursor.description]
156
- return dict(zip(fields, row, strict=True))