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

433 post sensor #767

Merged
merged 22 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa465c8
feat(sensors): adds fetch_one sensor endpoint to API
Jul 5, 2023
415f861
feat(sensors): adds post sensor to API
Jul 5, 2023
4e3f6c1
post sensor still needs work
Jul 18, 2023
714b1a0
feat(sensor): adds post sensor
Jul 20, 2023
058cb41
docs(sensor): changes the docstring of the post function
Jul 20, 2023
5691405
clearer names for the arguments to permission_required_for_context de…
nhoening Jul 20, 2023
ed53575
one more renaming
nhoening Jul 20, 2023
2376148
expanding possibilities in the require_permission_for_context decorat…
nhoening Jul 20, 2023
957c144
feat(sensor): post sensor without schema changes
Jul 24, 2023
3e37e08
feat(sensor): adds patch sensor
Jul 25, 2023
8b3dee8
feat(sensor): users services change import back
Jul 25, 2023
1e364ad
docs(sensor): remove prints and update times docstrings
Jul 25, 2023
e8d5c39
docs(sensor): update changelogs
Jul 25, 2023
8f0a511
docs(sensor): update change_log date
Jul 25, 2023
a997dbe
feat(sensor): changes to duration and event_resolution (untested)
Jul 25, 2023
3c467d4
feat(cli): adds support for both int and iso duration string for sens…
Jul 28, 2023
9f87d30
feat(sensor): changes times duration and sensor schema
Jul 28, 2023
0402fbe
feat(sensor): tests for unauthorized
Jul 31, 2023
e49a94f
feat(sensor): tests for unauthorized fetch one
Jul 31, 2023
a044ddd
feat(sensor): resolve merge conflicts
Jul 31, 2023
c8d475e
Merge branch 'main' into 433-post-sensor
GustaafL Jul 31, 2023
af9915c
feat(sensor): adds docstrings, changes test function names, changelog…
Aug 1, 2023
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
5 changes: 5 additions & 0 deletions .vscode/spellright.dict
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,8 @@ dataframe
dataframes
args
docstrings
Auth
ctx_loader
ctx_arg_name
ctx_arg_pos
dataset
2 changes: 1 addition & 1 deletion documentation/dev/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ You, as the endpoint author, need to make sure this is checked. Here is an examp
{"the_resource": ResourceIdField(data_key="resource_id")},
location="path",
)
@permission_required_for_context("read", arg_name="the_resource")
@permission_required_for_context("read", ctx_arg_name="the_resource")
@as_json
def view(resource_id: int, resource: Resource):
return dict(name=resource.name)
Expand Down
10 changes: 5 additions & 5 deletions flexmeasures/api/dev/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class SensorAPI(FlaskView):
},
location="query",
)
@permission_required_for_context("read", arg_name="sensor")
@permission_required_for_context("read", ctx_arg_name="sensor")
def get_chart(self, id: int, sensor: Sensor, **kwargs):
"""GET from /sensor/<id>/chart

Expand Down Expand Up @@ -85,7 +85,7 @@ def get_chart(self, id: int, sensor: Sensor, **kwargs):
},
location="query",
)
@permission_required_for_context("read", arg_name="sensor")
@permission_required_for_context("read", ctx_arg_name="sensor")
def get_chart_data(self, id: int, sensor: Sensor, **kwargs):
"""GET from /sensor/<id>/chart_data

Expand Down Expand Up @@ -118,7 +118,7 @@ def get_chart_data(self, id: int, sensor: Sensor, **kwargs):
},
location="query",
)
@permission_required_for_context("read", arg_name="sensor")
@permission_required_for_context("read", ctx_arg_name="sensor")
def get_chart_annotations(self, id: int, sensor: Sensor, **kwargs):
"""GET from /sensor/<id>/chart_annotations

Expand Down Expand Up @@ -147,7 +147,7 @@ def get_chart_annotations(self, id: int, sensor: Sensor, **kwargs):
{"sensor": SensorIdField(data_key="id")},
location="path",
)
@permission_required_for_context("read", arg_name="sensor")
@permission_required_for_context("read", ctx_arg_name="sensor")
def get(self, id: int, sensor: Sensor):
"""GET from /sensor/<id>

Expand All @@ -170,7 +170,7 @@ class AssetAPI(FlaskView):
{"asset": AssetIdField(data_key="id")},
location="path",
)
@permission_required_for_context("read", arg_name="asset")
@permission_required_for_context("read", ctx_arg_name="asset")
def get(self, id: int, asset: GenericAsset):
"""GET from /asset/<id>

Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/api/v3_0/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def index(self):

@route("/<id>", methods=["GET"])
@use_kwargs({"account": AccountIdField(data_key="id")}, location="path")
@permission_required_for_context("read", arg_name="account")
@permission_required_for_context("read", ctx_arg_name="account")
@as_json
def get(self, id: int, account: Account):
"""API endpoint to get an account.
Expand Down
14 changes: 7 additions & 7 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class AssetAPI(FlaskView):
},
location="query",
)
@permission_required_for_context("read", arg_name="account")
@permission_required_for_context("read", ctx_arg_name="account")
@as_json
def index(self, account: Account):
"""List all assets owned by a certain account.
Expand Down Expand Up @@ -103,7 +103,7 @@ def public(self):

@route("", methods=["POST"])
@permission_required_for_context(
"create-children", arg_loader=AccountIdField.load_current
"create-children", ctx_loader=AccountIdField.load_current
)
@use_args(asset_schema)
def post(self, asset_data: dict):
Expand Down Expand Up @@ -144,7 +144,7 @@ def post(self, asset_data: dict):

@route("/<id>", methods=["GET"])
@use_kwargs({"asset": AssetIdField(data_key="id")}, location="path")
@permission_required_for_context("read", arg_name="asset")
@permission_required_for_context("read", ctx_arg_name="asset")
@as_json
def fetch_one(self, id, asset):
"""Fetch a given asset.
Expand Down Expand Up @@ -180,7 +180,7 @@ def fetch_one(self, id, asset):
@route("/<id>", methods=["PATCH"])
@use_args(partial_asset_schema)
@use_kwargs({"db_asset": AssetIdField(data_key="id")}, location="path")
@permission_required_for_context("update", arg_name="db_asset")
@permission_required_for_context("update", ctx_arg_name="db_asset")
@as_json
def patch(self, asset_data: dict, id: int, db_asset: GenericAsset):
"""Update an asset given its identifier.
Expand Down Expand Up @@ -236,7 +236,7 @@ def patch(self, asset_data: dict, id: int, db_asset: GenericAsset):

@route("/<id>", methods=["DELETE"])
@use_kwargs({"asset": AssetIdField(data_key="id")}, location="path")
@permission_required_for_context("delete", arg_name="asset")
@permission_required_for_context("delete", ctx_arg_name="asset")
@as_json
def delete(self, id: int, asset: GenericAsset):
"""Delete an asset given its identifier.
Expand Down Expand Up @@ -278,7 +278,7 @@ def delete(self, id: int, asset: GenericAsset):
},
location="query",
)
@permission_required_for_context("read", arg_name="asset")
@permission_required_for_context("read", ctx_arg_name="asset")
def get_chart(self, id: int, asset: GenericAsset, **kwargs):
"""GET from /assets/<id>/chart

Expand All @@ -302,7 +302,7 @@ def get_chart(self, id: int, asset: GenericAsset, **kwargs):
},
location="query",
)
@permission_required_for_context("read", arg_name="asset")
@permission_required_for_context("read", ctx_arg_name="asset")
def get_chart_data(self, id: int, asset: GenericAsset, **kwargs):
"""GET from /assets/<id>/chart_data

Expand Down
93 changes: 92 additions & 1 deletion flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from flexmeasures.auth.decorators import permission_required_for_context
from flexmeasures.data import db
from flexmeasures.data.models.user import Account
from flexmeasures.data.models.generic_assets import GenericAsset
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.queries.utils import simplify_index
from flexmeasures.data.schemas.sensors import SensorSchema, SensorIdField
Expand All @@ -46,6 +47,7 @@
get_sensor_schema = GetSensorDataSchema()
post_sensor_schema = PostSensorDataSchema()
sensors_schema = SensorSchema(many=True)
sensor_schema = SensorSchema()


class SensorAPI(FlaskView):
Expand All @@ -63,7 +65,7 @@ class SensorAPI(FlaskView):
},
location="query",
)
@permission_required_for_context("read", arg_name="account")
@permission_required_for_context("read", ctx_arg_name="account")
@as_json
def index(self, account: Account):
"""API endpoint to list all sensors of an account.
Expand Down Expand Up @@ -494,3 +496,92 @@ def get_schedule(self, sensor: Sensor, job_id: str, duration: timedelta, **kwarg

d, s = request_processed()
return dict(**response, **d), s

@route("/<id>", methods=["GET"])
@use_kwargs({"sensor": SensorIdField(data_key="id")}, location="path")
@permission_required_for_context("read", ctx_arg_name="sensor")
@as_json
def fetch_one(self, id, sensor):
"""Fetch a given sensor.

.. :quickref: Sensor; Get a sensor

This endpoint gets a sensor.

**Example response**

.. sourcecode:: json

{
"name": "some gas sensor",
"unit": "m³/h",
"entity_address": "ea1.2023-08.localhost:fm1.1",
"event_resolution": 10,
"generic_asset_id": 4,
"timezone": "UTC",
}

:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 200: PROCESSED
:status 400: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""

sensor.resolution = sensor.event_resolution
sensor_dict = sensor_schema.dump(sensor)
del sensor_dict["event_resolution"]

return sensor_dict, 200

@route("", methods=["POST"])
@use_args(sensor_schema)
@permission_required_for_context(
"create-children",
ctx_arg_pos=1,
ctx_arg_name="generic_asset_id",
ctx_loader=GenericAsset,
pass_ctx_to_loader=True,
)
def post(self, sensor_data: dict):
"""Create new asset.

.. :quickref: Sensor; Create a new Sensor

This endpoint creates a new Sensor.

**Example request**

.. sourcecode:: json

{
"name": "power",
"resolution": "PT1H",
GustaafL marked this conversation as resolved.
Show resolved Hide resolved
"unit": "kWh",
"generic_asset_id": 1,
}


The newly posted sensor is returned in the response.

:reqheader Authorization: The authentication token
:reqheader Content-Type: application/json
:resheader Content-Type: application/json
:status 201: CREATED
:status 400: INVALID_REQUEST
:status 401: UNAUTHORIZED
:status 403: INVALID_SENDER
:status 422: UNPROCESSABLE_ENTITY
"""
print(sensor_data)
sensor_data["event_resolution"] = sensor_data.pop("resolution")
sensor = Sensor(**sensor_data)
db.session.add(sensor)
db.session.commit()
sensor.resolution = sensor.event_resolution
GustaafL marked this conversation as resolved.
Show resolved Hide resolved
sensor_json = sensor_schema.dump(sensor)
del sensor_json["event_resolution"]
return sensor_json, 201
55 changes: 55 additions & 0 deletions flexmeasures/api/v3_0/tests/test_sensors_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations


from flask import url_for


from flexmeasures import Sensor
from flexmeasures.api.tests.utils import get_auth_token
from flexmeasures.api.v3_0.tests.utils import get_sensor_post_data
from flexmeasures.data.schemas.sensors import SensorSchema

sensor_schema = SensorSchema()


def test_fetch_one_sensor(
client,
setup_api_test_data: dict[str, Sensor],
):
sensor_id = 1
headers = make_headers_for("[email protected]", client)
response = client.get(
url_for("SensorAPI:fetch_one", id=sensor_id),
headers=headers,
)
assert response.status_code == 200
assert response.json["name"] == "some gas sensor"
assert response.json["unit"] == "m³/h"
assert response.json["generic_asset_id"] == 4
assert response.json["timezone"] == "UTC"
assert response.json["resolution"] == "PT10M"


def make_headers_for(user_email: str | None, client) -> dict:
headers = {"content-type": "application/json"}
if user_email:
headers["Authorization"] = get_auth_token(client, user_email, "testtest")
return headers


def test_post_a_sensor(client, setup_api_test_data):
GustaafL marked this conversation as resolved.
Show resolved Hide resolved
auth_token = get_auth_token(client, "[email protected]", "testtest")
post_data = get_sensor_post_data()
post_sensor_response = client.post(
url_for("SensorAPI:post"),
json=post_data,
headers={"content-type": "application/json", "Authorization": auth_token},
)
print("Server responded with:\n%s" % post_sensor_response.json)
assert post_sensor_response.status_code == 201
assert post_sensor_response.json["name"] == "power"
assert post_sensor_response.json["resolution"] == "PT1H"

sensor: Sensor = Sensor.query.filter_by(name="power").one_or_none()
assert sensor is not None
assert sensor.unit == "kWh"
10 changes: 10 additions & 0 deletions flexmeasures/api/v3_0/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ def get_asset_post_data(account_id: int = 1, asset_type_id: int = 1) -> dict:
return post_data


def get_sensor_post_data(generic_asset_id: int = 1) -> dict:
post_data = {
"name": "power",
"resolution": "PT1H",
"unit": "kWh",
"generic_asset_id": generic_asset_id,
}
return post_data


def message_for_trigger_schedule(
unknown_prices: bool = False,
with_targets: bool = False,
Expand Down
8 changes: 4 additions & 4 deletions flexmeasures/api/v3_0/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class UserAPI(FlaskView):
},
location="query",
)
@permission_required_for_context("read", arg_name="account")
@permission_required_for_context("read", ctx_arg_name="account")
@as_json
def index(self, account: Account, include_inactive: bool = False):
"""API endpoint to list all users of an account.
Expand Down Expand Up @@ -90,7 +90,7 @@ def index(self, account: Account, include_inactive: bool = False):

@route("/<id>")
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("read", arg_name="user")
@permission_required_for_context("read", ctx_arg_name="user")
@as_json
def get(self, id: int, user: UserModel):
"""API endpoint to get a user.
Expand Down Expand Up @@ -128,7 +128,7 @@ def get(self, id: int, user: UserModel):
@route("/<id>", methods=["PATCH"])
@use_kwargs(partial_user_schema)
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("update", arg_name="user")
@permission_required_for_context("update", ctx_arg_name="user")
@as_json
def patch(self, id: int, user: UserModel, **user_data):
"""API endpoint to patch user data.
Expand Down Expand Up @@ -204,7 +204,7 @@ def patch(self, id: int, user: UserModel, **user_data):

@route("/<id>/password-reset", methods=["PATCH"])
@use_kwargs({"user": UserIdField(data_key="id")}, location="path")
@permission_required_for_context("update", arg_name="user")
@permission_required_for_context("update", ctx_arg_name="user")
@as_json
def reset_user_password(self, id: int, user: UserModel):
"""API endpoint to reset the user's current password, cookies and auth tokens, and to email a password reset link to the user.
Expand Down
Loading
Loading