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,138 @@
1
+ """Self-healing creation of lightweight settings SQLite databases.
2
+
3
+ FlexTool accepts three small, user-editable SQLite databases as
4
+ command-line inputs: ``output_info.sqlite`` (output-location mapping),
5
+ ``output_settings.sqlite`` (per-scenario output configuration), and
6
+ ``comparison_settings.sqlite`` (scenario-comparison configuration).
7
+ These were previously created only by ``flextool-update``, which users
8
+ had to remember to run at least once. On a fresh clone, forgetting
9
+ this leads to opaque "file not found" errors from ``spinedb_api``.
10
+
11
+ This module lets the main runtime paths seed them on demand from their
12
+ tracked JSON templates under ``schemas/``. The logic is intentionally
13
+ conservative:
14
+
15
+ * Only file basenames in :data:`SETTINGS_TEMPLATES` are auto-seeded.
16
+ Other ``*.sqlite`` paths the user passes are left alone.
17
+ * Existing files are never overwritten — user edits always persist.
18
+ * Parent directories are created as needed.
19
+
20
+ For heavier templates (``examples.sqlite``, ``results_template.sqlite``,
21
+ ``example_input.xlsx``) the canonical path remains ``flextool-update``;
22
+ those require migrations that aren't appropriate to run on every solve.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from pathlib import Path
29
+ from typing import Optional
30
+ from urllib.parse import urlparse
31
+
32
+ from .initialize_database import initialize_database
33
+
34
+
35
+ _LOGGER = logging.getLogger(__name__)
36
+
37
+
38
+ SETTINGS_TEMPLATES: dict[str, str] = {
39
+ "output_info.sqlite": "output_info_template.json",
40
+ "output_settings.sqlite": "output_settings_template.json",
41
+ "comparison_settings.sqlite": "comparison_settings_template.json",
42
+ }
43
+ """Mapping from settings-DB basename to the JSON template that seeds it.
44
+
45
+ Templates are looked up under ``flextool/schemas/`` via
46
+ ``importlib.resources`` (see :func:`ensure_settings_db`). Keys are
47
+ compared case-sensitively against ``Path.name`` of the target path, not
48
+ full paths — so a user-custom name like ``my_config.sqlite`` is
49
+ intentionally not auto-seeded.
50
+ """
51
+
52
+
53
+ def _sqlite_url_to_path(sqlite_url: str, cwd: Optional[Path] = None) -> Optional[Path]:
54
+ """Return the filesystem path for a ``sqlite://...`` URL, or ``None``
55
+ if the input isn't a plain SQLite URL.
56
+
57
+ Accepts both ``sqlite:///relative/path.sqlite`` and
58
+ ``sqlite:////absolute/path.sqlite`` forms. Unusual URL schemes
59
+ (in-memory, mysql, remote) are not handled here.
60
+ """
61
+ if sqlite_url is None:
62
+ return None
63
+ parsed = urlparse(sqlite_url)
64
+ # A bare Windows path like ``C:\foo`` urlparses with ``scheme='c'``; a
65
+ # single-letter scheme is a drive letter, not a URL scheme, so treat it as
66
+ # a plain path (otherwise every drive-anchored Windows path returned None).
67
+ scheme = "" if len(parsed.scheme) == 1 else parsed.scheme
68
+ if scheme and scheme != "sqlite":
69
+ return None
70
+ if scheme == "sqlite":
71
+ # ``sqlite:///relative/path`` → parsed.path == '/relative/path'
72
+ # (one leading slash from URL syntax;
73
+ # actual path is relative)
74
+ # ``sqlite:////abs/path`` → parsed.path == '//abs/path'
75
+ # (two leading slashes; path is absolute)
76
+ raw = parsed.path or parsed.netloc
77
+ if raw.startswith("//"):
78
+ fs_path = Path(raw[1:]) # ``//abs/path`` → ``/abs/path``
79
+ elif raw.startswith("/"):
80
+ fs_path = Path(raw[1:]) # ``/relative/path`` → ``relative/path``
81
+ else:
82
+ fs_path = Path(raw)
83
+ else:
84
+ # Bare filesystem path (no scheme). Use as-is; do NOT strip a
85
+ # leading slash — on POSIX that makes absolute paths relative.
86
+ fs_path = Path(sqlite_url)
87
+ if not fs_path.is_absolute() and cwd is not None:
88
+ fs_path = cwd / fs_path
89
+ return fs_path
90
+
91
+
92
+ def ensure_settings_db(
93
+ target: str | Path,
94
+ repo_root: Path | None = None,
95
+ *,
96
+ logger: Optional[logging.Logger] = None,
97
+ ) -> Optional[Path]:
98
+ """If ``target`` is a known settings DB (by basename) and does not
99
+ yet exist, create it from the corresponding JSON template.
100
+
101
+ ``target`` may be a filesystem path or an ``sqlite://...`` URL.
102
+ The JSON template ships inside the ``flextool`` package
103
+ (``flextool/schemas/<template>``); ``repo_root`` is accepted for
104
+ backward compatibility but ignored — templates are always read from
105
+ package data via :mod:`importlib.resources`.
106
+ """
107
+ log = logger or _LOGGER
108
+ if target is None:
109
+ return None
110
+ if isinstance(target, str):
111
+ path = _sqlite_url_to_path(target)
112
+ if path is None:
113
+ return None
114
+ else:
115
+ path = Path(target)
116
+
117
+ template_name = SETTINGS_TEMPLATES.get(path.name)
118
+ if template_name is None:
119
+ return None
120
+ if path.exists():
121
+ return None
122
+
123
+ from flextool._resources import package_data_path
124
+ template_path = package_data_path(f"schemas/{template_name}")
125
+ if not template_path.is_file():
126
+ log.warning(
127
+ "Cannot auto-seed %s: template %s is missing.",
128
+ path, template_path,
129
+ )
130
+ return None
131
+
132
+ path.parent.mkdir(parents=True, exist_ok=True)
133
+ initialize_database(str(template_path), str(path))
134
+ log.info("Seeded %s from %s", path, template_path)
135
+ return path
136
+
137
+
138
+ __all__ = ["SETTINGS_TEMPLATES", "ensure_settings_db"]
@@ -0,0 +1,103 @@
1
+ """Export a FlexTool Spine DB to deterministic JSON.
2
+
3
+ Companion to :func:`flextool.update_flextool.initialize_database`. The
4
+ round-trip ``export_database -> initialize_database -> export_database``
5
+ is bit-identical, which lets canonical example/template databases live
6
+ in git as JSON instead of binary ``.sqlite`` files.
7
+
8
+ Two callables sit at the heart of the round-trip:
9
+
10
+ ``keep_serialized_parse``
11
+ Passed as ``parse_value`` to :func:`spinedb_api.export_data`. Instead
12
+ of returning a parsed Python object (``Array``, ``Map``, ``DateTime``,
13
+ ...), it returns the raw ``(value_json_str, type_str)`` pair that
14
+ ``spinedb_api`` already stores in the DB. That keeps every shape
15
+ JSON-serialisable and avoids lossy round-trips through parsed
16
+ object identity (e.g. ``Map`` repr ordering, float printing).
17
+
18
+ ``keep_serialized_unparse``
19
+ Passed as ``unparse_value`` to :func:`spinedb_api.import_data` by
20
+ :func:`flextool.update_flextool.initialize_database` when the JSON
21
+ came from this exporter. Accepts either the ``(str, type)`` pair
22
+ (preserved from JSON) or a raw scalar (master-template style); in
23
+ the former case it bypasses :func:`spinedb_api.to_database` and
24
+ writes the bytes straight back.
25
+ """
26
+
27
+ import argparse
28
+ import json
29
+ from typing import Any
30
+
31
+ from spinedb_api import DatabaseMapping, export_data
32
+ from spinedb_api.parameter_value import to_database
33
+
34
+
35
+ def keep_serialized_parse(value: bytes | None, type_: str | None) -> Any:
36
+ """``parse_value`` callback that preserves the raw DB form.
37
+
38
+ Returns ``None`` for ``None`` and for the JSON-``null`` blob (those
39
+ are equivalent on the import side and collapsing them keeps the
40
+ exported JSON tidy and round-trip-stable). Otherwise returns a
41
+ ``[json_string, type_string]`` two-list — a list, not a tuple,
42
+ so it survives ``json.dumps``/``json.loads`` unchanged.
43
+ """
44
+ if value is None:
45
+ return None
46
+ if value == b"null" and type_ is None:
47
+ return None
48
+ return [value.decode("utf-8"), type_]
49
+
50
+
51
+ def keep_serialized_unparse(value: Any) -> tuple[bytes | None, str | None]:
52
+ """``unparse_value`` callback paired with :func:`keep_serialized_parse`.
53
+
54
+ Recognises the ``[json_string, type_string]`` shape produced by this
55
+ exporter and routes it back to the DB without re-parsing. Falls
56
+ back to the default :func:`spinedb_api.to_database` for any other
57
+ shape, so master templates that author raw scalars (``"no_method"``,
58
+ numbers, ``None``) keep working.
59
+ """
60
+ if (
61
+ isinstance(value, list)
62
+ and len(value) == 2
63
+ and (value[1] is None or isinstance(value[1], str))
64
+ ):
65
+ val_str, type_str = value
66
+ if val_str is None:
67
+ return None, None
68
+ return val_str.encode("utf-8"), type_str
69
+ return to_database(value)
70
+
71
+
72
+ def export_database(database_path: str, json_path: str) -> None:
73
+ """Export ``database_path`` to a deterministic JSON file.
74
+
75
+ The output is ``json.dumps``-ed with ``sort_keys=True`` and
76
+ ``indent=2`` so diffs are reviewable and ordering is stable. Lists
77
+ inside the dump preserve ``spinedb_api`` insertion order — that is
78
+ already deterministic per DB, and forcing a sort would invalidate
79
+ ordered structures such as ``parameter_value_lists``.
80
+ """
81
+ if database_path.startswith("sqlite://"):
82
+ url = database_path
83
+ elif database_path.endswith(".sqlite"):
84
+ url = "sqlite:///" + database_path
85
+ else:
86
+ raise ValueError(
87
+ f"Expected a .sqlite path or sqlite:// URL, got {database_path!r}"
88
+ )
89
+
90
+ with DatabaseMapping(url, create=False, upgrade=True) as db:
91
+ data = export_data(db, parse_value=keep_serialized_parse)
92
+
93
+ with open(json_path, "w", encoding="utf-8") as out:
94
+ json.dump(data, out, sort_keys=True, indent=2, ensure_ascii=False)
95
+ out.write("\n")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ parser = argparse.ArgumentParser(description=__doc__)
100
+ parser.add_argument("database_path", help="Source .sqlite path")
101
+ parser.add_argument("json_path", help="Destination JSON path")
102
+ args = parser.parse_args()
103
+ export_database(args.database_path, args.json_path)