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.
- flwr/cli/config_migration.py +99 -47
- flwr/cli/flower_config.py +5 -5
- flwr/cli/typing.py +43 -56
- flwr/cli/utils.py +1 -1
- flwr/supercore/sql_mixin.py +253 -0
- {flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/METADATA +1 -1
- {flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/RECORD +9 -8
- {flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/entry_points.txt +0 -0
flwr/cli/config_migration.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
return False, f"Failed to load TOML configuration: {toml_path}"
|
|
86
100
|
if errors:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
#
|
|
172
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
209
|
-
|
|
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.
|
|
221
|
-
SuperLinkConnectionTomlKey.ROOT_CERTIFICATES: connection.
|
|
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.
|
|
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.
|
|
230
|
-
options_dict = _serialize_simulation_options(connection.
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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.
|
|
119
|
-
self.
|
|
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.
|
|
123
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
166
|
+
+ f"expected str, but got {type(self.address).__name__}."
|
|
180
167
|
)
|
|
181
|
-
if self.
|
|
182
|
-
self.
|
|
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.
|
|
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.
|
|
191
|
-
if not Path(self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
{flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: flwr-nightly
|
|
3
|
-
Version: 1.26.0.
|
|
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
|
{flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/RECORD
RENAMED
|
@@ -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=
|
|
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=
|
|
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=
|
|
45
|
-
flwr/cli/utils.py,sha256=
|
|
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.
|
|
406
|
-
flwr_nightly-1.26.0.
|
|
407
|
-
flwr_nightly-1.26.0.
|
|
408
|
-
flwr_nightly-1.26.0.
|
|
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,,
|
{flwr_nightly-1.26.0.dev20260119.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|