flwr-nightly 1.26.0.dev20260117__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 -46
- flwr/cli/config_utils.py +1 -8
- flwr/cli/flower_config.py +3 -2
- flwr/cli/pull.py +1 -4
- flwr/cli/typing.py +64 -8
- flwr/cli/utils.py +8 -10
- flwr/supercore/constant.py +10 -0
- flwr/supercore/credential_store/__init__.py +33 -0
- flwr/supercore/credential_store/credential_store.py +34 -0
- flwr/supercore/credential_store/file_credential_store.py +76 -0
- flwr/supercore/sql_mixin.py +253 -0
- flwr/supercore/sqlite_mixin.py +4 -7
- flwr/supercore/state/schema/corestate_tables.py +16 -11
- flwr/supercore/state/schema/linkstate_tables.py +113 -113
- flwr/supercore/state/schema/objectstore_tables.py +58 -53
- {flwr_nightly-1.26.0.dev20260117.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/METADATA +1 -1
- {flwr_nightly-1.26.0.dev20260117.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/RECORD +19 -15
- {flwr_nightly-1.26.0.dev20260117.dist-info → flwr_nightly-1.26.0.dev20260120.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.26.0.dev20260117.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,29 +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
|
-
) -> None:
|
|
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
|
|
221
|
-
|
|
222
|
-
migrate(
|
|
223
|
-
app=Path(superlink),
|
|
224
|
-
toml_federation=args[0] if len(args) == 1 else None,
|
|
225
|
-
)
|
|
277
|
+
# Abort if legacy usage is detected to force user to adapt to new usage
|
|
278
|
+
raise typer.Exit(code=1)
|
flwr/cli/config_utils.py
CHANGED
|
@@ -307,16 +307,9 @@ def load_certificate_in_connection(
|
|
|
307
307
|
typer.Exit
|
|
308
308
|
If the configuration is invalid or the certificate file cannot be read.
|
|
309
309
|
"""
|
|
310
|
-
if connection.insecure is None:
|
|
311
|
-
raise ValueError(
|
|
312
|
-
f"SuperLink connection '{connection.name}' is missing insecure setting."
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
insecure = connection.insecure
|
|
316
|
-
|
|
317
310
|
# Process root certificates
|
|
318
311
|
if root_certificates := connection.root_certificates:
|
|
319
|
-
if insecure:
|
|
312
|
+
if connection.insecure:
|
|
320
313
|
typer.secho(
|
|
321
314
|
"❌ `root-certificates` were provided but the `insecure` parameter "
|
|
322
315
|
"is set to `True`.",
|
flwr/cli/flower_config.py
CHANGED
|
@@ -215,11 +215,12 @@ def serialize_superlink_connection(connection: SuperLinkConnection) -> dict[str,
|
|
|
215
215
|
dict[str, Any]
|
|
216
216
|
Dictionary representation suitable for TOML serialization.
|
|
217
217
|
"""
|
|
218
|
+
# pylint: disable=protected-access
|
|
218
219
|
conn_dict: dict[str, Any] = {
|
|
219
220
|
SuperLinkConnectionTomlKey.ADDRESS: connection.address,
|
|
220
221
|
SuperLinkConnectionTomlKey.ROOT_CERTIFICATES: connection.root_certificates,
|
|
221
|
-
SuperLinkConnectionTomlKey.INSECURE: connection.
|
|
222
|
-
SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH: connection.
|
|
222
|
+
SuperLinkConnectionTomlKey.INSECURE: connection._insecure,
|
|
223
|
+
SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH: connection._enable_account_auth,
|
|
223
224
|
SuperLinkConnectionTomlKey.FEDERATION: connection.federation,
|
|
224
225
|
}
|
|
225
226
|
# Remove None values
|
flwr/cli/pull.py
CHANGED
|
@@ -40,10 +40,7 @@ from .utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
|
|
|
40
40
|
def pull( # pylint: disable=R0914
|
|
41
41
|
run_id: Annotated[
|
|
42
42
|
int,
|
|
43
|
-
typer.
|
|
44
|
-
"--run-id",
|
|
45
|
-
help="Run ID to pull artifacts from.",
|
|
46
|
-
),
|
|
43
|
+
typer.Argument(help="Run ID to pull artifacts from."),
|
|
47
44
|
],
|
|
48
45
|
app: Annotated[
|
|
49
46
|
Path,
|
flwr/cli/typing.py
CHANGED
|
@@ -23,6 +23,8 @@ from flwr.cli.constant import (
|
|
|
23
23
|
SuperLinkConnectionTomlKey,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
+
_ERROR_MSG_FMT = "SuperLinkConnection.%s is None"
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
@dataclass
|
|
28
30
|
class SimulationClientResources:
|
|
@@ -91,16 +93,70 @@ class SuperLinkSimulationOptions:
|
|
|
91
93
|
|
|
92
94
|
@dataclass
|
|
93
95
|
class SuperLinkConnection:
|
|
94
|
-
"""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
|
+
"""
|
|
95
116
|
|
|
96
117
|
name: str
|
|
97
118
|
address: str | None = None
|
|
98
119
|
root_certificates: str | None = None
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
_insecure: bool | None = None
|
|
121
|
+
_enable_account_auth: bool | None = None
|
|
101
122
|
federation: str | None = None
|
|
102
123
|
options: SuperLinkSimulationOptions | None = None
|
|
103
124
|
|
|
125
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
name: str,
|
|
129
|
+
address: str | None = None,
|
|
130
|
+
root_certificates: str | None = None,
|
|
131
|
+
insecure: bool | None = None,
|
|
132
|
+
enable_account_auth: bool | None = None,
|
|
133
|
+
federation: str | None = None,
|
|
134
|
+
options: SuperLinkSimulationOptions | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
self.name = name
|
|
137
|
+
self.address = address
|
|
138
|
+
self.root_certificates = root_certificates
|
|
139
|
+
self._insecure = insecure
|
|
140
|
+
self._enable_account_auth = enable_account_auth
|
|
141
|
+
self.federation = federation
|
|
142
|
+
self.options = options
|
|
143
|
+
|
|
144
|
+
self.__post_init__()
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def insecure(self) -> bool:
|
|
148
|
+
"""Return the insecure flag or its default (False) if unset."""
|
|
149
|
+
if self._insecure is None:
|
|
150
|
+
return False
|
|
151
|
+
return self._insecure
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def enable_account_auth(self) -> bool:
|
|
155
|
+
"""Return the enable_account_auth flag or its default (False) if unset."""
|
|
156
|
+
if self._enable_account_auth is None:
|
|
157
|
+
return False
|
|
158
|
+
return self._enable_account_auth
|
|
159
|
+
|
|
104
160
|
def __post_init__(self) -> None:
|
|
105
161
|
"""Validate SuperLink connection configuration."""
|
|
106
162
|
err_prefix = f"Invalid value for key '%s' in connection '{self.name}': "
|
|
@@ -126,17 +182,17 @@ class SuperLinkConnection:
|
|
|
126
182
|
f"'{self.root_certificates}'."
|
|
127
183
|
)
|
|
128
184
|
|
|
129
|
-
if self.
|
|
185
|
+
if self._insecure is not None and not isinstance(self._insecure, bool):
|
|
130
186
|
raise ValueError(
|
|
131
187
|
err_prefix % SuperLinkConnectionTomlKey.INSECURE
|
|
132
|
-
+ f"expected bool, but got {type(self.
|
|
188
|
+
+ f"expected bool, but got {type(self._insecure).__name__}."
|
|
133
189
|
)
|
|
134
|
-
if self.
|
|
135
|
-
self.
|
|
190
|
+
if self._enable_account_auth is not None and not isinstance(
|
|
191
|
+
self._enable_account_auth, bool
|
|
136
192
|
):
|
|
137
193
|
raise ValueError(
|
|
138
194
|
err_prefix % SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH
|
|
139
|
-
+ f"expected bool, but got {type(self.
|
|
195
|
+
+ f"expected bool, but got {type(self._enable_account_auth).__name__}."
|
|
140
196
|
)
|
|
141
197
|
|
|
142
198
|
if self.federation is not None and not isinstance(self.federation, str):
|
flwr/cli/utils.py
CHANGED
|
@@ -458,13 +458,17 @@ def init_channel(
|
|
|
458
458
|
return channel
|
|
459
459
|
|
|
460
460
|
|
|
461
|
-
def init_channel_from_connection(
|
|
461
|
+
def init_channel_from_connection(
|
|
462
|
+
connection: SuperLinkConnection, auth_plugin: CliAuthPlugin | None = None
|
|
463
|
+
) -> grpc.Channel:
|
|
462
464
|
"""Initialize gRPC channel to the Control API.
|
|
463
465
|
|
|
464
466
|
Parameters
|
|
465
467
|
----------
|
|
466
468
|
connection : SuperLinkConnection
|
|
467
469
|
SuperLink connection configuration.
|
|
470
|
+
auth_plugin : CliAuthPlugin | None (default: None)
|
|
471
|
+
Authentication plugin instance for handling credentials.
|
|
468
472
|
|
|
469
473
|
Returns
|
|
470
474
|
-------
|
|
@@ -474,21 +478,15 @@ def init_channel_from_connection(connection: SuperLinkConnection) -> grpc.Channe
|
|
|
474
478
|
root_certificates_bytes = load_certificate_in_connection(connection)
|
|
475
479
|
|
|
476
480
|
# Load authentication plugin
|
|
477
|
-
auth_plugin
|
|
481
|
+
if auth_plugin is None:
|
|
482
|
+
auth_plugin = load_cli_auth_plugin_from_connection(connection)
|
|
478
483
|
|
|
479
484
|
# Load tokens
|
|
480
485
|
auth_plugin.load_tokens()
|
|
481
486
|
|
|
482
|
-
# Ensure address and insecure are set
|
|
483
|
-
if connection.address is None or connection.insecure is None:
|
|
484
|
-
raise ValueError(
|
|
485
|
-
f"Couldn't create channel. SuperLink connection '{connection.name}'"
|
|
486
|
-
" is missing address or insecure setting."
|
|
487
|
-
)
|
|
488
|
-
|
|
489
487
|
# Create the gRPC channel
|
|
490
488
|
channel = create_channel(
|
|
491
|
-
server_address=connection.address,
|
|
489
|
+
server_address=connection.address, # type: ignore
|
|
492
490
|
insecure=connection.insecure,
|
|
493
491
|
root_certificates=root_certificates_bytes,
|
|
494
492
|
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
flwr/supercore/constant.py
CHANGED
|
@@ -64,6 +64,16 @@ MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS = 3600
|
|
|
64
64
|
# System message type
|
|
65
65
|
SYSTEM_MESSAGE_TYPE = "system"
|
|
66
66
|
|
|
67
|
+
# SQLite PRAGMA settings for optimal performance and correctness
|
|
68
|
+
SQLITE_PRAGMAS = (
|
|
69
|
+
("journal_mode", "WAL"), # Enable Write-Ahead Logging for better concurrency
|
|
70
|
+
("synchronous", "NORMAL"),
|
|
71
|
+
("foreign_keys", "ON"),
|
|
72
|
+
("cache_size", "-64000"), # 64MB cache
|
|
73
|
+
("temp_store", "MEMORY"), # In-memory temp tables
|
|
74
|
+
("mmap_size", "268435456"), # 256MB memory-mapped I/O
|
|
75
|
+
)
|
|
76
|
+
|
|
67
77
|
|
|
68
78
|
class NodeStatus:
|
|
69
79
|
"""Event log writer types."""
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
"""Credential store for Flower."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .credential_store import CredentialStore
|
|
19
|
+
from .file_credential_store import FileCredentialStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_credential_store() -> CredentialStore:
|
|
23
|
+
"""Get the credential store instance.
|
|
24
|
+
|
|
25
|
+
Currently, only FileCredentialStore is implemented.
|
|
26
|
+
"""
|
|
27
|
+
return FileCredentialStore()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"CredentialStore",
|
|
32
|
+
"get_credential_store",
|
|
33
|
+
]
|
|
@@ -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)
|