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,188 @@
|
|
|
1
|
+
"""Input data reading: converts CSV/Excel/ODS files to Spine database format."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from flextool.process_inputs.read_tabular_with_specification import TabularReader
|
|
11
|
+
from flextool.process_inputs.write_to_input_db import write_to_flextool_input_db
|
|
12
|
+
from flextool.update_flextool import FLEXTOOL_DB_VERSION as CURRENT_FLEXTOOL_DB_VERSION
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'TabularReader', 'write_to_flextool_input_db',
|
|
16
|
+
'ExcelFormat', 'ExcelFormatInfo', 'detect_excel_format',
|
|
17
|
+
'CURRENT_FLEXTOOL_DB_VERSION',
|
|
18
|
+
'MIN_SUPPORTED_SPECIFICATION_VERSION',
|
|
19
|
+
'read_specification_changelog_version',
|
|
20
|
+
'unsupported_specification_message',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Oldest specification-format Excel *template changelog* version the importer
|
|
26
|
+
# can read. Below this the sheet/parameter layout diverges too much (the
|
|
27
|
+
# changelog-v11 source/sink -> input/output rename and earlier) to bridge with
|
|
28
|
+
# the parameter/sheet remaps in read_tabular_with_specification.py.
|
|
29
|
+
MIN_SUPPORTED_SPECIFICATION_VERSION = 12
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExcelFormat(Enum):
|
|
33
|
+
"""Excel input format variants recognised by FlexTool."""
|
|
34
|
+
SELF_DESCRIBING = "self_describing" # New format — metadata embedded in sheets
|
|
35
|
+
SPECIFICATION = "specification" # FlexTool 3.x — needs import_excel_input.json
|
|
36
|
+
OLD_V2 = "old_v2" # FlexTool 2.x .xlsm — completely different layout
|
|
37
|
+
UNKNOWN = "unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ExcelFormatInfo:
|
|
42
|
+
"""Result of :func:`detect_excel_format`."""
|
|
43
|
+
format: ExcelFormat
|
|
44
|
+
version: int | None = None # DB schema version found in the file (None = unknown)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def detect_excel_format(file_path: str | Path) -> ExcelFormatInfo:
|
|
48
|
+
"""Detect which FlexTool Excel format *file_path* uses.
|
|
49
|
+
|
|
50
|
+
Returns an :class:`ExcelFormatInfo` with the detected format and,
|
|
51
|
+
where possible, the database schema version embedded in the file.
|
|
52
|
+
|
|
53
|
+
Detection logic:
|
|
54
|
+
|
|
55
|
+
* **SELF_DESCRIBING** — ``version`` sheet, cell A1 starts with
|
|
56
|
+
``"Generated from FlexTool sqlite version"``. The version number
|
|
57
|
+
is parsed from that same cell.
|
|
58
|
+
* **SPECIFICATION** — ``version`` sheet, row 3 contains column
|
|
59
|
+
headers ``Version number, Date, Author, Description``
|
|
60
|
+
(FlexTool 3.x format that needs ``import_excel_input.json``).
|
|
61
|
+
The version is always 25 (the schema these files target).
|
|
62
|
+
* **OLD_V2** — file has a ``master`` sheet (FlexTool 2.x).
|
|
63
|
+
* **UNKNOWN** — none of the above.
|
|
64
|
+
"""
|
|
65
|
+
import openpyxl
|
|
66
|
+
|
|
67
|
+
file_path = Path(file_path)
|
|
68
|
+
try:
|
|
69
|
+
wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True)
|
|
70
|
+
except Exception:
|
|
71
|
+
logger.warning("Cannot open '%s' as an Excel file.", file_path)
|
|
72
|
+
return ExcelFormatInfo(ExcelFormat.UNKNOWN)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
sheet_names_lower = {s.lower(): s for s in wb.sheetnames}
|
|
76
|
+
|
|
77
|
+
# --- Check version sheet ---
|
|
78
|
+
if "version" in sheet_names_lower:
|
|
79
|
+
ws = wb[sheet_names_lower["version"]]
|
|
80
|
+
rows = list(ws.iter_rows(max_row=4, values_only=True))
|
|
81
|
+
|
|
82
|
+
# New self-describing: first cell starts with
|
|
83
|
+
# "Generated from FlexTool sqlite version: <N>"
|
|
84
|
+
if rows and rows[0] and rows[0][0]:
|
|
85
|
+
first_cell = str(rows[0][0]).strip()
|
|
86
|
+
if first_cell.lower().startswith("generated from flextool sqlite"):
|
|
87
|
+
version: int | None = None
|
|
88
|
+
m = re.search(r"version:\s*(\d+)", first_cell)
|
|
89
|
+
if m:
|
|
90
|
+
version = int(m.group(1))
|
|
91
|
+
logger.info(
|
|
92
|
+
"Detected SELF_DESCRIBING format (version %s): %s",
|
|
93
|
+
version, file_path.name,
|
|
94
|
+
)
|
|
95
|
+
return ExcelFormatInfo(ExcelFormat.SELF_DESCRIBING, version)
|
|
96
|
+
|
|
97
|
+
# Old 3.x: row 2 (0-indexed) has "Version number", "Date", …
|
|
98
|
+
if len(rows) >= 3 and rows[2]:
|
|
99
|
+
header = [str(c).strip().lower() if c else "" for c in rows[2]]
|
|
100
|
+
if "version number" in header and "date" in header:
|
|
101
|
+
# Specification-format files always target the v25 schema
|
|
102
|
+
logger.info(
|
|
103
|
+
"Detected SPECIFICATION (3.x) format: %s", file_path.name,
|
|
104
|
+
)
|
|
105
|
+
return ExcelFormatInfo(ExcelFormat.SPECIFICATION, 25)
|
|
106
|
+
|
|
107
|
+
# --- Check navigate sheet for v2 self-describing format ---
|
|
108
|
+
# When exported in v2 format, the version sheet is removed and
|
|
109
|
+
# the version is written to the navigate sheet (row 15, col F-G).
|
|
110
|
+
if "navigate" in sheet_names_lower:
|
|
111
|
+
ws_nav = wb[sheet_names_lower["navigate"]]
|
|
112
|
+
nav_version_label = ws_nav.cell(row=15, column=6).value
|
|
113
|
+
if nav_version_label and "flextool db version" in str(nav_version_label).lower():
|
|
114
|
+
version = None
|
|
115
|
+
nav_version_val = ws_nav.cell(row=15, column=7).value
|
|
116
|
+
if nav_version_val is not None:
|
|
117
|
+
try:
|
|
118
|
+
version = int(nav_version_val)
|
|
119
|
+
except (ValueError, TypeError):
|
|
120
|
+
pass
|
|
121
|
+
logger.info(
|
|
122
|
+
"Detected SELF_DESCRIBING v2 format (version %s): %s",
|
|
123
|
+
version, file_path.name,
|
|
124
|
+
)
|
|
125
|
+
return ExcelFormatInfo(ExcelFormat.SELF_DESCRIBING, version)
|
|
126
|
+
|
|
127
|
+
# --- Check for old 2.x .xlsm (has 'master' sheet) ---
|
|
128
|
+
if "master" in sheet_names_lower:
|
|
129
|
+
logger.info("Detected OLD_V2 format: %s", file_path.name)
|
|
130
|
+
return ExcelFormatInfo(ExcelFormat.OLD_V2)
|
|
131
|
+
|
|
132
|
+
logger.warning("Could not detect Excel format for '%s'.", file_path.name)
|
|
133
|
+
return ExcelFormatInfo(ExcelFormat.UNKNOWN)
|
|
134
|
+
|
|
135
|
+
finally:
|
|
136
|
+
wb.close()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_specification_changelog_version(file_path: str | Path) -> int | None:
|
|
140
|
+
"""Return the template version of a specification-format Excel.
|
|
141
|
+
|
|
142
|
+
Specification (3.x) Excels carry a ``version`` sheet whose changelog table
|
|
143
|
+
lists template versions in the first column (``Version number, Date,
|
|
144
|
+
Author, Description`` header on row 3). The file's version is the highest
|
|
145
|
+
integer in that column. Used to gate import support and to message the
|
|
146
|
+
user when a file's layout predates the importer.
|
|
147
|
+
|
|
148
|
+
Returns ``None`` when the file has no such changelog (e.g. a
|
|
149
|
+
self-describing Excel, a CSV directory, or an unreadable file), so callers
|
|
150
|
+
can safely skip the check for non-specification inputs.
|
|
151
|
+
"""
|
|
152
|
+
import openpyxl
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
wb = openpyxl.load_workbook(file_path, read_only=True, data_only=True)
|
|
156
|
+
except Exception:
|
|
157
|
+
return None
|
|
158
|
+
try:
|
|
159
|
+
sheet_names_lower = {s.lower(): s for s in wb.sheetnames}
|
|
160
|
+
if "version" not in sheet_names_lower:
|
|
161
|
+
return None
|
|
162
|
+
ws = wb[sheet_names_lower["version"]]
|
|
163
|
+
best: int | None = None
|
|
164
|
+
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
|
165
|
+
if i < 3 or not row: # skip the intro text + the header row (idx 2)
|
|
166
|
+
continue
|
|
167
|
+
cell = row[0]
|
|
168
|
+
if cell is None:
|
|
169
|
+
continue
|
|
170
|
+
m = re.match(r"\s*(\d+)", str(cell))
|
|
171
|
+
if m:
|
|
172
|
+
value = int(m.group(1))
|
|
173
|
+
best = value if best is None else max(best, value)
|
|
174
|
+
return best
|
|
175
|
+
finally:
|
|
176
|
+
wb.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def unsupported_specification_message(version: int | None) -> str:
|
|
180
|
+
"""User-facing message for specification Excels older than support."""
|
|
181
|
+
shown = f"v{version}" if version is not None else "an unsupported version"
|
|
182
|
+
return (
|
|
183
|
+
f"The layout of the input file corresponds to {shown}. This is not "
|
|
184
|
+
f"supported by the current importer, which supports imports from "
|
|
185
|
+
f"v{MIN_SUPPORTED_SPECIFICATION_VERSION} onward. You can get it "
|
|
186
|
+
f"imported by using legacy FlexTool (checkout branch 'master') with "
|
|
187
|
+
f"Spine Toolbox. Alternatively, convert it manually."
|
|
188
|
+
)
|