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

feat: search filter on sensor API #1191

Merged
merged 30 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4dc4e04
feat: search filter on sensor API
joshuaunity Sep 20, 2024
4ab4263
Merge branch 'main' into feat-sensors-filters
joshuaunity Sep 24, 2024
92c58a0
feat: sensor API pagintion and search
joshuaunity Sep 24, 2024
658b525
refactor: reduce code repitition and code size
joshuaunity Sep 24, 2024
2114eca
refactor: request changes
joshuaunity Sep 25, 2024
1930365
feat: api to fetch all sensors under an asset
joshuaunity Sep 25, 2024
ecd3966
feat: added filter feature for sensor table, both name and unit filte…
joshuaunity Sep 25, 2024
2a37dde
feat: unit filter features on get sensors API
joshuaunity Sep 25, 2024
f8ebe9b
refactor: asset sensor and sensors API changes
joshuaunity Sep 25, 2024
5d9b547
feat: test case for asset_sensors API
joshuaunity Sep 25, 2024
e93b1fb
chore: few changes variable namings and import arrangement
joshuaunity Sep 26, 2024
d80f1ba
refactor: updated testcase with more checks and removed redundant code
joshuaunity Sep 26, 2024
461f1d2
chore: little changes on test(waiting for comment resolution)
joshuaunity Sep 26, 2024
dc3e8df
chore: moved formatResolution func to base.html
joshuaunity Sep 27, 2024
807f0d0
refactor: changes to asset sensors API and ISO formatter logic on for…
joshuaunity Sep 27, 2024
e92a8dd
chore: removed hard coded asset ID in asset sensors test case
joshuaunity Sep 27, 2024
8652c23
feat: test to get all sensors
joshuaunity Sep 30, 2024
ec4938a
Merge branch 'main' into feat-sensors-filters
nhoening Sep 30, 2024
dbd300b
fix: fix wrong imports
joshuaunity Sep 30, 2024
b08901d
refactor: expandd test cases for get sensors
joshuaunity Sep 30, 2024
651006a
refactor: expanded test case
joshuaunity Oct 1, 2024
5c7f797
chore: added to changelog
joshuaunity Oct 2, 2024
e420fa1
chore: updated change log
joshuaunity Oct 3, 2024
646de9c
refactor: dynamic expected results for test case
joshuaunity Oct 3, 2024
f4f5bf6
refactor: dynamic sensor name for test case
joshuaunity Oct 3, 2024
c5fedaf
Merge branch 'main' into feat-sensors-filters
joshuaunity Oct 4, 2024
4f987b2
chore: simplified testcase
joshuaunity Oct 8, 2024
289e78b
Merge branch 'main' into feat-sensors-filters
joshuaunity Oct 8, 2024
e8d4ad8
Merge branch 'main' into feat-sensors-filters
joshuaunity Oct 11, 2024
fe5b6fa
Merge branch 'main' into feat-sensors-filters
nhoening Oct 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ New features
-------------
* New chart type on sensor page: histogram [see `PR #1143 <https://github.com/FlexMeasures/flexmeasures/pull/1143>`_]
* Add basic sensor info to sensor page [see `PR #1115 <https://github.com/FlexMeasures/flexmeasures/pull/1115>`_]
* Added new API to get all sensors under an asset [see `PR #1191 <https://github.com/FlexMeasures/flexmeasures/pull/1191>`_ ]
joshuaunity marked this conversation as resolved.
Show resolved Hide resolved
* Add `Statistics` table on the sensor page and also add `api/v3_0/sensors/<id>/stats` endpoint to get sensor statistics [see `PR #1116 <https://github.com/FlexMeasures/flexmeasures/pull/1116>`_]
* Support adding custom titles to the graphs on the asset page, by extending the ``sensors_to_show`` format [see `PR #1125 <https://github.com/FlexMeasures/flexmeasures/pull/1125>`_ and `PR #1177 <https://github.com/FlexMeasures/flexmeasures/pull/1177>`_]
* Support zoom-in action on the asset and sensor charts [see `PR #1130 <https://github.com/FlexMeasures/flexmeasures/pull/1130>`_]
Expand Down
18 changes: 1 addition & 17 deletions flexmeasures/api/common/schemas/generic_assets.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from __future__ import annotations

from shlex import join, split

from flask import abort
from marshmallow import fields, ValidationError
from marshmallow import fields
from sqlalchemy import select

from flexmeasures.data import db
Expand All @@ -25,17 +23,3 @@ def _deserialize(self, asset_id: int, attr, obj, **kwargs) -> GenericAsset:

def _serialize(self, asset: GenericAsset, attr, data, **kwargs) -> int:
return asset.id


class SearchFilterField(fields.Str):
"""Field that represents a search filter."""

def _deserialize(self, value, attr, data, **kwargs) -> list[str]:
try:
search_terms = split(value)
except ValueError as e:
raise ValidationError(str(e))
return search_terms

def _serialize(self, value: list[str], attr, obj, **kwargs) -> str:
return join(value)
19 changes: 19 additions & 0 deletions flexmeasures/api/common/schemas/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from shlex import join, split

from marshmallow import fields, ValidationError


class SearchFilterField(fields.Str):
"""Field that represents a search filter."""

def _deserialize(self, value, attr, data, **kwargs) -> list[str]:
try:
search_terms = split(value)
except ValueError as e:
raise ValidationError(str(e))
return search_terms

def _serialize(self, value: list[str], attr, obj, **kwargs) -> str:
return join(value)
15 changes: 14 additions & 1 deletion flexmeasures/api/common/schemas/sensors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from flask import abort
from marshmallow import fields
from marshmallow import fields, ValidationError
from sqlalchemy import select

from flexmeasures.data import db
Expand All @@ -9,6 +9,7 @@
EntityAddressException,
)
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.utils.unit_utils import is_valid_unit


class EntityAddressValidationError(FMValidationError):
Expand Down Expand Up @@ -80,3 +81,15 @@ def _serialize(self, value: Sensor, attr, data, **kwargs):
return value.entity_address_fm0
else:
return value.entity_address


class UnitField(fields.Str):
"""Field that represents a unit."""

def _deserialize(self, value, attr, data, **kwargs) -> str:
if not is_valid_unit(value):
raise ValidationError(f"Invalid unit: {value}")
return value

def _serialize(self, value: str, attr, obj, **kwargs) -> str:
return value
2 changes: 1 addition & 1 deletion flexmeasures/api/v3_0/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from flexmeasures.data.services.accounts import get_accounts, get_audit_log_records
from flexmeasures.api.common.schemas.users import AccountIdField
from flexmeasures.data.schemas.account import AccountSchema
from flexmeasures.api.common.schemas.generic_assets import SearchFilterField
from flexmeasures.api.common.schemas.search import SearchFilterField
from flexmeasures.utils.time_utils import server_now

"""
Expand Down
122 changes: 114 additions & 8 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from humanize import naturaldelta

from flask import current_app
from flask_classful import FlaskView, route
Expand All @@ -22,19 +23,21 @@
from flexmeasures.data.queries.generic_assets import query_assets_by_search_terms
from flexmeasures.data.schemas import AwareDateTimeField
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema as AssetSchema
from flexmeasures.api.common.schemas.generic_assets import (
AssetIdField,
SearchFilterField,
)
from flexmeasures.api.common.schemas.generic_assets import AssetIdField
from flexmeasures.api.common.schemas.search import SearchFilterField
from flexmeasures.api.common.schemas.users import AccountIdField
from flexmeasures.utils.coding_utils import flatten_unique
from flexmeasures.ui.utils.view_utils import set_session_variables
from flexmeasures.auth.policy import check_access
from werkzeug.exceptions import Forbidden, Unauthorized
from flexmeasures.data.schemas.sensors import SensorSchema
from flexmeasures.data.models.time_series import Sensor


asset_schema = AssetSchema()
assets_schema = AssetSchema(many=True)
sensor_schema = SensorSchema()
sensors_schema = SensorSchema(many=True)
partial_asset_schema = AssetSchema(partial=True, exclude=["account_id"])


Expand Down Expand Up @@ -160,10 +163,6 @@ def index(
if all_accessible:
filter_statement = filter_statement | GenericAsset.account_id.is_(None)

num_records = db.session.scalar(
select(func.count(GenericAsset.id)).where(filter_statement)
)

query = query_assets_by_search_terms(
search_terms=filter, filter_statement=filter_statement
)
Expand All @@ -176,6 +175,9 @@ def index(
select_pagination: SelectPagination = db.paginate(
query, per_page=per_page, page=page
)
num_records = db.session.scalar(
select(func.count(GenericAsset.id)).filter(filter_statement)
)
response = {
"data": asset_schema.dump(select_pagination.items, many=True),
"num-records": num_records,
Expand All @@ -184,6 +186,110 @@ def index(

return response, 200

@route(
"/<id>/sensors",
methods=["GET"],
)
@use_kwargs(
{
"asset": AssetIdField(data_key="id"),
},
location="path",
)
@use_kwargs(
{
"page": fields.Int(
required=False, validate=validate.Range(min=1), default=1
joshuaunity marked this conversation as resolved.
Show resolved Hide resolved
),
"per_page": fields.Int(
required=False, validate=validate.Range(min=1), default=10
),
},
location="query",
)
@as_json
def asset_sensors(
self,
id: int,
asset: GenericAsset | None,
page: int | None = None,
per_page: int | None = None,
):
"""
List all sensors under an asset.

.. :quickref: Asset; Return all sensors under an asset.

This endpoint returns all sensors under an asset.

The endpoint supports pagination of the asset list using the `page` and `per_page` query parameters.

- If the `page` parameter is not provided, all sensors are returned, without pagination information. The result will be a list of sensors.
- If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).
The response schema for pagination is inspired by https://datatables.net/manual/server-side#Returned-data


**Example response**

An example of one asset being returned in a paginated response:

.. sourcecode:: json

{
"data" : [
{
"id": 1,
"name": "Test battery",
"latitude": 10,
"longitude": 100,
"account_id": 2,
"generic_asset_type": {"id": 1, "name": "battery"}
}
],
"num-records" : 1,
"filtered-records" : 1

}

If no pagination is requested, the response only consists of the list under the "data" key.

:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
query_statement = Sensor.generic_asset_id == asset.id

query = select(Sensor).filter(query_statement)

select_pagination: SelectPagination = db.paginate(
query, per_page=per_page, page=page
)

num_records = db.session.scalar(
select(func.count(Sensor.id)).where(query_statement)
)

sensors_response: list = [
{
**sensor_schema.dump(sensor),
"event_resolution": naturaldelta(sensor.event_resolution),
}
for sensor in select_pagination.items
]

response = {
"data": sensors_response,
"num-records": num_records,
"filtered-records": select_pagination.total,
}

return response, 200

@route("/public", methods=["GET"])
@as_json
def public(self):
Expand Down
Loading
Loading