flextool 4.0.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 (322) hide show
  1. flextool/__init__.py +41 -0
  2. flextool/_mem_sampler.py +193 -0
  3. flextool/_resources.py +43 -0
  4. flextool/calibrate/__init__.py +51 -0
  5. flextool/calibrate/__main__.py +11 -0
  6. flextool/calibrate/_cli.py +316 -0
  7. flextool/calibrate/_db_alt.py +166 -0
  8. flextool/calibrate/_final_outputs.py +110 -0
  9. flextool/calibrate/_guard.py +151 -0
  10. flextool/calibrate/_loop.py +558 -0
  11. flextool/calibrate/_readers.py +223 -0
  12. flextool/calibrate/_report.py +263 -0
  13. flextool/calibrate/_sizing.py +699 -0
  14. flextool/calibrate/_solve.py +134 -0
  15. flextool/calibrate/_solve_status.py +495 -0
  16. flextool/cli/__init__.py +9 -0
  17. flextool/cli/_console.py +51 -0
  18. flextool/cli/_timing.py +147 -0
  19. flextool/cli/cmd_execute_flextool_workflow.py +187 -0
  20. flextool/cli/cmd_export_to_tabular.py +56 -0
  21. flextool/cli/cmd_import_sensitivities.py +75 -0
  22. flextool/cli/cmd_migrate_database.py +13 -0
  23. flextool/cli/cmd_open_results_db.py +269 -0
  24. flextool/cli/cmd_read_matpower.py +66 -0
  25. flextool/cli/cmd_read_old_flextool.py +63 -0
  26. flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
  27. flextool/cli/cmd_read_tabular_input.py +81 -0
  28. flextool/cli/cmd_run_flextool.py +1095 -0
  29. flextool/cli/cmd_scenario_results.py +284 -0
  30. flextool/cli/cmd_solve_mps.py +169 -0
  31. flextool/cli/cmd_update_flextool.py +17 -0
  32. flextool/cli/cmd_write_outputs.py +125 -0
  33. flextool/common_utils/__init__.py +1 -0
  34. flextool/common_utils/plot_mem_shape.py +77 -0
  35. flextool/common_utils/precision.py +451 -0
  36. flextool/decomposition/__init__.py +0 -0
  37. flextool/decomposition/region_decomposition.py +128 -0
  38. flextool/decomposition/region_filter.py +1261 -0
  39. flextool/engine_polars/__init__.py +110 -0
  40. flextool/engine_polars/_axis_enums.py +742 -0
  41. flextool/engine_polars/_benders.py +3462 -0
  42. flextool/engine_polars/_block_layout.py +1479 -0
  43. flextool/engine_polars/_blocks.py +1515 -0
  44. flextool/engine_polars/_commodity_ladder.py +660 -0
  45. flextool/engine_polars/_cumulative_invest.py +1165 -0
  46. flextool/engine_polars/_db_loader.py +153 -0
  47. flextool/engine_polars/_db_reader.py +127 -0
  48. flextool/engine_polars/_dc_power_flow.py +445 -0
  49. flextool/engine_polars/_delay.py +442 -0
  50. flextool/engine_polars/_derived_arithmetic.py +432 -0
  51. flextool/engine_polars/_derived_block.py +990 -0
  52. flextool/engine_polars/_derived_branch.py +769 -0
  53. flextool/engine_polars/_derived_existing.py +1353 -0
  54. flextool/engine_polars/_derived_npv.py +1297 -0
  55. flextool/engine_polars/_derived_params.py +9850 -0
  56. flextool/engine_polars/_derived_profile.py +881 -0
  57. flextool/engine_polars/_derived_walks.py +276 -0
  58. flextool/engine_polars/_determinism.py +70 -0
  59. flextool/engine_polars/_direct_params.py +2186 -0
  60. flextool/engine_polars/_dump_csvs.py +1009 -0
  61. flextool/engine_polars/_emit_arc_unions.py +1631 -0
  62. flextool/engine_polars/_emit_calc_params.py +729 -0
  63. flextool/engine_polars/_emit_chain_params.py +709 -0
  64. flextool/engine_polars/_emit_co2_accumulators.py +400 -0
  65. flextool/engine_polars/_emit_dispatchers.py +690 -0
  66. flextool/engine_polars/_emit_energy_margin.py +125 -0
  67. flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
  68. flextool/engine_polars/_emit_entity_annual.py +428 -0
  69. flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
  70. flextool/engine_polars/_emit_leaf_sets.py +550 -0
  71. flextool/engine_polars/_emit_lp_scaling.py +665 -0
  72. flextool/engine_polars/_emit_mid_sets.py +859 -0
  73. flextool/engine_polars/_emit_pdt_params.py +759 -0
  74. flextool/engine_polars/_emit_per_solve.py +774 -0
  75. flextool/engine_polars/_emit_period_calc.py +504 -0
  76. flextool/engine_polars/_emit_period_params.py +2398 -0
  77. flextool/engine_polars/_emit_provider_io.py +141 -0
  78. flextool/engine_polars/_emit_reserve.py +574 -0
  79. flextool/engine_polars/_emit_solve_time.py +311 -0
  80. flextool/engine_polars/_emit_solve_writers.py +1249 -0
  81. flextool/engine_polars/_flex_data_accumulator.py +388 -0
  82. flextool/engine_polars/_flex_data_provider.py +478 -0
  83. flextool/engine_polars/_group_slack.py +1253 -0
  84. flextool/engine_polars/_inmemory_reader.py +140 -0
  85. flextool/engine_polars/_input_source.py +336 -0
  86. flextool/engine_polars/_invest_seeds.py +191 -0
  87. flextool/engine_polars/_native_input_writer.py +100 -0
  88. flextool/engine_polars/_native_run_model.py +1348 -0
  89. flextool/engine_polars/_orchestration.py +4314 -0
  90. flextool/engine_polars/_output_writer.py +439 -0
  91. flextool/engine_polars/_param_shapes.py +1595 -0
  92. flextool/engine_polars/_parquet_bundle.py +723 -0
  93. flextool/engine_polars/_pdt_join.py +167 -0
  94. flextool/engine_polars/_pdt_lookup.py +547 -0
  95. flextool/engine_polars/_per_solve_sets.py +335 -0
  96. flextool/engine_polars/_projection_params.py +2056 -0
  97. flextool/engine_polars/_provider_keys.py +173 -0
  98. flextool/engine_polars/_provider_translators.py +225 -0
  99. flextool/engine_polars/_recursive_solve.py +703 -0
  100. flextool/engine_polars/_region_filter.py +2508 -0
  101. flextool/engine_polars/_reserve.py +649 -0
  102. flextool/engine_polars/_solve_acceptance.py +331 -0
  103. flextool/engine_polars/_solve_config.py +1001 -0
  104. flextool/engine_polars/_solve_context.py +885 -0
  105. flextool/engine_polars/_solve_handoff.py +164 -0
  106. flextool/engine_polars/_solve_state.py +232 -0
  107. flextool/engine_polars/_solver_base.py +36 -0
  108. flextool/engine_polars/_solver_dispatch.py +511 -0
  109. flextool/engine_polars/_spinedb_reader.py +1165 -0
  110. flextool/engine_polars/_stochastic.py +593 -0
  111. flextool/engine_polars/_subprocess_solve.py +1838 -0
  112. flextool/engine_polars/_timeline.py +1416 -0
  113. flextool/engine_polars/_vectorize.py +438 -0
  114. flextool/engine_polars/_warm.py +858 -0
  115. flextool/engine_polars/autoscale/__init__.py +107 -0
  116. flextool/engine_polars/autoscale/_config.py +218 -0
  117. flextool/engine_polars/autoscale/_layer2.py +1253 -0
  118. flextool/engine_polars/autoscale/_layer2_types.py +584 -0
  119. flextool/engine_polars/autoscale/_quantity_types.py +621 -0
  120. flextool/engine_polars/autoscale/_report.py +336 -0
  121. flextool/engine_polars/chain.py +259 -0
  122. flextool/engine_polars/input.py +6638 -0
  123. flextool/engine_polars/model.py +4754 -0
  124. flextool/env_check.py +388 -0
  125. flextool/export_to_tabular/__init__.py +5 -0
  126. flextool/export_to_tabular/db_reader.py +224 -0
  127. flextool/export_to_tabular/excel_writer.py +3559 -0
  128. flextool/export_to_tabular/export_settings.yaml +377 -0
  129. flextool/export_to_tabular/export_to_excel.py +227 -0
  130. flextool/export_to_tabular/formatting.py +543 -0
  131. flextool/export_to_tabular/sheet_config.py +876 -0
  132. flextool/gui/__init__.py +0 -0
  133. flextool/gui/__main__.py +118 -0
  134. flextool/gui/calibrate_commands.py +184 -0
  135. flextool/gui/calibrate_jobs.py +424 -0
  136. flextool/gui/check_tree.py +142 -0
  137. flextool/gui/cli_format.py +83 -0
  138. flextool/gui/config_parser.py +68 -0
  139. flextool/gui/data_models.py +362 -0
  140. flextool/gui/db_editor_integration.py +202 -0
  141. flextool/gui/db_version_check.py +269 -0
  142. flextool/gui/dialogs/__init__.py +0 -0
  143. flextool/gui/dialogs/add_dialog.py +1098 -0
  144. flextool/gui/dialogs/calibrate_dialog.py +1259 -0
  145. flextool/gui/dialogs/file_picker.py +473 -0
  146. flextool/gui/dialogs/group_picker.py +299 -0
  147. flextool/gui/dialogs/migration_consent_dialog.py +106 -0
  148. flextool/gui/dialogs/migration_progress_dialog.py +237 -0
  149. flextool/gui/dialogs/plot_dialog.py +459 -0
  150. flextool/gui/dialogs/plot_settings_picker.py +2184 -0
  151. flextool/gui/dialogs/project_dialog.py +426 -0
  152. flextool/gui/dialogs/update_dialog.py +212 -0
  153. flextool/gui/downsampling.py +88 -0
  154. flextool/gui/error_handling.py +50 -0
  155. flextool/gui/execution_manager.py +1715 -0
  156. flextool/gui/execution_window.py +1377 -0
  157. flextool/gui/hover_tooltip.py +111 -0
  158. flextool/gui/input_sources.py +730 -0
  159. flextool/gui/main_window.py +6181 -0
  160. flextool/gui/network_graph.py +215 -0
  161. flextool/gui/output_actions.py +393 -0
  162. flextool/gui/output_log_window.py +159 -0
  163. flextool/gui/platform_utils.py +421 -0
  164. flextool/gui/plot_cache.py +88 -0
  165. flextool/gui/plot_canvas.py +543 -0
  166. flextool/gui/plot_config_reader.py +272 -0
  167. flextool/gui/project_utils.py +100 -0
  168. flextool/gui/result_viewer.py +4394 -0
  169. flextool/gui/scenario_key.py +162 -0
  170. flextool/gui/scenario_lists.py +516 -0
  171. flextool/gui/settings_io.py +360 -0
  172. flextool/gui/solve_reader.py +103 -0
  173. flextool/gui/tree_reorder.py +88 -0
  174. flextool/gui/ui_metrics.py +420 -0
  175. flextool/input_derivation/__init__.py +281 -0
  176. flextool/input_derivation/_commodity_ladder.py +375 -0
  177. flextool/input_derivation/_commodity_ladder_sets.py +70 -0
  178. flextool/input_derivation/_dc_power_flow.py +377 -0
  179. flextool/input_derivation/_method_constants.py +77 -0
  180. flextool/input_derivation/_process_method.py +258 -0
  181. flextool/input_derivation/_specs.py +1026 -0
  182. flextool/input_derivation/_validators.py +321 -0
  183. flextool/lean_parquet.py +159 -0
  184. flextool/model_builder/__init__.py +5 -0
  185. flextool/model_builder/build_model.py +589 -0
  186. flextool/model_builder/encoding.py +67 -0
  187. flextool/model_builder/names.py +34 -0
  188. flextool/model_builder/profiles.py +129 -0
  189. flextool/plot_outputs/__init__.py +14 -0
  190. flextool/plot_outputs/axis_helpers.py +355 -0
  191. flextool/plot_outputs/color_template.py +888 -0
  192. flextool/plot_outputs/config.py +171 -0
  193. flextool/plot_outputs/format_helpers.py +345 -0
  194. flextool/plot_outputs/legend_helpers.py +143 -0
  195. flextool/plot_outputs/orchestrator.py +1141 -0
  196. flextool/plot_outputs/perf.py +37 -0
  197. flextool/plot_outputs/plan.py +1787 -0
  198. flextool/plot_outputs/plot_bars.py +1510 -0
  199. flextool/plot_outputs/plot_bars_detail.py +753 -0
  200. flextool/plot_outputs/plot_lines.py +951 -0
  201. flextool/plot_outputs/shared_manifest.py +564 -0
  202. flextool/plot_outputs/subplot_helpers.py +137 -0
  203. flextool/process_inputs/__init__.py +188 -0
  204. flextool/process_inputs/import_old_excel_input.json +4159 -0
  205. flextool/process_inputs/read_matpower.py +451 -0
  206. flextool/process_inputs/read_old_flextool.py +1288 -0
  207. flextool/process_inputs/read_self_describing_excel.py +1423 -0
  208. flextool/process_inputs/read_tabular_with_specification.py +1114 -0
  209. flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
  210. flextool/process_inputs/write_self_describing_to_db.py +977 -0
  211. flextool/process_inputs/write_to_input_db.py +269 -0
  212. flextool/process_outputs/__init__.py +7 -0
  213. flextool/process_outputs/_annualize.py +55 -0
  214. flextool/process_outputs/_inmemory_helpers.py +292 -0
  215. flextool/process_outputs/_output_meta.py +672 -0
  216. flextool/process_outputs/calc_capacity_flows.py +107 -0
  217. flextool/process_outputs/calc_connections.py +136 -0
  218. flextool/process_outputs/calc_costs.py +260 -0
  219. flextool/process_outputs/calc_group_flows.py +192 -0
  220. flextool/process_outputs/calc_slacks.py +103 -0
  221. flextool/process_outputs/calc_storage_vre.py +160 -0
  222. flextool/process_outputs/drop_levels.py +208 -0
  223. flextool/process_outputs/handoff_writers.py +1315 -0
  224. flextool/process_outputs/out_ancillary.py +544 -0
  225. flextool/process_outputs/out_capacity.py +179 -0
  226. flextool/process_outputs/out_costs.py +334 -0
  227. flextool/process_outputs/out_flowgroup.py +189 -0
  228. flextool/process_outputs/out_flows.py +301 -0
  229. flextool/process_outputs/out_group.py +475 -0
  230. flextool/process_outputs/out_node.py +190 -0
  231. flextool/process_outputs/persist_realized_slice.py +601 -0
  232. flextool/process_outputs/process_results.py +24 -0
  233. flextool/process_outputs/read_highs_solution.py +2256 -0
  234. flextool/process_outputs/read_parameters.py +1799 -0
  235. flextool/process_outputs/read_sets.py +1095 -0
  236. flextool/process_outputs/read_variables.py +553 -0
  237. flextool/process_outputs/solve_order.py +81 -0
  238. flextool/process_outputs/spinedb_replay.py +412 -0
  239. flextool/process_outputs/union_realized_slice.py +224 -0
  240. flextool/process_outputs/write_outputs.py +1286 -0
  241. flextool/process_outputs/write_spinedb.py +1267 -0
  242. flextool/representative_periods/__init__.py +5 -0
  243. flextool/representative_periods/clustering.py +165 -0
  244. flextool/representative_periods/force_include.py +563 -0
  245. flextool/representative_periods/netload.py +365 -0
  246. flextool/representative_periods/netload_inputs.py +345 -0
  247. flextool/representative_periods/netload_iterate.py +722 -0
  248. flextool/representative_periods/preprocess.py +948 -0
  249. flextool/representative_periods/scenario_stack.py +195 -0
  250. flextool/representative_periods/weights.py +124 -0
  251. flextool/scenario_comparison/__init__.py +13 -0
  252. flextool/scenario_comparison/config_builder.py +158 -0
  253. flextool/scenario_comparison/constants.py +20 -0
  254. flextool/scenario_comparison/data_models.py +222 -0
  255. flextool/scenario_comparison/db_reader.py +399 -0
  256. flextool/scenario_comparison/dispatch_data.py +1002 -0
  257. flextool/scenario_comparison/dispatch_mappings.py +205 -0
  258. flextool/scenario_comparison/dispatch_plots.py +691 -0
  259. flextool/scenario_comparison/input_entity_colors.py +319 -0
  260. flextool/scenario_comparison/orchestrator.py +453 -0
  261. flextool/scenario_comparison/plan_union.py +244 -0
  262. flextool/scenario_comparison/plot_settings_seed.py +205 -0
  263. flextool/schemas/AXIS_CONTRACT.md +71 -0
  264. flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
  265. flextool/schemas/canonical_databases/howto_connections.json +5606 -0
  266. flextool/schemas/canonical_databases/howto_demand.json +5518 -0
  267. flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
  268. flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
  269. flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
  270. flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
  271. flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
  272. flextool/schemas/canonical_databases/templates_examples.json +13532 -0
  273. flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
  274. flextool/schemas/comparison_settings_template.json +197 -0
  275. flextool/schemas/default_plot_settings.yaml +260 -0
  276. flextool/schemas/default_plots.yaml +2293 -0
  277. flextool/schemas/flextool_axis_contract.json +303 -0
  278. flextool/schemas/flextool_axis_contract.schema.json +247 -0
  279. flextool/schemas/old_flextool_import_template.json +4443 -0
  280. flextool/schemas/output_info_template.json +48 -0
  281. flextool/schemas/output_settings_template.json +256 -0
  282. flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
  283. flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
  284. flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
  285. flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
  286. flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
  287. flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
  288. flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
  289. flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
  290. flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
  291. flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
  292. flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
  293. flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
  294. flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
  295. flextool/schemas/spinedb_results_schema.json +581 -0
  296. flextool/schemas/spinedb_schema.json +4636 -0
  297. flextool/solver_config/copt.opt.template +18 -0
  298. flextool/solver_config/cplex.opt.template +25 -0
  299. flextool/solver_config/gurobi.opt.template +18 -0
  300. flextool/solver_config/highs.opt.template +18 -0
  301. flextool/solver_config/xpress.opt.template +26 -0
  302. flextool/spinedb_backend/__init__.py +26 -0
  303. flextool/spinedb_backend/_axis_enums.py +1119 -0
  304. flextool/spinedb_backend/_backend.py +1139 -0
  305. flextool/update_flextool/__init__.py +12 -0
  306. flextool/update_flextool/canonical_databases.py +251 -0
  307. flextool/update_flextool/db_migration.py +7108 -0
  308. flextool/update_flextool/ensure_settings_db.py +138 -0
  309. flextool/update_flextool/export_database.py +103 -0
  310. flextool/update_flextool/extend_tests_fixture.py +772 -0
  311. flextool/update_flextool/generate_canonical.py +274 -0
  312. flextool/update_flextool/initialize_database.py +42 -0
  313. flextool/update_flextool/install_info.py +225 -0
  314. flextool/update_flextool/self_update.py +464 -0
  315. flextool/update_flextool/sync_master_json_template.py +125 -0
  316. flextool/update_flextool/test_fixtures.py +187 -0
  317. flextool-4.0.0.dist-info/METADATA +217 -0
  318. flextool-4.0.0.dist-info/RECORD +322 -0
  319. flextool-4.0.0.dist-info/WHEEL +5 -0
  320. flextool-4.0.0.dist-info/entry_points.txt +17 -0
  321. flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
  322. flextool-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,269 @@
1
+ """Database version checking and upgrade utilities for FlexTool GUI.
2
+
3
+ Checks both the SpineDB API schema version and the FlexTool data version,
4
+ upgrading automatically when needed. All errors are caught and returned
5
+ as human-readable messages so the GUI never crashes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import shutil
12
+ from pathlib import Path
13
+ from typing import Callable
14
+
15
+ from flextool.update_flextool import FLEXTOOL_DB_VERSION
16
+ from flextool.update_flextool.db_migration import MigrationCancelled
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ ISSUE_TRACKER_URL = "https://github.com/irena-flextool/flextool/issues"
21
+
22
+
23
+ def _backup_path(db_path: Path) -> Path:
24
+ """Return the sidecar path used to hold the pre-migration backup."""
25
+ return db_path.with_name(db_path.name + ".premigration.bak")
26
+
27
+
28
+ def _restore_from_backup(db_path: Path, backup: Path) -> bool:
29
+ """Restore *db_path* from *backup*, returning ``True`` on success.
30
+
31
+ Removes any stale SQLite ``-wal`` / ``-shm`` sidecars first so the
32
+ restored main file is not shadowed by a write-ahead log left behind
33
+ by the aborted migration.
34
+ """
35
+ try:
36
+ for suffix in ("-wal", "-shm"):
37
+ side = db_path.with_name(db_path.name + suffix)
38
+ if side.exists():
39
+ side.unlink()
40
+ shutil.copy2(backup, db_path)
41
+ return True
42
+ except Exception:
43
+ logger.error(
44
+ "Failed to restore %s from backup %s", db_path, backup, exc_info=True
45
+ )
46
+ return False
47
+
48
+
49
+ def _read_flextool_version(db_url: str) -> int | None:
50
+ """Read the current FlexTool data version from a database.
51
+
52
+ Returns the integer version, or ``None`` if it cannot be determined.
53
+ """
54
+ try:
55
+ from spinedb_api import DatabaseMapping, from_database
56
+
57
+ with DatabaseMapping(db_url, create=False, upgrade=True) as db:
58
+ sq = db.object_parameter_definition_sq
59
+ settings_param = (
60
+ db.query(sq)
61
+ .filter(sq.c.object_class_name == "model")
62
+ .filter(sq.c.parameter_name == "version")
63
+ .one_or_none()
64
+ )
65
+ if settings_param is None:
66
+ return 0
67
+ return int(
68
+ from_database(settings_param.default_value, settings_param.default_type)
69
+ )
70
+ except Exception:
71
+ logger.debug("Could not read FlexTool version from %s", db_url, exc_info=True)
72
+ return None
73
+
74
+
75
+ def get_target_flextool_version() -> int:
76
+ """Return the FlexTool DB version this build migrates to."""
77
+ return int(FLEXTOOL_DB_VERSION)
78
+
79
+
80
+ def needs_flextool_migration(db_path: Path) -> bool | None:
81
+ """Return True if this file's FlexTool data version is below the target.
82
+
83
+ Returns ``None`` if the version cannot be determined (file unreadable,
84
+ not a FlexTool DB, etc.).
85
+ """
86
+ db_url = f"sqlite:///{db_path}"
87
+ current = _read_flextool_version(db_url)
88
+ if current is None:
89
+ return None
90
+ return current < FLEXTOOL_DB_VERSION
91
+
92
+
93
+ def check_and_upgrade_database(
94
+ db_path: Path,
95
+ *,
96
+ progress_callback: Callable[[int, int, int], None] | None = None,
97
+ cancel_check: Callable[[], bool] | None = None,
98
+ ) -> tuple[bool, bool, list[str]]:
99
+ """Check and upgrade a FlexTool database if needed.
100
+
101
+ Performs two levels of upgrade:
102
+
103
+ 1. **SpineDB API schema upgrade** -- handled automatically by opening
104
+ the database with ``DatabaseMapping(url, upgrade=True)``.
105
+ 2. **FlexTool data version upgrade** -- delegates to
106
+ :func:`~flextool.update_flextool.db_migration.migrate_database`.
107
+
108
+ Both steps mutate the file in place (migration commits per step, while
109
+ the version stamp is written only at the very end), so before the first
110
+ mutating operation a byte-for-byte backup copy is taken. On success the
111
+ backup is deleted; if the migration is cancelled or fails, the backup is
112
+ restored so the database is always left either fully upgraded or exactly
113
+ as it was found. The backup is taken lazily — a database that needs no
114
+ upgrade is never copied.
115
+
116
+ Args:
117
+ db_path: Path to the ``.sqlite`` file.
118
+ progress_callback: Optional callable forwarded to
119
+ :func:`migrate_database`. Invoked before each migration
120
+ step with ``(current_version, target_version, next_version)``.
121
+ cancel_check: Optional callable forwarded to
122
+ :func:`migrate_database`. When it returns ``True``, the
123
+ migration stops cleanly between steps, the database is restored
124
+ to its original state, and this function returns instead of
125
+ raising.
126
+
127
+ Returns:
128
+ A ``(was_upgraded, failed, messages)`` tuple where *was_upgraded* is
129
+ ``True`` if any upgrade was performed and persisted, *failed* is
130
+ ``True`` only if a migration error left work that had to be rolled
131
+ back (the caller should not proceed to use the database), and
132
+ *messages* is a list of human-readable descriptions of what happened.
133
+
134
+ This function never raises -- all exceptions are caught and reported
135
+ as messages.
136
+ """
137
+ messages: list[str] = []
138
+ was_upgraded = False
139
+ failed = False
140
+
141
+ try:
142
+ from spinedb_api import DatabaseMapping
143
+ except ImportError as exc:
144
+ messages.append(f"Cannot check database version (spinedb_api not available): {exc}")
145
+ return was_upgraded, failed, messages
146
+
147
+ db_url = f"sqlite:///{db_path}"
148
+
149
+ # Probe the schema read-only first so a database that is already current
150
+ # is never copied. An out-of-date schema makes this raise.
151
+ try:
152
+ with DatabaseMapping(db_url, create=False, upgrade=False):
153
+ pass
154
+ schema_current = True
155
+ except Exception:
156
+ schema_current = False
157
+
158
+ backup: Path | None = None
159
+
160
+ def _ensure_backup() -> bool:
161
+ """Take the pre-migration backup once; return ``True`` if available."""
162
+ nonlocal backup
163
+ if backup is not None:
164
+ return True
165
+ candidate = _backup_path(db_path)
166
+ try:
167
+ shutil.copy2(db_path, candidate)
168
+ backup = candidate
169
+ logger.info("Pre-migration backup written: %s", candidate)
170
+ return True
171
+ except Exception:
172
+ logger.error("Could not create migration backup for %s", db_path, exc_info=True)
173
+ return False
174
+
175
+ try:
176
+ # ── Step 1: SpineDB API schema upgrade ─────────────────────
177
+ if not schema_current:
178
+ if not _ensure_backup():
179
+ messages.append(
180
+ f"{db_path.name}: could not create a safety backup before "
181
+ f"upgrading; the database was left untouched."
182
+ )
183
+ return was_upgraded, True, messages
184
+ with DatabaseMapping(db_url, create=False, upgrade=True):
185
+ pass
186
+ messages.append(f"{db_path.name}: SpineDB schema upgraded to latest version.")
187
+ was_upgraded = True
188
+ logger.info("SpineDB schema upgraded for %s", db_path)
189
+
190
+ # ── Step 2: FlexTool data version upgrade ──────────────────
191
+ # Schema is current now, so reading the version does not mutate.
192
+ version_before = _read_flextool_version(db_url)
193
+ if version_before is not None and version_before < FLEXTOOL_DB_VERSION:
194
+ if not _ensure_backup():
195
+ messages.append(
196
+ f"{db_path.name}: could not create a safety backup before "
197
+ f"migrating; the database was left untouched."
198
+ )
199
+ return was_upgraded, True, messages
200
+
201
+ from flextool.update_flextool.db_migration import migrate_database
202
+
203
+ migrate_database(
204
+ str(db_path),
205
+ progress_callback=progress_callback,
206
+ cancel_check=cancel_check,
207
+ )
208
+
209
+ version_after = _read_flextool_version(db_url)
210
+ if version_after is not None and version_after > version_before:
211
+ messages.append(
212
+ f"{db_path.name}: FlexTool data upgraded from version "
213
+ f"{version_before} to {version_after}."
214
+ )
215
+ was_upgraded = True
216
+ logger.info(
217
+ "FlexTool data upgraded %s: v%s -> v%s",
218
+ db_path,
219
+ version_before,
220
+ version_after,
221
+ )
222
+
223
+ except MigrationCancelled:
224
+ was_upgraded = False
225
+ if backup is not None and _restore_from_backup(db_path, backup):
226
+ messages.append(
227
+ f"{db_path.name}: migration cancelled — the database was "
228
+ f"restored to its original state. Re-run to migrate it."
229
+ )
230
+ else:
231
+ failed = True
232
+ messages.append(
233
+ f"{db_path.name}: migration cancelled, but the database could "
234
+ f"NOT be restored automatically. Do not use it; restore it "
235
+ f"from your own backup and report this at {ISSUE_TRACKER_URL}."
236
+ )
237
+ logger.info("FlexTool migration cancelled for %s", db_path)
238
+
239
+ except Exception as exc:
240
+ import traceback as _tb
241
+ tb_text = _tb.format_exc()
242
+ was_upgraded = False
243
+ failed = True
244
+ if backup is not None and _restore_from_backup(db_path, backup):
245
+ messages.append(
246
+ f"{db_path.name}: migration FAILED — the database was restored "
247
+ f"to its original state, so it is safe to keep using the "
248
+ f"unmigrated copy. Please report this at {ISSUE_TRACKER_URL}: "
249
+ f"copy the traceback below and, if possible, share the database "
250
+ f"file.\n\nError: {exc}\n\nTraceback:\n{tb_text}"
251
+ )
252
+ else:
253
+ messages.append(
254
+ f"{db_path.name}: migration FAILED and the database could NOT be "
255
+ f"restored automatically. Do not use it; restore it from your "
256
+ f"own backup. Please report this at {ISSUE_TRACKER_URL}: copy the "
257
+ f"traceback below and, if possible, share the database file."
258
+ f"\n\nError: {exc}\n\nTraceback:\n{tb_text}"
259
+ )
260
+ logger.warning("FlexTool migration failed for %s: %s", db_path, exc, exc_info=True)
261
+
262
+ finally:
263
+ if backup is not None:
264
+ try:
265
+ backup.unlink(missing_ok=True)
266
+ except Exception:
267
+ logger.debug("Could not remove migration backup %s", backup, exc_info=True)
268
+
269
+ return was_upgraded, failed, messages
File without changes