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,12 @@
1
+ """Update and migration: GitHub update, database schema versioning."""
2
+
3
+ #: The current FlexTool database schema version.
4
+ #: This is the single source of truth — all other modules import from here.
5
+ #: Defined before submodule imports to avoid circular-import issues.
6
+ FLEXTOOL_DB_VERSION: int = 69
7
+
8
+ from flextool.update_flextool.self_update import update_flextool, ensure_runtime_files # noqa: E402 # FLEXTOOL_DB_VERSION must precede submodule imports to break circular dep
9
+ from flextool.update_flextool.db_migration import migrate_database # noqa: E402 # see FLEXTOOL_DB_VERSION ordering note above
10
+ from flextool.update_flextool.initialize_database import initialize_database # noqa: E402 # see FLEXTOOL_DB_VERSION ordering note above
11
+
12
+ __all__ = ['update_flextool', 'ensure_runtime_files', 'migrate_database', 'initialize_database', 'FLEXTOOL_DB_VERSION']
@@ -0,0 +1,251 @@
1
+ """Canonical FlexTool databases stored as JSON in the package.
2
+
3
+ Each entry in :data:`CANONICAL_DATABASES` pairs a JSON resource — the
4
+ source of truth, shipped inside ``flextool/schemas/canonical_databases/``
5
+ in the wheel — with the on-disk ``.sqlite`` path it materialises into
6
+ relative to the user's current working directory.
7
+
8
+ The user-facing workflow is one of:
9
+
10
+ ``python -m flextool.update_flextool.canonical_databases materialize``
11
+ Recreate any missing ``.sqlite`` files from the bundled JSON sources.
12
+ Idempotent for files that already exist. Called automatically by
13
+ :func:`flextool.update_flextool.update_flextool` and by
14
+ ``tests/conftest.py`` so users and CI always have ``.sqlite`` files
15
+ to point Spine Toolbox at.
16
+
17
+ ``python -m flextool.update_flextool.canonical_databases migrate-all``
18
+ Round-trip every canonical JSON through
19
+ ``initialize_database -> migrate_database -> export_database``.
20
+ Run from a source checkout (editable install) whenever
21
+ ``FLEXTOOL_DB_VERSION`` is bumped; commit the resulting JSON diffs
22
+ alongside the migration step. See ``CONTRIBUTING.md``.
23
+
24
+ ``python -m flextool.update_flextool.canonical_databases verify``
25
+ Check that every committed canonical JSON is byte-stable through
26
+ the current schema. Intended for CI: exits non-zero with a per-file
27
+ diff summary when any committed JSON has drifted from what the
28
+ current migration chain would produce.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import json
35
+ import os
36
+ import tempfile
37
+ from dataclasses import dataclass
38
+ from pathlib import Path
39
+
40
+ from flextool._resources import package_data_path
41
+ from flextool.update_flextool.db_migration import migrate_database
42
+ from flextool.update_flextool.export_database import export_database
43
+ from flextool.update_flextool.initialize_database import initialize_database
44
+
45
+
46
+ # ``json_name`` is resolved against the bundled
47
+ # ``flextool/schemas/canonical_databases/`` directory via
48
+ # ``importlib.resources``. ``sqlite_path`` is interpreted relative to
49
+ # the user's current working directory — Spine Toolbox project files and
50
+ # scenario references still see the same layout they always have.
51
+ @dataclass(frozen=True)
52
+ class CanonicalDatabase:
53
+ json_name: str
54
+ sqlite_path: str
55
+
56
+
57
+ CANONICAL_DATABASES: tuple[CanonicalDatabase, ...] = (
58
+ CanonicalDatabase("templates_examples.json", "templates/examples.sqlite"),
59
+ CanonicalDatabase("templates_time_settings_only.json", "templates/time_settings_only.sqlite"),
60
+ CanonicalDatabase("howto_aggregate_output.json", "how to example databases/aggregate_output.sqlite"),
61
+ CanonicalDatabase("howto_connections.json", "how to example databases/connections.sqlite"),
62
+ CanonicalDatabase("howto_demand.json", "how to example databases/Demand.sqlite"),
63
+ CanonicalDatabase("howto_hydro_reservoir.json", "how to example databases/hydro_reservoir.sqlite"),
64
+ CanonicalDatabase("howto_hydro_reservoir_with_pump.json", "how to example databases/hydro_reservoir_with_pump.sqlite"),
65
+ CanonicalDatabase("howto_non_sync_and_curtailment.json", "how to example databases/non_sync_and_curtailment.sqlite"),
66
+ CanonicalDatabase("howto_ramp_and_start_up.json", "how to example databases/ramp_and_start_up.sqlite"),
67
+ CanonicalDatabase("howto_stochastics.json", "how to example databases/stochastics.sqlite"),
68
+ )
69
+
70
+
71
+ def _json_source(db: CanonicalDatabase) -> Path:
72
+ return package_data_path(f"schemas/canonical_databases/{db.json_name}")
73
+
74
+
75
+ def materialize(overwrite: bool = False) -> None:
76
+ """Create every canonical SQLite from its JSON source.
77
+
78
+ With ``overwrite=False`` (the default), existing ``.sqlite`` files
79
+ in the working tree are left untouched — preserves user edits.
80
+ Pass ``overwrite=True`` to force a refresh (used by the migrate-all
81
+ command and by callers that need a guaranteed-current SQLite).
82
+
83
+ Destination paths are interpreted relative to the current working
84
+ directory; the canonical JSON sources are read from the installed
85
+ ``flextool`` package via :mod:`importlib.resources`.
86
+ """
87
+ for db in CANONICAL_DATABASES:
88
+ json_abs = _json_source(db)
89
+ sqlite_abs = Path.cwd() / db.sqlite_path
90
+ if not json_abs.is_file():
91
+ print(f"!! canonical JSON missing in package: {db.json_name}")
92
+ continue
93
+ if sqlite_abs.exists():
94
+ if not overwrite:
95
+ continue
96
+ sqlite_abs.unlink()
97
+ sqlite_abs.parent.mkdir(parents=True, exist_ok=True)
98
+ initialize_database(str(json_abs), str(sqlite_abs))
99
+
100
+
101
+ def migrate_all() -> None:
102
+ """Round-trip every canonical JSON through the current schema.
103
+
104
+ For each entry:
105
+
106
+ 1. Materialise the bundled JSON into a throwaway ``.sqlite``.
107
+ 2. Run :func:`migrate_database` against it — walks
108
+ ``model.version`` up to :data:`FLEXTOOL_DB_VERSION`.
109
+ 3. Re-export the migrated ``.sqlite`` back over the bundled JSON
110
+ file in place.
111
+
112
+ Requires an editable install: the JSON files are written back to the
113
+ on-disk package source, which only exists for ``pip install -e .``
114
+ checkouts. Wheel installs (``site-packages/``) are read-only and
115
+ will fail here — that's intentional; only the maintainer running
116
+ a schema bump should be regenerating canonical JSONs.
117
+ """
118
+ for db in CANONICAL_DATABASES:
119
+ json_abs = _json_source(db)
120
+ if not json_abs.is_file():
121
+ print(f"!! canonical JSON missing in package: {db.json_name}")
122
+ continue
123
+ with tempfile.TemporaryDirectory() as tmp:
124
+ staging = os.path.join(tmp, "staging.sqlite")
125
+ initialize_database(str(json_abs), staging)
126
+ migrate_database(staging)
127
+ export_database(staging, str(json_abs))
128
+ print(f"migrated canonical JSON: {db.json_name}")
129
+
130
+
131
+ def _summarise_diff(committed: Path, regenerated: Path) -> str:
132
+ """Return a short human-readable summary of which top-level sections
133
+ differ between two canonical JSONs.
134
+
135
+ Used by ``verify`` to surface drift without flooding the CI log.
136
+ Mirrors :func:`flextool.update_flextool.test_fixtures._summarise_diff`.
137
+ """
138
+ with open(committed) as f:
139
+ a = json.load(f)
140
+ with open(regenerated) as f:
141
+ b = json.load(f)
142
+ keys = sorted(set(a) | set(b))
143
+ lines = []
144
+ for key in keys:
145
+ av = a.get(key)
146
+ bv = b.get(key)
147
+ if av == bv:
148
+ continue
149
+ la = len(av) if isinstance(av, list) else "?"
150
+ lb = len(bv) if isinstance(bv, list) else "?"
151
+ lines.append(f" {key}: committed={la} regenerated={lb}")
152
+ return "\n".join(lines) if lines else " (no top-level section differs — check value-level)"
153
+
154
+
155
+ def verify_all() -> int:
156
+ """Verify that every committed canonical JSON matches its migrated form.
157
+
158
+ Round-trips each ``CANONICAL_DATABASES`` entry through
159
+ ``initialize_database -> migrate_database -> export_database`` into a
160
+ temp file and compares to the committed source. Returns 0 when every
161
+ file is byte-stable, or a positive count of mismatched files
162
+ otherwise. Intended for CI use via
163
+ ``python -m flextool.update_flextool.canonical_databases verify``.
164
+
165
+ Mirrors :func:`flextool.update_flextool.test_fixtures.verify_all`.
166
+ """
167
+ mismatches = 0
168
+ for db in CANONICAL_DATABASES:
169
+ json_abs = _json_source(db)
170
+ if not json_abs.is_file():
171
+ print(f"!! canonical JSON missing in package: {db.json_name}")
172
+ mismatches += 1
173
+ continue
174
+ with tempfile.TemporaryDirectory() as tmp:
175
+ staging = os.path.join(tmp, "staging.sqlite")
176
+ regenerated = Path(tmp) / db.json_name
177
+ initialize_database(str(json_abs), staging)
178
+ migrate_database(staging)
179
+ export_database(staging, str(regenerated))
180
+ with open(json_abs) as f:
181
+ committed_text = f.read()
182
+ with open(regenerated) as f:
183
+ regenerated_text = f.read()
184
+ if committed_text == regenerated_text:
185
+ print(f"ok: {db.json_name}")
186
+ else:
187
+ mismatches += 1
188
+ print(f"DRIFT: {db.json_name}")
189
+ print(_summarise_diff(json_abs, regenerated))
190
+ if mismatches:
191
+ print(
192
+ f"\n{mismatches} canonical JSON(s) out of sync with current schema. "
193
+ "Run `python -m flextool.update_flextool.canonical_databases migrate-all`."
194
+ )
195
+ return mismatches
196
+
197
+
198
+ def _seed_from_cwd_sqlites() -> None:
199
+ """One-time: populate JSONs from the SQLites currently in the tree.
200
+
201
+ Reads each ``CanonicalDatabase.sqlite_path`` (CWD-relative) and
202
+ exports it back over the package-bundled JSON source. Only useful
203
+ in a source checkout where the JSON files live on disk and can be
204
+ written.
205
+ """
206
+ for db in CANONICAL_DATABASES:
207
+ sqlite_abs = Path.cwd() / db.sqlite_path
208
+ json_abs = _json_source(db)
209
+ if not sqlite_abs.exists():
210
+ print(f"!! cannot seed {db.json_name}: source SQLite missing at {db.sqlite_path}")
211
+ continue
212
+ json_abs.parent.mkdir(parents=True, exist_ok=True)
213
+ export_database(str(sqlite_abs), str(json_abs))
214
+ print(f"seeded {db.json_name} from {db.sqlite_path}")
215
+
216
+
217
+ def main(argv: list[str] | None = None) -> int:
218
+ parser = argparse.ArgumentParser(description=__doc__)
219
+ sub = parser.add_subparsers(dest="cmd", required=True)
220
+ sub.add_parser("materialize", help="Create missing .sqlite files from JSON")
221
+ sub.add_parser(
222
+ "materialize-force", help="Recreate every canonical .sqlite from JSON"
223
+ )
224
+ sub.add_parser(
225
+ "migrate-all", help="Round-trip every JSON through the current schema"
226
+ )
227
+ sub.add_parser(
228
+ "verify",
229
+ help="Check every canonical JSON is byte-stable through the current schema",
230
+ )
231
+ sub.add_parser(
232
+ "seed",
233
+ help="One-time: export every CWD .sqlite to its canonical JSON",
234
+ )
235
+ args = parser.parse_args(argv)
236
+
237
+ if args.cmd == "materialize":
238
+ materialize(overwrite=False)
239
+ elif args.cmd == "materialize-force":
240
+ materialize(overwrite=True)
241
+ elif args.cmd == "migrate-all":
242
+ migrate_all()
243
+ elif args.cmd == "verify":
244
+ return verify_all()
245
+ elif args.cmd == "seed":
246
+ _seed_from_cwd_sqlites()
247
+ return 0
248
+
249
+
250
+ if __name__ == "__main__":
251
+ raise SystemExit(main())