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,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)
|