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,315 @@
1
+ # Copyright 2026 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Mixin providing common SQL connection and initialization logic via SQLAlchemy."""
16
+
17
+
18
+ import re
19
+ from abc import ABC
20
+ from collections.abc import Iterator, Sequence
21
+ from contextlib import contextmanager
22
+ from contextvars import ContextVar
23
+ from logging import DEBUG, ERROR, WARNING
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from sqlalchemy import Engine, MetaData, create_engine, event, inspect, text
28
+ from sqlalchemy.exc import SQLAlchemyError
29
+ from sqlalchemy.orm import Session, sessionmaker
30
+
31
+ from flwr.common.logger import log
32
+ from flwr.supercore.constant import FLWR_IN_MEMORY_SQLITE_DB_URL, SQLITE_PRAGMAS
33
+ from flwr.supercore.state.alembic.utils import run_migrations
34
+
35
+ _current_session: ContextVar[Session | None] = ContextVar(
36
+ "current_sqlalchemy_session",
37
+ default=None,
38
+ )
39
+
40
+
41
+ def _set_sqlite_pragmas(dbapi_conn: Any, _connection_record: Any) -> None:
42
+ """Set SQLite pragmas for performance and correctness."""
43
+ cursor = dbapi_conn.cursor()
44
+ for pragma, value in SQLITE_PRAGMAS:
45
+ cursor.execute(f"PRAGMA {pragma} = {value};")
46
+ cursor.close()
47
+
48
+
49
+ def _log_query( # pylint: disable=W0613,R0913,R0917
50
+ conn: Any,
51
+ cursor: Any,
52
+ statement: str,
53
+ parameters: Any,
54
+ context: Any,
55
+ executemany: bool,
56
+ ) -> None:
57
+ """Log SQL queries via Flower logger."""
58
+ log(DEBUG, {"query": statement, "params": parameters})
59
+
60
+
61
+ class SqlMixin(ABC):
62
+ """Mixin providing common SQLite connection and initialization logic.
63
+
64
+ This mixin uses SQLAlchemy Core API for SQLite database access. It accepts either a
65
+ database file path or a SQLite URL, automatically converting file paths to SQLite
66
+ URLs.
67
+ """
68
+
69
+ def __init__(self, database_path: str) -> None:
70
+ """Initialize the SqlMixin.
71
+
72
+ Parameters
73
+ ----------
74
+ database_path : str
75
+ Database location specifier. Can be:
76
+ - A file path (relative or absolute): "state.db", "/var/data/state.db"
77
+ - The special value ":memory:" for an in-memory database
78
+ - A SQLite URL: "sqlite:///absolute/path/to/db.db"
79
+
80
+ File paths are automatically converted to absolute paths and formatted
81
+ as SQLite URLs. Empty or whitespace-only strings default to ":memory:".
82
+
83
+ Warnings
84
+ --------
85
+ Providing an empty or whitespace-only string will log a warning and fall
86
+ back to an in-memory database. For temporary databases, explicitly use
87
+ ":memory:" to avoid warnings.
88
+ """
89
+ if not database_path or not database_path.strip():
90
+ log(
91
+ WARNING,
92
+ "Empty `database_path` provided, defaulting to in-memory SQLite "
93
+ "database",
94
+ )
95
+ database_path = ":memory:"
96
+
97
+ # Auto-convert file path to SQLAlchemy SQLite URL if needed
98
+ if database_path == ":memory:":
99
+ self.database_url = FLWR_IN_MEMORY_SQLITE_DB_URL
100
+ elif not database_path.startswith("sqlite://"):
101
+ # Treat as file path, convert to absolute and create SQLite URL
102
+ abs_path = Path(database_path).resolve()
103
+ self.database_url = f"sqlite:///{abs_path}"
104
+ else:
105
+ # Already a SQLite URL
106
+ self.database_url = database_path
107
+
108
+ self._engine: Engine | None = None
109
+ self._session_factory: sessionmaker[Session] | None = None
110
+
111
+ @contextmanager
112
+ def session(self) -> Iterator[Session]:
113
+ """Provide a transactional database session context.
114
+
115
+ Yields a SQLAlchemy Session that automatically commits on success or rolls
116
+ back on exceptions. Re-entrant: nested calls reuse the same session. Use for
117
+ multi-statement transactions; prefer `query()` for single statements.
118
+
119
+ Yields
120
+ ------
121
+ Session
122
+ SQLAlchemy session. Commits on context exit, rolls back on exceptions.
123
+
124
+ Examples
125
+ --------
126
+ with self.session() as session:
127
+ session.execute(text("DELETE FROM t WHERE id = :id"), {"id": 1})
128
+ session.execute(text("INSERT INTO t2 SELECT * FROM t"))
129
+ """
130
+ existing = _current_session.get()
131
+
132
+ # Re-entrant: reuse the session if in a session context, no begin()
133
+ if existing is not None:
134
+ yield existing
135
+ return
136
+
137
+ if self._session_factory is None:
138
+ raise AttributeError("Database not initialized. Call initialize() first.")
139
+
140
+ # Create new session; outermost scope owns the transaction
141
+ session = self._session_factory()
142
+ token = _current_session.set(session)
143
+
144
+ try:
145
+ with session.begin():
146
+ yield session
147
+ finally:
148
+ _current_session.reset(token)
149
+ session.close()
150
+
151
+ def get_metadata(self) -> MetaData | None:
152
+ """Return the MetaData object for this class.
153
+
154
+ Subclasses can override this to provide their SQLAlchemy MetaData.
155
+ The base implementation returns None.
156
+
157
+ Returns
158
+ -------
159
+ MetaData | None
160
+ SQLAlchemy MetaData object for this class.
161
+ """
162
+ return None
163
+
164
+ def initialize(self, log_queries: bool = False) -> list[str]:
165
+ """Connect to the DB and create tables if needed.
166
+
167
+ This method creates the SQLAlchemy engine and session factory,
168
+ and creates tables returned by `get_metadata()`.
169
+
170
+ Parameters
171
+ ----------
172
+ log_queries : bool
173
+ Log each query which is executed.
174
+
175
+ Returns
176
+ -------
177
+ list[str]
178
+ The list of all tables in the DB.
179
+ """
180
+ # Create engine with SQLite-specific settings
181
+ engine_kwargs: dict[str, Any] = {
182
+ # SQLite needs check_same_thread=False for multi-threaded access
183
+ "connect_args": {"check_same_thread": False}
184
+ }
185
+ self._engine = create_engine(self.database_url, **engine_kwargs)
186
+
187
+ # Set SQLite pragmas via event listener for optimal performance and correctness
188
+ event.listen(self._engine, "connect", _set_sqlite_pragmas)
189
+
190
+ if log_queries:
191
+ # Set up query logging via event listener
192
+ event.listen(self._engine, "before_cursor_execute", _log_query)
193
+
194
+ # Create session factory
195
+ self._session_factory = sessionmaker(bind=self._engine)
196
+
197
+ # Create database
198
+ metadata: MetaData | None = self.get_metadata()
199
+ if metadata and self.database_url == FLWR_IN_MEMORY_SQLITE_DB_URL:
200
+ # In-memory databases: create tables directly from SQLAlchemy metadata
201
+ metadata.create_all(self._engine)
202
+ else:
203
+ # File-based databases: use Alembic migrations for schema versioning
204
+ run_migrations(self._engine)
205
+
206
+ # Get all table names using inspector
207
+ inspector = inspect(self._engine)
208
+ return inspector.get_table_names()
209
+
210
+ def query(
211
+ self,
212
+ query: str,
213
+ data: Sequence[dict[str, Any]] | dict[str, Any] | None = None,
214
+ ) -> list[dict[str, Any]]:
215
+ """Execute a SQL query and return the results as list of dicts.
216
+
217
+ TRANSACTION SEMANTICS:
218
+ ----------------------
219
+ If called outside a session context, each call to query() runs in its own
220
+ isolated transaction that is automatically committed. This is suitable for
221
+ single SQL statements.
222
+
223
+ If called within a session() context, query() reuses the existing session
224
+ and transaction. This enables atomic multi-query operations:
225
+
226
+ with self.session() as session:
227
+ self.query("UPDATE ...", {...}) # Shares same transaction
228
+ self.query("INSERT ...", {...}) # Shares same transaction
229
+ # Both succeed or fail together
230
+
231
+ You can also use session.execute() directly for the same effect:
232
+
233
+ with self.session() as session:
234
+ session.execute(text("UPDATE ..."), {...})
235
+ session.execute(text("INSERT ..."), {...})
236
+
237
+ Parameters
238
+ ----------
239
+ query : str
240
+ SQL query string with named parameter placeholders.
241
+ Use :name syntax for parameters: "SELECT * FROM t WHERE a = :a AND b = :b"
242
+ data : Sequence[dict[str, Any]] | dict[str, Any] | None
243
+ Query parameters using named parameter syntax:
244
+ - Single execution: pass dict, e.g., {"a": value1, "b": value2}
245
+ - Batch execution: pass sequence of dicts, e.g., [{"a": 1}, {"a": 2}]
246
+
247
+ Returns
248
+ -------
249
+ list[dict[str, Any]]
250
+ Query results as a list of dictionaries.
251
+
252
+ Examples
253
+ --------
254
+ # Single query with named parameters (auto-committed transaction)
255
+ rows = self.query(
256
+ "SELECT * FROM node WHERE node_id = :id AND status = :status",
257
+ {"id": node_id, "status": status}
258
+ )
259
+
260
+ # Batch insert with named parameters (auto-committed transaction)
261
+ rows = self.query(
262
+ "INSERT INTO node (node_id, status) VALUES (:id, :status)",
263
+ [{"id": 1, "status": "online"}, {"id": 2, "status": "offline"}]
264
+ )
265
+
266
+ # Multi-statement transaction - query() calls share the same session
267
+ with self.session():
268
+ # Both statements succeed or fail together
269
+ self.query("DELETE FROM token_store WHERE active_until < :time", {...})
270
+ self.query("UPDATE run SET status = :status WHERE id = :id", {...})
271
+
272
+ # Nested session() - query() calls share the same session
273
+ with self.session():
274
+ self.query("DELETE FROM token_store WHERE active_until < :time", {...})
275
+ with self.session():
276
+ self.query("UPDATE run SET status = :status WHERE id = :id", {...})
277
+ """
278
+ if self._engine is None:
279
+ raise AttributeError(
280
+ "LinkState is not initialized. Call initialize() first."
281
+ )
282
+
283
+ if data is None:
284
+ data = {}
285
+
286
+ # Clean up whitespace to make the logs nicer
287
+ query = re.sub(r"\s+", " ", query.strip())
288
+
289
+ try:
290
+ # Wrap query in text() to enable SQLAlchemy named parameter syntax (:param).
291
+ sql = text(query)
292
+
293
+ def execute_and_fetch(session: Session) -> list[dict[str, Any]]:
294
+ """Execute query and fetch results from the given session."""
295
+ # Execute query (results live in database cursor).
296
+ # There is no need to check for batch vs single execution;
297
+ # SQLAlchemy handles both cases automatically.
298
+ result = session.execute(sql, data)
299
+
300
+ # Fetch results into Python memory before commit.
301
+ # mappings() returns dict-like rows (works for SELECT and RETURNING).
302
+ if result.returns_rows: # type: ignore
303
+ return [dict(row) for row in result.mappings()]
304
+
305
+ # For statements without RETURNING (INSERT/UPDATE/DELETE),
306
+ # returns_rows is False, so we return empty list.
307
+ return []
308
+
309
+ # Not in a session context, create a new session context for this query
310
+ with self.session() as session:
311
+ return execute_and_fetch(session)
312
+
313
+ except SQLAlchemyError as exc:
314
+ log(ERROR, {"query": query, "data": data, "exception": exc})
315
+ raise
@@ -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,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 helpers and migration environment for SQL state."""
@@ -0,0 +1,103 @@
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 environment configuration for State migrations."""
16
+
17
+
18
+ from logging.config import fileConfig
19
+
20
+ from alembic import context
21
+ from sqlalchemy import engine_from_config, pool
22
+
23
+ from flwr.supercore.state.alembic.utils import get_combined_metadata
24
+
25
+ # Alembic Config object
26
+ config = context.config # pylint: disable=no-member
27
+
28
+ # Interpret the config file for Python logging
29
+ if config.config_file_name is not None:
30
+ fileConfig(config.config_file_name)
31
+
32
+
33
+ # Target metadata for autogenerate
34
+ target_metadata = get_combined_metadata()
35
+
36
+
37
+ def run_migrations_offline() -> None:
38
+ """Run migrations in 'offline' mode.
39
+
40
+ This configures the context with just a URL and not an Engine, though an Engine is
41
+ acceptable here as well. By skipping the Engine creation we don't even need a DBAPI
42
+ to be available.
43
+
44
+ Calls to context.execute() here emit the given string to the script output.
45
+ """
46
+ url = config.get_main_option("sqlalchemy.url")
47
+ context.configure( # pylint: disable=no-member
48
+ url=url,
49
+ target_metadata=target_metadata,
50
+ literal_binds=True,
51
+ dialect_opts={"paramstyle": "named"},
52
+ )
53
+
54
+ with context.begin_transaction(): # pylint: disable=no-member
55
+ context.run_migrations() # pylint: disable=no-member
56
+
57
+
58
+ def run_migrations_online() -> None:
59
+ """Run migrations in 'online' mode.
60
+
61
+ Creates an engine and associates a connection with the context. Supports two modes:
62
+
63
+ 1. Standard: Creates a new connection from the configured URL.
64
+ 2. Pre-connected: Uses an existing connection from config.attributes["connection"].
65
+
66
+ Pre-connected mode is necessary for in-memory SQLite databases, where each new
67
+ connection creates a separate database instance. This allows
68
+ _get_baseline_metadata() to run migrations and reflect schema from the same
69
+ in-memory database without requiring filesystem write access.
70
+ """
71
+ # Check if a connection was provided (e.g., for in-memory databases).
72
+ # This allows callers to pass an active connection that should be reused
73
+ # instead of creating a new one from the URL.
74
+ connection = config.attributes.get("connection", None)
75
+
76
+ if connection is None:
77
+ # Standard path: create engine from config
78
+ connectable = engine_from_config(
79
+ config.get_section(config.config_ini_section, {}),
80
+ prefix="sqlalchemy.",
81
+ poolclass=pool.NullPool,
82
+ )
83
+
84
+ with connectable.connect() as connection:
85
+ # pylint: disable-next=no-member
86
+ context.configure(connection=connection, target_metadata=target_metadata)
87
+
88
+ with context.begin_transaction(): # pylint: disable=no-member
89
+ context.run_migrations() # pylint: disable=no-member
90
+ else:
91
+ # Use the provided connection directly (for in-memory databases)
92
+ context.configure( # pylint: disable=no-member
93
+ connection=connection, target_metadata=target_metadata
94
+ )
95
+
96
+ with context.begin_transaction(): # pylint: disable=no-member
97
+ context.run_migrations() # pylint: disable=no-member
98
+
99
+
100
+ if context.is_offline_mode(): # pylint: disable=no-member
101
+ run_migrations_offline()
102
+ else:
103
+ run_migrations_online()
@@ -0,0 +1,43 @@
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
+ """${message}.
16
+
17
+ Revision ID: ${up_revision}
18
+ Revises: ${down_revision | comma,n}
19
+ Create Date: ${create_date}
20
+ """
21
+ from typing import Sequence, Union
22
+
23
+ from alembic import op
24
+ import sqlalchemy as sa
25
+ ${imports if imports else ""}
26
+
27
+ # pylint: disable=no-member
28
+
29
+ # revision identifiers, used by Alembic.
30
+ revision: str = ${repr(up_revision)}
31
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
32
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
33
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
34
+
35
+
36
+ def upgrade() -> None:
37
+ """Upgrade schema."""
38
+ ${upgrades if upgrades else "pass"}
39
+
40
+
41
+ def downgrade() -> None:
42
+ """Downgrade schema."""
43
+ ${downgrades if downgrades else "pass"}