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,274 @@
|
|
|
1
|
+
"""Generate flextool/schemas/canonical_databases/*.json from tests/fixtures/*.json.
|
|
2
|
+
|
|
3
|
+
Reads recipes from ``flextool/schemas/canonical_databases/_recipes.yaml``.
|
|
4
|
+
|
|
5
|
+
Format bridge
|
|
6
|
+
-------------
|
|
7
|
+
``tests/fixtures/*.json`` uses **base64-packed** values (via
|
|
8
|
+
:mod:`tests.db_utils`). ``flextool/schemas/canonical_databases/*.json``
|
|
9
|
+
uses **UTF-8 decoded** values (via
|
|
10
|
+
:mod:`flextool.update_flextool.export_database`). Rather than translating
|
|
11
|
+
between formats directly, each converter is used in its native direction:
|
|
12
|
+
|
|
13
|
+
1. Read the source JSON via :func:`tests.db_utils.json_to_db` into a temp
|
|
14
|
+
SQLite (base64 -> bytes).
|
|
15
|
+
2. Run :func:`flextool.update_flextool.db_migration.migrate_database` —
|
|
16
|
+
idempotent after Stage 2a of the test_fixtures migration.
|
|
17
|
+
3. Filter: delete excluded scenarios, alternatives, and entities listed
|
|
18
|
+
in the recipe. ``spinedb_api`` cascade-removes scenario_alternatives,
|
|
19
|
+
entity_alternatives, parameter_values, and multi-dim child entities
|
|
20
|
+
automatically.
|
|
21
|
+
4. Export the SQLite via :func:`flextool.update_flextool.export_database`
|
|
22
|
+
(bytes -> UTF-8) over the canonical JSON.
|
|
23
|
+
|
|
24
|
+
CLI
|
|
25
|
+
---
|
|
26
|
+
``python -m flextool.update_flextool.generate_canonical``
|
|
27
|
+
Generate every recipe.
|
|
28
|
+
|
|
29
|
+
``python -m flextool.update_flextool.generate_canonical templates_examples``
|
|
30
|
+
Generate just one recipe.
|
|
31
|
+
|
|
32
|
+
``python -m flextool.update_flextool.generate_canonical --verify``
|
|
33
|
+
Regenerate to a temp dir and diff against the committed JSONs; exit
|
|
34
|
+
non-zero with a short summary on drift. Intended for CI.
|
|
35
|
+
|
|
36
|
+
Out of scope (still hand-maintained, follow-up work)
|
|
37
|
+
----------------------------------------------------
|
|
38
|
+
``howto_*.json`` (eight files) and ``templates_time_settings_only.json``
|
|
39
|
+
are not yet covered by a recipe. Adding them requires per-database
|
|
40
|
+
investigation (each carves a different slice of the test universe); they
|
|
41
|
+
will pick up recipes one at a time in follow-up commits.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import json
|
|
48
|
+
import tempfile
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Any
|
|
51
|
+
|
|
52
|
+
import yaml
|
|
53
|
+
|
|
54
|
+
from flextool.update_flextool.db_migration import migrate_database
|
|
55
|
+
from flextool.update_flextool.export_database import export_database
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ``Path(__file__).resolve().parents[1]`` -> ``flextool/`` package root
|
|
59
|
+
# inside the source checkout. Both the recipe file and the destination
|
|
60
|
+
# JSON tree live under ``flextool/schemas/canonical_databases/``.
|
|
61
|
+
RECIPES_FILE = (
|
|
62
|
+
Path(__file__).resolve().parents[1]
|
|
63
|
+
/ "schemas"
|
|
64
|
+
/ "canonical_databases"
|
|
65
|
+
/ "_recipes.yaml"
|
|
66
|
+
)
|
|
67
|
+
OUTPUT_DIR = (
|
|
68
|
+
Path(__file__).resolve().parents[1] / "schemas" / "canonical_databases"
|
|
69
|
+
)
|
|
70
|
+
# Source paths in the recipe (``tests/fixtures/...``) are resolved against
|
|
71
|
+
# the repository root — one level above ``flextool/``.
|
|
72
|
+
SOURCE_ROOT = Path(__file__).resolve().parents[2]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_recipes() -> dict[str, dict[str, Any]]:
|
|
76
|
+
"""Parse ``_recipes.yaml`` and return its top-level dict.
|
|
77
|
+
|
|
78
|
+
Fails loudly if the file is missing — this module is a source-checkout
|
|
79
|
+
maintenance tool, never invoked from a wheel install.
|
|
80
|
+
"""
|
|
81
|
+
if not RECIPES_FILE.is_file():
|
|
82
|
+
raise RuntimeError(f"Recipes file missing: {RECIPES_FILE}")
|
|
83
|
+
with open(RECIPES_FILE) as f:
|
|
84
|
+
return yaml.safe_load(f) or {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _filter_db(
|
|
88
|
+
sqlite_url: str,
|
|
89
|
+
exclude_scenarios: list[str],
|
|
90
|
+
exclude_alternatives: list[str],
|
|
91
|
+
exclude_entities: list[list[str]],
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Apply the recipe's exclusion lists to a staging SQLite.
|
|
94
|
+
|
|
95
|
+
Order matters: scenarios first (cascades scenario_alternatives),
|
|
96
|
+
then alternatives (cascades entity_alternatives + parameter_values
|
|
97
|
+
bound to those alternatives), then entities (cascades multi-dim
|
|
98
|
+
child entities and any remaining parameter_values).
|
|
99
|
+
"""
|
|
100
|
+
# spinedb_api is the canonical handle for filtering Spine DBs — it
|
|
101
|
+
# owns the cascade semantics so we don't have to track FK ordering
|
|
102
|
+
# by hand.
|
|
103
|
+
from spinedb_api import DatabaseMapping
|
|
104
|
+
|
|
105
|
+
if not (exclude_scenarios or exclude_alternatives or exclude_entities):
|
|
106
|
+
# Recipe wants the source verbatim — skip the DatabaseMapping
|
|
107
|
+
# open/commit cycle entirely (spinedb_api raises NothingToCommit
|
|
108
|
+
# if commit_session is called with no pending changes).
|
|
109
|
+
return
|
|
110
|
+
with DatabaseMapping(sqlite_url, create=False, upgrade=False) as db:
|
|
111
|
+
for name in exclude_scenarios:
|
|
112
|
+
db.remove_scenario(name=name)
|
|
113
|
+
for name in exclude_alternatives:
|
|
114
|
+
db.remove_alternative(name=name)
|
|
115
|
+
for cls_name, ent_name in exclude_entities:
|
|
116
|
+
db.remove_entity(entity_class_name=cls_name, name=ent_name)
|
|
117
|
+
db.commit_session("Filtered for canonical export")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def generate_one(recipe_name: str, output_path: Path | None = None) -> Path:
|
|
121
|
+
"""Generate a single recipe's output JSON.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
recipe_name: Top-level key in ``_recipes.yaml``.
|
|
125
|
+
output_path: Override the default ``OUTPUT_DIR / <recipe>.json``
|
|
126
|
+
destination. Used by :func:`verify_all` to stage into a
|
|
127
|
+
temp directory.
|
|
128
|
+
|
|
129
|
+
Returns the path written.
|
|
130
|
+
"""
|
|
131
|
+
recipes = _load_recipes()
|
|
132
|
+
if recipe_name not in recipes:
|
|
133
|
+
raise KeyError(
|
|
134
|
+
f"Unknown recipe {recipe_name!r}. "
|
|
135
|
+
f"Known: {sorted(recipes)}"
|
|
136
|
+
)
|
|
137
|
+
recipe = recipes[recipe_name]
|
|
138
|
+
source_rel = recipe["source"]
|
|
139
|
+
source_abs = (SOURCE_ROOT / source_rel).resolve()
|
|
140
|
+
if not source_abs.is_file():
|
|
141
|
+
raise FileNotFoundError(f"Recipe source missing: {source_abs}")
|
|
142
|
+
|
|
143
|
+
exclude_scenarios = recipe.get("exclude_scenarios") or []
|
|
144
|
+
exclude_alternatives = recipe.get("exclude_alternatives") or []
|
|
145
|
+
exclude_entities = recipe.get("exclude_entities") or []
|
|
146
|
+
|
|
147
|
+
dest = output_path if output_path is not None else (
|
|
148
|
+
OUTPUT_DIR / f"{recipe_name}.json"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# The base64 round-trip lives in ``tests.db_utils`` — imported lazily
|
|
152
|
+
# so the module can still be referenced in environments where the
|
|
153
|
+
# ``tests`` package isn't on ``sys.path`` (this matters only for
|
|
154
|
+
# accidental wheel-install invocation; generate_canonical itself is
|
|
155
|
+
# source-checkout only).
|
|
156
|
+
from tests.db_utils import json_to_db
|
|
157
|
+
|
|
158
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
159
|
+
staging_sqlite = Path(tmp) / f"{recipe_name}.sqlite"
|
|
160
|
+
url = json_to_db(source_abs, staging_sqlite)
|
|
161
|
+
migrate_database(url)
|
|
162
|
+
_filter_db(url, exclude_scenarios, exclude_alternatives, exclude_entities)
|
|
163
|
+
export_database(str(staging_sqlite), str(dest))
|
|
164
|
+
|
|
165
|
+
return dest
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def generate_all() -> list[Path]:
|
|
169
|
+
"""Generate every recipe defined in ``_recipes.yaml``.
|
|
170
|
+
|
|
171
|
+
Returns the list of destination paths written, in YAML declaration
|
|
172
|
+
order.
|
|
173
|
+
"""
|
|
174
|
+
recipes = _load_recipes()
|
|
175
|
+
written: list[Path] = []
|
|
176
|
+
for name in recipes:
|
|
177
|
+
dest = generate_one(name)
|
|
178
|
+
print(f"generated {dest.relative_to(SOURCE_ROOT)}")
|
|
179
|
+
written.append(dest)
|
|
180
|
+
return written
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _summarise_diff(committed: Path, regenerated: Path) -> str:
|
|
184
|
+
"""Short top-level-section summary for ``verify``.
|
|
185
|
+
|
|
186
|
+
Lists every key whose list length differs (or that's missing on one
|
|
187
|
+
side). Value-level drift inside a same-length list is reported as
|
|
188
|
+
``length match, value-level drift``.
|
|
189
|
+
"""
|
|
190
|
+
with open(committed) as f:
|
|
191
|
+
a = json.load(f)
|
|
192
|
+
with open(regenerated) as f:
|
|
193
|
+
b = json.load(f)
|
|
194
|
+
keys = sorted(set(a) | set(b))
|
|
195
|
+
lines: list[str] = []
|
|
196
|
+
for key in keys:
|
|
197
|
+
av = a.get(key)
|
|
198
|
+
bv = b.get(key)
|
|
199
|
+
if av == bv:
|
|
200
|
+
continue
|
|
201
|
+
la = len(av) if isinstance(av, list) else "?"
|
|
202
|
+
lb = len(bv) if isinstance(bv, list) else "?"
|
|
203
|
+
if la == lb:
|
|
204
|
+
lines.append(f" {key}: length match ({la}) — value-level drift")
|
|
205
|
+
else:
|
|
206
|
+
lines.append(f" {key}: committed={la} regenerated={lb}")
|
|
207
|
+
return "\n".join(lines) if lines else " (top-level sections match — check whitespace)"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def verify_all() -> int:
|
|
211
|
+
"""Regenerate every recipe to a temp dir and diff against committed.
|
|
212
|
+
|
|
213
|
+
Returns the count of mismatched recipes (0 on success). Intended for
|
|
214
|
+
CI: surfaces drift between ``tests/fixtures/*.json`` and the
|
|
215
|
+
committed ``canonical_databases/*.json``.
|
|
216
|
+
"""
|
|
217
|
+
recipes = _load_recipes()
|
|
218
|
+
mismatches = 0
|
|
219
|
+
for name in recipes:
|
|
220
|
+
committed = OUTPUT_DIR / f"{name}.json"
|
|
221
|
+
if not committed.is_file():
|
|
222
|
+
print(f"MISSING: {committed}")
|
|
223
|
+
mismatches += 1
|
|
224
|
+
continue
|
|
225
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
226
|
+
regenerated = Path(tmp) / f"{name}.json"
|
|
227
|
+
generate_one(name, output_path=regenerated)
|
|
228
|
+
with open(committed) as f:
|
|
229
|
+
a = f.read()
|
|
230
|
+
with open(regenerated) as f:
|
|
231
|
+
b = f.read()
|
|
232
|
+
if a == b:
|
|
233
|
+
print(f"ok: {name}.json")
|
|
234
|
+
else:
|
|
235
|
+
mismatches += 1
|
|
236
|
+
print(f"DRIFT: {name}.json")
|
|
237
|
+
print(_summarise_diff(committed, regenerated))
|
|
238
|
+
if mismatches:
|
|
239
|
+
print(
|
|
240
|
+
f"\n{mismatches} canonical database(s) out of sync. "
|
|
241
|
+
"Run `python -m flextool.update_flextool.generate_canonical`."
|
|
242
|
+
)
|
|
243
|
+
return mismatches
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def main(argv: list[str] | None = None) -> int:
|
|
247
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"recipe",
|
|
250
|
+
nargs="?",
|
|
251
|
+
default=None,
|
|
252
|
+
help="Recipe name to generate (default: generate all).",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--verify",
|
|
256
|
+
action="store_true",
|
|
257
|
+
help="CI mode: regenerate to a temp dir and diff against committed.",
|
|
258
|
+
)
|
|
259
|
+
args = parser.parse_args(argv)
|
|
260
|
+
|
|
261
|
+
if args.verify:
|
|
262
|
+
if args.recipe is not None:
|
|
263
|
+
parser.error("--verify takes no positional argument")
|
|
264
|
+
return verify_all()
|
|
265
|
+
if args.recipe is None:
|
|
266
|
+
generate_all()
|
|
267
|
+
else:
|
|
268
|
+
dest = generate_one(args.recipe)
|
|
269
|
+
print(f"generated {dest.relative_to(SOURCE_ROOT)}")
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import argparse
|
|
3
|
+
from spinedb_api import import_data, DatabaseMapping
|
|
4
|
+
|
|
5
|
+
from flextool.update_flextool.export_database import keep_serialized_unparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def initialize_database(json_template, database_name="new_database.sqlite"):
|
|
9
|
+
|
|
10
|
+
if not database_name.endswith(".sqlite"):
|
|
11
|
+
print("Give a name with .sqlite file extension")
|
|
12
|
+
exit(-1)
|
|
13
|
+
|
|
14
|
+
with open(json_template) as json_file:
|
|
15
|
+
template = json.load(json_file)
|
|
16
|
+
|
|
17
|
+
with DatabaseMapping('sqlite:///' + database_name, create=True) as new_db:
|
|
18
|
+
# Remove any default alternatives created by spinedb_api (e.g. 'Base')
|
|
19
|
+
# so they don't conflict with the template's own alternatives.
|
|
20
|
+
for alt in new_db.find_alternatives():
|
|
21
|
+
try:
|
|
22
|
+
new_db.remove_alternative(name=alt['name'])
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
# ``keep_serialized_unparse`` handles both author styles:
|
|
26
|
+
# * raw scalars (``"no_method"``, ``None``, ``5``) used by the
|
|
27
|
+
# hand-maintained ``schemas/spinedb_schema.json`` and
|
|
28
|
+
# other static templates.
|
|
29
|
+
# * ``[json_str, type_str]`` pairs emitted by ``export_database``
|
|
30
|
+
# for canonical databases that carry full parameter values.
|
|
31
|
+
(num, log) = import_data(new_db, unparse_value=keep_serialized_unparse, **template)
|
|
32
|
+
print(str(num) + " imports made")
|
|
33
|
+
print("Initialized " + database_name)
|
|
34
|
+
new_db.commit_session("Initialized")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == '__main__':
|
|
38
|
+
parser = argparse.ArgumentParser()
|
|
39
|
+
parser.add_argument('json_filepath',help= "The filepath of the source JSON")
|
|
40
|
+
parser.add_argument('db_filepath',help= "The filepath of the new database")
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
initialize_database(args.json_filepath, args.db_filepath)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Detect how FlexTool is installed and build its self-upgrade command.
|
|
2
|
+
|
|
3
|
+
Two supported install shapes:
|
|
4
|
+
|
|
5
|
+
* an editable **git checkout** (``pip install -e .``) — upgraded with
|
|
6
|
+
``git pull`` followed by an editable reinstall;
|
|
7
|
+
* a **PyPI wheel** install — upgraded with ``pip install --upgrade``.
|
|
8
|
+
|
|
9
|
+
The standalone Tkinter GUI uses this to offer an in-app "Update FlexTool"
|
|
10
|
+
action that works regardless of which shape is in place. The richer Spine
|
|
11
|
+
Toolbox workflow updater (``git restore`` + project-file rebuild) stays in
|
|
12
|
+
:mod:`flextool.update_flextool.self_update`; that behaviour is intentionally
|
|
13
|
+
*not* used from the GUI.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib.metadata as _im
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from urllib.request import Request, urlopen
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def git_checkout_root() -> Path | None:
|
|
31
|
+
"""Return the repo root if FlexTool runs from an editable git checkout.
|
|
32
|
+
|
|
33
|
+
Relies on the imported package's own location: an editable install imports
|
|
34
|
+
``flextool`` straight from the source tree, so its parent directory holds
|
|
35
|
+
both ``.git`` and ``pyproject.toml``. A wheel install imports from
|
|
36
|
+
``site-packages`` and fails this test (returns ``None``).
|
|
37
|
+
"""
|
|
38
|
+
import flextool
|
|
39
|
+
|
|
40
|
+
pkg_dir = Path(flextool.__file__).resolve().parent # …/flextool
|
|
41
|
+
repo_root = pkg_dir.parent
|
|
42
|
+
if (repo_root / ".git").is_dir() and (repo_root / "pyproject.toml").is_file():
|
|
43
|
+
return repo_root
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_git_install() -> bool:
|
|
48
|
+
"""Whether FlexTool runs from an editable git checkout."""
|
|
49
|
+
return git_checkout_root() is not None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def flextool_version() -> str:
|
|
53
|
+
"""Installed FlexTool version, or ``"unknown"`` if it cannot be read."""
|
|
54
|
+
try:
|
|
55
|
+
return _im.version("flextool")
|
|
56
|
+
except _im.PackageNotFoundError:
|
|
57
|
+
return "unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def toolbox_installed() -> bool:
|
|
61
|
+
"""Whether Spine Toolbox (and thus the Spine DB Editor) is installed."""
|
|
62
|
+
try:
|
|
63
|
+
_im.distribution("spinetoolbox")
|
|
64
|
+
return True
|
|
65
|
+
except _im.PackageNotFoundError:
|
|
66
|
+
return shutil.which("spine-db-editor") is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def describe_install() -> str:
|
|
70
|
+
"""One-line, user-facing description of the current install."""
|
|
71
|
+
root = git_checkout_root()
|
|
72
|
+
if root is not None:
|
|
73
|
+
return f"git checkout at {root}\n(version {flextool_version()})"
|
|
74
|
+
return f"PyPI install (version {flextool_version()})"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def upgrade_steps(include_toolbox: bool) -> tuple[list[list[str]], Path | None]:
|
|
78
|
+
"""Build the command(s) that upgrade FlexTool in place.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
include_toolbox: when ``True`` the upgrade targets the ``[toolbox]``
|
|
82
|
+
extra, so Spine Toolbox is installed (or kept) alongside FlexTool.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
``(steps, cwd)`` where *steps* is a list of ``argv`` lists to run in
|
|
86
|
+
order (stop at the first failure) and *cwd* is the directory they run
|
|
87
|
+
in — the repo root for a git checkout, otherwise ``None``.
|
|
88
|
+
"""
|
|
89
|
+
extra = "[toolbox]" if include_toolbox else ""
|
|
90
|
+
py = sys.executable
|
|
91
|
+
root = git_checkout_root()
|
|
92
|
+
if root is not None:
|
|
93
|
+
if _has_upstream(root):
|
|
94
|
+
pull = ["git", "pull", "--ff-only"]
|
|
95
|
+
else:
|
|
96
|
+
# The branch has no upstream tracking ref, so a bare `git pull`
|
|
97
|
+
# fails with "no tracking information"; name the remote and branch
|
|
98
|
+
# explicitly instead.
|
|
99
|
+
remote = _default_remote(root)
|
|
100
|
+
branch = current_branch(root) or "main"
|
|
101
|
+
pull = ["git", "pull", "--ff-only", remote, branch]
|
|
102
|
+
steps = [
|
|
103
|
+
pull,
|
|
104
|
+
[py, "-m", "pip", "install", "--upgrade", "-e", f".{extra}"],
|
|
105
|
+
]
|
|
106
|
+
return steps, root
|
|
107
|
+
steps = [[py, "-m", "pip", "install", "--upgrade", f"flextool{extra}"]]
|
|
108
|
+
return steps, None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _git(root: Path, *args: str, timeout: float = 10.0) -> subprocess.CompletedProcess:
|
|
112
|
+
return subprocess.run(
|
|
113
|
+
["git", *args], cwd=root, timeout=timeout, capture_output=True, text=True
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def current_branch(root: Path) -> str | None:
|
|
118
|
+
"""Current branch name, or ``None`` (detached HEAD / error)."""
|
|
119
|
+
try:
|
|
120
|
+
result = _git(root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
121
|
+
name = result.stdout.strip()
|
|
122
|
+
return name if result.returncode == 0 and name and name != "HEAD" else None
|
|
123
|
+
except Exception:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _has_upstream(root: Path) -> bool:
|
|
128
|
+
"""Whether the current branch has an upstream tracking ref."""
|
|
129
|
+
try:
|
|
130
|
+
return _git(
|
|
131
|
+
root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"
|
|
132
|
+
).returncode == 0
|
|
133
|
+
except Exception:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _default_remote(root: Path) -> str:
|
|
138
|
+
"""Pick a remote to pull from — ``origin`` if present, else the first."""
|
|
139
|
+
try:
|
|
140
|
+
remotes = _git(root, "remote").stdout.split()
|
|
141
|
+
except Exception:
|
|
142
|
+
remotes = []
|
|
143
|
+
if "origin" in remotes:
|
|
144
|
+
return "origin"
|
|
145
|
+
return remotes[0] if remotes else "origin"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def update_available(timeout: float = 5.0) -> bool:
|
|
149
|
+
"""Best-effort check for a newer FlexTool version.
|
|
150
|
+
|
|
151
|
+
For a git checkout this fetches and reports whether the tracked upstream
|
|
152
|
+
branch is ahead; for a PyPI install it compares the installed version with
|
|
153
|
+
the latest on PyPI. Returns ``True`` only on a confident positive — any
|
|
154
|
+
error, offline state, or ambiguity returns ``False`` so the UI never nags
|
|
155
|
+
spuriously. Performs network I/O; call it off the UI thread.
|
|
156
|
+
"""
|
|
157
|
+
root = git_checkout_root()
|
|
158
|
+
if root is not None:
|
|
159
|
+
return _git_behind_upstream(root, timeout)
|
|
160
|
+
return _pypi_has_newer(timeout)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _git_behind_upstream(root: Path, timeout: float) -> bool:
|
|
164
|
+
"""Whether the checkout's tracked upstream has commits we don't have."""
|
|
165
|
+
import os
|
|
166
|
+
|
|
167
|
+
# Never let fetch block on an interactive credential / host-key prompt;
|
|
168
|
+
# it must fail fast and silently if auth is needed.
|
|
169
|
+
env = {
|
|
170
|
+
**os.environ,
|
|
171
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
172
|
+
"GCM_INTERACTIVE": "never",
|
|
173
|
+
"GIT_SSH_COMMAND": "ssh -oBatchMode=yes",
|
|
174
|
+
}
|
|
175
|
+
try:
|
|
176
|
+
subprocess.run(
|
|
177
|
+
["git", "fetch", "--quiet"],
|
|
178
|
+
cwd=root, timeout=timeout, check=True, capture_output=True, env=env,
|
|
179
|
+
)
|
|
180
|
+
# Prefer the configured upstream; fall back to the remote branch of the
|
|
181
|
+
# same name when the branch has no tracking ref set.
|
|
182
|
+
if _has_upstream(root):
|
|
183
|
+
ref = "@{u}"
|
|
184
|
+
else:
|
|
185
|
+
branch = current_branch(root)
|
|
186
|
+
if branch is None:
|
|
187
|
+
return False
|
|
188
|
+
ref = f"{_default_remote(root)}/{branch}"
|
|
189
|
+
result = subprocess.run(
|
|
190
|
+
["git", "rev-list", "--count", f"HEAD..{ref}"],
|
|
191
|
+
cwd=root, timeout=timeout, check=True, capture_output=True, text=True,
|
|
192
|
+
)
|
|
193
|
+
return int(result.stdout.strip() or "0") > 0
|
|
194
|
+
except Exception:
|
|
195
|
+
# No upstream, no git, no network, auth needed, detached HEAD, … —
|
|
196
|
+
# don't nag.
|
|
197
|
+
logger.debug("git update check inconclusive for %s", root, exc_info=True)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _pypi_latest_version(timeout: float) -> str | None:
|
|
202
|
+
"""Latest FlexTool version string on PyPI, or ``None`` on any failure."""
|
|
203
|
+
request = Request(
|
|
204
|
+
"https://pypi.org/pypi/flextool/json",
|
|
205
|
+
headers={"User-Agent": "flextool-gui"},
|
|
206
|
+
)
|
|
207
|
+
try:
|
|
208
|
+
with urlopen(request, timeout=timeout) as response:
|
|
209
|
+
return json.load(response)["info"]["version"]
|
|
210
|
+
except Exception:
|
|
211
|
+
logger.debug("PyPI update check failed", exc_info=True)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _pypi_has_newer(timeout: float) -> bool:
|
|
216
|
+
latest = _pypi_latest_version(timeout)
|
|
217
|
+
if latest is None:
|
|
218
|
+
return False
|
|
219
|
+
try:
|
|
220
|
+
from packaging.version import Version
|
|
221
|
+
|
|
222
|
+
return Version(latest) > Version(flextool_version())
|
|
223
|
+
except Exception:
|
|
224
|
+
logger.debug("Version comparison failed (latest=%s)", latest, exc_info=True)
|
|
225
|
+
return False
|