flwr 1.25.0__py3-none-any.whl → 1.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +18 -69
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +28 -58
  25. flwr/cli/new/new.py +9 -29
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +85 -93
  28. flwr/cli/run_utils.py +1 -1
  29. flwr/cli/stop.py +32 -73
  30. flwr/cli/supernode/ls.py +25 -57
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -275
  35. flwr/client/grpc_rere_client/connection.py +3 -3
  36. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  37. flwr/client/message_handler/message_handler.py +2 -1
  38. flwr/client/mod/centraldp_mods.py +1 -1
  39. flwr/client/mod/localdp_mod.py +1 -1
  40. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  41. flwr/client/run_info_store.py +2 -1
  42. flwr/clientapp/client_app.py +2 -1
  43. flwr/common/__init__.py +3 -2
  44. flwr/common/args.py +5 -5
  45. flwr/common/config.py +12 -17
  46. flwr/common/constant.py +3 -16
  47. flwr/common/context.py +2 -1
  48. flwr/common/exit/exit.py +4 -4
  49. flwr/common/exit/exit_code.py +6 -0
  50. flwr/common/grpc.py +2 -1
  51. flwr/common/logger.py +1 -1
  52. flwr/common/message.py +1 -1
  53. flwr/common/retry_invoker.py +13 -5
  54. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  55. flwr/common/serde.py +7 -5
  56. flwr/common/telemetry.py +1 -1
  57. flwr/common/typing.py +4 -3
  58. flwr/compat/client/app.py +6 -9
  59. flwr/compat/client/grpc_client/connection.py +2 -1
  60. flwr/compat/common/constant.py +29 -0
  61. flwr/compat/server/app.py +1 -1
  62. flwr/proto/clientappio_pb2.py +2 -2
  63. flwr/proto/clientappio_pb2_grpc.py +104 -88
  64. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  65. flwr/proto/federation_pb2.py +5 -3
  66. flwr/proto/federation_pb2.pyi +32 -2
  67. flwr/proto/run_pb2.py +5 -13
  68. flwr/proto/run_pb2.pyi +0 -57
  69. flwr/proto/serverappio_pb2.py +2 -2
  70. flwr/proto/serverappio_pb2_grpc.py +138 -207
  71. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  72. flwr/proto/simulationio_pb2.py +2 -2
  73. flwr/proto/simulationio_pb2_grpc.py +62 -90
  74. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  75. flwr/server/app.py +6 -13
  76. flwr/server/compat/grid_client_proxy.py +2 -1
  77. flwr/server/grid/grpc_grid.py +5 -5
  78. flwr/server/serverapp/app.py +11 -4
  79. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  80. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  81. flwr/server/superlink/fleet/message_handler/message_handler.py +6 -5
  82. flwr/server/superlink/linkstate/__init__.py +2 -2
  83. flwr/server/superlink/linkstate/in_memory_linkstate.py +2 -10
  84. flwr/server/superlink/linkstate/linkstate.py +2 -21
  85. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  86. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +432 -534
  87. flwr/server/superlink/linkstate/utils.py +49 -2
  88. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  89. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  90. flwr/server/utils/validator.py +1 -1
  91. flwr/server/workflow/default_workflows.py +2 -1
  92. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  93. flwr/serverapp/strategy/bulyan.py +7 -1
  94. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  95. flwr/serverapp/strategy/fedavg.py +1 -1
  96. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  97. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  98. flwr/simulation/run_simulation.py +3 -12
  99. flwr/simulation/simulationio_connection.py +3 -3
  100. flwr/{common → supercore}/address.py +7 -33
  101. flwr/supercore/app_utils.py +2 -1
  102. flwr/supercore/constant.py +24 -2
  103. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  104. flwr/supercore/credential_store/__init__.py +33 -0
  105. flwr/supercore/credential_store/credential_store.py +34 -0
  106. flwr/supercore/credential_store/file_credential_store.py +76 -0
  107. flwr/{common → supercore}/date.py +0 -11
  108. flwr/supercore/ffs/disk_ffs.py +1 -1
  109. flwr/supercore/object_store/object_store_factory.py +14 -6
  110. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  111. flwr/supercore/sql_mixin.py +315 -0
  112. flwr/supercore/state/__init__.py +15 -0
  113. flwr/supercore/state/alembic/__init__.py +15 -0
  114. flwr/supercore/state/alembic/env.py +103 -0
  115. flwr/supercore/state/alembic/script.py.mako +43 -0
  116. flwr/supercore/state/alembic/utils.py +239 -0
  117. flwr/supercore/state/alembic/versions/__init__.py +15 -0
  118. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  119. flwr/supercore/state/schema/README.md +121 -0
  120. flwr/supercore/state/schema/__init__.py +15 -0
  121. flwr/supercore/state/schema/corestate_tables.py +36 -0
  122. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  123. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  124. flwr/supercore/superexec/run_superexec.py +2 -2
  125. flwr/supercore/utils.py +36 -1
  126. flwr/superlink/federation/federation_manager.py +2 -2
  127. flwr/superlink/federation/noop_federation_manager.py +8 -6
  128. flwr/superlink/servicer/control/control_servicer.py +19 -17
  129. flwr/supernode/cli/flower_supernode.py +2 -1
  130. flwr/supernode/runtime/run_clientapp.py +14 -14
  131. flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -8
  132. flwr/supernode/start_client_internal.py +10 -6
  133. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/METADATA +7 -5
  134. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/RECORD +137 -116
  135. flwr/cli/federation/show.py +0 -318
  136. flwr/common/pyproject.py +0 -42
  137. flwr/supercore/sqlite_mixin.py +0 -159
  138. /flwr/{common → supercore}/version.py +0 -0
  139. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  140. {flwr-1.25.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,239 @@
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
+ """Helpers for running and validating Alembic migrations."""
16
+
17
+
18
+ from logging import INFO
19
+ from pathlib import Path
20
+
21
+ from alembic import command
22
+ from alembic.config import Config
23
+ from sqlalchemy import MetaData, create_engine, inspect, pool
24
+ from sqlalchemy.engine import Engine
25
+
26
+ from flwr.common.exit import ExitCode, flwr_exit
27
+ from flwr.common.logger import log
28
+ from flwr.supercore.state.schema.corestate_tables import create_corestate_metadata
29
+ from flwr.supercore.state.schema.linkstate_tables import create_linkstate_metadata
30
+ from flwr.supercore.state.schema.objectstore_tables import create_objectstore_metadata
31
+
32
+ ALEMBIC_DIR = Path(__file__).resolve().parent
33
+ ALEMBIC_VERSION_TABLE = "alembic_version"
34
+ FLWR_STATE_BASELINE_REVISION = "8e65d8ae60b0"
35
+
36
+
37
+ def get_combined_metadata() -> MetaData:
38
+ """Combine all Flower state metadata objects into a single MetaData instance.
39
+
40
+ This ensures Alembic can track all tables across CoreState, LinkState, and
41
+ ObjectStore.
42
+
43
+ Returns
44
+ -------
45
+ MetaData
46
+ Combined SQLAlchemy MetaData with all Flower state tables.
47
+ """
48
+ # Start with linkstate tables
49
+ metadata = create_linkstate_metadata()
50
+
51
+ # Add corestate tables
52
+ corestate_metadata = create_corestate_metadata()
53
+ for table in corestate_metadata.tables.values():
54
+ table.to_metadata(metadata)
55
+
56
+ # Add objectstore tables
57
+ objectstore_metadata = create_objectstore_metadata()
58
+ for table in objectstore_metadata.tables.values():
59
+ table.to_metadata(metadata)
60
+
61
+ return metadata
62
+
63
+
64
+ def run_migrations(engine: Engine) -> None:
65
+ """Run pending Alembic migrations, handling pre-Alembic legacy databases.
66
+
67
+ Expected scenarios:
68
+ - If alembic_version exists: run migrations normally.
69
+ - If DB is empty, e.g. when newly created: run migrations normally.
70
+ - If DB is pre-Alembic and schema is mismatched: exit with guidance.
71
+ - If DB is pre-Alembic and schema matches baseline: stamp, then upgrade.
72
+ """
73
+ config = build_alembic_config(engine)
74
+ has_version_table = _has_alembic_version_table(engine)
75
+
76
+ # Standard database with version tracking: just upgrade.
77
+ if has_version_table:
78
+ command.upgrade(config, "head")
79
+ return
80
+
81
+ table_names = _get_user_table_names(engine)
82
+
83
+ # Empty/new database: run all migrations from scratch.
84
+ if not table_names:
85
+ command.upgrade(config, "head")
86
+ return
87
+
88
+ # Pre-Alembic database detected without version tracking: verify database matches
89
+ # baseline schema before stamping version and upgrading.
90
+ is_valid, error_msg = _verify_legacy_schema_matches_baseline(engine)
91
+
92
+ # This is an edge case and unlikely to happen since SuperLink requires a specific
93
+ # schema to operate normally.
94
+ if not is_valid:
95
+ flwr_exit(
96
+ ExitCode.SUPERLINK_DATABASE_SCHEMA_MISMATCH,
97
+ "Detected a pre-Alembic Flower state database, but its schema does not "
98
+ f"match the baseline migration (revision {FLWR_STATE_BASELINE_REVISION}). "
99
+ "Back up the database and either migrate it manually to the baseline "
100
+ "schema or start with a fresh database. "
101
+ f"{error_msg}",
102
+ )
103
+
104
+ log(
105
+ INFO,
106
+ "Detected pre-Alembic state database without alembic_version; stamping to %s "
107
+ "before upgrading.",
108
+ FLWR_STATE_BASELINE_REVISION,
109
+ )
110
+ stamp_existing_database(engine, FLWR_STATE_BASELINE_REVISION)
111
+ command.upgrade(config, "head")
112
+ log(INFO, "Flower state database stamped and upgraded successfully!")
113
+
114
+
115
+ def build_alembic_config(engine: Engine) -> Config:
116
+ """Create Alembic config with script location and DB URL."""
117
+ config = Config()
118
+ config.set_main_option("script_location", str(ALEMBIC_DIR))
119
+ config.set_main_option("sqlalchemy.url", str(engine.url))
120
+ return config
121
+
122
+
123
+ def _get_user_table_names(engine: Engine) -> set[str]:
124
+ """Return non-internal table names for the given engine."""
125
+ inspector = inspect(engine)
126
+ table_names = set(inspector.get_table_names())
127
+ # Exclude SQLite internal tables (for example, sqlite_sequence)
128
+ return {name for name in table_names if not name.startswith("sqlite_")}
129
+
130
+
131
+ def _has_alembic_version_table(engine: Engine) -> bool:
132
+ """Return True if the Alembic version table exists."""
133
+ inspector = inspect(engine)
134
+ return inspector.has_table(ALEMBIC_VERSION_TABLE)
135
+
136
+
137
+ def _get_baseline_metadata() -> MetaData:
138
+ """Create an in-memory DB at baseline revision and reflect its schema.
139
+
140
+ Uses an in-memory SQLite database instead of a temporary file to avoid requiring
141
+ filesystem write access. Note that this function is only invoked for pre-Alembic
142
+ databases.
143
+
144
+ The implementation uses StaticPool and passes an active connection via
145
+ config.attributes to Alembic's env.py. This ensures the same in-memory database
146
+ instance is used throughout migration and reflection, since each new connection to
147
+ sqlite:///:memory: creates a separate empty database.
148
+ """
149
+ # Create an in-memory SQLite database with StaticPool to ensure connection reuse.
150
+ # This is needed because in-memory databases are instance-specific per connection.
151
+ engine = create_engine(
152
+ "sqlite:///:memory:",
153
+ connect_args={"check_same_thread": False},
154
+ poolclass=pool.StaticPool,
155
+ )
156
+ try:
157
+ # Open a connection and pass it to Alembic to ensure the in-memory database
158
+ # persists throughout the migration process. Without this, Alembic would
159
+ # create a new connection (and thus a new empty database) from the URL.
160
+ with engine.begin() as connection:
161
+ config = build_alembic_config(engine)
162
+ # Store the connection in config.attributes so env.py can use it directly.
163
+ # This prevents Alembic from creating a new connection and losing our data.
164
+ config.attributes["connection"] = connection
165
+ command.upgrade(config, FLWR_STATE_BASELINE_REVISION)
166
+
167
+ # Reflect the baseline schema from the in-memory database.
168
+ # At this point, the StaticPool ensures we're still connected to the same
169
+ # database instance that contains the migrated tables.
170
+ baseline_metadata = MetaData()
171
+ baseline_metadata.reflect(
172
+ bind=engine,
173
+ only=lambda table_name, _: table_name != ALEMBIC_VERSION_TABLE,
174
+ )
175
+ finally:
176
+ engine.dispose()
177
+
178
+ return baseline_metadata
179
+
180
+
181
+ def _verify_legacy_schema_matches_baseline(engine: Engine) -> tuple[bool, str]:
182
+ """Verify legacy schema matches baseline tables and columns.
183
+
184
+ Only missing tables/columns are reported as errors.
185
+
186
+ Returns
187
+ -------
188
+ tuple[bool, str]
189
+ (is_valid, error_message). If valid, error_message is empty.
190
+ """
191
+ inspector = inspect(engine)
192
+
193
+ # Get the baseline schema by running migrations up to the baseline revision
194
+ # in a temporary database and reflecting its schema
195
+ baseline_metadata = _get_baseline_metadata()
196
+ existing_tables = set(inspector.get_table_names())
197
+
198
+ # Filter out SQLite internal tables and alembic_version
199
+ existing_tables = {
200
+ t
201
+ for t in existing_tables
202
+ if not t.startswith("sqlite_") and t != ALEMBIC_VERSION_TABLE
203
+ }
204
+
205
+ # Exclude alembic_version from baseline comparison
206
+ expected_tables = {
207
+ t for t in baseline_metadata.tables.keys() if t != ALEMBIC_VERSION_TABLE
208
+ }
209
+ missing_tables = expected_tables - existing_tables
210
+
211
+ if missing_tables:
212
+ table_list = ", ".join(sorted(existing_tables))
213
+ missing_str = ", ".join(sorted(missing_tables))
214
+ return False, (
215
+ f"Detected tables: [{table_list}]. "
216
+ f"Missing baseline tables: [{missing_str}]."
217
+ )
218
+
219
+ # Verify columns for each expected table
220
+ for table_name in expected_tables:
221
+ table = baseline_metadata.tables[table_name]
222
+ expected_columns = {col.name for col in table.columns}
223
+ actual_columns = {col["name"] for col in inspector.get_columns(table_name)}
224
+
225
+ missing_cols = expected_columns - actual_columns
226
+ if missing_cols:
227
+ missing_cols_str = ", ".join(sorted(missing_cols))
228
+ return False, (
229
+ f"Table '{table_name}' missing columns: [{missing_cols_str}]."
230
+ )
231
+
232
+ return True, ""
233
+
234
+
235
+ def stamp_existing_database(
236
+ engine: Engine, revision: str = FLWR_STATE_BASELINE_REVISION
237
+ ) -> None:
238
+ """Stamp an existing legacy database to the baseline Alembic revision."""
239
+ command.stamp(build_alembic_config(engine), revision)
@@ -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
+ """Alembic migration versions."""
@@ -0,0 +1,200 @@
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
+ """Initialize migration of state tables.
16
+
17
+ Revision ID: 8e65d8ae60b0
18
+ Revises:
19
+ Create Date: 2026-01-28 11:03:18.038794
20
+ """
21
+ from collections.abc import Sequence
22
+
23
+ import sqlalchemy as sa
24
+ from alembic import op
25
+
26
+ # pylint: disable=no-member
27
+
28
+ # revision identifiers, used by Alembic.
29
+ revision: str = "8e65d8ae60b0"
30
+ down_revision: str | Sequence[str] | None = None
31
+ branch_labels: str | Sequence[str] | None = None
32
+ depends_on: str | Sequence[str] | None = None
33
+
34
+
35
+ def upgrade() -> None:
36
+ """Upgrade schema."""
37
+ # ### commands auto generated by Alembic - please adjust! ###
38
+ # LinkState tables
39
+ op.create_table(
40
+ "node",
41
+ sa.Column("node_id", sa.Integer(), nullable=True),
42
+ sa.Column("owner_aid", sa.String(), nullable=True),
43
+ sa.Column("owner_name", sa.String(), nullable=True),
44
+ sa.Column("status", sa.String(), nullable=True),
45
+ sa.Column("registered_at", sa.String(), nullable=True),
46
+ sa.Column("last_activated_at", sa.String(), nullable=True),
47
+ sa.Column("last_deactivated_at", sa.String(), nullable=True),
48
+ sa.Column("unregistered_at", sa.String(), nullable=True),
49
+ sa.Column("online_until", sa.TIMESTAMP(), nullable=True),
50
+ sa.Column("heartbeat_interval", sa.Float(), nullable=True),
51
+ sa.Column("public_key", sa.LargeBinary(), nullable=True),
52
+ sa.UniqueConstraint("node_id"),
53
+ sa.UniqueConstraint("public_key"),
54
+ )
55
+ op.create_index("idx_node_owner_aid", "node", ["owner_aid"], unique=False)
56
+ op.create_index("idx_node_status", "node", ["status"], unique=False)
57
+ op.create_index("idx_online_until", "node", ["online_until"], unique=False)
58
+ op.create_table(
59
+ "run",
60
+ sa.Column("run_id", sa.Integer(), nullable=True),
61
+ sa.Column("fab_id", sa.String(), nullable=True),
62
+ sa.Column("fab_version", sa.String(), nullable=True),
63
+ sa.Column("fab_hash", sa.String(), nullable=True),
64
+ sa.Column("override_config", sa.String(), nullable=True),
65
+ sa.Column("pending_at", sa.String(), nullable=True),
66
+ sa.Column("starting_at", sa.String(), nullable=True),
67
+ sa.Column("running_at", sa.String(), nullable=True),
68
+ sa.Column("finished_at", sa.String(), nullable=True),
69
+ sa.Column("sub_status", sa.String(), nullable=True),
70
+ sa.Column("details", sa.String(), nullable=True),
71
+ sa.Column("federation", sa.String(), nullable=True),
72
+ sa.Column("federation_options", sa.LargeBinary(), nullable=True),
73
+ sa.Column("flwr_aid", sa.String(), nullable=True),
74
+ sa.Column("bytes_sent", sa.Integer(), server_default="0", nullable=True),
75
+ sa.Column("bytes_recv", sa.Integer(), server_default="0", nullable=True),
76
+ sa.Column("clientapp_runtime", sa.Float(), server_default="0.0", nullable=True),
77
+ sa.UniqueConstraint("run_id"),
78
+ )
79
+ op.create_table(
80
+ "logs",
81
+ sa.Column("timestamp", sa.Float(), nullable=True),
82
+ sa.Column("run_id", sa.Integer(), nullable=True),
83
+ sa.Column("node_id", sa.Integer(), nullable=True),
84
+ sa.Column("log", sa.String(), nullable=True),
85
+ sa.ForeignKeyConstraint(
86
+ ["run_id"],
87
+ ["run.run_id"],
88
+ ),
89
+ sa.UniqueConstraint("timestamp", "run_id", "node_id"),
90
+ )
91
+ op.create_table(
92
+ "context",
93
+ sa.Column("run_id", sa.Integer(), nullable=True),
94
+ sa.Column("context", sa.LargeBinary(), nullable=True),
95
+ sa.ForeignKeyConstraint(
96
+ ["run_id"],
97
+ ["run.run_id"],
98
+ ),
99
+ sa.UniqueConstraint("run_id"),
100
+ )
101
+ op.create_table(
102
+ "message_ins",
103
+ sa.Column("message_id", sa.String(), nullable=True),
104
+ sa.Column("group_id", sa.String(), nullable=True),
105
+ sa.Column("run_id", sa.Integer(), nullable=True),
106
+ sa.Column("src_node_id", sa.Integer(), nullable=True),
107
+ sa.Column("dst_node_id", sa.Integer(), nullable=True),
108
+ sa.Column("reply_to_message_id", sa.String(), nullable=True),
109
+ sa.Column("created_at", sa.Float(), nullable=True),
110
+ sa.Column("delivered_at", sa.String(), nullable=True),
111
+ sa.Column("ttl", sa.Float(), nullable=True),
112
+ sa.Column("message_type", sa.String(), nullable=True),
113
+ sa.Column("content", sa.LargeBinary(), nullable=True),
114
+ sa.Column("error", sa.LargeBinary(), nullable=True),
115
+ sa.ForeignKeyConstraint(
116
+ ["run_id"],
117
+ ["run.run_id"],
118
+ ),
119
+ sa.UniqueConstraint("message_id"),
120
+ )
121
+ op.create_table(
122
+ "message_res",
123
+ sa.Column("message_id", sa.String(), nullable=True),
124
+ sa.Column("group_id", sa.String(), nullable=True),
125
+ sa.Column("run_id", sa.Integer(), nullable=True),
126
+ sa.Column("src_node_id", sa.Integer(), nullable=True),
127
+ sa.Column("dst_node_id", sa.Integer(), nullable=True),
128
+ sa.Column("reply_to_message_id", sa.String(), nullable=True),
129
+ sa.Column("created_at", sa.Float(), nullable=True),
130
+ sa.Column("delivered_at", sa.String(), nullable=True),
131
+ sa.Column("ttl", sa.Float(), nullable=True),
132
+ sa.Column("message_type", sa.String(), nullable=True),
133
+ sa.Column("content", sa.LargeBinary(), nullable=True),
134
+ sa.Column("error", sa.LargeBinary(), nullable=True),
135
+ sa.ForeignKeyConstraint(
136
+ ["run_id"],
137
+ ["run.run_id"],
138
+ ),
139
+ sa.UniqueConstraint("message_id"),
140
+ )
141
+ # CoreState tables
142
+ op.create_table(
143
+ "token_store",
144
+ sa.Column("run_id", sa.Integer(), nullable=True),
145
+ sa.Column("token", sa.String(), nullable=False),
146
+ sa.Column("active_until", sa.Float(), nullable=True),
147
+ sa.PrimaryKeyConstraint("run_id"),
148
+ sa.UniqueConstraint("token"),
149
+ )
150
+ # ObjectStore tables
151
+ op.create_table(
152
+ "objects",
153
+ sa.Column("object_id", sa.String(), nullable=True),
154
+ sa.Column("content", sa.LargeBinary(), nullable=True),
155
+ sa.Column("is_available", sa.Integer(), server_default="0", nullable=False),
156
+ sa.Column("ref_count", sa.Integer(), server_default="0", nullable=False),
157
+ sa.CheckConstraint("is_available IN (0, 1)", name="ck_objects_is_available"),
158
+ sa.PrimaryKeyConstraint("object_id"),
159
+ )
160
+ op.create_table(
161
+ "object_children",
162
+ sa.Column("parent_id", sa.String(), nullable=False),
163
+ sa.Column("child_id", sa.String(), nullable=False),
164
+ sa.ForeignKeyConstraint(
165
+ ["child_id"], ["objects.object_id"], ondelete="CASCADE"
166
+ ),
167
+ sa.ForeignKeyConstraint(
168
+ ["parent_id"], ["objects.object_id"], ondelete="CASCADE"
169
+ ),
170
+ sa.PrimaryKeyConstraint("parent_id", "child_id"),
171
+ )
172
+ op.create_table(
173
+ "run_objects",
174
+ sa.Column("run_id", sa.Integer(), nullable=False),
175
+ sa.Column("object_id", sa.String(), nullable=False),
176
+ sa.ForeignKeyConstraint(
177
+ ["object_id"], ["objects.object_id"], ondelete="CASCADE"
178
+ ),
179
+ sa.PrimaryKeyConstraint("run_id", "object_id"),
180
+ )
181
+ # ### end Alembic commands ###
182
+
183
+
184
+ def downgrade() -> None:
185
+ """Downgrade schema."""
186
+ # ### commands auto generated by Alembic - please adjust! ###
187
+ op.drop_table("run_objects")
188
+ op.drop_table("object_children")
189
+ op.drop_table("objects")
190
+ op.drop_table("token_store")
191
+ op.drop_table("message_res")
192
+ op.drop_table("message_ins")
193
+ op.drop_table("context")
194
+ op.drop_table("logs")
195
+ op.drop_table("run")
196
+ op.drop_index("idx_online_until", table_name="node")
197
+ op.drop_index("idx_node_status", table_name="node")
198
+ op.drop_index("idx_node_owner_aid", table_name="node")
199
+ op.drop_table("node")
200
+ # ### end Alembic commands ###
@@ -0,0 +1,121 @@
1
+ # State Entity Relationship Diagram
2
+
3
+ ## Schema
4
+
5
+ <!-- BEGIN_SQLALCHEMY_DOCS -->
6
+ ```mermaid
7
+
8
+ ---
9
+ config:
10
+ layout: elk
11
+ ---
12
+ erDiagram
13
+ context {
14
+ INTEGER run_id FK "nullable"
15
+ BLOB context "nullable"
16
+ }
17
+
18
+ logs {
19
+ INTEGER run_id FK "nullable"
20
+ VARCHAR log "nullable"
21
+ INTEGER node_id "nullable"
22
+ FLOAT timestamp "nullable"
23
+ }
24
+
25
+ message_ins {
26
+ INTEGER run_id FK "nullable"
27
+ BLOB content "nullable"
28
+ FLOAT created_at "nullable"
29
+ VARCHAR delivered_at "nullable"
30
+ INTEGER dst_node_id "nullable"
31
+ BLOB error "nullable"
32
+ VARCHAR group_id "nullable"
33
+ VARCHAR message_id UK "nullable"
34
+ VARCHAR message_type "nullable"
35
+ VARCHAR reply_to_message_id "nullable"
36
+ INTEGER src_node_id "nullable"
37
+ FLOAT ttl "nullable"
38
+ }
39
+
40
+ message_res {
41
+ INTEGER run_id FK "nullable"
42
+ BLOB content "nullable"
43
+ FLOAT created_at "nullable"
44
+ VARCHAR delivered_at "nullable"
45
+ INTEGER dst_node_id "nullable"
46
+ BLOB error "nullable"
47
+ VARCHAR group_id "nullable"
48
+ VARCHAR message_id UK "nullable"
49
+ VARCHAR message_type "nullable"
50
+ VARCHAR reply_to_message_id "nullable"
51
+ INTEGER src_node_id "nullable"
52
+ FLOAT ttl "nullable"
53
+ }
54
+
55
+ node {
56
+ FLOAT heartbeat_interval "nullable"
57
+ VARCHAR last_activated_at "nullable"
58
+ VARCHAR last_deactivated_at "nullable"
59
+ INTEGER node_id UK "nullable"
60
+ TIMESTAMP online_until "nullable"
61
+ VARCHAR owner_aid "nullable"
62
+ VARCHAR owner_name "nullable"
63
+ BLOB public_key UK "nullable"
64
+ VARCHAR registered_at "nullable"
65
+ VARCHAR status "nullable"
66
+ VARCHAR unregistered_at "nullable"
67
+ }
68
+
69
+ object_children {
70
+ VARCHAR child_id PK,FK
71
+ VARCHAR parent_id PK,FK
72
+ }
73
+
74
+ objects {
75
+ VARCHAR object_id PK "nullable"
76
+ BLOB content "nullable"
77
+ INTEGER is_available
78
+ INTEGER ref_count
79
+ }
80
+
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
+ run_objects {
102
+ VARCHAR object_id PK,FK
103
+ INTEGER run_id PK
104
+ }
105
+
106
+ token_store {
107
+ INTEGER run_id PK "nullable"
108
+ FLOAT active_until "nullable"
109
+ VARCHAR token UK
110
+ }
111
+
112
+ run ||--o| context : run_id
113
+ run ||--o{ logs : run_id
114
+ run ||--o{ message_ins : run_id
115
+ run ||--o{ message_res : run_id
116
+ objects ||--o| object_children : parent_id
117
+ objects ||--o| object_children : child_id
118
+ objects ||--o| run_objects : object_id
119
+
120
+ ```
121
+ <!-- END_SQLALCHEMY_DOCS -->
@@ -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,36 @@
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
+
21
+ def create_corestate_metadata() -> MetaData:
22
+ """Create and return MetaData with CoreState table definitions."""
23
+ metadata = MetaData()
24
+
25
+ # --------------------------------------------------------------------------
26
+ # Table: token_store
27
+ # --------------------------------------------------------------------------
28
+ Table(
29
+ "token_store",
30
+ metadata,
31
+ Column("run_id", Integer, primary_key=True, nullable=True),
32
+ Column("token", String, unique=True, nullable=False),
33
+ Column("active_until", Float),
34
+ )
35
+
36
+ return metadata