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.
- flextool/__init__.py +41 -0
- flextool/_mem_sampler.py +193 -0
- flextool/_resources.py +43 -0
- flextool/calibrate/__init__.py +51 -0
- flextool/calibrate/__main__.py +11 -0
- flextool/calibrate/_cli.py +316 -0
- flextool/calibrate/_db_alt.py +166 -0
- flextool/calibrate/_final_outputs.py +110 -0
- flextool/calibrate/_guard.py +151 -0
- flextool/calibrate/_loop.py +558 -0
- flextool/calibrate/_readers.py +223 -0
- flextool/calibrate/_report.py +263 -0
- flextool/calibrate/_sizing.py +699 -0
- flextool/calibrate/_solve.py +134 -0
- flextool/calibrate/_solve_status.py +495 -0
- flextool/cli/__init__.py +9 -0
- flextool/cli/_console.py +51 -0
- flextool/cli/_timing.py +147 -0
- flextool/cli/cmd_execute_flextool_workflow.py +187 -0
- flextool/cli/cmd_export_to_tabular.py +56 -0
- flextool/cli/cmd_import_sensitivities.py +75 -0
- flextool/cli/cmd_migrate_database.py +13 -0
- flextool/cli/cmd_open_results_db.py +269 -0
- flextool/cli/cmd_read_matpower.py +66 -0
- flextool/cli/cmd_read_old_flextool.py +63 -0
- flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
- flextool/cli/cmd_read_tabular_input.py +81 -0
- flextool/cli/cmd_run_flextool.py +1095 -0
- flextool/cli/cmd_scenario_results.py +284 -0
- flextool/cli/cmd_solve_mps.py +169 -0
- flextool/cli/cmd_update_flextool.py +17 -0
- flextool/cli/cmd_write_outputs.py +125 -0
- flextool/common_utils/__init__.py +1 -0
- flextool/common_utils/plot_mem_shape.py +77 -0
- flextool/common_utils/precision.py +451 -0
- flextool/decomposition/__init__.py +0 -0
- flextool/decomposition/region_decomposition.py +128 -0
- flextool/decomposition/region_filter.py +1261 -0
- flextool/engine_polars/__init__.py +110 -0
- flextool/engine_polars/_axis_enums.py +742 -0
- flextool/engine_polars/_benders.py +3462 -0
- flextool/engine_polars/_block_layout.py +1479 -0
- flextool/engine_polars/_blocks.py +1515 -0
- flextool/engine_polars/_commodity_ladder.py +660 -0
- flextool/engine_polars/_cumulative_invest.py +1165 -0
- flextool/engine_polars/_db_loader.py +153 -0
- flextool/engine_polars/_db_reader.py +127 -0
- flextool/engine_polars/_dc_power_flow.py +445 -0
- flextool/engine_polars/_delay.py +442 -0
- flextool/engine_polars/_derived_arithmetic.py +432 -0
- flextool/engine_polars/_derived_block.py +990 -0
- flextool/engine_polars/_derived_branch.py +769 -0
- flextool/engine_polars/_derived_existing.py +1353 -0
- flextool/engine_polars/_derived_npv.py +1297 -0
- flextool/engine_polars/_derived_params.py +9850 -0
- flextool/engine_polars/_derived_profile.py +881 -0
- flextool/engine_polars/_derived_walks.py +276 -0
- flextool/engine_polars/_determinism.py +70 -0
- flextool/engine_polars/_direct_params.py +2186 -0
- flextool/engine_polars/_dump_csvs.py +1009 -0
- flextool/engine_polars/_emit_arc_unions.py +1631 -0
- flextool/engine_polars/_emit_calc_params.py +729 -0
- flextool/engine_polars/_emit_chain_params.py +709 -0
- flextool/engine_polars/_emit_co2_accumulators.py +400 -0
- flextool/engine_polars/_emit_dispatchers.py +690 -0
- flextool/engine_polars/_emit_energy_margin.py +125 -0
- flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
- flextool/engine_polars/_emit_entity_annual.py +428 -0
- flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
- flextool/engine_polars/_emit_leaf_sets.py +550 -0
- flextool/engine_polars/_emit_lp_scaling.py +665 -0
- flextool/engine_polars/_emit_mid_sets.py +859 -0
- flextool/engine_polars/_emit_pdt_params.py +759 -0
- flextool/engine_polars/_emit_per_solve.py +774 -0
- flextool/engine_polars/_emit_period_calc.py +504 -0
- flextool/engine_polars/_emit_period_params.py +2398 -0
- flextool/engine_polars/_emit_provider_io.py +141 -0
- flextool/engine_polars/_emit_reserve.py +574 -0
- flextool/engine_polars/_emit_solve_time.py +311 -0
- flextool/engine_polars/_emit_solve_writers.py +1249 -0
- flextool/engine_polars/_flex_data_accumulator.py +388 -0
- flextool/engine_polars/_flex_data_provider.py +478 -0
- flextool/engine_polars/_group_slack.py +1253 -0
- flextool/engine_polars/_inmemory_reader.py +140 -0
- flextool/engine_polars/_input_source.py +336 -0
- flextool/engine_polars/_invest_seeds.py +191 -0
- flextool/engine_polars/_native_input_writer.py +100 -0
- flextool/engine_polars/_native_run_model.py +1348 -0
- flextool/engine_polars/_orchestration.py +4314 -0
- flextool/engine_polars/_output_writer.py +439 -0
- flextool/engine_polars/_param_shapes.py +1595 -0
- flextool/engine_polars/_parquet_bundle.py +723 -0
- flextool/engine_polars/_pdt_join.py +167 -0
- flextool/engine_polars/_pdt_lookup.py +547 -0
- flextool/engine_polars/_per_solve_sets.py +335 -0
- flextool/engine_polars/_projection_params.py +2056 -0
- flextool/engine_polars/_provider_keys.py +173 -0
- flextool/engine_polars/_provider_translators.py +225 -0
- flextool/engine_polars/_recursive_solve.py +703 -0
- flextool/engine_polars/_region_filter.py +2508 -0
- flextool/engine_polars/_reserve.py +649 -0
- flextool/engine_polars/_solve_acceptance.py +331 -0
- flextool/engine_polars/_solve_config.py +1001 -0
- flextool/engine_polars/_solve_context.py +885 -0
- flextool/engine_polars/_solve_handoff.py +164 -0
- flextool/engine_polars/_solve_state.py +232 -0
- flextool/engine_polars/_solver_base.py +36 -0
- flextool/engine_polars/_solver_dispatch.py +511 -0
- flextool/engine_polars/_spinedb_reader.py +1165 -0
- flextool/engine_polars/_stochastic.py +593 -0
- flextool/engine_polars/_subprocess_solve.py +1838 -0
- flextool/engine_polars/_timeline.py +1416 -0
- flextool/engine_polars/_vectorize.py +438 -0
- flextool/engine_polars/_warm.py +858 -0
- flextool/engine_polars/autoscale/__init__.py +107 -0
- flextool/engine_polars/autoscale/_config.py +218 -0
- flextool/engine_polars/autoscale/_layer2.py +1253 -0
- flextool/engine_polars/autoscale/_layer2_types.py +584 -0
- flextool/engine_polars/autoscale/_quantity_types.py +621 -0
- flextool/engine_polars/autoscale/_report.py +336 -0
- flextool/engine_polars/chain.py +259 -0
- flextool/engine_polars/input.py +6638 -0
- flextool/engine_polars/model.py +4754 -0
- flextool/env_check.py +388 -0
- flextool/export_to_tabular/__init__.py +5 -0
- flextool/export_to_tabular/db_reader.py +224 -0
- flextool/export_to_tabular/excel_writer.py +3559 -0
- flextool/export_to_tabular/export_settings.yaml +377 -0
- flextool/export_to_tabular/export_to_excel.py +227 -0
- flextool/export_to_tabular/formatting.py +543 -0
- flextool/export_to_tabular/sheet_config.py +876 -0
- flextool/gui/__init__.py +0 -0
- flextool/gui/__main__.py +118 -0
- flextool/gui/calibrate_commands.py +184 -0
- flextool/gui/calibrate_jobs.py +424 -0
- flextool/gui/check_tree.py +142 -0
- flextool/gui/cli_format.py +83 -0
- flextool/gui/config_parser.py +68 -0
- flextool/gui/data_models.py +362 -0
- flextool/gui/db_editor_integration.py +202 -0
- flextool/gui/db_version_check.py +269 -0
- flextool/gui/dialogs/__init__.py +0 -0
- flextool/gui/dialogs/add_dialog.py +1098 -0
- flextool/gui/dialogs/calibrate_dialog.py +1259 -0
- flextool/gui/dialogs/file_picker.py +473 -0
- flextool/gui/dialogs/group_picker.py +299 -0
- flextool/gui/dialogs/migration_consent_dialog.py +106 -0
- flextool/gui/dialogs/migration_progress_dialog.py +237 -0
- flextool/gui/dialogs/plot_dialog.py +459 -0
- flextool/gui/dialogs/plot_settings_picker.py +2184 -0
- flextool/gui/dialogs/project_dialog.py +426 -0
- flextool/gui/dialogs/update_dialog.py +212 -0
- flextool/gui/downsampling.py +88 -0
- flextool/gui/error_handling.py +50 -0
- flextool/gui/execution_manager.py +1715 -0
- flextool/gui/execution_window.py +1377 -0
- flextool/gui/hover_tooltip.py +111 -0
- flextool/gui/input_sources.py +730 -0
- flextool/gui/main_window.py +6181 -0
- flextool/gui/network_graph.py +215 -0
- flextool/gui/output_actions.py +393 -0
- flextool/gui/output_log_window.py +159 -0
- flextool/gui/platform_utils.py +421 -0
- flextool/gui/plot_cache.py +88 -0
- flextool/gui/plot_canvas.py +543 -0
- flextool/gui/plot_config_reader.py +272 -0
- flextool/gui/project_utils.py +100 -0
- flextool/gui/result_viewer.py +4394 -0
- flextool/gui/scenario_key.py +162 -0
- flextool/gui/scenario_lists.py +516 -0
- flextool/gui/settings_io.py +360 -0
- flextool/gui/solve_reader.py +103 -0
- flextool/gui/tree_reorder.py +88 -0
- flextool/gui/ui_metrics.py +420 -0
- flextool/input_derivation/__init__.py +281 -0
- flextool/input_derivation/_commodity_ladder.py +375 -0
- flextool/input_derivation/_commodity_ladder_sets.py +70 -0
- flextool/input_derivation/_dc_power_flow.py +377 -0
- flextool/input_derivation/_method_constants.py +77 -0
- flextool/input_derivation/_process_method.py +258 -0
- flextool/input_derivation/_specs.py +1026 -0
- flextool/input_derivation/_validators.py +321 -0
- flextool/lean_parquet.py +159 -0
- flextool/model_builder/__init__.py +5 -0
- flextool/model_builder/build_model.py +589 -0
- flextool/model_builder/encoding.py +67 -0
- flextool/model_builder/names.py +34 -0
- flextool/model_builder/profiles.py +129 -0
- flextool/plot_outputs/__init__.py +14 -0
- flextool/plot_outputs/axis_helpers.py +355 -0
- flextool/plot_outputs/color_template.py +888 -0
- flextool/plot_outputs/config.py +171 -0
- flextool/plot_outputs/format_helpers.py +345 -0
- flextool/plot_outputs/legend_helpers.py +143 -0
- flextool/plot_outputs/orchestrator.py +1141 -0
- flextool/plot_outputs/perf.py +37 -0
- flextool/plot_outputs/plan.py +1787 -0
- flextool/plot_outputs/plot_bars.py +1510 -0
- flextool/plot_outputs/plot_bars_detail.py +753 -0
- flextool/plot_outputs/plot_lines.py +951 -0
- flextool/plot_outputs/shared_manifest.py +564 -0
- flextool/plot_outputs/subplot_helpers.py +137 -0
- flextool/process_inputs/__init__.py +188 -0
- flextool/process_inputs/import_old_excel_input.json +4159 -0
- flextool/process_inputs/read_matpower.py +451 -0
- flextool/process_inputs/read_old_flextool.py +1288 -0
- flextool/process_inputs/read_self_describing_excel.py +1423 -0
- flextool/process_inputs/read_tabular_with_specification.py +1114 -0
- flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
- flextool/process_inputs/write_self_describing_to_db.py +977 -0
- flextool/process_inputs/write_to_input_db.py +269 -0
- flextool/process_outputs/__init__.py +7 -0
- flextool/process_outputs/_annualize.py +55 -0
- flextool/process_outputs/_inmemory_helpers.py +292 -0
- flextool/process_outputs/_output_meta.py +672 -0
- flextool/process_outputs/calc_capacity_flows.py +107 -0
- flextool/process_outputs/calc_connections.py +136 -0
- flextool/process_outputs/calc_costs.py +260 -0
- flextool/process_outputs/calc_group_flows.py +192 -0
- flextool/process_outputs/calc_slacks.py +103 -0
- flextool/process_outputs/calc_storage_vre.py +160 -0
- flextool/process_outputs/drop_levels.py +208 -0
- flextool/process_outputs/handoff_writers.py +1315 -0
- flextool/process_outputs/out_ancillary.py +544 -0
- flextool/process_outputs/out_capacity.py +179 -0
- flextool/process_outputs/out_costs.py +334 -0
- flextool/process_outputs/out_flowgroup.py +189 -0
- flextool/process_outputs/out_flows.py +301 -0
- flextool/process_outputs/out_group.py +475 -0
- flextool/process_outputs/out_node.py +190 -0
- flextool/process_outputs/persist_realized_slice.py +601 -0
- flextool/process_outputs/process_results.py +24 -0
- flextool/process_outputs/read_highs_solution.py +2256 -0
- flextool/process_outputs/read_parameters.py +1799 -0
- flextool/process_outputs/read_sets.py +1095 -0
- flextool/process_outputs/read_variables.py +553 -0
- flextool/process_outputs/solve_order.py +81 -0
- flextool/process_outputs/spinedb_replay.py +412 -0
- flextool/process_outputs/union_realized_slice.py +224 -0
- flextool/process_outputs/write_outputs.py +1286 -0
- flextool/process_outputs/write_spinedb.py +1267 -0
- flextool/representative_periods/__init__.py +5 -0
- flextool/representative_periods/clustering.py +165 -0
- flextool/representative_periods/force_include.py +563 -0
- flextool/representative_periods/netload.py +365 -0
- flextool/representative_periods/netload_inputs.py +345 -0
- flextool/representative_periods/netload_iterate.py +722 -0
- flextool/representative_periods/preprocess.py +948 -0
- flextool/representative_periods/scenario_stack.py +195 -0
- flextool/representative_periods/weights.py +124 -0
- flextool/scenario_comparison/__init__.py +13 -0
- flextool/scenario_comparison/config_builder.py +158 -0
- flextool/scenario_comparison/constants.py +20 -0
- flextool/scenario_comparison/data_models.py +222 -0
- flextool/scenario_comparison/db_reader.py +399 -0
- flextool/scenario_comparison/dispatch_data.py +1002 -0
- flextool/scenario_comparison/dispatch_mappings.py +205 -0
- flextool/scenario_comparison/dispatch_plots.py +691 -0
- flextool/scenario_comparison/input_entity_colors.py +319 -0
- flextool/scenario_comparison/orchestrator.py +453 -0
- flextool/scenario_comparison/plan_union.py +244 -0
- flextool/scenario_comparison/plot_settings_seed.py +205 -0
- flextool/schemas/AXIS_CONTRACT.md +71 -0
- flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
- flextool/schemas/canonical_databases/howto_connections.json +5606 -0
- flextool/schemas/canonical_databases/howto_demand.json +5518 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
- flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
- flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
- flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
- flextool/schemas/canonical_databases/templates_examples.json +13532 -0
- flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
- flextool/schemas/comparison_settings_template.json +197 -0
- flextool/schemas/default_plot_settings.yaml +260 -0
- flextool/schemas/default_plots.yaml +2293 -0
- flextool/schemas/flextool_axis_contract.json +303 -0
- flextool/schemas/flextool_axis_contract.schema.json +247 -0
- flextool/schemas/old_flextool_import_template.json +4443 -0
- flextool/schemas/output_info_template.json +48 -0
- flextool/schemas/output_settings_template.json +256 -0
- flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
- flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
- flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
- flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
- flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
- flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
- flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
- flextool/schemas/spinedb_results_schema.json +581 -0
- flextool/schemas/spinedb_schema.json +4636 -0
- flextool/solver_config/copt.opt.template +18 -0
- flextool/solver_config/cplex.opt.template +25 -0
- flextool/solver_config/gurobi.opt.template +18 -0
- flextool/solver_config/highs.opt.template +18 -0
- flextool/solver_config/xpress.opt.template +26 -0
- flextool/spinedb_backend/__init__.py +26 -0
- flextool/spinedb_backend/_axis_enums.py +1119 -0
- flextool/spinedb_backend/_backend.py +1139 -0
- flextool/update_flextool/__init__.py +12 -0
- flextool/update_flextool/canonical_databases.py +251 -0
- flextool/update_flextool/db_migration.py +7108 -0
- flextool/update_flextool/ensure_settings_db.py +138 -0
- flextool/update_flextool/export_database.py +103 -0
- flextool/update_flextool/extend_tests_fixture.py +772 -0
- flextool/update_flextool/generate_canonical.py +274 -0
- flextool/update_flextool/initialize_database.py +42 -0
- flextool/update_flextool/install_info.py +225 -0
- flextool/update_flextool/self_update.py +464 -0
- flextool/update_flextool/sync_master_json_template.py +125 -0
- flextool/update_flextool/test_fixtures.py +187 -0
- flextool-4.0.0.dist-info/METADATA +217 -0
- flextool-4.0.0.dist-info/RECORD +322 -0
- flextool-4.0.0.dist-info/WHEEL +5 -0
- flextool-4.0.0.dist-info/entry_points.txt +17 -0
- flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- 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
|