From 0a6f4d335b006d1432a2140b7d52f25abb51074c Mon Sep 17 00:00:00 2001 From: Mohamed Belhsan Hmida Date: Thu, 6 Aug 2026 00:13:03 +0100 Subject: [PATCH 1/2] cli: show a data source's account and the sensors it wrote to Signed-off-by: Mohamed Belhsan Hmida --- documentation/changelog.rst | 1 + documentation/cli/change_log.rst | 1 + flexmeasures/cli/data_show.py | 52 +++++++++++++- flexmeasures/cli/tests/test_data_show.py | 89 ++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index a734982fe4..eb162549ea 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -15,6 +15,7 @@ v1.0.0 | July XX, 2026 New features ------------- +* ``flexmeasures show data-sources`` now shows which organisation a data source belongs to, and can list the sensors holding data recorded by a given source [see `PR #XXXX `_] * New ``inflexible-consumption`` and ``inflexible-production`` flex-context fields make explicit how the sign of each inflexible device's power data should be read (positive values denote consumption resp. production), accepting sensor references with optional source filters; they replace the now-deprecated ``inflexible-device-sensors`` field (bare sensor IDs, sign read from each sensor's ``consumption_is_positive`` attribute), which remains supported [see `PR #2358 `_] * An inflexible (unschedulable) device can be modelled as its own asset by giving its flex-model entry a single ``inflexible-consumption`` or ``inflexible-production`` sensor reference; such a device joins a ``group`` like any other member, so its fixed (measured) load counts towards the group's intermediate power constraint [see `PR #2374 `_] * The API is now rate-limited, with a generous default limit on all endpoints and a stricter limit on triggering schedules and forecasts (which only counts triggers we accepted). Limits are configurable, and can be set per organisation by putting its account on a plan, which hosts create with ``flexmeasures add plan`` and platform admins assign from the organisation's page in the UI; play servers are exempt, as they run simulations. See :ref:`plans-and-rate-limiting` [see `PR #2306 `_] diff --git a/documentation/cli/change_log.rst b/documentation/cli/change_log.rst index d7505eb447..1173a8f4fb 100644 --- a/documentation/cli/change_log.rst +++ b/documentation/cli/change_log.rst @@ -12,6 +12,7 @@ since v1.0.0 | July XX, 2026 * Add ``flexmeasures add plan``, ``flexmeasures show plans`` and ``flexmeasures edit plan``, to manage the rate limits and quotas which apply to the accounts on a plan. * Add ``flexmeasures edit secret`` to store an encrypted secret on an account or asset. * Add ``flexmeasures delete secret`` to remove an encrypted secret from an account or asset. +* ``flexmeasures show data-sources`` now shows the account a data source belongs to, and lists the sensors holding data recorded by a single source with ``--show-sensors``. since v0.33.0 | June 01, 2026 ================================= diff --git a/flexmeasures/cli/data_show.py b/flexmeasures/cli/data_show.py index 7a76bd9e6a..695ac09770 100644 --- a/flexmeasures/cli/data_show.py +++ b/flexmeasures/cli/data_show.py @@ -421,10 +421,26 @@ def _format_sensor_plot(plot: dict) -> str: help="Whether to show the attributes of the DataSource.", is_flag=True, ) -def list_data_sources(source: DataSource | None = None, show_attributes: bool = False): +@click.option( + "--show-sensors", + "show_sensors", + type=bool, + help="Whether to list the sensors which hold data recorded by the DataSource. Requires --id.", + is_flag=True, +) +def list_data_sources( + source: DataSource | None = None, + show_attributes: bool = False, + show_sensors: bool = False, +): """ Show available data sources """ + if show_sensors and source is None: + # Looking up the sensors of a source queries the (potentially huge) timed_belief table, + # so we only do it for the single source the user asked about. + raise click.UsageError("--show-sensors requires --id.") + if source is None: sources = db.session.scalars( select(DataSource) @@ -440,7 +456,7 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool = click.secho("No data sources created yet.", **MsgStyle.WARN) raise click.Abort() - headers = ["ID", "Name", "User ID", "Model", "Version"] + headers = ["ID", "Name", "Account ID", "User ID", "Model", "Version"] if show_attributes: headers.append("Attributes") @@ -451,6 +467,7 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool = row = [ source.id, source.name, + source.account_id, source.user_id, source.model, source.version, @@ -469,6 +486,37 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool = click.echo(tabulate(row, headers=headers)) click.echo("\n") + if show_sensors: + # At this point we know there is exactly one source, as --show-sensors requires --id. + _list_sensors_of_source(sources[0]) + + +def _list_sensors_of_source(source: DataSource): + """List the sensors which hold data recorded by the given data source.""" + sensors = sorted(source.sensors, key=lambda sensor: sensor.id) + if not sensors: + click.secho( + f"No sensors hold data recorded by data source {source.id}.", + **MsgStyle.WARN, + ) + return + + click.echo(f"Sensors with data from data source {source.id}:\n") + click.echo( + tabulate( + [ + [ + sensor.id, + sensor.name, + sensor.unit, + f"{sensor.generic_asset.name} (ID: {sensor.generic_asset.id})", + ] + for sensor in sensors + ], + headers=["ID", "Name", "Unit", "Asset"], + ) + ) + @fm_show_data.command("chart") @with_appcontext diff --git a/flexmeasures/cli/tests/test_data_show.py b/flexmeasures/cli/tests/test_data_show.py index 017aafc000..fb53590ec5 100644 --- a/flexmeasures/cli/tests/test_data_show.py +++ b/flexmeasures/cli/tests/test_data_show.py @@ -1,5 +1,6 @@ import os import pytest +from sqlalchemy import select from flexmeasures.cli.tests.utils import ( check_command_ran_without_error, @@ -93,6 +94,94 @@ def test_list_sources(app, fresh_db, setup_sources_fresh_db): check_command_ran_without_error(result) +def test_list_sources_shows_account(app, fresh_db, setup_accounts_fresh_db): + """The account a source belongs to is what tells apart otherwise identical sources.""" + from flexmeasures.cli.data_show import list_data_sources + from flexmeasures.data.models.data_sources import DataSource + + account = setup_accounts_fresh_db["Prosumer"] + fresh_db.session.add( + DataSource(name="Ada", type="demo script", account_id=account.id) + ) + fresh_db.session.commit() + + runner = app.test_cli_runner() + result = runner.invoke(list_data_sources) + + check_command_ran_without_error(result) + assert "Account ID" in result.output + assert str(account.id) in result.output + + +def test_list_source_sensors(app, fresh_db, setup_dummy_data): + """A source which recorded beliefs on two sensors lists both, with their asset.""" + from flexmeasures.cli.data_show import list_data_sources + from flexmeasures.data.models.data_sources import DataSource + + source = fresh_db.session.execute( + select(DataSource).filter_by(name="source1") + ).scalar_one() + + runner = app.test_cli_runner() + result = runner.invoke( + list_data_sources, ["--id", str(source.id), "--show-sensors"] + ) + + check_command_ran_without_error(result) + assert f"Sensors with data from data source {source.id}" in result.output + for sensor_name in ("sensor 1", "sensor 2"): + assert sensor_name in result.output + # The sensors' asset is shown, and sensors without data from this source are not listed + assert "DummyGenericAsset" in result.output + assert "report sensor" not in result.output + + +def test_list_source_sensors_without_any_data(app, fresh_db, setup_sources_fresh_db): + """A source which recorded no beliefs at all says so, rather than showing an empty table.""" + from flexmeasures.cli.data_show import list_data_sources + + fresh_db.session.commit() # get IDs in DB + source = setup_sources_fresh_db["Seita"] + + runner = app.test_cli_runner() + result = runner.invoke( + list_data_sources, ["--id", str(source.id), "--show-sensors"] + ) + + check_command_ran_without_error(result) + assert f"No sensors hold data recorded by data source {source.id}" in result.output + + +def test_list_source_sensors_requires_a_single_source(app, fresh_db): + """Looking up sensors scans the timed_belief table, so it is not allowed for a full listing.""" + from flexmeasures.cli.data_show import list_data_sources + + runner = app.test_cli_runner() + result = runner.invoke(list_data_sources, ["--show-sensors"]) + + assert result.exit_code != 0 + assert "--show-sensors requires --id" in result.output + + +def test_list_sources_with_deleted_user_and_account(app, fresh_db): + """The user and account columns have no DB-level FK, so a source can outlive what they point to.""" + from flexmeasures.cli.data_show import list_data_sources + from flexmeasures.data.models.data_sources import DataSource + + orphaned_source = DataSource(name="Orphan", type="demo script") + orphaned_source.user_id = 999999 + orphaned_source.account_id = 999999 + fresh_db.session.add(orphaned_source) + fresh_db.session.commit() + + runner = app.test_cli_runner() + result = runner.invoke(list_data_sources, ["--id", str(orphaned_source.id)]) + + check_command_ran_without_error(result) + assert "Orphan" in result.output + assert "999999" in result.output + + def test_show_accounts(app, fresh_db, setup_accounts_fresh_db): from flexmeasures.cli.data_show import show_account From 559f6bc3dc8acde71573c7fd012e2418dfc77ae2 Mon Sep 17 00:00:00 2001 From: Mohamed Belhsan Hmida Date: Thu, 6 Aug 2026 00:18:29 +0100 Subject: [PATCH 2/2] docs: add PR number in changelog entry Signed-off-by: Mohamed Belhsan Hmida --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index eb162549ea..eba77d899c 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -15,7 +15,7 @@ v1.0.0 | July XX, 2026 New features ------------- -* ``flexmeasures show data-sources`` now shows which organisation a data source belongs to, and can list the sensors holding data recorded by a given source [see `PR #XXXX `_] +* ``flexmeasures show data-sources`` now shows which organisation a data source belongs to, and can list the sensors holding data recorded by a given source [see `PR #2401 `_] * New ``inflexible-consumption`` and ``inflexible-production`` flex-context fields make explicit how the sign of each inflexible device's power data should be read (positive values denote consumption resp. production), accepting sensor references with optional source filters; they replace the now-deprecated ``inflexible-device-sensors`` field (bare sensor IDs, sign read from each sensor's ``consumption_is_positive`` attribute), which remains supported [see `PR #2358 `_] * An inflexible (unschedulable) device can be modelled as its own asset by giving its flex-model entry a single ``inflexible-consumption`` or ``inflexible-production`` sensor reference; such a device joins a ``group`` like any other member, so its fixed (measured) load counts towards the group's intermediate power constraint [see `PR #2374 `_] * The API is now rate-limited, with a generous default limit on all endpoints and a stricter limit on triggering schedules and forecasts (which only counts triggers we accepted). Limits are configurable, and can be set per organisation by putting its account on a plan, which hosts create with ``flexmeasures add plan`` and platform admins assign from the organisation's page in the UI; play servers are exempt, as they run simulations. See :ref:`plans-and-rate-limiting` [see `PR #2306 `_]