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,319 @@
1
+ """Upfront seeding of input-DB entity colors into ``plot_settings.yaml``.
2
+
3
+ The dispatch pipeline already seeds the *dispatch* entities it discovers
4
+ (:mod:`flextool.scenario_comparison.config_builder` /
5
+ :mod:`flextool.scenario_comparison.orchestrator`). This module covers the
6
+ *regular* (non-dispatch) plot pipeline: when a GUI scenario-execution batch
7
+ starts, do one upfront fetch of ALL entities in the relevant entity classes
8
+ from each project INPUT DB and additively seed their default-palette colors
9
+ into the project's ``plot_settings.yaml``. The normal time-series / stack
10
+ plots then get a complete, editable color list, and the user can reorder /
11
+ recolor while the scenarios run.
12
+
13
+ Design (decided with the user):
14
+
15
+ * **Upfront, at job start** — source is the INPUT DB, not the output parquet.
16
+ * **One DB fetch of ALL entities** in the relevant classes per distinct input
17
+ DB — never per scenario / per alternative (DB fetches are slow; one pass
18
+ gets them all). If a batch references multiple distinct input DBs, each
19
+ distinct one is fetched once and the results unioned.
20
+ * **Single writer in the GUI process** — scenarios run as parallel
21
+ subprocesses, so this seeding must happen once, in the GUI thread, before
22
+ (or while) those subprocesses run. This module does not write concurrently.
23
+
24
+ The relevant FlexTool entity classes — verified against the model schema
25
+ (``flextool/schemas/spinedb_schema.json``) and the ``entities`` subsections of
26
+ ``schemas/default_plot_settings.yaml`` — are ``unit``, ``connection``,
27
+ ``node`` and ``group``: exactly the classes whose entity names appear as plot
28
+ legend labels.
29
+
30
+ This is the GUI-execution path only (the color-editing workflow is GUI). The
31
+ headless CLI path is intentionally out of scope.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import logging
37
+ from pathlib import Path
38
+
39
+ from spinedb_api import DatabaseMapping, from_database
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # ``flowGroup.flow_aggregator`` enum values that request dispatch bands — i.e.
44
+ # the flowGroup aggregates the flows of its members into a dispatch-plot band.
45
+ # ``none`` / ``standalone_aggregator_only`` do NOT draw dispatch bands and so
46
+ # are not dispatch aggregators for the picker's flowGroup display filter.
47
+ _DISPATCH_AGGREGATOR_VALUES = frozenset({"dispatch_plots_only", "both"})
48
+
49
+
50
+ # ``plot_settings.yaml`` ``entities`` subsections that plot legend labels map
51
+ # to. v58 split: ``flowGroup`` is now its own top-level SpineDB class read
52
+ # directly; ``nodeGroup`` is a ``group`` with ``group__node`` membership (see
53
+ # :func:`fetch_entities_by_class`). The rest map 1:1 to top-level classes.
54
+ RELEVANT_ENTITY_CLASSES = ("nodeGroup", "flowGroup", "unit", "connection", "node")
55
+
56
+ # SpineDB membership relationship class that makes a ``group`` a nodeGroup
57
+ # (a collection of nodes — the dispatch subject). A group with no
58
+ # ``group__node`` membership has no nodes to colour and is dropped.
59
+ _NODEGROUP_MEMBERSHIP_CLASSES = (
60
+ "group__node",
61
+ )
62
+
63
+
64
+ def fetch_entities_by_class(db_url: str) -> dict[str, list[str]]:
65
+ """Open one input DB and return entity names grouped by plot entity class.
66
+
67
+ Opens a single :class:`~spinedb_api.DatabaseMapping` (``create=False``),
68
+ reads ALL entity items in ONE pass (units / connections / nodes / groups /
69
+ flowGroups plus the ``group__node`` membership class), classifies each
70
+ ``group`` as a ``nodeGroup`` by its membership, reads ``flowGroup``
71
+ directly, and closes the mapping. Returns ``{class -> sorted unique
72
+ names}`` for the classes in :data:`RELEVANT_ENTITY_CLASSES` that have at
73
+ least one entity. Never queries per scenario / alternative — one fetch
74
+ returns everything.
75
+
76
+ Classification (v58): ``flowGroup`` is read directly from the real
77
+ top-level ``flowGroup`` class; a ``group`` with
78
+ :data:`_NODEGROUP_MEMBERSHIP_CLASSES` (``group__node``) membership is a
79
+ **nodeGroup**. A dual-role entity legitimately appearing in both buckets
80
+ is correct and left as-is (no collision-precedence logic).
81
+
82
+ The base pattern mirrors
83
+ :func:`flextool.export_to_tabular.db_reader._read_entities`
84
+ (``db.get_entity_items()`` grouped by ``entity_class_name``).
85
+ """
86
+ groups: set[str] = set()
87
+ units: set[str] = set()
88
+ connections: set[str] = set()
89
+ nodes: set[str] = set()
90
+ flow_group_names: set[str] = set()
91
+ node_group_names: set[str] = set()
92
+
93
+ db = DatabaseMapping(db_url, create=False)
94
+ try:
95
+ for item in db.get_entity_items():
96
+ cls = item["entity_class_name"]
97
+ if cls == "group":
98
+ groups.add(item["name"])
99
+ elif cls == "flowGroup":
100
+ flow_group_names.add(item["name"]) # v58 real top-level class
101
+ elif cls == "unit":
102
+ units.add(item["name"])
103
+ elif cls == "connection":
104
+ connections.add(item["name"])
105
+ elif cls == "node":
106
+ nodes.add(item["name"])
107
+ elif cls in _NODEGROUP_MEMBERSHIP_CLASSES:
108
+ byname = item.get("entity_byname")
109
+ if byname:
110
+ node_group_names.add(byname[0]) # group is the 1st member
111
+ finally:
112
+ db.close()
113
+
114
+ by_class: dict[str, list[str]] = {
115
+ # A group lands in nodeGroup if it has group__node members.
116
+ "nodeGroup": sorted(g for g in groups if g in node_group_names),
117
+ # flowGroup is the real top-level class read directly.
118
+ "flowGroup": sorted(flow_group_names),
119
+ "unit": sorted(units),
120
+ "connection": sorted(connections),
121
+ "node": sorted(nodes),
122
+ }
123
+ return {cls: names for cls, names in by_class.items() if names}
124
+
125
+
126
+ def _decode_flow_aggregator_value(item) -> str | None:
127
+ """Best-effort decode of a ``flow_aggregator`` parameter value to a string.
128
+
129
+ ``flow_aggregator`` is a single-valued enum method, so its stored value is a
130
+ plain string (spinedb JSON-encodes it as ``b'"both"'`` with ``type is
131
+ None``). Prefer the already-decoded ``parsed_value`` when the mapping
132
+ provides one, else decode the raw ``(value, type)`` via
133
+ :func:`~spinedb_api.from_database`. Defensively unwrap a ``{"type": ...,
134
+ "data"/"value": ...}`` JSON wrapper and a bytes payload so a comparison
135
+ always runs against a bare ``str``. Returns ``None`` (never raises) when the
136
+ value is missing / unparseable / not a string.
137
+ """
138
+ value = None
139
+ # ``parsed_value`` (when present) is already decoded to a Python object.
140
+ try:
141
+ value = item["parsed_value"]
142
+ except (KeyError, TypeError, IndexError):
143
+ value = None
144
+ if value is None:
145
+ try:
146
+ value = from_database(item["value"], item["type"])
147
+ except Exception:
148
+ return None
149
+ # Defensively unwrap a dict / bytes payload down to a plain string.
150
+ if isinstance(value, dict):
151
+ value = value.get("data", value.get("value"))
152
+ if isinstance(value, bytes):
153
+ try:
154
+ value = value.decode("utf-8")
155
+ except Exception:
156
+ return None
157
+ return value if isinstance(value, str) else None
158
+
159
+
160
+ def fetch_flow_aggregator_flowgroups(db_url: str) -> set[str]:
161
+ """Return flowGroup names whose ``flow_aggregator`` requests dispatch bands.
162
+
163
+ Opens one :class:`~spinedb_api.DatabaseMapping` (``create=False``), reads the
164
+ ``flowGroup`` class' ``flow_aggregator`` parameter values (via
165
+ :meth:`DatabaseMapping.find_parameter_values`, the same filtered read
166
+ :mod:`flextool.input_derivation._validators` uses), decodes each value
167
+ (:func:`_decode_flow_aggregator_value`) and collects the flowGroup names
168
+ whose value is ``dispatch_plots_only`` or ``both`` — i.e. the dispatch
169
+ aggregators. Non-aggregators (``none`` / ``standalone_aggregator_only`` /
170
+ absent) are excluded.
171
+
172
+ Robust by construction: a missing class / parameter, an unparseable value,
173
+ an un-openable or already-closed DB all contribute nothing and NEVER raise —
174
+ the caller gets whatever could be read (possibly an empty set).
175
+ """
176
+ result: set[str] = set()
177
+ try:
178
+ db = DatabaseMapping(db_url, create=False)
179
+ except Exception as exc: # un-openable / missing DB
180
+ logger.warning("Cannot open %s for flow_aggregator read: %s", db_url, exc)
181
+ return result
182
+ try:
183
+ try:
184
+ items = db.find_parameter_values(
185
+ entity_class_name="flowGroup",
186
+ parameter_definition_name="flow_aggregator",
187
+ )
188
+ except Exception as exc: # missing class / parameter
189
+ logger.warning(
190
+ "Cannot read flow_aggregator from %s: %s", db_url, exc,
191
+ )
192
+ items = []
193
+ for pv in items:
194
+ if _decode_flow_aggregator_value(pv) not in _DISPATCH_AGGREGATOR_VALUES:
195
+ continue
196
+ try:
197
+ byname = pv["entity_byname"]
198
+ except (KeyError, TypeError):
199
+ byname = None
200
+ if byname:
201
+ result.add(byname[0]) # flowGroup is the single dimension
202
+ finally:
203
+ try:
204
+ db.close()
205
+ except Exception:
206
+ pass
207
+ return result
208
+
209
+
210
+ def _url_to_path(db_url: str) -> Path | None:
211
+ """Return the on-disk path for a ``sqlite:///`` URL, else ``None``.
212
+
213
+ Non-sqlite URLs (which FlexTool does not currently use for input DBs)
214
+ return ``None`` and are skipped by the existence check.
215
+ """
216
+ prefix = "sqlite:///"
217
+ if db_url.startswith(prefix):
218
+ return Path(db_url[len(prefix):])
219
+ return None
220
+
221
+
222
+ def seed_input_entity_colors(
223
+ project_path: Path,
224
+ input_db_urls: list[str],
225
+ ) -> bool:
226
+ """Fetch input-DB entities and seed (add + prune) their colors in settings.
227
+
228
+ For each DISTINCT *input_db_url* whose backing sqlite file exists on disk,
229
+ fetch all entities in the relevant classes (one DB open each), union the
230
+ results across DBs, assign default palette colors with the same logic the
231
+ dispatch seeding uses
232
+ (:func:`flextool.scenario_comparison.config_builder.assign_palette_colors`),
233
+ and write them into the project ``plot_settings.yaml`` via the structured
234
+ writer
235
+ (:func:`flextool.scenario_comparison.plot_settings_seed.seed_colors_into_plot_settings`).
236
+ Because the input DB is the AUTHORITATIVE full entity set, this seed passes
237
+ ``prune_entities`` (the full per-class DB name set) so entities no longer
238
+ in the DB are REMOVED as well as new ones added.
239
+
240
+ The project file is resolved via
241
+ :func:`flextool.plot_outputs.color_template.resolve_plot_settings_path`;
242
+ if the resolver returns the bundled default (the project has no own file
243
+ yet) the project copy is seeded first via
244
+ :func:`flextool.gui.project_utils.seed_plot_settings` so the packaged file
245
+ is never written. When the file changes, the color-template cache is
246
+ cleared so any reader in this same process sees the new colors.
247
+
248
+ Returns ``True`` when the project file was modified, ``False`` otherwise
249
+ (no new entities / nothing to seed — the idempotent case).
250
+
251
+ URLs whose backing file does not exist are skipped (e.g. xlsx sources that
252
+ point at a not-yet-created ``intermediate/<stem>.sqlite``). This is a
253
+ known limitation: such sources are seeded later, on their next batch, once
254
+ the intermediate DB has been built.
255
+ """
256
+ from flextool.plot_outputs.color_template import (
257
+ _clear_cache,
258
+ _default_path,
259
+ resolve_plot_settings_path,
260
+ )
261
+ from flextool.scenario_comparison.config_builder import assign_palette_colors
262
+ from flextool.scenario_comparison.plot_settings_seed import (
263
+ seed_colors_into_plot_settings,
264
+ )
265
+
266
+ # Distinct, existing-on-disk input DBs only.
267
+ seen: set[str] = set()
268
+ distinct_urls: list[str] = []
269
+ for url in input_db_urls:
270
+ if not url or url in seen:
271
+ continue
272
+ seen.add(url)
273
+ path = _url_to_path(url)
274
+ if path is None or not path.is_file():
275
+ # xlsx source -> intermediate/<stem>.sqlite not yet created; skip
276
+ # (known limitation, see docstring).
277
+ continue
278
+ distinct_urls.append(url)
279
+
280
+ if not distinct_urls:
281
+ return False
282
+
283
+ # Union all entities across the distinct input DBs (one fetch each).
284
+ union: dict[str, set[str]] = {cls: set() for cls in RELEVANT_ENTITY_CLASSES}
285
+ for url in distinct_urls:
286
+ per_db = fetch_entities_by_class(url)
287
+ for cls, names in per_db.items():
288
+ union[cls].update(names)
289
+
290
+ entity_colors: dict[str, dict[str, str]] = {}
291
+ prune_entities: dict[str, set[str]] = {}
292
+ for cls in RELEVANT_ENTITY_CLASSES:
293
+ names = sorted(union[cls])
294
+ if names:
295
+ entity_colors[cls] = assign_palette_colors(names)
296
+ # The input DB is the AUTHORITATIVE full entity set per class: prune
297
+ # any entity in the file that the DB no longer has. Pass the full
298
+ # (possibly empty) lowercased name set for every relevant class so a
299
+ # class that lost all its entities is fully cleared.
300
+ prune_entities[cls] = {n.lower() for n in names}
301
+
302
+ if not any(entity_colors.values()):
303
+ return False
304
+
305
+ # Resolve to the project's own file; if the resolver handed back the
306
+ # bundled default, seed the project copy first (never write the package
307
+ # file).
308
+ target = resolve_plot_settings_path(project_path)
309
+ if target == Path(_default_path()):
310
+ from flextool.gui.project_utils import seed_plot_settings
311
+ target = seed_plot_settings(project_path)
312
+
313
+ changed = seed_colors_into_plot_settings(
314
+ target, entity_colors, {}, prune_entities=prune_entities
315
+ )
316
+ if changed:
317
+ _clear_cache()
318
+ logger.info("Seeded input-DB entity colors into %s", target)
319
+ return changed