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

Add endpoint for getting asset annotations #1113

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
88 changes: 76 additions & 12 deletions flexmeasures/api/dev/sensors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

import json
import warnings
from typing import Any

from flask_classful import FlaskView, route
from flask_security import current_user
Expand All @@ -20,6 +23,10 @@
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.services.annotations import prepare_annotations_for_chart
from flexmeasures.ui.utils.view_utils import set_session_variables
from flexmeasures.data.models.annotations import (
SensorAnnotationRelationship,
GenericAssetAnnotationRelationship,
)


class SensorAPI(FlaskView):
Expand Down Expand Up @@ -132,20 +139,13 @@ def get_chart_annotations(self, id: int, sensor: Sensor, **kwargs):

Annotations for use in charts (in case you have the chart specs already).
"""
event_starts_after = kwargs.get("event_starts_after", None)
event_ends_before = kwargs.get("event_ends_before", None)
df = sensor.generic_asset.search_annotations(
annotations_after=event_starts_after,
annotations_before=event_ends_before,
as_frame=True,
df = get_annotations_data(
sensor_id=id,
asset_or_sensor=sensor,
relationship_module=SensorAnnotationRelationship,
kwargs=kwargs,
)

# Wrap and stack annotations
df = prepare_annotations_for_chart(df)

# Return JSON records
df = df.reset_index()
df["source"] = df["source"].astype(str)
return df.to_json(orient="records")

@route("/<id>", strict_slashes=False)
Expand Down Expand Up @@ -186,6 +186,70 @@ def get(self, id: int, asset: GenericAsset):
attributes = ["name", "timezone", "timerange_of_sensors_to_show"]
return {attr: getattr(asset, attr) for attr in attributes}

@route("/<id>/chart_annotations", strict_slashes=False)
@use_kwargs(
{"asset": AssetIdField(data_key="id")},
location="path",
)
@use_kwargs(
{
"event_starts_after": AwareDateTimeField(format="iso", required=False),
"event_ends_before": AwareDateTimeField(format="iso", required=False),
"beliefs_after": AwareDateTimeField(format="iso", required=False),
"beliefs_before": AwareDateTimeField(format="iso", required=False),
},
location="query",
)
@permission_required_for_context("read", ctx_arg_name="asset")
def get_chart_annotations(self, id: int, asset: GenericAsset, **kwargs):
"""GET from /asset/<id>/chart_annotations

.. :quickref: Chart; Download annotations for use in charts

Annotations for use in charts (in case you have the chart specs already).
"""
df = get_annotations_data(
sensor_id=None,
asset_or_sensor=asset,
relationship_module=GenericAssetAnnotationRelationship,
kwargs=kwargs,
)

return df.to_json(orient="records")


def get_annotations_data(
sensor_id: int | None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter seems redundant, since we are already passing the sensor or asset instance itself.

asset_or_sensor: GenericAsset | Sensor,
relationship_module: Any,
kwargs,
):
"""
This function fetches a sensor or an asset annotations data
"""
event_starts_after = kwargs.get("event_starts_after", None)
event_ends_before = kwargs.get("event_ends_before", None)
if asset_or_sensor is GenericAsset:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always False, because this compares an Asset or Sensor instance (in asset_or_sensor) to an Asset class.

asset_or_sensor_class = asset_or_sensor.generic_asset
else:
asset_or_sensor_class = asset_or_sensor
Comment on lines +232 to +235
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if asset_or_sensor is GenericAsset:
asset_or_sensor_class = asset_or_sensor.generic_asset
else:
asset_or_sensor_class = asset_or_sensor
asset_or_sensor_class = asset_or_sensor.__class__


df = asset_or_sensor_class.search_annotations(
Copy link
Contributor

@Flix6x Flix6x Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the class, actually? Isn't search_annotations a method we can call on the instance rather than the class? Its function signature does suggest so, because its first parameter is self rather than cls.

Suggested change
df = asset_or_sensor_class.search_annotations(
df = asset_or_sensor.search_annotations(

annotations_after=event_starts_after,
annotations_before=event_ends_before,
as_frame=True,
sensor_id=sensor_id,
relationship_module=relationship_module,
)

# Wrap and stack annotations
df = prepare_annotations_for_chart(df)

# Return JSON records
df = df.reset_index()
df["source"] = df["source"].astype(str)
return df


def get_sensor_or_abort(id: int) -> Sensor:
"""
Expand Down
11 changes: 9 additions & 2 deletions flexmeasures/data/models/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from timely_beliefs import BeliefsDataFrame, utils as tb_utils

from flexmeasures.data import db
from flexmeasures.data.models.annotations import Annotation, to_annotation_frame
from flexmeasures.data.models.annotations import (
Annotation,
to_annotation_frame,
GenericAssetAnnotationRelationship,
)
from flexmeasures.data.models.charts import chart_type_to_chart_specs
from flexmeasures.data.models.data_sources import DataSource
from flexmeasures.data.models.parsing_utils import parse_source_arg
Expand Down Expand Up @@ -355,6 +359,8 @@ def search_annotations(
annotation_type: str = None,
include_account_annotations: bool = False,
as_frame: bool = False,
sensor_id: int = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be named asset_id (or asset_or_sensor_id)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think we need it. See my comment about using calling this method on the instance rather than the class.

relationship_module: Any = GenericAssetAnnotationRelationship,
) -> list[Annotation] | pd.DataFrame:
"""Return annotations assigned to this asset, and optionally, also those assigned to the asset's account.

Expand All @@ -366,11 +372,12 @@ def search_annotations(
parsed_sources = parse_source_arg(source)
annotations = db.session.scalars(
query_asset_annotations(
asset_id=self.id,
asset_or_sensor_id=self.id if sensor_id is None else sensor_id,
annotations_after=annotations_after,
annotations_before=annotations_before,
sources=parsed_sources,
annotation_type=annotation_type,
relationship_module=relationship_module,
)
).all()
if include_account_annotations and self.owner is not None:
Expand Down
21 changes: 14 additions & 7 deletions flexmeasures/data/queries/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,33 @@
from flexmeasures.data.models.annotations import (
Annotation,
GenericAssetAnnotationRelationship,
SensorAnnotationRelationship,
)
from flexmeasures.data.models.data_sources import DataSource


def query_asset_annotations(
asset_id: int,
asset_or_sensor_id: int,
relationship_module,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing type annotation, possibly an Enum with the 3 relationship choices we currently have (with Accounts, GenericAssets and Sensors).

annotations_after: datetime | None = None,
annotations_before: datetime | None = None,
sources: list[DataSource] | None = None,
annotation_type: str | None = None,
) -> Query:
"""Match annotations assigned to the given asset."""
query = (
select(Annotation)
.join(GenericAssetAnnotationRelationship)
.filter(
GenericAssetAnnotationRelationship.generic_asset_id == asset_id,
query = select(Annotation)
if relationship_module is GenericAssetAnnotationRelationship:
Ahmad-Wahid marked this conversation as resolved.
Show resolved Hide resolved
query = query.join(
GenericAssetAnnotationRelationship,
GenericAssetAnnotationRelationship.annotation_id == Annotation.id,
).filter(
GenericAssetAnnotationRelationship.generic_asset_id == asset_or_sensor_id
)
)
else:
query = query.join(
SensorAnnotationRelationship,
SensorAnnotationRelationship.annotation_id == Annotation.id,
).filter(SensorAnnotationRelationship.sensor_id == asset_or_sensor_id)

if annotations_after is not None:
query = query.filter(
Expand Down
12 changes: 6 additions & 6 deletions flexmeasures/ui/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@
async function embedAndLoad(chartSpecsPath, elementId, datasetName, previousResult, startDate, endDate) {
var combineLegend = 'true';

await vegaEmbed('#'+elementId, chartSpecsPath + 'dataset_name=' + datasetName + '&combine_legend='+ combineLegend + '&width=container&include_sensor_annotations=false&include_asset_annotations=false&chart_type=' + chartType, {{ chart_options | safe }})
await vegaEmbed('#'+elementId, chartSpecsPath + 'dataset_name=' + datasetName + '&combine_legend='+ combineLegend + '&width=container&include_sensor_annotations=true&include_asset_annotations=true&chart_type=' + chartType, {{ chart_options | safe }})
.then(function (result) {

// Create a custom menu item for exporting to CSV
Expand Down Expand Up @@ -445,15 +445,15 @@
})
.then(function(response) { return response.json(); }),

/**

// Fetch annotations
fetch(dataPath + '/chart_annotations?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate, {
fetch(dataDevPath + '/chart_annotations?event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate, {
method: "GET",
headers: {"Content-Type": "application/json"},
signal: signal,
})
.then(function(response) { return response.json(); }),
*/


// Embed chart
embedAndLoad(chartSpecsPath + 'event_starts_after=' + queryStartDate + '&event_ends_before=' + queryEndDate + '&', elementId, datasetName, previousResult, startDate, endDate),
Expand All @@ -462,9 +462,9 @@
vegaView.change(datasetName, vega.changeset().remove(vega.truthy).insert(result[0])).resize().run();
previousResult = result[0];
checkSourceMasking(previousResult);
/**

vegaView.change(datasetName + '_annotations', vega.changeset().remove(vega.truthy).insert(result[1])).resize().run();
*/

}).catch(console.error);
});

Expand Down
Loading