Skip to content

Commit

Permalink
Asset crud API (pull request #103)
Browse files Browse the repository at this point in the history
Approved-by: Felix Claessen
  • Loading branch information
nhoening committed Dec 7, 2020
1 parent 3c6e1d5 commit b976ed8
Show file tree
Hide file tree
Showing 54 changed files with 1,988 additions and 870 deletions.
4 changes: 3 additions & 1 deletion bvp/api/__init__.py
Expand Up @@ -70,7 +70,7 @@ def get_versions() -> dict:
"message": "For these API versions a public endpoint is available, listing its service. For example: "
"/api/v1/getService and /api/v1_1/getService. An authentication token can be requested at: "
"/api/requestAuthToken",
"versions": ["v1", "v1_1", "v1_2", "v1_3"],
"versions": ["v1", "v1_1", "v1_2", "v1_3", "v2_0"],
}
return response

Expand Down Expand Up @@ -101,8 +101,10 @@ def register_at(app: Flask):
from bvp.api.v1_1 import register_at as v1_1_register_at
from bvp.api.v1_2 import register_at as v1_2_register_at
from bvp.api.v1_3 import register_at as v1_3_register_at
from bvp.api.v2_0 import register_at as v2_0_register_at

v1_register_at(app)
v1_1_register_at(app)
v1_2_register_at(app)
v1_3_register_at(app)
v2_0_register_at(app)
2 changes: 1 addition & 1 deletion bvp/api/common/responses.py
Expand Up @@ -41,7 +41,7 @@ def already_received_and_successfully_processed(message: str) -> ResponseTuple:


@BaseMessage("Some of the required information is missing from the request.")
def required_info_missing(fields: Sequence[str], message: str) -> ResponseTuple:
def required_info_missing(fields: Sequence[str], message: str = "") -> ResponseTuple:
return (
dict(
results="Rejected",
Expand Down
2 changes: 1 addition & 1 deletion bvp/api/common/utils/api_utils.py
Expand Up @@ -17,7 +17,7 @@
from bvp.api.common.responses import unrecognized_sensor


def check_access(service_listing, service_name):
def list_access(service_listing, service_name):
"""
For a given USEF service name (API endpoint) in a service listing,
return the list of USEF roles that are allowed to access the service.
Expand Down
8 changes: 6 additions & 2 deletions bvp/api/common/utils/validators.py
Expand Up @@ -833,15 +833,19 @@ def wrapper(fn):
@as_json
def decorated_service(*args, **kwargs):
perm = Permission(*[RoleNeed(role) for role in usef_roles])
if current_user.has_role("anonymous"):
if current_user.has_role(
"anonymous"
): # TODO: this role needs to go, we should not mix permissive and restrictive roles
current_app.logger.warning(
"Anonymous user is not accepted for this service"
)
return invalid_sender("anonymous user", "non-anonymous user")
elif perm.can() or current_user.has_role("admin"):
return fn(*args, **kwargs)
else:
current_app.logger.warning("User role is not accepted for this service")
current_app.logger.warning(
"User does not have necessary authorization for this service"
)
return invalid_sender(
[role.name for role in current_user.roles], *usef_roles
)
Expand Down
26 changes: 26 additions & 0 deletions bvp/api/tests/utils.py
Expand Up @@ -2,6 +2,9 @@

from flask import url_for, current_app

from bvp.data.config import db
from bvp.data.services.users import find_user_by_email

"""
Useful things for API testing
"""
Expand All @@ -24,6 +27,29 @@ def get_auth_token(client, user_email, password):
return auth_response.json["auth_token"]


class UserContext(object):
"""
Context manager for a temporary user instance from the DB,
which is expunged from the session at Exit.
Expunging is useful, so that the API call being tested still operates on
a "fresh" session.
While the context is alive, you can collect any useful information, like
the user's assets:
with UserContext("test_prosumer@seita.nl") as prosumer:
assets = prosumer.assets
"""

def __init__(self, user_email: str):
self.the_user = find_user_by_email(user_email)

def __enter__(self):
return self.the_user

def __exit__(self, type, value, traceback):
db.session.expunge(self.the_user)


def get_task_run(client, task_name: str, token=None):
"""Utility for getting task run information"""
headers = {"Authorization": token}
Expand Down
8 changes: 5 additions & 3 deletions bvp/api/v1/implementations.py
Expand Up @@ -12,7 +12,7 @@
from bvp.data.models.assets import Asset, Power
from bvp.data.services.resources import get_assets
from bvp.data.services.forecasting import create_forecasting_jobs
from bvp.data.utils import save_to_database
from bvp.data.utils import save_to_session
from bvp.api.common.responses import (
already_received_and_successfully_processed,
invalid_domain,
Expand Down Expand Up @@ -324,21 +324,23 @@ def create_connection_and_value_groups( # noqa: C901

current_app.logger.info("SAVING TO DB AND QUEUEING...")
try:
save_to_database(power_measurements)
save_to_session(power_measurements)
db.session.flush()
[current_app.queues["forecasting"].enqueue_job(job) for job in forecasting_jobs]
db.session.commit()
return request_processed()
except IntegrityError as e:
current_app.logger.warning(e)
db.session.rollback()

# Allow meter data to be replaced only in play mode
if current_app.config.get("BVP_MODE", "") == "play":
save_to_database(power_measurements, overwrite=True)
save_to_session(power_measurements, overwrite=True)
[
current_app.queues["forecasting"].enqueue_job(job)
for job in forecasting_jobs
]
db.session.commit()
return request_processed()
else:
return already_received_and_successfully_processed()
6 changes: 3 additions & 3 deletions bvp/api/v1/routes.py
@@ -1,6 +1,6 @@
from flask_security import auth_token_required

from bvp.api.common.utils.api_utils import check_access
from bvp.api.common.utils.api_utils import list_access
from bvp.api.common.utils.decorators import as_response_type
from bvp.api.common.utils.validators import usef_roles_accepted
from bvp.api.v1 import bvp_api as bvp_api_v1, implementations as v1_implementations
Expand All @@ -27,7 +27,7 @@
@bvp_api_v1.route("/getMeterData", methods=["GET", "POST"])
@as_response_type("GetMeterDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_service_listing, "getMeterData"))
@usef_roles_accepted(*list_access(v1_service_listing, "getMeterData"))
def get_meter_data():
"""API endpoint to get meter data.
Expand Down Expand Up @@ -91,7 +91,7 @@ def get_meter_data():
@bvp_api_v1.route("/postMeterData", methods=["POST"])
@as_response_type("PostMeterDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_service_listing, "postMeterData"))
@usef_roles_accepted(*list_access(v1_service_listing, "postMeterData"))
def post_meter_data():
"""API endpoint to post meter data.
Expand Down
16 changes: 10 additions & 6 deletions bvp/api/v1_1/implementations.py
Expand Up @@ -38,7 +38,7 @@
from bvp.data.config import db
from bvp.data.models.markets import Market, Price
from bvp.data.models.weather import Weather
from bvp.data.utils import save_to_database
from bvp.data.utils import save_to_session
from bvp.data.services.resources import get_assets
from bvp.data.services.forecasting import create_forecasting_jobs

Expand Down Expand Up @@ -136,21 +136,23 @@ def post_price_data_response(
# Put these into the database
current_app.logger.info("SAVING TO DB...")
try:
save_to_database(prices)
save_to_session(prices)
db.session.flush()
[current_app.queues["forecasting"].enqueue_job(job) for job in forecasting_jobs]
db.session.commit()
return request_processed()
except IntegrityError as e:
current_app.logger.warning(e)
db.session.rollback()

# Allow price data to be replaced only in play mode
if current_app.config.get("BVP_MODE", "") == "play":
save_to_database(prices, overwrite=True)
save_to_session(prices, overwrite=True)
[
current_app.queues["forecasting"].enqueue_job(job)
for job in forecasting_jobs
]
db.session.commit()
return request_processed()
else:
return already_received_and_successfully_processed()
Expand Down Expand Up @@ -239,21 +241,23 @@ def post_weather_data_response( # noqa: C901
# Put these into the database
current_app.logger.info("SAVING TO DB...")
try:
save_to_database(weather_measurements)
[current_app.queues["forecasting"].enqueue_job(job) for job in forecasting_jobs]
save_to_session(weather_measurements)
db.session.flush()
[current_app.queues["forecasting"].enqueue_job(job) for job in forecasting_jobs]
db.session.commit()
return request_processed()
except IntegrityError as e:
current_app.logger.warning(e)
db.session.rollback()

# Allow meter data to be replaced only in play mode
if current_app.config.get("BVP_MODE", "") == "play":
save_to_database(weather_measurements, overwrite=True)
save_to_session(weather_measurements, overwrite=True)
[
current_app.queues["forecasting"].enqueue_job(job)
for job in forecasting_jobs
]
db.session.commit()
return request_processed()
else:
return already_received_and_successfully_processed()
Expand Down
16 changes: 8 additions & 8 deletions bvp/api/v1_1/routes.py
@@ -1,6 +1,6 @@
from flask_security import auth_token_required

from bvp.api.common.utils.api_utils import check_access, append_doc_of
from bvp.api.common.utils.api_utils import list_access, append_doc_of
from bvp.api.common.utils.decorators import as_response_type
from bvp.api.common.utils.validators import usef_roles_accepted
from bvp.api.v1 import routes as v1_routes, implementations as v1_implementations
Expand Down Expand Up @@ -55,7 +55,7 @@
@bvp_api_v1_1.route("/getConnection", methods=["GET"])
@as_response_type("GetConnectionResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "getConnection"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "getConnection"))
def get_connection():
"""API endpoint to get the user's connections as entity addresses ordered from newest to oldest.
Expand Down Expand Up @@ -108,7 +108,7 @@ def get_connection():
@bvp_api_v1_1.route("/postPriceData", methods=["POST"])
@as_response_type("PostPriceDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "postPriceData"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "postPriceData"))
def post_price_data():
"""API endpoint to post price data.
Expand Down Expand Up @@ -190,7 +190,7 @@ def post_price_data():
@bvp_api_v1_1.route("/postWeatherData", methods=["POST"])
@as_response_type("PostWeatherDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "postWeatherData"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "postWeatherData"))
def post_weather_data():
"""API endpoint to post weather data, such as:
Expand Down Expand Up @@ -264,7 +264,7 @@ def post_weather_data():
@bvp_api_v1_1.route("/getPrognosis", methods=["GET"])
@as_response_type("GetPrognosisResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "getPrognosis"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "getPrognosis"))
def get_prognosis():
"""API endpoint to get prognosis.
Expand Down Expand Up @@ -331,7 +331,7 @@ def get_prognosis():
@bvp_api_v1_1.route("/postPrognosis", methods=["POST"])
@as_response_type("PostPrognosisResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "postPrognosis"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "postPrognosis"))
def post_prognosis():
"""API endpoint to post prognoses about meter data.
Expand Down Expand Up @@ -414,7 +414,7 @@ def post_prognosis():
@bvp_api_v1_1.route("/getMeterData", methods=["GET"])
@as_response_type("GetMeterDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "getMeterData"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "getMeterData"))
@append_doc_of(v1_routes.get_meter_data)
def get_meter_data():
return v1_implementations.get_meter_data_response()
Expand All @@ -423,7 +423,7 @@ def get_meter_data():
@bvp_api_v1_1.route("/postMeterData", methods=["POST"])
@as_response_type("PostMeterDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_1_service_listing, "postMeterData"))
@usef_roles_accepted(*list_access(v1_1_service_listing, "postMeterData"))
@append_doc_of(v1_routes.post_meter_data)
def post_meter_data():
return v1_implementations.post_meter_data_response()
Expand Down
2 changes: 2 additions & 0 deletions bvp/api/v1_2/implementations.py
Expand Up @@ -29,6 +29,7 @@
units_accepted,
parse_isodate_str,
)
from bvp.data.config import db
from bvp.data.models.assets import Asset
from bvp.data.models.planning.battery import schedule_battery
from bvp.data.models.planning.exceptions import UnknownPricesException
Expand Down Expand Up @@ -206,4 +207,5 @@ def post_udi_event_response(unit): # noqa: C901
asset.soc_udi_event_id = event_id
asset.soc_in_mwh = value

db.session.commit()
return request_processed("Request has been processed.")
18 changes: 9 additions & 9 deletions bvp/api/v1_2/routes.py
Expand Up @@ -2,7 +2,7 @@

from flask_security import auth_token_required

from bvp.api.common.utils.api_utils import check_access, append_doc_of
from bvp.api.common.utils.api_utils import list_access, append_doc_of
from bvp.api.common.utils.decorators import as_response_type
from bvp.api.common.utils.validators import usef_roles_accepted
from bvp.api.v1 import implementations as v1_implementations
Expand Down Expand Up @@ -36,7 +36,7 @@
@bvp_api_v1_2.route("/getDeviceMessage", methods=["GET"])
@as_response_type("GetDeviceMessageResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "getDeviceMessage"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "getDeviceMessage"))
def get_device_message():
"""API endpoint to get device message.
Expand Down Expand Up @@ -93,7 +93,7 @@ def get_device_message():
@bvp_api_v1_2.route("/postUdiEvent", methods=["POST"])
@as_response_type("PostUdiEventResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "postUdiEvent"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "postUdiEvent"))
def post_udi_event():
"""API endpoint to post UDI event.
Expand Down Expand Up @@ -141,7 +141,7 @@ def post_udi_event():
@bvp_api_v1_2.route("/getConnection", methods=["GET"])
@as_response_type("GetConnectionResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "getConnection"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "getConnection"))
@append_doc_of(v1_1_routes.get_connection)
def get_connection():
return v1_1_implementations.get_connection_response()
Expand All @@ -150,7 +150,7 @@ def get_connection():
@bvp_api_v1_2.route("/postPriceData", methods=["POST"])
@as_response_type("PostPriceDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "postPriceData"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "postPriceData"))
@append_doc_of(v1_1_routes.post_price_data)
def post_price_data():
return v1_1_implementations.post_price_data_response()
Expand All @@ -159,7 +159,7 @@ def post_price_data():
@bvp_api_v1_2.route("/postWeatherData", methods=["POST"])
@as_response_type("PostWeatherDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "postWeatherData"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "postWeatherData"))
@append_doc_of(v1_1_routes.post_weather_data)
def post_weather_data():
return v1_1_implementations.post_weather_data_response()
Expand All @@ -168,7 +168,7 @@ def post_weather_data():
@bvp_api_v1_2.route("/getPrognosis", methods=["GET"])
@as_response_type("GetPrognosisResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "getPrognosis"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "getPrognosis"))
@append_doc_of(v1_1_routes.get_prognosis)
def get_prognosis():
return v1_1_implementations.get_prognosis_response()
Expand All @@ -177,7 +177,7 @@ def get_prognosis():
@bvp_api_v1_2.route("/getMeterData", methods=["GET"])
@as_response_type("GetMeterDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "getMeterData"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "getMeterData"))
@append_doc_of(v1_1_routes.get_meter_data)
def get_meter_data():
return v1_implementations.get_meter_data_response()
Expand All @@ -186,7 +186,7 @@ def get_meter_data():
@bvp_api_v1_2.route("/postMeterData", methods=["POST"])
@as_response_type("PostMeterDataResponse")
@auth_token_required
@usef_roles_accepted(*check_access(v1_2_service_listing, "postMeterData"))
@usef_roles_accepted(*list_access(v1_2_service_listing, "postMeterData"))
@append_doc_of(v1_1_routes.post_meter_data)
def post_meter_data():
return v1_implementations.post_meter_data_response()
Expand Down

0 comments on commit b976ed8

Please sign in to comment.