flwr-nightly 1.26.0.dev20260115__py3-none-any.whl → 1.26.0.dev20260117__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.
@@ -0,0 +1,225 @@
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
+ """Utilities for migrating old TOML configurations to Flower config."""
16
+
17
+
18
+ import re
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import click
23
+ import typer
24
+
25
+ from .config_utils import load_and_validate, validate_federation_in_project_config
26
+ from .flower_config import (
27
+ init_flwr_config,
28
+ parse_superlink_connection,
29
+ set_default_superlink_connection,
30
+ write_superlink_connection,
31
+ )
32
+
33
+ CONFIG_MIGRATION_NOTICE = """
34
+ ##################################################################
35
+ # CONFIGURATION MIGRATION NOTICE:
36
+ #
37
+ # What was previously called "federation config" for SuperLink
38
+ # connections in pyproject.toml has been renamed and moved.
39
+ #
40
+ # These settings are now **SuperLink connection configuration**
41
+ # and are defined in the Flower configuration file.
42
+ #
43
+ # The entries below are commented out intentionally and are kept
44
+ # only as a migration reference.
45
+ #
46
+ # Docs: <link to Flower config docs>
47
+ ##################################################################
48
+
49
+ """
50
+
51
+ CLI_NOTICE = (
52
+ typer.style("\n🌸 Heads up from Flower!\n\n", fg=typer.colors.MAGENTA, bold=True)
53
+ + "We detected legacy usage of this command that relies on connection\n"
54
+ + "settings from your pyproject.toml.\n\n"
55
+ + "Flower will migrate any relevant settings to the new Flower config.\n\n"
56
+ + "Learn more: https://flower.ai/docs\n"
57
+ )
58
+
59
+
60
+ def _is_legacy_usage(superlink: str, args: list[str]) -> bool:
61
+ """Check if legacy usage is detected in the given arguments."""
62
+ # If one and only one extra argument is given, assume legacy usage
63
+ if len(args) == 1:
64
+ return True
65
+
66
+ # If the `superlink` looks like a path, assume legacy usage
67
+ pth = Path(superlink)
68
+ if pth.is_absolute() or len(pth.parts) > 1 or superlink in (".", ".."):
69
+ return True
70
+
71
+ # Lastly, check if a pyproject.toml file exists at the given superlink
72
+ if (pth / "pyproject.toml").exists():
73
+ return True
74
+
75
+ return False
76
+
77
+
78
+ def _check_is_migratable(app: Path) -> None:
79
+ """Check if the given app path contains legacy TOML configuration."""
80
+ toml_path = app / "pyproject.toml"
81
+ if not toml_path.exists():
82
+ raise FileNotFoundError(f"No pyproject.toml found in '{app}'")
83
+ config, errors, _ = load_and_validate(toml_path, check_module=False)
84
+ if config is None:
85
+ raise ValueError(f"Failed to load TOML configuration: {toml_path}")
86
+ 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
+ )
91
+ try:
92
+ _ = config["tool"]["flwr"]["federations"]
93
+ return
94
+ except KeyError:
95
+ raise ValueError(
96
+ f"No '[tool.flwr.federations]' section found in '{toml_path}'"
97
+ ) from None
98
+
99
+
100
+ def _migrate_pyproject_toml_to_flower_config(
101
+ app: Path, toml_federation: str | None
102
+ ) -> tuple[list[str], str | None]:
103
+ """Migrate old TOML configuration to Flower config."""
104
+ # Load and validate the old TOML configuration
105
+ toml_path = app / "pyproject.toml"
106
+ config, _, _ = load_and_validate(toml_path, check_module=False)
107
+ if config is None:
108
+ raise ValueError(f"Failed to load TOML configuration: {toml_path}")
109
+ validate_federation_in_project_config(toml_federation, config)
110
+
111
+ # Construct SuperLinkConnection
112
+ toml_federations: dict[str, Any] = config["tool"]["flwr"]["federations"]
113
+ migrated_conn_names: list[str] = []
114
+ for name, toml_fed_config in toml_federations.items():
115
+ if isinstance(toml_fed_config, dict):
116
+ # Resolve relative root-certificates path
117
+ if cert_path := toml_fed_config.get("root-certificates"):
118
+ if not Path(cert_path).is_absolute():
119
+ toml_fed_config["root-certificates"] = str(
120
+ (app / cert_path).resolve()
121
+ )
122
+ # Parse and write SuperLink connection
123
+ conn = parse_superlink_connection(toml_fed_config, name)
124
+ write_superlink_connection(conn)
125
+ migrated_conn_names.append(name)
126
+
127
+ # Set default federation if applicable
128
+ default_toml_federation: str | None = toml_federations.get("default")
129
+ if default_toml_federation in toml_federations:
130
+ set_default_superlink_connection(default_toml_federation)
131
+
132
+ return migrated_conn_names, default_toml_federation
133
+
134
+
135
+ def _comment_out_legacy_toml_config(app: Path) -> None:
136
+ """Comment out legacy TOML configuration in pyproject.toml."""
137
+ # Read pyproject.toml lines
138
+ toml_path = app / "pyproject.toml"
139
+ lines = toml_path.read_text(encoding="utf-8").splitlines(keepends=True)
140
+ section_pattern = re.compile(r"\s*\[(.*)\]")
141
+
142
+ # Comment out the [tool.flwr.federations] section
143
+ notice_added = False
144
+ in_federation_section = False
145
+ with toml_path.open("w", encoding="utf-8") as f:
146
+ for line in lines:
147
+ # Detect section headers
148
+ if match := section_pattern.match(line):
149
+ section = match.group(1)
150
+ in_federation_section = section.startswith("tool.flwr.federations")
151
+
152
+ # Comment out lines in the federation section
153
+ if in_federation_section:
154
+ if not notice_added:
155
+ f.write(CONFIG_MIGRATION_NOTICE)
156
+ notice_added = True
157
+ # Preserve empty lines and comment out others
158
+ f.write(f"# {line}" if line.strip() != "" else line)
159
+ else:
160
+ f.write(line)
161
+
162
+
163
+ def migrate(
164
+ app: Path,
165
+ toml_federation: str | None,
166
+ ) -> None:
167
+ """Migrate legacy TOML configuration to Flower config."""
168
+ # Initialize Flower config
169
+ init_flwr_config()
170
+
171
+ # Print migration notice
172
+ typer.echo(CLI_NOTICE)
173
+
174
+ # Check if migration is applicable
175
+ 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
+
181
+ try:
182
+ migrated_conns, default_conn = _migrate_pyproject_toml_to_flower_config(
183
+ app, toml_federation
184
+ )
185
+ except Exception as e:
186
+ raise click.ClickException(
187
+ f"Failed to migrate legacy TOML configuration to Flower config:\n{e!r}"
188
+ ) from e
189
+
190
+ typer.secho("✅ Migration completed successfully!\n", fg=typer.colors.GREEN)
191
+
192
+ # Print migrated connections
193
+ typer.secho("Migrated SuperLink connections:", fg=typer.colors.BLUE)
194
+ for conn_name in migrated_conns:
195
+ typer.secho(f" {conn_name}", fg=typer.colors.GREEN, nl=False)
196
+ if conn_name == default_conn:
197
+ typer.secho(" (default)", fg=typer.colors.WHITE, nl=False)
198
+ typer.echo()
199
+
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
+ _comment_out_legacy_toml_config(app)
206
+
207
+
208
+ def migrate_if_legacy_usage(
209
+ superlink: str,
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
+ )
flwr/cli/config_utils.py CHANGED
@@ -21,6 +21,7 @@ from typing import Any
21
21
  import tomli
22
22
  import typer
23
23
 
24
+ from flwr.cli.typing import SuperLinkConnection
24
25
  from flwr.common.config import (
25
26
  fuse_dicts,
26
27
  get_fab_config,
@@ -283,6 +284,65 @@ def validate_certificate_in_federation_config(
283
284
  return insecure, root_certificates_bytes
284
285
 
285
286
 
287
+ def load_certificate_in_connection(
288
+ connection: SuperLinkConnection,
289
+ ) -> bytes | None:
290
+ """Validate TLS-related settings and load root certificates if provided.
291
+
292
+ Parameters
293
+ ----------
294
+ connection : SuperLinkConnection
295
+ The SuperLink connection configuration.
296
+
297
+ Returns
298
+ -------
299
+ bytes | None
300
+ The loaded root certificate bytes if a custom certificate is configured.
301
+ None if TLS is disabled or if gRPC should use its default trust store.
302
+
303
+ Raises
304
+ ------
305
+ ValueError
306
+ If required TLS settings are missing.
307
+ typer.Exit
308
+ If the configuration is invalid or the certificate file cannot be read.
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
+ # Process root certificates
318
+ if root_certificates := connection.root_certificates:
319
+ if insecure:
320
+ typer.secho(
321
+ "❌ `root-certificates` were provided but the `insecure` parameter "
322
+ "is set to `True`.",
323
+ fg=typer.colors.RED,
324
+ bold=True,
325
+ err=True,
326
+ )
327
+ raise typer.Exit(code=1)
328
+
329
+ # TLS is enabled with self-signed certificates: attempt to read the file
330
+ try:
331
+ root_certificates_bytes = Path(root_certificates).read_bytes()
332
+ except Exception as e:
333
+ typer.secho(
334
+ f"❌ Failed to read certificate file `{root_certificates}`: {e}",
335
+ fg=typer.colors.RED,
336
+ bold=True,
337
+ err=True,
338
+ )
339
+ raise typer.Exit(code=1) from e
340
+ else:
341
+ root_certificates_bytes = None
342
+
343
+ return root_certificates_bytes
344
+
345
+
286
346
  def exit_if_no_address(federation_config: dict[str, Any], cmd: str) -> None:
287
347
  """Exit if the provided federation_config has no "address" key.
288
348
 
flwr/cli/flower_config.py CHANGED
@@ -234,7 +234,7 @@ def serialize_superlink_connection(connection: SuperLinkConnection) -> dict[str,
234
234
 
235
235
  def read_superlink_connection(
236
236
  connection_name: str | None = None,
237
- ) -> SuperLinkConnection | None:
237
+ ) -> SuperLinkConnection:
238
238
  """Read a SuperLink connection from the Flower configuration file.
239
239
 
240
240
  Parameters
@@ -245,9 +245,8 @@ def read_superlink_connection(
245
245
 
246
246
  Returns
247
247
  -------
248
- SuperLinkConnection | None
249
- The SuperLink connection, or None if the config file is missing or the
250
- requested connection (or default) cannot be found.
248
+ SuperLinkConnection
249
+ The SuperLink connection.
251
250
 
252
251
  Raises
253
252
  ------
@@ -345,10 +344,29 @@ def write_superlink_connection(connection: SuperLinkConnection) -> None:
345
344
  # Add/update the connection
346
345
  superlink_config[connection.name] = conn_dict
347
346
 
348
- # Flatten SuperLink connections
349
- for name in list(superlink_config.keys()):
350
- if isinstance(superlink_config[name], dict):
351
- superlink_config[name] = flatten_dict(superlink_config[name])
347
+ # Write back to file
348
+ write_flower_config(toml_dict)
349
+
350
+
351
+ def set_default_superlink_connection(connection_name: str) -> None:
352
+ """Set the default SuperLink connection."""
353
+ toml_dict, _ = read_flower_config()
354
+
355
+ # Get superlink section
356
+ superlink_config = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
357
+
358
+ # Check if the connection exists
359
+ if connection_name not in superlink_config:
360
+ typer.secho(
361
+ f"❌ SuperLink connection '{connection_name}' not found in the Flower "
362
+ "configuration file. Cannot set as default.",
363
+ fg=typer.colors.RED,
364
+ err=True,
365
+ )
366
+ raise typer.Exit(code=1)
367
+
368
+ # Set default connection
369
+ superlink_config[SuperLinkConnectionTomlKey.DEFAULT] = connection_name
352
370
 
353
371
  # Write back to file
354
372
  write_flower_config(toml_dict)
@@ -402,6 +420,12 @@ def write_flower_config(toml_dict: dict[str, Any]) -> Path:
402
420
  """
403
421
  config_path = get_flwr_home() / FLOWER_CONFIG_FILE
404
422
 
423
+ # Flatten SuperLink connections
424
+ superlink_config: dict[str, Any] = toml_dict[SuperLinkConnectionTomlKey.SUPERLINK]
425
+ for name in list(superlink_config.keys()):
426
+ if isinstance(superlink_config[name], dict):
427
+ superlink_config[name] = flatten_dict(superlink_config[name])
428
+
405
429
  # Get the standard TOML text
406
430
  toml_content = tomli_w.dumps(toml_dict)
407
431
 
flwr/cli/typing.py CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
 
18
18
  from dataclasses import dataclass
19
+ from pathlib import Path
19
20
 
20
21
  from flwr.cli.constant import (
21
22
  DEFAULT_SIMULATION_BACKEND_NAME,
@@ -102,38 +103,46 @@ class SuperLinkConnection:
102
103
 
103
104
  def __post_init__(self) -> None:
104
105
  """Validate SuperLink connection configuration."""
106
+ err_prefix = f"Invalid value for key '%s' in connection '{self.name}': "
105
107
  if self.address is not None and not isinstance(self.address, str):
106
108
  raise ValueError(
107
- f"Invalid value for key '{SuperLinkConnectionTomlKey.ADDRESS}': "
108
- f"expected str, but got {type(self.address).__name__}."
109
+ err_prefix % SuperLinkConnectionTomlKey.ADDRESS
110
+ + f"expected str, but got {type(self.address).__name__}."
109
111
  )
110
112
  if self.root_certificates is not None and not isinstance(
111
113
  self.root_certificates, str
112
114
  ):
113
115
  raise ValueError(
114
- "Invalid value for key "
115
- f"'{SuperLinkConnectionTomlKey.ROOT_CERTIFICATES}': "
116
- f"expected str, but got {type(self.root_certificates).__name__}."
116
+ err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
117
+ + f"expected str, but got {type(self.root_certificates).__name__}."
117
118
  )
119
+
120
+ # Ensure root certificates path is absolute
121
+ if self.root_certificates is not None:
122
+ if not Path(self.root_certificates).is_absolute():
123
+ raise ValueError(
124
+ err_prefix % SuperLinkConnectionTomlKey.ROOT_CERTIFICATES
125
+ + "expected absolute path, but got relative path "
126
+ f"'{self.root_certificates}'."
127
+ )
128
+
118
129
  if self.insecure is not None and not isinstance(self.insecure, bool):
119
130
  raise ValueError(
120
- f"Invalid value for key '{SuperLinkConnectionTomlKey.INSECURE}': "
121
- f"but got {type(self.insecure).__name__}."
131
+ err_prefix % SuperLinkConnectionTomlKey.INSECURE
132
+ + f"expected bool, but got {type(self.insecure).__name__}."
122
133
  )
123
134
  if self.enable_account_auth is not None and not isinstance(
124
135
  self.enable_account_auth, bool
125
136
  ):
126
137
  raise ValueError(
127
- "Invalid value for key "
128
- f"'{SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH}': "
129
- f"expected bool, but got {type(self.enable_account_auth).__name__}."
138
+ err_prefix % SuperLinkConnectionTomlKey.ENABLE_ACCOUNT_AUTH
139
+ + f"expected bool, but got {type(self.enable_account_auth).__name__}."
130
140
  )
131
141
 
132
142
  if self.federation is not None and not isinstance(self.federation, str):
133
143
  raise ValueError(
134
- "Invalid value for key "
135
- f"'{SuperLinkConnectionTomlKey.FEDERATION}': "
136
- f"expected str, but got {type(self.federation).__name__}."
144
+ err_prefix % SuperLinkConnectionTomlKey.FEDERATION
145
+ + f"expected str, but got {type(self.federation).__name__}."
137
146
  )
138
147
 
139
148
  # The connection needs to have either an address or options (or both).
flwr/cli/utils.py CHANGED
@@ -27,6 +27,7 @@ import grpc
27
27
  import pathspec
28
28
  import typer
29
29
 
30
+ from flwr.cli.typing import SuperLinkConnection
30
31
  from flwr.common.constant import (
31
32
  ACCESS_TOKEN_KEY,
32
33
  AUTHN_TYPE_JSON_KEY,
@@ -47,10 +48,14 @@ from flwr.common.grpc import (
47
48
  create_channel,
48
49
  on_channel_state_change,
49
50
  )
51
+ from flwr.supercore.utils import get_flwr_home
50
52
 
51
53
  from .auth_plugin import CliAuthPlugin, get_cli_plugin_class
52
54
  from .cli_account_auth_interceptor import CliAccountAuthInterceptor
53
- from .config_utils import validate_certificate_in_federation_config
55
+ from .config_utils import (
56
+ load_certificate_in_connection,
57
+ validate_certificate_in_federation_config,
58
+ )
54
59
 
55
60
 
56
61
  def prompt_text(
@@ -369,6 +374,52 @@ def load_cli_auth_plugin(
369
374
  raise typer.Exit(code=1) from None
370
375
 
371
376
 
377
+ def load_cli_auth_plugin_from_connection(
378
+ connection: SuperLinkConnection,
379
+ authn_type: str | None = None,
380
+ ) -> CliAuthPlugin:
381
+ """Load the CLI-side account auth plugin for the given connection.
382
+
383
+ Parameters
384
+ ----------
385
+ connection : SuperLinkConnection
386
+ The SuperLink connection configuration.
387
+ authn_type : str | None
388
+ Authentication type. If None, will be determined from config.
389
+
390
+ Returns
391
+ -------
392
+ CliAuthPlugin
393
+ The loaded authentication plugin instance.
394
+
395
+ Raises
396
+ ------
397
+ typer.Exit
398
+ If the authentication type is unknown.
399
+ """
400
+ # Locate the credentials directory
401
+ flwr_dir = get_flwr_home()
402
+ credentials_dir = flwr_dir / CREDENTIALS_DIR
403
+ credentials_dir.mkdir(parents=True, exist_ok=True)
404
+
405
+ # Find the path to the account auth config file
406
+ config_path = get_account_auth_config_path(flwr_dir, connection.name)
407
+
408
+ # Determine the auth type if not provided
409
+ if authn_type is None:
410
+ authn_type = AuthnType.NOOP
411
+ if connection.enable_account_auth:
412
+ authn_type = retrieve_authn_type(config_path)
413
+
414
+ # Retrieve auth plugin class and instantiate it
415
+ try:
416
+ auth_plugin_class = get_cli_plugin_class(authn_type)
417
+ return auth_plugin_class(config_path)
418
+ except ValueError:
419
+ typer.echo(f"❌ Unknown account authentication type: {authn_type}")
420
+ raise typer.Exit(code=1) from None
421
+
422
+
372
423
  def init_channel(
373
424
  app: Path, federation_config: dict[str, Any], auth_plugin: CliAuthPlugin
374
425
  ) -> grpc.Channel:
@@ -407,6 +458,46 @@ def init_channel(
407
458
  return channel
408
459
 
409
460
 
461
+ def init_channel_from_connection(connection: SuperLinkConnection) -> grpc.Channel:
462
+ """Initialize gRPC channel to the Control API.
463
+
464
+ Parameters
465
+ ----------
466
+ connection : SuperLinkConnection
467
+ SuperLink connection configuration.
468
+
469
+ Returns
470
+ -------
471
+ grpc.Channel
472
+ Configured gRPC channel with authentication interceptors.
473
+ """
474
+ root_certificates_bytes = load_certificate_in_connection(connection)
475
+
476
+ # Load authentication plugin
477
+ auth_plugin = load_cli_auth_plugin_from_connection(connection)
478
+
479
+ # Load tokens
480
+ auth_plugin.load_tokens()
481
+
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
+ # Create the gRPC channel
490
+ channel = create_channel(
491
+ server_address=connection.address,
492
+ insecure=connection.insecure,
493
+ root_certificates=root_certificates_bytes,
494
+ max_message_length=GRPC_MAX_MESSAGE_LENGTH,
495
+ interceptors=[CliAccountAuthInterceptor(auth_plugin)],
496
+ )
497
+ channel.subscribe(on_channel_state_change)
498
+ return channel
499
+
500
+
410
501
  @contextmanager
411
502
  def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-branches
412
503
  """Context manager to handle specific gRPC errors.
@@ -0,0 +1,15 @@
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
+ """Flower SuperCore state components."""
@@ -0,0 +1,125 @@
1
+ # State Entity Relationship Diagram
2
+
3
+ ## Schema
4
+
5
+ ```mermaid
6
+
7
+ ---
8
+ config:
9
+ layout: elk
10
+ ---
11
+ erDiagram
12
+ context {
13
+ INTEGER run_id FK "nullable"
14
+ BLOB context "nullable"
15
+ }
16
+
17
+ logs {
18
+ INTEGER run_id FK "nullable"
19
+ VARCHAR log "nullable"
20
+ INTEGER node_id "nullable"
21
+ FLOAT timestamp "nullable"
22
+ }
23
+
24
+ message_ins {
25
+ INTEGER run_id FK "nullable"
26
+ BLOB content "nullable"
27
+ FLOAT created_at "nullable"
28
+ VARCHAR delivered_at "nullable"
29
+ INTEGER dst_node_id "nullable"
30
+ BLOB error "nullable"
31
+ VARCHAR group_id "nullable"
32
+ VARCHAR message_id UK "nullable"
33
+ VARCHAR message_type "nullable"
34
+ VARCHAR reply_to_message_id "nullable"
35
+ INTEGER src_node_id "nullable"
36
+ FLOAT ttl "nullable"
37
+ }
38
+
39
+ message_res {
40
+ INTEGER run_id FK "nullable"
41
+ BLOB content "nullable"
42
+ FLOAT created_at "nullable"
43
+ VARCHAR delivered_at "nullable"
44
+ INTEGER dst_node_id "nullable"
45
+ BLOB error "nullable"
46
+ VARCHAR group_id "nullable"
47
+ VARCHAR message_id UK "nullable"
48
+ VARCHAR message_type "nullable"
49
+ VARCHAR reply_to_message_id "nullable"
50
+ INTEGER src_node_id "nullable"
51
+ FLOAT ttl "nullable"
52
+ }
53
+
54
+ node {
55
+ FLOAT heartbeat_interval "nullable"
56
+ VARCHAR last_activated_at "nullable"
57
+ VARCHAR last_deactivated_at "nullable"
58
+ INTEGER node_id UK "nullable"
59
+ TIMESTAMP online_until "nullable"
60
+ VARCHAR owner_aid "nullable"
61
+ VARCHAR owner_name "nullable"
62
+ BLOB public_key UK "nullable"
63
+ VARCHAR registered_at "nullable"
64
+ VARCHAR status "nullable"
65
+ VARCHAR unregistered_at "nullable"
66
+ }
67
+
68
+ object_children {
69
+ VARCHAR child_id PK,FK
70
+ VARCHAR parent_id PK,FK
71
+ }
72
+
73
+ objects {
74
+ VARCHAR object_id PK "nullable"
75
+ BLOB content "nullable"
76
+ INTEGER is_available
77
+ INTEGER ref_count
78
+ }
79
+
80
+ <<<<<<< HEAD
81
+ run {
82
+ INTEGER bytes_recv "nullable"
83
+ INTEGER bytes_sent "nullable"
84
+ FLOAT clientapp_runtime "nullable"
85
+ VARCHAR details "nullable"
86
+ VARCHAR fab_hash "nullable"
87
+ VARCHAR fab_id "nullable"
88
+ VARCHAR fab_version "nullable"
89
+ VARCHAR federation "nullable"
90
+ BLOB federation_options "nullable"
91
+ VARCHAR finished_at "nullable"
92
+ VARCHAR flwr_aid "nullable"
93
+ VARCHAR override_config "nullable"
94
+ VARCHAR pending_at "nullable"
95
+ INTEGER run_id UK "nullable"
96
+ VARCHAR running_at "nullable"
97
+ VARCHAR starting_at "nullable"
98
+ VARCHAR sub_status "nullable"
99
+ }
100
+
101
+ =======
102
+ >>>>>>> main
103
+ run_objects {
104
+ VARCHAR object_id PK,FK
105
+ INTEGER run_id PK
106
+ }
107
+
108
+ token_store {
109
+ INTEGER run_id PK "nullable"
110
+ FLOAT active_until "nullable"
111
+ VARCHAR token UK
112
+ }
113
+
114
+ <<<<<<< HEAD
115
+ run ||--o| context : run_id
116
+ run ||--o{ logs : run_id
117
+ run ||--o{ message_ins : run_id
118
+ run ||--o{ message_res : run_id
119
+ =======
120
+ >>>>>>> main
121
+ objects ||--o| object_children : parent_id
122
+ objects ||--o| object_children : child_id
123
+ objects ||--o| run_objects : object_id
124
+
125
+ ```
@@ -0,0 +1,15 @@
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
+ """Flower SQLAlchemy database schema."""
@@ -0,0 +1,31 @@
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
+ """SQLAlchemy Core Table definitions for CoreState."""
16
+
17
+
18
+ from sqlalchemy import Column, Float, Integer, MetaData, String, Table
19
+
20
+ corestate_metadata = MetaData()
21
+
22
+ # ------------------------------------------------------------------------------
23
+ # Table: token_store
24
+ # ------------------------------------------------------------------------------
25
+ token_store = Table(
26
+ "token_store",
27
+ corestate_metadata,
28
+ Column("run_id", Integer, primary_key=True, nullable=True),
29
+ Column("token", String, unique=True, nullable=False),
30
+ Column("active_until", Float),
31
+ )
@@ -0,0 +1,152 @@
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
+ """SQLAlchemy Core Table definitions for LinkState."""
16
+
17
+
18
+ from sqlalchemy import (
19
+ TIMESTAMP,
20
+ Column,
21
+ Float,
22
+ ForeignKey,
23
+ Index,
24
+ Integer,
25
+ LargeBinary,
26
+ MetaData,
27
+ String,
28
+ Table,
29
+ UniqueConstraint,
30
+ )
31
+
32
+ linkstate_metadata = MetaData()
33
+
34
+ # ------------------------------------------------------------------------------
35
+ # Table: node
36
+ # ------------------------------------------------------------------------------
37
+ node = Table(
38
+ "node",
39
+ linkstate_metadata,
40
+ Column("node_id", Integer, unique=True),
41
+ Column("owner_aid", String),
42
+ Column("owner_name", String),
43
+ Column("status", String),
44
+ Column("registered_at", String),
45
+ Column("last_activated_at", String, nullable=True),
46
+ Column("last_deactivated_at", String, nullable=True),
47
+ Column("unregistered_at", String, nullable=True),
48
+ Column("online_until", TIMESTAMP, nullable=True),
49
+ Column("heartbeat_interval", Float),
50
+ Column("public_key", LargeBinary, unique=True),
51
+ # Indexes
52
+ # Used in delete_node and get_node_info (security/filtering)
53
+ Index("idx_node_owner_aid", "owner_aid"),
54
+ # Used in get_nodes and activation checks (frequent filtering)
55
+ Index("idx_node_status", "status"),
56
+ # Used in heartbeat checks to efficiently find expired nodes
57
+ Index("idx_online_until", "online_until"),
58
+ )
59
+
60
+
61
+ # ------------------------------------------------------------------------------
62
+ # Table: run
63
+ # ------------------------------------------------------------------------------
64
+ run = Table(
65
+ "run",
66
+ linkstate_metadata,
67
+ Column("run_id", Integer, unique=True),
68
+ Column("fab_id", String),
69
+ Column("fab_version", String),
70
+ Column("fab_hash", String),
71
+ Column("override_config", String),
72
+ Column("pending_at", String),
73
+ Column("starting_at", String),
74
+ Column("running_at", String),
75
+ Column("finished_at", String),
76
+ Column("sub_status", String),
77
+ Column("details", String),
78
+ Column("federation", String),
79
+ Column("federation_options", LargeBinary),
80
+ Column("flwr_aid", String),
81
+ Column("bytes_sent", Integer, server_default="0"),
82
+ Column("bytes_recv", Integer, server_default="0"),
83
+ Column("clientapp_runtime", Float, server_default="0.0"),
84
+ )
85
+
86
+
87
+ # ------------------------------------------------------------------------------
88
+ # Table: logs
89
+ # ------------------------------------------------------------------------------
90
+ logs = Table(
91
+ "logs",
92
+ linkstate_metadata,
93
+ Column("timestamp", Float),
94
+ Column("run_id", Integer, ForeignKey("run.run_id")),
95
+ Column("node_id", Integer),
96
+ Column("log", String),
97
+ # Composite PK
98
+ UniqueConstraint("timestamp", "run_id", "node_id"),
99
+ )
100
+
101
+
102
+ # ------------------------------------------------------------------------------
103
+ # Table: context
104
+ # ------------------------------------------------------------------------------
105
+ context = Table(
106
+ "context",
107
+ linkstate_metadata,
108
+ Column("run_id", Integer, ForeignKey("run.run_id"), unique=True),
109
+ Column("context", LargeBinary),
110
+ )
111
+
112
+
113
+ # ------------------------------------------------------------------------------
114
+ # Table: message_ins
115
+ # ------------------------------------------------------------------------------
116
+ message_ins = Table(
117
+ "message_ins",
118
+ linkstate_metadata,
119
+ Column("message_id", String, unique=True),
120
+ Column("group_id", String),
121
+ Column("run_id", Integer, ForeignKey("run.run_id")),
122
+ Column("src_node_id", Integer),
123
+ Column("dst_node_id", Integer),
124
+ Column("reply_to_message_id", String),
125
+ Column("created_at", Float),
126
+ Column("delivered_at", String),
127
+ Column("ttl", Float),
128
+ Column("message_type", String),
129
+ Column("content", LargeBinary, nullable=True),
130
+ Column("error", LargeBinary, nullable=True),
131
+ )
132
+
133
+
134
+ # ------------------------------------------------------------------------------
135
+ # Table: message_res
136
+ # ------------------------------------------------------------------------------
137
+ message_res = Table(
138
+ "message_res",
139
+ linkstate_metadata,
140
+ Column("message_id", String, unique=True),
141
+ Column("group_id", String),
142
+ Column("run_id", Integer, ForeignKey("run.run_id")),
143
+ Column("src_node_id", Integer),
144
+ Column("dst_node_id", Integer),
145
+ Column("reply_to_message_id", String),
146
+ Column("created_at", Float),
147
+ Column("delivered_at", String),
148
+ Column("ttl", Float),
149
+ Column("message_type", String),
150
+ Column("content", LargeBinary, nullable=True),
151
+ Column("error", LargeBinary, nullable=True),
152
+ )
@@ -0,0 +1,85 @@
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
+ """SQLAlchemy Core Table definitions for ObjectStore."""
16
+
17
+
18
+ from sqlalchemy import (
19
+ CheckConstraint,
20
+ Column,
21
+ ForeignKey,
22
+ Integer,
23
+ LargeBinary,
24
+ MetaData,
25
+ PrimaryKeyConstraint,
26
+ String,
27
+ Table,
28
+ )
29
+
30
+ objectstore_metadata = MetaData()
31
+
32
+ # ------------------------------------------------------------------------------
33
+ # Table: objects
34
+ # ------------------------------------------------------------------------------
35
+ objects = Table(
36
+ "objects",
37
+ objectstore_metadata,
38
+ Column("object_id", String, primary_key=True, nullable=True),
39
+ Column("content", LargeBinary),
40
+ Column(
41
+ "is_available",
42
+ Integer,
43
+ nullable=False,
44
+ server_default="0",
45
+ ),
46
+ Column("ref_count", Integer, nullable=False, server_default="0"),
47
+ CheckConstraint("is_available IN (0, 1)", name="ck_objects_is_available"),
48
+ )
49
+
50
+ # ------------------------------------------------------------------------------
51
+ # Table: object_children
52
+ # ------------------------------------------------------------------------------
53
+ object_children = Table(
54
+ "object_children",
55
+ objectstore_metadata,
56
+ Column(
57
+ "parent_id",
58
+ String,
59
+ ForeignKey("objects.object_id", ondelete="CASCADE"),
60
+ nullable=False,
61
+ ),
62
+ Column(
63
+ "child_id",
64
+ String,
65
+ ForeignKey("objects.object_id", ondelete="CASCADE"),
66
+ nullable=False,
67
+ ),
68
+ PrimaryKeyConstraint("parent_id", "child_id"),
69
+ )
70
+
71
+ # ------------------------------------------------------------------------------
72
+ # Table: run_objects
73
+ # ------------------------------------------------------------------------------
74
+ run_objects = Table(
75
+ "run_objects",
76
+ objectstore_metadata,
77
+ Column("run_id", Integer, nullable=False),
78
+ Column(
79
+ "object_id",
80
+ String,
81
+ ForeignKey("objects.object_id", ondelete="CASCADE"),
82
+ nullable=False,
83
+ ),
84
+ PrimaryKeyConstraint("run_id", "object_id"),
85
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.26.0.dev20260115
3
+ Version: 1.26.0.dev20260117
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,12 +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_utils.py,sha256=Yt7Y9dHSNULrRjrmNgUFuzx9ehdPj7wVokj3QcjzxFI,11155
21
+ flwr/cli/config_migration.py,sha256=Jr9HMyWGS3xeNf9SELuJhPl7NhtfJMHlw37yG4r4jw4,8169
22
+ flwr/cli/config_utils.py,sha256=oxny7Gx2crsBYwpgcQ3OBw3hBE-o1oQNAg0o7UBB4u8,13015
22
23
  flwr/cli/constant.py,sha256=b5WsK_KXg9yDDk2lxEiN7rggsnZYNu1fiBC9NJVnnNs,3291
23
24
  flwr/cli/example.py,sha256=SNTorkKPrx1rOryGREUyZu8TcOc1-vFv1zEddaysdY0,2216
24
25
  flwr/cli/federation/__init__.py,sha256=okxswL4fAjApI9gV_alU1lRkTUcQRbwlzvtUTLz61fE,793
25
26
  flwr/cli/federation/ls.py,sha256=dGNw6quUAbUmqTNIVAPgJ9UgCJddITjKr48ETXQ9LTE,11581
26
- flwr/cli/flower_config.py,sha256=hRU7gLkgPiWyme1vl8McZ4BVlJEpUwTsDEqwka0LNWI,15000
27
+ flwr/cli/flower_config.py,sha256=Rfw7ThkIdNiEfU-MsPdV8PFxCRcB8gx4NWcWtWJFXeM,15741
27
28
  flwr/cli/install.py,sha256=yhgUWd_Psyc18DaoQHEQ5Ji45ZQ-3LclueM9uRxUEPw,10063
28
29
  flwr/cli/log.py,sha256=ZWveENLdDC0Dm10YIMcc3qdGV1aO-2YZXyHBhnB6ofI,7848
29
30
  flwr/cli/login/__init__.py,sha256=B1SXKU3HCQhWfFDMJhlC7FOl8UsvH4mxysxeBnrfyUE,800
@@ -40,8 +41,8 @@ flwr/cli/supernode/__init__.py,sha256=DBkjoPo2hS2Y-ghJxwLbrAbCQFBgD82_Itl2_892UB
40
41
  flwr/cli/supernode/ls.py,sha256=LNx3C9-Vfmw1ffVPDmjmex8TD_e0sTAXEC97aqg4Lqk,8958
41
42
  flwr/cli/supernode/register.py,sha256=2aQTT2wEp2pxxuIacC6FQkN8q_QKifZhWjD7LNw1R2A,6527
42
43
  flwr/cli/supernode/unregister.py,sha256=PXugcJJrM7cP3HbULjgcO22DciDP2uWN7YNgce4hv5E,4632
43
- flwr/cli/typing.py,sha256=Zxry4yfaAdMuiGRn9Foi37gQ1bhatRksswBhtvi7UCA,5708
44
- flwr/cli/utils.py,sha256=GWlonSmHUdezWf-7UTX4QmlWH7686R3SDQqiovYfomI,20528
44
+ flwr/cli/typing.py,sha256=QMph442kkoXjJoubevuYueMNGcuCJo29ekmrtiYzPK8,6108
45
+ flwr/cli/utils.py,sha256=pQtnKvMKGan-GaiKWTo98Oi4F6RW1ItqHaCgKEQuRvU,23348
45
46
  flwr/client/__init__.py,sha256=xwkPJfdeWxIIfmiPE5vnmnY_JbTlErP0Qs9eBP6qRFg,1252
46
47
  flwr/client/client.py,sha256=3HAchxvknKG9jYbB7swNyDj-e5vUWDuMKoLvbT7jCVM,7895
47
48
  flwr/client/dpfedavg_numpy_client.py,sha256=ELDHyEJcTB-FlLhHC-JXy8HuB3ZFHfT0HL3g1VSWY5w,7451
@@ -352,6 +353,12 @@ flwr/supercore/primitives/__init__.py,sha256=Tx8GOjnmMo8Y74RsDGrMpfr-E0Nl8dcUDF7
352
353
  flwr/supercore/primitives/asymmetric.py,sha256=1643niHYj3uEbfPd06VuMHwN3tKVwg0uVyR3RhTdWIU,3778
353
354
  flwr/supercore/primitives/asymmetric_ed25519.py,sha256=eIhOTMibQW0FJX4MXdplHdL3HcfCiKuFu2mQ8GQTUz8,5025
354
355
  flwr/supercore/sqlite_mixin.py,sha256=ZvtqDLRwCMzyrPsOTpAwJeHSpthrrtvEfUmWZmss6OA,5317
356
+ flwr/supercore/state/__init__.py,sha256=FkKhsNVM4LjlRlOgXTz6twINmw5ohIUKS_OER0BNo_w,724
357
+ flwr/supercore/state/schema/README.md,sha256=-0QrXDhnv30gEYoIUJo7aLUolY0r_t_nC24yk7B2agM,2892
358
+ flwr/supercore/state/schema/__init__.py,sha256=Egnde6OY01wrpT4PuhL4NGn_NY4jdAH7kf7ktagN4Ws,724
359
+ flwr/supercore/state/schema/corestate_tables.py,sha256=EEVz9lMCFES8y87nTTmU8X6sH7_FIlGZQ_3rUo0JI6Y,1252
360
+ flwr/supercore/state/schema/linkstate_tables.py,sha256=pJ0y-83TYTBJzkcxd6a9CJiNCk72eW7pB91Awg2nwuk,5186
361
+ flwr/supercore/state/schema/objectstore_tables.py,sha256=cCayjB9J1k9j4mcCQnl6Y-U68c__DMg_DUQxtaZRnpE,2637
355
362
  flwr/supercore/superexec/__init__.py,sha256=XKX208hZ6a9gZ4KT9kMqfpCtp_8VGxekzKFfHSu2esQ,707
356
363
  flwr/supercore/superexec/plugin/__init__.py,sha256=GNwq8uNdE8RB7ywEFRAvKjLFzgS3YXgz39-HBGsemWw,1035
357
364
  flwr/supercore/superexec/plugin/base_exec_plugin.py,sha256=QmTr7AJTZO1cfmoVRjZUEhSlxNCou-XotGFAXkCqwWQ,2043
@@ -392,7 +399,7 @@ flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca
392
399
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
393
400
  flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=rRL4CQ0L78jF_p0ct4-JMGREt6wWRy__wy4czF4f54Y,11639
394
401
  flwr/supernode/start_client_internal.py,sha256=BYk69UBQ2gQJaDQxXhccUgfOWrb7ShAstrbcMOCZIIs,26173
395
- flwr_nightly-1.26.0.dev20260115.dist-info/METADATA,sha256=cLPSDRssZax--llHpFGkMKlG7Tys3LoNFhaICv2PReo,14398
396
- flwr_nightly-1.26.0.dev20260115.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
397
- flwr_nightly-1.26.0.dev20260115.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
398
- flwr_nightly-1.26.0.dev20260115.dist-info/RECORD,,
402
+ flwr_nightly-1.26.0.dev20260117.dist-info/METADATA,sha256=IuT7FnpFScBkGOQJlpTXFUh3_kSZyt1HKQ0yZ7gjKds,14398
403
+ flwr_nightly-1.26.0.dev20260117.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
404
+ flwr_nightly-1.26.0.dev20260117.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
405
+ flwr_nightly-1.26.0.dev20260117.dist-info/RECORD,,