Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: flexmeasures show beliefs #379

Merged
merged 25 commits into from Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2dc13e6
first version of flexmeasures show beliefs
nhoening Feb 28, 2022
5d02ed5
add types lib for mypy
nhoening Feb 28, 2022
25a4721
construct title at the end when we have all info updated
nhoening Feb 28, 2022
01ac3de
update changelogs
nhoening Feb 28, 2022
22f1412
Merge branch 'main' into cli-show-beliefs
nhoening Feb 28, 2022
482919f
add -belief-time-before option
nhoening Feb 28, 2022
833d4a4
add simple tests for all show data commands
nhoening Feb 28, 2022
01e5c50
flexmeasures/conftest.py
nhoening Feb 28, 2022
1ab9605
verifying that CLI tests do not run on GithubActions
nhoening Feb 28, 2022
3b8fa00
move back to ignoring CLI commands on Github Actions
nhoening Feb 28, 2022
fd15bb0
small doc improvement
nhoening Mar 1, 2022
844724d
Merge branch 'main' into cli-show-beliefs
nhoening Mar 7, 2022
c1a0188
Merge branch 'main' into cli-show-beliefs
nhoening Mar 7, 2022
94c5bec
better handling of DEBUG var
nhoening Mar 7, 2022
a57971f
improve docstring of two fixtures
nhoening Mar 7, 2022
7c4d4b2
if sensor share the same unit, still show it on the y-axis ; Also int…
nhoening Mar 7, 2022
cedd21f
Use AwareDateTimeField for --start
nhoening Mar 7, 2022
47d8b6a
also use schemas for --duration and --belief-time
nhoening Mar 7, 2022
2d46d47
use SensorIdField schema for --sensor-id
nhoening Mar 7, 2022
63f54a9
use schemas in show commands for account and asset
nhoening Mar 7, 2022
ccdcae1
simplify possible traces we need to show with one_deterministic_belie…
nhoening Mar 7, 2022
1e1f82c
use source.description in title
nhoening Mar 7, 2022
c26f6ee
improve in-code documentation
nhoening Mar 8, 2022
49f0150
resample data from different sensors to minimal resolution if possibl…
nhoening Mar 8, 2022
84d0926
simpler way of making sure data is resample to minimal resolution
nhoening Mar 8, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/run_mypy.sh
@@ -1,7 +1,7 @@
#!/bin/bash
set -e
pip install --upgrade 'mypy>=0.902'
pip install types-pytz types-requests types-Flask types-click types-redis types-tzlocal types-python-dateutil types-setuptools
pip install types-pytz types-requests types-Flask types-click types-redis types-tzlocal types-python-dateutil types-setuptools types-tabulate
# We are checking python files which have type hints, and leave out bigger issues we made issues for
# * data/scripts: We'll remove legacy code: https://trello.com/c/1wEnHOkK/7-remove-custom-data-scripts
# * data/models and data/services: https://trello.com/c/rGxZ9h2H/540-makequery-call-signature-is-incoherent
Expand Down
4 changes: 2 additions & 2 deletions documentation/changelog.rst
Expand Up @@ -13,7 +13,8 @@ New features
* Add CLI option to pass a data unit when reading in time series data from CSV, so data can automatically be converted to the sensor unit [see `PR #341 <http://www.github.com/FlexMeasures/flexmeasures/pull/341>`_]
* Add CLI option to specify custom strings that should be interpreted as NaN values when reading in time series data from CSV [see `PR #357 <http://www.github.com/FlexMeasures/flexmeasures/pull/357>`_]
* Add CLI commands ``flexmeasures add sensor``, ``flexmeasures add asset-type``, ``flexmeasures add beliefs`` (which were experimental features before) [see `PR #337 <http://www.github.com/FlexMeasures/flexmeasures/pull/337>`_]
* Add CLI commands for showing data [see `PR #339 <http://www.github.com/FlexMeasures/flexmeasures/pull/339>`_]
* Add CLI commands for showing organisational structure [see `PR #339 <http://www.github.com/FlexMeasures/flexmeasures/pull/339>`_]
* Add a CLI command for showing time series data [see `PR #379 <http://www.github.com/FlexMeasures/flexmeasures/pull/379>`_]
* Add CLI command for attaching annotations to assets: ``flexmeasures add holidays`` adds public holidays [see `PR #343 <http://www.github.com/FlexMeasures/flexmeasures/pull/343>`_]
* Add CLI command for resampling existing sensor data to new resolution [see `PR #360 <http://www.github.com/FlexMeasures/flexmeasures/pull/360>`_]
* Add CLI command to edit/add an attribute on an asset or sensor. [see `PR #380 <http://www.github.com/FlexMeasures/flexmeasures/pull/380>`_]
Expand All @@ -27,7 +28,6 @@ Bugfixes
Infrastructure / Support
----------------------
* Plugins can import common FlexMeasures classes (like ``Asset`` and ``Sensor``) from a central place, using ``from flexmeasures import Asset, Sensor`` [see `PR #354 <http://www.github.com/FlexMeasures/flexmeasures/pull/354>`_]

* Adapt CLI command for entering some initial structure (``flexmeasures add structure``) to new datamodel [see `PR #349 <http://www.github.com/FlexMeasures/flexmeasures/pull/349>`_]
* Align documentation requirements with pip-tools [see `PR #384 <http://www.github.com/FlexMeasures/flexmeasures/pull/384>`_]

Expand Down
5 changes: 3 additions & 2 deletions documentation/cli/change_log.rst
Expand Up @@ -7,8 +7,9 @@ FlexMeasures CLI Changelog
since v0.9.0 | January 26, 2022
=====================

* Add CLI commands for showing data ``flexmeasures show accounts``, ``flexmeasures show account``, ``flexmeasures show roles``, ``flexmeasures show asset-types``, ``flexmeasures show asset`` and ``flexmeasures show data-sources``.
* Add ``flexmeasures edit resample-data`` CLI command to resample sensor data to a different resolution.

* Add CLI commands for showing data ``flexmeasures show accounts``, ``flexmeasures show account``, ``flexmeasures show roles``, ``flexmeasures show asset-types``, ``flexmeasures show asset``, ``flexmeasures show data-sources``, and ``flexmeasures show beliefs``.
* Add ``flexmeasures db-ops resample-data`` CLI command to resample sensor data to a different resolution.
* Add ``flexmeasures edit attribute`` CLI command to edit/add an attribute on an asset or sensor.
* Add ``flexmeasures add toy-account`` for tutorials and trying things.
* Add ``flexmeasures add schedule`` to create a new schedule for a given power sensor.
Expand Down
1 change: 1 addition & 0 deletions documentation/cli/commands.rst
Expand Up @@ -49,6 +49,7 @@ of which some are referred to in this documentation.
``flexmeasures show asset`` Show an asset and its sensors.
``flexmeasures show roles`` List available account- and user roles.
``flexmeasures show data-sources`` List available data sources.
``flexmeasures show beliefs`` Plot time series data.
================================================= =======================================


Expand Down
133 changes: 115 additions & 18 deletions flexmeasures/cli/data_show.py
@@ -1,15 +1,24 @@
"""CLI Tasks for listing database contents - most useful in development"""

from typing import Optional, List
from datetime import datetime, timedelta

import click
from flask import current_app as app
from flask.cli import with_appcontext
from tabulate import tabulate
from humanize import naturaldelta, naturaltime
import uniplot

from flexmeasures.data.models.user import Account, AccountRole, User, Role
from flexmeasures.data.models.data_sources import DataSource
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.models.time_series import Sensor, TimedBelief
from flexmeasures.data.schemas.generic_assets import GenericAssetIdField
from flexmeasures.data.schemas.sensors import SensorIdField
from flexmeasures.data.schemas.account import AccountIdField
from flexmeasures.data.schemas.sources import DataSourceIdField
from flexmeasures.data.schemas.times import AwareDateTimeField, DurationField


@click.group("show")
Expand Down Expand Up @@ -72,16 +81,11 @@ def list_roles():

@fm_show_data.command("account")
@with_appcontext
@click.option("--id", "account_id", type=int, required=True)
def show_account(account_id):
@click.option("--id", "account", type=AccountIdField(), required=True)
def show_account(account):
"""
Show information about an account, including users and assets.
"""
account: Account = Account.query.get(account_id)
if not account:
click.echo(f"No account with id {account_id} known.")
raise click.Abort

click.echo(f"========{len(account.name) * '='}==========")
click.echo(f"Account {account.name} (ID:{account.id}):")
click.echo(f"========{len(account.name) * '='}==========\n")
Expand All @@ -94,7 +98,7 @@ def show_account(account_id):
click.echo("Account has no roles.")
click.echo()

users = User.query.filter_by(account_id=account_id).order_by(User.username).all()
users = User.query.filter_by(account_id=account.id).order_by(User.username).all()
if not users:
click.echo("No users in account ...")
else:
Expand All @@ -115,7 +119,7 @@ def show_account(account_id):

click.echo()
assets = (
GenericAsset.query.filter_by(account_id=account_id)
GenericAsset.query.filter_by(account_id=account.id)
.order_by(GenericAsset.name)
.all()
)
Expand Down Expand Up @@ -150,16 +154,11 @@ def list_asset_types():

@fm_show_data.command("asset")
@with_appcontext
@click.option("--id", "asset_id", type=int, required=True)
def show_generic_asset(asset_id):
@click.option("--id", "asset", type=GenericAssetIdField(), required=True)
def show_generic_asset(asset):
"""
Show asset info and list sensors
"""
asset = GenericAsset.query.get(asset_id)
if not asset:
click.echo(f"No asset with id {asset_id} known.")
raise click.Abort

click.echo(f"======{len(asset.name) * '='}==========")
click.echo(f"Asset {asset.name} (ID:{asset.id}):")
click.echo(f"======{len(asset.name) * '='}==========\n")
Expand All @@ -175,7 +174,7 @@ def show_generic_asset(asset_id):

click.echo()
sensors = (
Sensor.query.filter_by(generic_asset_id=asset_id).order_by(Sensor.name).all()
Sensor.query.filter_by(generic_asset_id=asset.id).order_by(Sensor.name).all()
)
if not sensors:
click.echo("No sensors in asset ...")
Expand Down Expand Up @@ -218,4 +217,102 @@ def list_data_sources():
)


@fm_show_data.command("beliefs")
@with_appcontext
@click.option(
"--sensor-id",
"sensors",
required=True,
multiple=True,
type=SensorIdField(),
help="ID of sensor(s). This argument can be given multiple times.",
)
@click.option(
"--from",
"start",
type=AwareDateTimeField(),
required=True,
help="Plot starting at this datetime. Follow up with a timezone-aware datetime in ISO 6801 format.",
)
@click.option(
"--duration",
"duration",
type=DurationField(),
required=True,
help="Duration of the plot, after --from. Follow up with a duration in ISO 6801 format, e.g. PT1H (1 hour) or PT45M (45 minutes).",
)
@click.option(
"--belief-time-before",
"belief_time_before",
type=AwareDateTimeField(),
required=False,
help="Time at which beliefs had been known. Follow up with a timezone-aware datetime in ISO 6801 format.",
)
@click.option(
"--source-id",
"source",
required=False,
type=DataSourceIdField(),
help="Source of the beliefs (an existing source id).",
)
def plot_beliefs(
sensors: List[Sensor],
start: datetime,
duration: timedelta,
belief_time_before: Optional[datetime],
source: Optional[DataSource],
):
"""
Show a simple plot of belief data directly in the terminal.
"""
# query data
beliefs_by_sensor = TimedBelief.search(
sensors=list(sensors),
event_starts_after=start,
event_ends_before=start + duration,
beliefs_before=belief_time_before,
source=source,
nhoening marked this conversation as resolved.
Show resolved Hide resolved
one_deterministic_belief_per_event=True,
sum_multiple=False,
)
# only keep non-empty
beliefs_by_sensor = {
sensor_name: beliefs
for (sensor_name, beliefs) in beliefs_by_sensor.items()
if not beliefs.empty
}
if len(beliefs_by_sensor.keys()) == 0:
nhoening marked this conversation as resolved.
Show resolved Hide resolved
click.echo("No data found!")
raise click.Abort()
first_df = beliefs_by_sensor[sensors[0].name]

# Build title
if len(sensors) == 1:
title = f"Beliefs for Sensor '{sensors[0].name}' (Id {sensors[0].id}).\n"
else:
title = f"Beliefs for Sensor(s) [{','.join([s.name for s in sensors])}], (Id(s): [{','.join([str(s.id) for s in sensors])}]).\n"
title += f"Data spans {naturaldelta(duration)} and starts at {start}."
if belief_time_before:
title += f"\nOnly beliefs made before: {belief_time_before}."
if source:
title += f"\nSource: {source.description}"
if len(beliefs_by_sensor) == 1:
title += f"\nThe time resolution (x-axis) is {naturaldelta(first_df.sensor.event_resolution)}."
nhoening marked this conversation as resolved.
Show resolved Hide resolved

uniplot.plot(
nhoening marked this conversation as resolved.
Show resolved Hide resolved
[
beliefs.event_value
for beliefs in [beliefs_by_sensor[sn] for sn in [s.name for s in sensors]]
],
title=title,
color=True,
lines=True,
y_unit=first_df.sensor.unit
if len(beliefs_by_sensor) == 1
or all(sensor.unit == first_df.sensor.unit for sensor in sensors)
else "",
legend_labels=[s.name for s in sensors],
)


app.cli.add_command(fm_show_data)
112 changes: 112 additions & 0 deletions flexmeasures/cli/tests/test_data_show.py
@@ -0,0 +1,112 @@
import pytest

from flexmeasures.data.models.time_series import Sensor


@pytest.mark.skip_github
def test_list_accounts(app, fresh_db, setup_accounts_fresh_db):
from flexmeasures.cli.data_show import list_accounts

runner = app.test_cli_runner()
result = runner.invoke(list_accounts)

assert "All accounts on this" in result.output
for account in setup_accounts_fresh_db.values():
assert account.name in result.output
assert result.exit_code == 0


@pytest.mark.skip_github
def test_list_roles(app, fresh_db, setup_roles_users_fresh_db):
from flexmeasures.cli.data_show import list_roles

runner = app.test_cli_runner()
result = runner.invoke(list_roles)

assert "Account roles" in result.output
assert "User roles" in result.output
for role in ("account-admin", "Supplier", "Dummy"):
assert role in result.output
assert result.exit_code == 0


@pytest.mark.skip_github
def test_list_asset_types(app, fresh_db, setup_generic_asset_types_fresh_db):
from flexmeasures.cli.data_show import list_asset_types

runner = app.test_cli_runner()
result = runner.invoke(list_asset_types)

for asset_type in setup_generic_asset_types_fresh_db.values():
assert asset_type.name in result.output
assert result.exit_code == 0


@pytest.mark.skip_github
def test_list_sources(app, fresh_db, setup_sources_fresh_db):
from flexmeasures.cli.data_show import list_data_sources

runner = app.test_cli_runner()
result = runner.invoke(list_data_sources)

for source in setup_sources_fresh_db.values():
assert source.name in result.output
assert result.exit_code == 0


@pytest.mark.skip_github
def test_show_accounts(app, fresh_db, setup_accounts_fresh_db):
from flexmeasures.cli.data_show import show_account

fresh_db.session.flush() # get IDs in DB

runner = app.test_cli_runner()
result = runner.invoke(
show_account, ["--id", setup_accounts_fresh_db["Prosumer"].id]
)

assert "Account Test Prosumer Account" in result.output
assert "No users in account" in result.output
assert result.exit_code == 0


@pytest.mark.skip_github
def test_show_asset(app, fresh_db, setup_generic_assets_fresh_db):
from flexmeasures.cli.data_show import show_generic_asset

fresh_db.session.flush() # get IDs in DB

runner = app.test_cli_runner()
result = runner.invoke(
show_generic_asset,
["--id", setup_generic_assets_fresh_db["test_wind_turbine"].id],
)

assert "Asset Test wind turbine" in result.output
assert "No sensors in asset" in result.output
assert result.exit_code == 0


@pytest.mark.skip_github
def test_plot_beliefs(app, fresh_db, setup_beliefs_fresh_db):
from flexmeasures.cli.data_show import plot_beliefs

sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()

runner = app.test_cli_runner()
result = runner.invoke(
plot_beliefs,
[
"--sensor-id",
sensor.id,
"--from",
"2021-03-28T16:00+01",
"--duration",
"PT1H",
],
)

assert "Beliefs for Sensor 'epex_da'" in result.output
assert "Data spans an hour" in result.output

assert result.exit_code == 0
32 changes: 31 additions & 1 deletion flexmeasures/conftest.py
Expand Up @@ -247,6 +247,15 @@ def create_test_markets(db) -> Dict[str, Market]:

@pytest.fixture(scope="module")
def setup_sources(db) -> Dict[str, DataSource]:
return create_sources(db)


@pytest.fixture(scope="function")
def setup_sources_fresh_db(fresh_db) -> Dict[str, DataSource]:
return create_sources(fresh_db)


def create_sources(db) -> Dict[str, DataSource]:
seita_source = DataSource(name="Seita", type="demo script")
db.session.add(seita_source)
entsoe_source = DataSource(name="ENTSO-E", type="demo script")
Expand Down Expand Up @@ -417,7 +426,28 @@ def setup_assets(


@pytest.fixture(scope="module")
def setup_beliefs(db: SQLAlchemy, setup_markets, setup_sources) -> int:
def setup_beliefs(db, setup_markets, setup_sources) -> int:
"""
Make some beliefs.

:returns: the number of beliefs set up
"""
return create_beliefs(db, setup_markets, setup_sources)


@pytest.fixture(scope="function")
def setup_beliefs_fresh_db(
fresh_db, setup_markets_fresh_db, setup_sources_fresh_db
) -> int:
"""
Make some beliefs.

:returns: the number of beliefs set up
"""
return create_beliefs(fresh_db, setup_markets_fresh_db, setup_sources_fresh_db)


def create_beliefs(db: SQLAlchemy, setup_markets, setup_sources) -> int:
"""
:returns: the number of beliefs set up
"""
Expand Down