flwr-nightly 1.26.0.dev20260119__py3-none-any.whl → 1.26.0.dev20260120__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.
@@ -57,8 +57,11 @@ CLI_NOTICE = (
57
57
  )
58
58
 
59
59
 
60
- def _is_legacy_usage(superlink: str, args: list[str]) -> bool:
60
+ def _is_legacy_usage(superlink: str | None, args: list[str]) -> bool:
61
61
  """Check if legacy usage is detected in the given arguments."""
62
+ if superlink is None:
63
+ return False
64
+
62
65
  # If one and only one extra argument is given, assume legacy usage
63
66
  if len(args) == 1:
64
67
  return True
@@ -75,26 +78,34 @@ def _is_legacy_usage(superlink: str, args: list[str]) -> bool:
75
78
  return False
76
79
 
77
80
 
78
- def _check_is_migratable(app: Path) -> None:
79
- """Check if the given app path contains legacy TOML configuration."""
81
+ def _is_migratable(app: Path) -> tuple[bool, str | None]:
82
+ """Check if the given app path contains legacy TOML configuration.
83
+
84
+ Parameters
85
+ ----------
86
+ app : Path
87
+ Path to the Flower App.
88
+
89
+ Returns
90
+ -------
91
+ tuple[bool, str | None]
92
+ Returns (True, None) if migratable, else (False, reason).
93
+ """
80
94
  toml_path = app / "pyproject.toml"
81
95
  if not toml_path.exists():
82
- raise FileNotFoundError(f"No pyproject.toml found in '{app}'")
96
+ return False, f"No pyproject.toml found in '{app}'"
83
97
  config, errors, _ = load_and_validate(toml_path, check_module=False)
84
98
  if config is None:
85
- raise ValueError(f"Failed to load TOML configuration: {toml_path}")
99
+ return False, f"Failed to load TOML configuration: {toml_path}"
86
100
  if errors:
87
- raise ValueError(
88
- f"Invalid TOML configuration found in '{toml_path}':\n"
89
- + "\n".join(f"- {err}" for err in errors)
90
- )
101
+ err_msg = f"Invalid TOML configuration found in '{toml_path}':\n"
102
+ err_msg += "\n".join(f"- {err}" for err in errors)
103
+ return False, err_msg
91
104
  try:
92
105
  _ = config["tool"]["flwr"]["federations"]
93
- return
106
+ return True, None
94
107
  except KeyError:
95
- raise ValueError(
96
- f"No '[tool.flwr.federations]' section found in '{toml_path}'"
97
- ) from None
108
+ return False, f"No '[tool.flwr.federations]' section found in '{toml_path}'"
98
109
 
99
110
 
100
111
  def _migrate_pyproject_toml_to_flower_config(
@@ -161,23 +172,81 @@ def _comment_out_legacy_toml_config(app: Path) -> None:
161
172
 
162
173
 
163
174
  def migrate(
164
- app: Path,
165
- toml_federation: str | None,
175
+ superlink: str | None,
176
+ args: list[str],
177
+ ignore_legacy_usage: bool = False,
166
178
  ) -> None:
167
- """Migrate legacy TOML configuration to Flower config."""
179
+ """Migrate legacy TOML configuration to Flower config.
180
+
181
+ Migrates SuperLink connection settings from `[tool.flwr.federations]` in
182
+ pyproject.toml to the new Flower config format when legacy usage is detected
183
+ or the migration is applicable.
184
+
185
+ `flwr run` should call `migrate(superlink, [], ignore_legacy_usage=True)` to skip
186
+ legacy usage check. Other CLI commands should call `migrate(superlink, ctx.args)`.
187
+
188
+ Parameters
189
+ ----------
190
+ superlink : str | None
191
+ Value of the `[SUPERLINK]` argument provided to the CLI command.
192
+ args : list[str]
193
+ Additional arguments. In legacy usage, this is the TOML federation name.
194
+ ignore_legacy_usage : bool (default: False)
195
+ Set to `True` only for `flwr run` command to skip legacy usage check.
196
+
197
+ Raises
198
+ ------
199
+ click.UsageError
200
+ If more than one extra argument is provided.
201
+ click.ClickException
202
+ If legacy usage detected but migration fails.
203
+
204
+ Examples
205
+ --------
206
+ The following usages will trigger migration if applicable:
207
+ - `flwr <CMD> . my-federation`
208
+ - `flwr <CMD> ./my-app`
209
+ - `flwr <CMD>`
210
+
211
+ The following usages will NOT trigger migration:
212
+ - `flwr <CMD> named-conn`
213
+
214
+ Notes
215
+ -----
216
+ This function will NOT return when legacy usage is detected to force the user
217
+ to adapt to the new usage pattern after migration.
218
+ """
168
219
  # Initialize Flower config
169
220
  init_flwr_config()
170
221
 
171
- # Print migration notice
172
- typer.echo(CLI_NOTICE)
222
+ # Trigger the same typer error when detecting unexpected extra args
223
+ if len(args) > 1:
224
+ raise click.UsageError(f"Got unexpected extra arguments ({' '.join(args[1:])})")
173
225
 
174
- # Check if migration is applicable
226
+ # Determine app path for migration
227
+ app = Path(superlink) if superlink else Path(".")
175
228
  app = app.resolve()
176
- try:
177
- _check_is_migratable(app)
178
- except (FileNotFoundError, ValueError) as e:
179
- raise click.ClickException(f"Cannot migrate configuration:\n{e}") from e
180
229
 
230
+ # Check if migration is applicable and if legacy usage is detected
231
+ is_migratable, reason = _is_migratable(app)
232
+ is_legacy = _is_legacy_usage(superlink, args) if not ignore_legacy_usage else False
233
+
234
+ # Print notice once if legacy usage detected or migration is applicable
235
+ if is_legacy or is_migratable:
236
+ typer.echo(CLI_NOTICE)
237
+
238
+ if not is_migratable:
239
+ # Raise error if legacy usage is detected but migration is not applicable
240
+ if is_legacy:
241
+ raise click.ClickException(
242
+ f"Cannot migrate configuration:\n{reason}. \nThis is expected if the "
243
+ "migration has been previously carried out. Use `--help` after your "
244
+ "command to see the new usage pattern."
245
+ )
246
+ return # Nothing to migrate
247
+
248
+ # Perform migration
249
+ toml_federation = args[0] if len(args) == 1 else None
181
250
  try:
182
251
  migrated_conns, default_conn = _migrate_pyproject_toml_to_flower_config(
183
252
  app, toml_federation
@@ -197,30 +266,13 @@ def migrate(
197
266
  typer.secho(" (default)", fg=typer.colors.WHITE, nl=False)
198
267
  typer.echo()
199
268
 
200
- # print usage
201
- typer.secho("\nYou should now use the Flower CLI as follows:")
202
- ctx = click.get_current_context()
203
- typer.secho(ctx.get_usage() + "\n", bold=True)
204
-
205
269
  _comment_out_legacy_toml_config(app)
206
270
 
271
+ if is_legacy:
272
+ # print usage
273
+ typer.secho("\nYou should now use the Flower CLI as follows:")
274
+ ctx = click.get_current_context()
275
+ typer.secho(ctx.get_usage() + "\n", bold=True)
207
276
 
208
- def migrate_if_legacy_usage(
209
- superlink: str,
210
- args: list[str],
211
- ) -> bool:
212
- """Migrate legacy TOML configuration to Flower config if legacy usage is
213
- detected."""
214
- # Trigger the same typer error when detecting unexpected extra args
215
- if len(args) > 1:
216
- raise click.UsageError(f"Got unexpected extra arguments ({' '.join(args[1:])})")
217
-
218
- # Skip migration if no legacy usage is detected
219
- if not _is_legacy_usage(superlink, args):
220
- return False
221
-
222
- migrate(
223
- app=Path(superlink),
224
- toml_federation=args[0] if len(args) == 1 else None,
225
- )
226
- return True
277
+ # Abort if legacy usage is detected to force user to adapt to new usage
278
+ raise typer.Exit(code=1)
flwr/cli/flower_config.py CHANGED
@@ -217,17 +217,17 @@ def serialize_superlink_connection(connection: SuperLinkConnection) -> dict[str,
217
217
  """
218
218
  # pylint: disable=protected-access
219
219
  conn_dict: dict[str, Any] = {
220
- SuperLinkConnectionTomlKey.ADDRESS: connection._address,
221
- SuperLinkConnectionTomlKey.ROOT_CERTIFICATES: connection._root_certificates,
220
+ SuperLinkConnectionTomlKey.ADDRESS: connection.address,
221
+ SuperLinkConnectionTomlKey.ROOT_CERTIFICATES: connection.root_certificates,
222
222
  SuperLinkConnectionTomlKey.INSECURE: connection._insecure,
223
223
  SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH: connection._enable_account_auth,
224
- SuperLinkConnectionTomlKey.FEDERATION: connection._federation,
224
+ SuperLinkConnectionTomlKey.FEDERATION: connection.federation,
225
225
  }
226
226
  # Remove None values
227
227
  conn_dict = {k: v for k, v in conn_dict.items() if v is not None}
228
228
 
229
- if connection._options is not None:
230
- options_dict = _serialize_simulation_options(connection._options)
229
+ if connection.options is not None:
230
+ options_dict = _serialize_simulation_options(connection.options)
231
231
  conn_dict[SuperLinkConnectionTomlKey.OPTIONS] = options_dict
232
232
 
233
233
  return conn_dict
flwr/cli/typing.py CHANGED
@@ -93,15 +93,34 @@ class SuperLinkSimulationOptions:
93
93
 
94
94
  @dataclass
95
95
  class SuperLinkConnection:
96
- """SuperLink connection configuration for CLI commands."""
96
+ """SuperLink connection configuration for CLI commands.
97
+
98
+ Attributes
99
+ ----------
100
+ name : str
101
+ The name of the connection configuration.
102
+ address : str
103
+ The address of the SuperLink (Control API).
104
+ root_certificates : str
105
+ The absolute path to the root CA certificate file.
106
+ insecure : bool (default: False)
107
+ Whether to use an insecure channel. If True, the
108
+ connection will not use TLS encryption.
109
+ enable_account_auth : bool (default: False)
110
+ Whether to enable account authentication.
111
+ federation : str
112
+ The name of the federation to interface with.
113
+ options : SuperLinkSimulationOptions
114
+ Configuration options for the simulation runtime.
115
+ """
97
116
 
98
117
  name: str
99
- _address: str | None = None
100
- _root_certificates: str | None = None
118
+ address: str | None = None
119
+ root_certificates: str | None = None
101
120
  _insecure: bool | None = None
102
121
  _enable_account_auth: bool | None = None
103
- _federation: str | None = None
104
- _options: SuperLinkSimulationOptions | None = None
122
+ federation: str | None = None
123
+ options: SuperLinkSimulationOptions | None = None
105
124
 
106
125
  # pylint: disable=too-many-arguments,too-many-positional-arguments
107
126
  def __init__(
@@ -115,84 +134,52 @@ class SuperLinkConnection:
115
134
  options: SuperLinkSimulationOptions | None = None,
116
135
  ) -> None:
117
136
  self.name = name
118
- self._address = address
119
- self._root_certificates = root_certificates
137
+ self.address = address
138
+ self.root_certificates = root_certificates
120
139
  self._insecure = insecure
121
140
  self._enable_account_auth = enable_account_auth
122
- self._federation = federation
123
- self._options = options
141
+ self.federation = federation
142
+ self.options = options
124
143
 
125
144
  self.__post_init__()
126
145
 
127
- @property
128
- def address(self) -> str:
129
- """Return the address."""
130
- if self._address is None:
131
- raise ValueError(_ERROR_MSG_FMT % SuperLinkConnectionTomlKey.ADDRESS)
132
- return self._address
133
-
134
- @property
135
- def root_certificates(self) -> str:
136
- """Return the root certificates."""
137
- if self._root_certificates is None:
138
- raise ValueError(
139
- _ERROR_MSG_FMT % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
140
- )
141
- return self._root_certificates
142
-
143
146
  @property
144
147
  def insecure(self) -> bool:
145
- """Return the insecure flag."""
148
+ """Return the insecure flag or its default (False) if unset."""
146
149
  if self._insecure is None:
147
- raise ValueError(_ERROR_MSG_FMT % SuperLinkConnectionTomlKey.INSECURE)
150
+ return False
148
151
  return self._insecure
149
152
 
150
153
  @property
151
154
  def enable_account_auth(self) -> bool:
152
- """Return the enable_account_auth flag."""
155
+ """Return the enable_account_auth flag or its default (False) if unset."""
153
156
  if self._enable_account_auth is None:
154
- raise ValueError(
155
- _ERROR_MSG_FMT % SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH
156
- )
157
+ return False
157
158
  return self._enable_account_auth
158
159
 
159
- @property
160
- def federation(self) -> str:
161
- """Return the federation."""
162
- if self._federation is None:
163
- raise ValueError(_ERROR_MSG_FMT % SuperLinkConnectionTomlKey.FEDERATION)
164
- return self._federation
165
-
166
- @property
167
- def options(self) -> SuperLinkSimulationOptions:
168
- """Return the simulation options."""
169
- if self._options is None:
170
- raise ValueError(_ERROR_MSG_FMT % SuperLinkConnectionTomlKey.OPTIONS)
171
- return self._options
172
-
173
160
  def __post_init__(self) -> None:
174
161
  """Validate SuperLink connection configuration."""
175
162
  err_prefix = f"Invalid value for key '%s' in connection '{self.name}': "
176
- if self._address is not None and not isinstance(self._address, str):
163
+ if self.address is not None and not isinstance(self.address, str):
177
164
  raise ValueError(
178
165
  err_prefix % SuperLinkConnectionTomlKey.ADDRESS
179
- + f"expected str, but got {type(self._address).__name__}."
166
+ + f"expected str, but got {type(self.address).__name__}."
180
167
  )
181
- if self._root_certificates is not None and not isinstance(
182
- self._root_certificates, str
168
+ if self.root_certificates is not None and not isinstance(
169
+ self.root_certificates, str
183
170
  ):
184
171
  raise ValueError(
185
172
  err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
186
- + f"expected str, but got {type(self._root_certificates).__name__}."
173
+ + f"expected str, but got {type(self.root_certificates).__name__}."
187
174
  )
188
175
 
189
176
  # Ensure root certificates path is absolute
190
- if self._root_certificates is not None:
191
- if not Path(self._root_certificates).is_absolute():
177
+ if self.root_certificates is not None:
178
+ if not Path(self.root_certificates).is_absolute():
192
179
  raise ValueError(
193
180
  err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
194
181
  + "expected absolute path, but got relative path "
195
- f"'{self._root_certificates}'."
182
+ f"'{self.root_certificates}'."
196
183
  )
197
184
 
198
185
  if self._insecure is not None and not isinstance(self._insecure, bool):
@@ -208,14 +195,14 @@ class SuperLinkConnection:
208
195
  + f"expected bool, but got {type(self._enable_account_auth).__name__}."
209
196
  )
210
197
 
211
- if self._federation is not None and not isinstance(self._federation, str):
198
+ if self.federation is not None and not isinstance(self.federation, str):
212
199
  raise ValueError(
213
200
  err_prefix % SuperLinkConnectionTomlKey.FEDERATION
214
- + f"expected str, but got {type(self._federation).__name__}."
201
+ + f"expected str, but got {type(self.federation).__name__}."
215
202
  )
216
203
 
217
204
  # The connection needs to have either an address or options (or both).
218
- if self._address is None and self._options is None:
205
+ if self.address is None and self.options is None:
219
206
  raise ValueError(
220
207
  "Invalid SuperLink connection format: "
221
208
  f"'{SuperLinkConnectionTomlKey.ADDRESS}' and/or "
flwr/cli/utils.py CHANGED
@@ -486,7 +486,7 @@ def init_channel_from_connection(
486
486
 
487
487
  # Create the gRPC channel
488
488
  channel = create_channel(
489
- server_address=connection.address,
489
+ server_address=connection.address, # type: ignore
490
490
  insecure=connection.insecure,
491
491
  root_certificates=root_certificates_bytes,
492
492
  max_message_length=GRPC_MAX_MESSAGE_LENGTH,
@@ -0,0 +1,253 @@
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
+ """Mixin providing common SQL connection and initialization logic via SQLAlchemy."""
16
+
17
+
18
+ import re
19
+ from abc import ABC
20
+ from collections.abc import Sequence
21
+ from logging import DEBUG, ERROR
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from sqlalchemy import Engine, MetaData, create_engine, event, inspect, text
26
+ from sqlalchemy.engine import Result
27
+ from sqlalchemy.exc import SQLAlchemyError
28
+ from sqlalchemy.orm import Session, sessionmaker
29
+
30
+ from flwr.common.logger import log
31
+ from flwr.supercore.constant import SQLITE_PRAGMAS
32
+
33
+
34
+ def _set_sqlite_pragmas(dbapi_conn: Any, _connection_record: Any) -> None:
35
+ """Set SQLite pragmas for performance and correctness."""
36
+ cursor = dbapi_conn.cursor()
37
+ for pragma, value in SQLITE_PRAGMAS:
38
+ cursor.execute(f"PRAGMA {pragma} = {value};")
39
+ cursor.close()
40
+
41
+
42
+ def _log_query( # pylint: disable=W0613,R0913,R0917
43
+ conn: Any,
44
+ cursor: Any,
45
+ statement: str,
46
+ parameters: Any,
47
+ context: Any,
48
+ executemany: bool,
49
+ ) -> None:
50
+ """Log SQL queries via Flower logger."""
51
+ log(DEBUG, {"query": statement, "params": parameters})
52
+
53
+
54
+ class SqlMixin(ABC):
55
+ """Mixin providing common SQLite connection and initialization logic.
56
+
57
+ This mixin uses SQLAlchemy Core API for SQLite database access. It accepts either a
58
+ database file path or a SQLite URL, automatically converting file paths to SQLite
59
+ URLs.
60
+ """
61
+
62
+ def __init__(self, database_path: str) -> None:
63
+ """Initialize the SqlMixin.
64
+
65
+ Parameters
66
+ ----------
67
+ database_path : str
68
+ Either a file path or SQLite database URL. Examples:
69
+ - "path/to/db.db" or "/absolute/path/to/db.db"
70
+ - ":memory:" for in-memory SQLite
71
+ - "sqlite:///path/to/db.db" for explicit SQLite URL
72
+ """
73
+ # Auto-convert file path to SQLAlchemy SQLite URL if needed
74
+ if database_path == ":memory:":
75
+ self.database_url = "sqlite:///:memory:"
76
+ elif not database_path.startswith("sqlite://"):
77
+ # Treat as file path, convert to absolute and create SQLite URL
78
+ abs_path = Path(database_path).resolve()
79
+ self.database_url = f"sqlite:///{abs_path}"
80
+ else:
81
+ # Already a SQLite URL
82
+ self.database_url = database_path
83
+
84
+ self._engine: Engine | None = None
85
+ self._session_factory: sessionmaker[Session] | None = None
86
+
87
+ def session(self) -> Session:
88
+ """Create a new database session.
89
+
90
+ Returns
91
+ -------
92
+ Session
93
+ A new SQLAlchemy session. Use as context manager:
94
+
95
+ with self.session() as session:
96
+ session.execute(text("SELECT ..."))
97
+ session.commit()
98
+ """
99
+ if self._session_factory is None:
100
+ raise AttributeError("Database not initialized. Call initialize() first.")
101
+ return self._session_factory()
102
+
103
+ def get_metadata(self) -> MetaData | None:
104
+ """Return the MetaData object for this class.
105
+
106
+ Subclasses can override this to provide their SQLAlchemy MetaData.
107
+ The base implementation returns None.
108
+
109
+ Returns
110
+ -------
111
+ MetaData | None
112
+ SQLAlchemy MetaData object for this class.
113
+ """
114
+ return None
115
+
116
+ def initialize(self, log_queries: bool = False) -> list[str]:
117
+ """Connect to the DB and create tables if needed.
118
+
119
+ This method creates the SQLAlchemy engine and session factory,
120
+ and creates tables returned by `get_metadata()`.
121
+
122
+ Parameters
123
+ ----------
124
+ log_queries : bool
125
+ Log each query which is executed.
126
+
127
+ Returns
128
+ -------
129
+ list[str]
130
+ The list of all tables in the DB.
131
+ """
132
+ # Create engine with SQLite-specific settings
133
+ engine_kwargs: dict[str, Any] = {
134
+ # SQLite needs check_same_thread=False for multi-threaded access
135
+ "connect_args": {"check_same_thread": False}
136
+ }
137
+ self._engine = create_engine(self.database_url, **engine_kwargs)
138
+
139
+ # Set SQLite pragmas via event listener for optimal performance and correctness
140
+ event.listen(self._engine, "connect", _set_sqlite_pragmas)
141
+
142
+ if log_queries:
143
+ # Set up query logging via event listener
144
+ event.listen(self._engine, "before_cursor_execute", _log_query)
145
+
146
+ # Create session factory
147
+ self._session_factory = sessionmaker(bind=self._engine)
148
+
149
+ # Create tables defined in metadata (idempotent - only creates missing tables)
150
+ if (metadata := self.get_metadata()) is not None:
151
+ metadata.create_all(self._engine)
152
+
153
+ # Get all table names using inspector
154
+ inspector = inspect(self._engine)
155
+ return inspector.get_table_names()
156
+
157
+ def query(
158
+ self,
159
+ query: str,
160
+ data: Sequence[dict[str, Any]] | dict[str, Any] | None = None,
161
+ ) -> list[dict[str, Any]]:
162
+ """Execute a SQL query and return the results as list of dicts.
163
+
164
+ TRANSACTION SEMANTICS:
165
+ ----------------------
166
+ Each call to query() runs in its own isolated transaction that is
167
+ automatically committed. This is suitable for single SQL statements.
168
+
169
+ For complex operations requiring multiple SQL statements in a single
170
+ transaction, use session() directly instead:
171
+
172
+ with self.session() as session:
173
+ session.execute(text("UPDATE ..."), {...})
174
+ session.execute(text("INSERT ..."), {...})
175
+ session.commit() # Commits both statements atomically
176
+
177
+ Parameters
178
+ ----------
179
+ query : str
180
+ SQL query string with named parameter placeholders.
181
+ Use :name syntax for parameters: "SELECT * FROM t WHERE a = :a AND b = :b"
182
+ data : Sequence[dict[str, Any]] | dict[str, Any] | None
183
+ Query parameters using named parameter syntax:
184
+ - Single execution: pass dict, e.g., {"a": value1, "b": value2}
185
+ - Batch execution: pass sequence of dicts, e.g., [{"a": 1}, {"a": 2}]
186
+
187
+ Returns
188
+ -------
189
+ list[dict[str, Any]]
190
+ Query results as a list of dictionaries.
191
+
192
+ Examples
193
+ --------
194
+ # Single query with named parameters (auto-committed transaction)
195
+ rows = self.query(
196
+ "SELECT * FROM node WHERE node_id = :id AND status = :status",
197
+ {"id": node_id, "status": status}
198
+ )
199
+
200
+ # Batch insert with named parameters (auto-committed transaction)
201
+ rows = self.query(
202
+ "INSERT INTO node (node_id, status) VALUES (:id, :status)",
203
+ [{"id": 1, "status": "online"}, {"id": 2, "status": "offline"}]
204
+ )
205
+
206
+ # Multi-statement transaction - use session() directly
207
+ with self.session() as session:
208
+ # Both statements succeed or fail together
209
+ session.execute(text("DELETE FROM old_data WHERE date < :cutoff"), {...})
210
+ session.execute(text("INSERT INTO archive SELECT * FROM old_data"), {})
211
+ session.commit()
212
+ """
213
+ if self._engine is None:
214
+ raise AttributeError(
215
+ "LinkState is not initialized. Call initialize() first."
216
+ )
217
+
218
+ if data is None:
219
+ data = {}
220
+
221
+ # Clean up whitespace to make the logs nicer
222
+ query = re.sub(r"\s+", " ", query.strip())
223
+
224
+ try:
225
+ with self.session() as session:
226
+ sql = text(query)
227
+
228
+ # Execute query (results live in database cursor)
229
+ # There is no need to check for batch vs single execution;
230
+ # SQLAlchemy handles both cases automatically.
231
+ result: Result[Any] = session.execute(sql, data)
232
+
233
+ # Fetch results into Python memory before commit.
234
+ # mappings() returns dict-like rows (works for SELECT and RETURNING).
235
+ if result.returns_rows: # type: ignore
236
+ rows = [dict(row) for row in result.mappings()]
237
+ else:
238
+ # For statements without RETURNING (INSERT/UPDATE/DELETE),
239
+ # returns_rows is False, so we return empty list.
240
+ rows = []
241
+
242
+ # Commit transaction (finalizes database changes)
243
+ # NOTE: This commits after EVERY query() call, making each call
244
+ # an isolated transaction. For multi-statement transactions,
245
+ # use session() directly.
246
+ session.commit()
247
+
248
+ # Return the fetched data
249
+ return rows
250
+
251
+ except SQLAlchemyError as exc:
252
+ log(ERROR, {"query": query, "data": data, "exception": exc})
253
+ raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.26.0.dev20260119
3
+ Version: 1.26.0.dev20260120
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  License: Apache-2.0
6
6
  Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
@@ -18,13 +18,13 @@ flwr/cli/build.py,sha256=A9lzUNuGqLNId6PQppSWSlbMACl_LeW5Ispp9DuuX6k,10383
18
18
  flwr/cli/cli_account_auth_interceptor.py,sha256=mXgxThpZjU_2Xlae9xT8ewOw60GeE64comDd57asLIY,3680
19
19
  flwr/cli/config/__init__.py,sha256=46z6whA3hvKkl9APRs-UG7Ym3K9VOqKx_pYcgelRjtE,788
20
20
  flwr/cli/config/ls.py,sha256=ktRga1KWt4IDK5TQNT6ixfP66zcspkf-F2j3CkkpsGo,3665
21
- flwr/cli/config_migration.py,sha256=5rMAT4f3DpvIS4Hon2O-_elnTPhpyP-mWqaC-pCPQBA,8191
21
+ flwr/cli/config_migration.py,sha256=_Ci20RoS8MPfSPh0qqeAKzN59IHOmAX5Yj6lUW3ac-o,10222
22
22
  flwr/cli/config_utils.py,sha256=PiYlb1bAwpxtMH3OQ7V0VeCATfX4gwhULpkfe61SRJI,12832
23
23
  flwr/cli/constant.py,sha256=b5WsK_KXg9yDDk2lxEiN7rggsnZYNu1fiBC9NJVnnNs,3291
24
24
  flwr/cli/example.py,sha256=SNTorkKPrx1rOryGREUyZu8TcOc1-vFv1zEddaysdY0,2216
25
25
  flwr/cli/federation/__init__.py,sha256=okxswL4fAjApI9gV_alU1lRkTUcQRbwlzvtUTLz61fE,793
26
26
  flwr/cli/federation/ls.py,sha256=dGNw6quUAbUmqTNIVAPgJ9UgCJddITjKr48ETXQ9LTE,11581
27
- flwr/cli/flower_config.py,sha256=vD6eUaHE-4iePA-vqRrTqcPkISJkXcAzKxDWFdYYp2I,15787
27
+ flwr/cli/flower_config.py,sha256=RXYIkz3199iXu5eNodExM8KcXAQww4WhtaVOd6y4uZw,15782
28
28
  flwr/cli/install.py,sha256=yhgUWd_Psyc18DaoQHEQ5Ji45ZQ-3LclueM9uRxUEPw,10063
29
29
  flwr/cli/log.py,sha256=ZWveENLdDC0Dm10YIMcc3qdGV1aO-2YZXyHBhnB6ofI,7848
30
30
  flwr/cli/login/__init__.py,sha256=B1SXKU3HCQhWfFDMJhlC7FOl8UsvH4mxysxeBnrfyUE,800
@@ -41,8 +41,8 @@ flwr/cli/supernode/__init__.py,sha256=DBkjoPo2hS2Y-ghJxwLbrAbCQFBgD82_Itl2_892UB
41
41
  flwr/cli/supernode/ls.py,sha256=LNx3C9-Vfmw1ffVPDmjmex8TD_e0sTAXEC97aqg4Lqk,8958
42
42
  flwr/cli/supernode/register.py,sha256=2aQTT2wEp2pxxuIacC6FQkN8q_QKifZhWjD7LNw1R2A,6527
43
43
  flwr/cli/supernode/unregister.py,sha256=PXugcJJrM7cP3HbULjgcO22DciDP2uWN7YNgce4hv5E,4632
44
- flwr/cli/typing.py,sha256=1aCIvZOjK5BOWSZ7EpyPyYwvsy9NGpr0EaIp_9qXyeg,8469
45
- flwr/cli/utils.py,sha256=2X-NxhY41rlpdzzUobMmf2jLjEWTZ5BuvyehDllt_cE,23267
44
+ flwr/cli/typing.py,sha256=MaY3NAca2PgmNByogksDKSCoRQLQpXTgO8NO9nLP0yA,8008
45
+ flwr/cli/utils.py,sha256=HA-YGqyvjDazsvrpia2dOBWkpD1Q4GTLWjn1cKY-rRM,23283
46
46
  flwr/client/__init__.py,sha256=xwkPJfdeWxIIfmiPE5vnmnY_JbTlErP0Qs9eBP6qRFg,1252
47
47
  flwr/client/client.py,sha256=3HAchxvknKG9jYbB7swNyDj-e5vUWDuMKoLvbT7jCVM,7895
48
48
  flwr/client/dpfedavg_numpy_client.py,sha256=ELDHyEJcTB-FlLhHC-JXy8HuB3ZFHfT0HL3g1VSWY5w,7451
@@ -355,6 +355,7 @@ flwr/supercore/object_store/sqlite_object_store.py,sha256=oDfqGe6TEr1HT6_ZoxLoi7
355
355
  flwr/supercore/primitives/__init__.py,sha256=Tx8GOjnmMo8Y74RsDGrMpfr-E0Nl8dcUDF784_ge6F8,745
356
356
  flwr/supercore/primitives/asymmetric.py,sha256=1643niHYj3uEbfPd06VuMHwN3tKVwg0uVyR3RhTdWIU,3778
357
357
  flwr/supercore/primitives/asymmetric_ed25519.py,sha256=eIhOTMibQW0FJX4MXdplHdL3HcfCiKuFu2mQ8GQTUz8,5025
358
+ flwr/supercore/sql_mixin.py,sha256=owyflbGoK7WgN9cuUTb_7-41n7AhCglKhCOO9cEOTbo,9423
358
359
  flwr/supercore/sqlite_mixin.py,sha256=KrHxJQlJSuTdfc8YlFTft-COzT6FvVtQDWAWkdJ-jGQ,5064
359
360
  flwr/supercore/state/__init__.py,sha256=FkKhsNVM4LjlRlOgXTz6twINmw5ohIUKS_OER0BNo_w,724
360
361
  flwr/supercore/state/schema/README.md,sha256=-0QrXDhnv30gEYoIUJo7aLUolY0r_t_nC24yk7B2agM,2892
@@ -402,7 +403,7 @@ flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca
402
403
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
403
404
  flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=rRL4CQ0L78jF_p0ct4-JMGREt6wWRy__wy4czF4f54Y,11639
404
405
  flwr/supernode/start_client_internal.py,sha256=BYk69UBQ2gQJaDQxXhccUgfOWrb7ShAstrbcMOCZIIs,26173
405
- flwr_nightly-1.26.0.dev20260119.dist-info/METADATA,sha256=QWoln95HbQwCmpCtN4waDrOah5wr1zjfTDSXBzUZgwI,14398
406
- flwr_nightly-1.26.0.dev20260119.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
407
- flwr_nightly-1.26.0.dev20260119.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
408
- flwr_nightly-1.26.0.dev20260119.dist-info/RECORD,,
406
+ flwr_nightly-1.26.0.dev20260120.dist-info/METADATA,sha256=OiJyzqVqCMP8f7INcXZAXUn5k_p5E1nc052WUxULZlA,14398
407
+ flwr_nightly-1.26.0.dev20260120.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
408
+ flwr_nightly-1.26.0.dev20260120.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
409
+ flwr_nightly-1.26.0.dev20260120.dist-info/RECORD,,