diff --git a/documentation/changelog.rst b/documentation/changelog.rst index e579b7c57..1aaeea434 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -19,6 +19,7 @@ Infrastructure / Support ---------------------- * Make assets use MW as their default unit and enforce that in CLI, as well (API already did) [see `PR #108 `_] * Re-use the database between automated tests, if possible. This shaves 2/3rd off of the time it takes for the FlexMeasures test suite to run [see `PR #115 `_] +* Let CLI package and plugins use Marshmallow Field definitions [see `PR #125 `_] v0.4.1 | May 7, 2021 diff --git a/documentation/dev/plugins.rst b/documentation/dev/plugins.rst index ba65e2a9c..c70498e2c 100644 --- a/documentation/dev/plugins.rst +++ b/documentation/dev/plugins.rst @@ -80,6 +80,45 @@ All else that is needed for this showcase (not shown here) is ``/ou .. note:: Plugin views can also be added to the FlexMeasures UI menu ― just name them in the config setting :ref:`menu-config`. +Validating data with marshmallow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +FlexMeasures validates input data using `marshmallow `_. +Data fields can be made suitable for use in CLI commands through our ``MarshmallowClickMixin``. +An example: + +.. code-block:: python + + from datetime import datetime + from typing import Optional + + import click + from flexmeasures.data.schemas.times import AwareDateTimeField + from flexmeasures.data.schemas.utils import MarshmallowClickMixin + from marshmallow import fields + + class StrField(fields.Str, MarshmallowClickMixin): + """String field validator usable for UI routes and CLI functions.""" + + @click.command("meet") + @click.option( + "--where", + required=True, + type=StrField(), # see above: we just made this field suitable for CLI functions + help="(Required) Where we meet", + ) + @click.option( + "--when", + required=False, + type=AwareDateTimeField(format="iso"), # FlexMeasures already made this field suitable for CLI functions + help="[Optional] When we meet (expects timezone-aware ISO 8601 datetime format)", + ) + def schedule_meeting( + where: str, + when: Optional[datetime] = None, + ): + print(f"Okay, see you {where} on {when}.") + Using other files in your plugin ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/flexmeasures/api/__init__.py b/flexmeasures/api/__init__.py index 32b008f51..64ec35607 100644 --- a/flexmeasures/api/__init__.py +++ b/flexmeasures/api/__init__.py @@ -6,9 +6,9 @@ from flexmeasures import __version__ as flexmeasures_version from flexmeasures.data.models.user import User from flexmeasures.api.common.utils.args_parsing import ( - FMValidationError, validation_error_handler, ) +from flexmeasures.data.schemas.utils import FMValidationError # The api blueprint. It is registered with the Flask app (see app.py) flexmeasures_api = Blueprint("flexmeasures_api", __name__) diff --git a/flexmeasures/api/common/utils/args_parsing.py b/flexmeasures/api/common/utils/args_parsing.py index 46d9a0862..32383ff7f 100644 --- a/flexmeasures/api/common/utils/args_parsing.py +++ b/flexmeasures/api/common/utils/args_parsing.py @@ -1,4 +1,5 @@ from flask import jsonify +from flexmeasures.data.schemas.utils import FMValidationError from webargs.multidictproxy import MultiDictProxy from webargs import ValidationError from webargs.flaskparser import parser @@ -18,18 +19,6 @@ def handle_error(error, req, schema, *, error_status_code, error_headers): raise error -class FMValidationError(ValidationError): - """ - Custom validation error class. - It differs from the classic validation error by having two - attributes, according to the USEF 2015 reference implementation. - Subclasses of this error might adjust the `status` attribute accordingly. - """ - - result = "Rejected" - status = "UNPROCESSABLE_ENTITY" - - def validation_error_handler(error: FMValidationError): """Handles errors during parsing. Aborts the current HTTP request and responds with a 422 error. diff --git a/flexmeasures/api/common/utils/validators.py b/flexmeasures/api/common/utils/validators.py index e41c4d80f..19d5a3323 100644 --- a/flexmeasures/api/common/utils/validators.py +++ b/flexmeasures/api/common/utils/validators.py @@ -16,7 +16,7 @@ from webargs.flaskparser import parser -from flexmeasures.api.common.schemas.times import DurationField +from flexmeasures.data.schemas.times import DurationField from flexmeasures.api.common.responses import ( # noqa: F401 required_info_missing, invalid_horizon, diff --git a/flexmeasures/api/dev/sensors.py b/flexmeasures/api/dev/sensors.py index ca6794159..96e9b1272 100644 --- a/flexmeasures/api/dev/sensors.py +++ b/flexmeasures/api/dev/sensors.py @@ -7,7 +7,7 @@ from webargs.flaskparser import use_kwargs from werkzeug.exceptions import abort -from flexmeasures.api.common.schemas.times import AwareDateTimeField +from flexmeasures.data.schemas.times import AwareDateTimeField from flexmeasures.data.models.time_series import Sensor diff --git a/flexmeasures/api/v2_0/implementations/assets.py b/flexmeasures/api/v2_0/implementations/assets.py index 0d05533f0..e23e4d2e2 100644 --- a/flexmeasures/api/v2_0/implementations/assets.py +++ b/flexmeasures/api/v2_0/implementations/assets.py @@ -9,7 +9,8 @@ from marshmallow import fields from flexmeasures.data.services.resources import get_assets -from flexmeasures.data.models.assets import Asset as AssetModel, AssetSchema +from flexmeasures.data.models.assets import Asset as AssetModel +from flexmeasures.data.schemas.assets import AssetSchema from flexmeasures.data.auth_setup import unauthorized_handler from flexmeasures.data.config import db from flexmeasures.api.common.responses import required_info_missing diff --git a/flexmeasures/api/v2_0/implementations/users.py b/flexmeasures/api/v2_0/implementations/users.py index ddbd7ca78..ac7904629 100644 --- a/flexmeasures/api/v2_0/implementations/users.py +++ b/flexmeasures/api/v2_0/implementations/users.py @@ -1,16 +1,15 @@ from functools import wraps from flask import current_app, abort -from marshmallow import ValidationError, validate, validates, fields +from marshmallow import fields from sqlalchemy.exc import IntegrityError from webargs.flaskparser import use_args from flask_security import current_user from flask_security.recoverable import send_reset_password_instructions from flask_json import as_json -from pytz import all_timezones -from flexmeasures.data import ma from flexmeasures.data.models.user import User as UserModel +from flexmeasures.data.schemas.users import UserSchema from flexmeasures.data.services.users import ( get_users, set_random_password, @@ -26,28 +25,6 @@ Both POST (to create) and DELETE are not accessible via the API, but as CLI functions. """ - -class UserSchema(ma.SQLAlchemySchema): - """ - This schema lists fields we support through this API (e.g. no password). - """ - - class Meta: - model = UserModel - - @validates("timezone") - def validate_timezone(self, timezone): - if timezone not in all_timezones: - raise ValidationError(f"Timezone {timezone} doesn't exist.") - - id = ma.auto_field() - email = ma.auto_field(required=True, validate=validate.Email) - username = ma.auto_field(required=True) - active = ma.auto_field() - timezone = ma.auto_field() - flexmeasures_roles = ma.auto_field() - - user_schema = UserSchema() users_schema = UserSchema(many=True) diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py index 8fb5f1ac8..83cf4dcf8 100644 --- a/flexmeasures/data/models/assets.py +++ b/flexmeasures/data/models/assets.py @@ -3,13 +3,9 @@ import isodate import timely_beliefs as tb from sqlalchemy.orm import Query -from marshmallow import ValidationError, validate, validates, fields, validates_schema from flexmeasures.data.config import db -from flexmeasures.data import ma -from flexmeasures.data.models.time_series import Sensor, SensorSchemaMixin, TimedValue -from flexmeasures.data.models.markets import Market -from flexmeasures.data.models.user import User +from flexmeasures.data.models.time_series import Sensor, TimedValue from flexmeasures.utils.entity_address_utils import build_entity_address from flexmeasures.utils.flexmeasures_inflection import humanize, pluralize @@ -182,67 +178,6 @@ def __repr__(self): ) -class AssetSchema(SensorSchemaMixin, ma.SQLAlchemySchema): - """ - Asset schema, with validations. - """ - - class Meta: - model = Asset - - @validates("name") - def validate_name(self, name: str): - asset = Asset.query.filter(Asset.name == name).one_or_none() - if asset: - raise ValidationError(f"An asset with the name {name} already exists.") - - @validates("owner_id") - def validate_owner(self, owner_id: int): - owner = User.query.get(owner_id) - if not owner: - raise ValidationError(f"Owner with id {owner_id} doesn't exist.") - if "Prosumer" not in owner.flexmeasures_roles: - raise ValidationError( - "Asset owner must have role 'Prosumer'." - f" User {owner_id} has roles {[r.name for r in owner.flexmeasures_roles]}." - ) - - @validates("market_id") - def validate_market(self, market_id: int): - market = Market.query.get(market_id) - if not market: - raise ValidationError(f"Market with id {market_id} doesn't exist.") - - @validates("asset_type_name") - def validate_asset_type(self, asset_type_name: str): - asset_type = AssetType.query.get(asset_type_name) - if not asset_type: - raise ValidationError(f"Asset type {asset_type_name} doesn't exist.") - - @validates_schema(skip_on_field_errors=False) - def validate_soc_constraints(self, data, **kwargs): - if "max_soc_in_mwh" in data and "min_soc_in_mwh" in data: - if data["max_soc_in_mwh"] < data["min_soc_in_mwh"]: - errors = { - "max_soc_in_mwh": "This value must be equal or higher than the minimum soc." - } - raise ValidationError(errors) - - id = ma.auto_field() - display_name = fields.Str(validate=validate.Length(min=4)) - capacity_in_mw = fields.Float(required=True, validate=validate.Range(min=0.0001)) - min_soc_in_mwh = fields.Float(validate=validate.Range(min=0)) - max_soc_in_mwh = fields.Float(validate=validate.Range(min=0)) - soc_in_mwh = ma.auto_field() - soc_datetime = ma.auto_field() - soc_udi_event_id = ma.auto_field() - latitude = fields.Float(required=True, validate=validate.Range(min=-90, max=90)) - longitude = fields.Float(required=True, validate=validate.Range(min=-180, max=180)) - asset_type_name = ma.auto_field(required=True) - owner_id = ma.auto_field(required=True) - market_id = ma.auto_field(required=True) - - def assets_share_location(assets: List[Asset]) -> bool: """ Return True if all assets in this list are located on the same spot. diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index aafdc5542..88c3290df 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -6,10 +6,8 @@ from sqlalchemy.orm import Query, Session import timely_beliefs as tb import timely_beliefs.utils as tb_utils -from marshmallow import Schema, fields from flexmeasures.data.config import db -from flexmeasures.data import ma from flexmeasures.data.queries.utils import ( add_belief_timing_filter, add_user_source_filter, @@ -240,35 +238,6 @@ def __repr__(self) -> str: return tb.TimedBelief.__repr__(self) -class SensorSchemaMixin(Schema): - """ - Base sensor schema. - - Here we include all fields which are implemented by timely_beliefs.SensorDBMixin - All classes inheriting from timely beliefs sensor don't need to repeat these. - In a while, this schema can represent our unified Sensor class. - - When subclassing, also subclass from `ma.SQLAlchemySchema` and add your own DB model class, e.g.: - - class Meta: - model = Asset - """ - - name = ma.auto_field(required=True) - unit = ma.auto_field(required=True) - timezone = ma.auto_field() - event_resolution = fields.TimeDelta(required=True, precision="minutes") - - -class SensorSchema(SensorSchemaMixin, ma.SQLAlchemySchema): - """ - Sensor schema, with validations. - """ - - class Meta: - model = Sensor - - class TimedValue(object): """ A mixin of all tables that store time series data, either forecasts or measurements. diff --git a/flexmeasures/data/models/weather.py b/flexmeasures/data/models/weather.py index a2d36c16c..b1d4505a4 100644 --- a/flexmeasures/data/models/weather.py +++ b/flexmeasures/data/models/weather.py @@ -6,12 +6,10 @@ from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.sql.expression import func from sqlalchemy.schema import UniqueConstraint -from marshmallow import ValidationError, validates, validate, fields from flexmeasures.data.config import db -from flexmeasures.data import ma -from flexmeasures.data.models.time_series import Sensor, SensorSchemaMixin, TimedValue +from flexmeasures.data.models.time_series import Sensor, TimedValue from flexmeasures.utils.geo_utils import parse_lat_lng from flexmeasures.utils.flexmeasures_inflection import humanize @@ -171,37 +169,6 @@ def to_dict(self) -> Dict[str, str]: return dict(name=self.name, sensor_type=self.weather_sensor_type_name) -class WeatherSensorSchema(SensorSchemaMixin, ma.SQLAlchemySchema): - """ - WeatherSensor schema, with validations. - """ - - class Meta: - model = WeatherSensor - - @validates("name") - def validate_name(self, name: str): - sensor = WeatherSensor.query.filter( - WeatherSensor.name == name.lower() - ).one_or_none() - if sensor: - raise ValidationError( - f"A weather sensor with the name {name} already exists." - ) - - @validates("weather_sensor_type_name") - def validate_weather_sensor_type(self, weather_sensor_type_name: str): - weather_sensor_type = WeatherSensorType.query.get(weather_sensor_type_name) - if not weather_sensor_type: - raise ValidationError( - f"Weather sensor type {weather_sensor_type_name} doesn't exist." - ) - - weather_sensor_type_name = ma.auto_field(required=True) - latitude = fields.Float(required=True, validate=validate.Range(min=-90, max=90)) - longitude = fields.Float(required=True, validate=validate.Range(min=-180, max=180)) - - class Weather(TimedValue, db.Model): """ All weather measurements are stored in one slim table. diff --git a/flexmeasures/api/common/schemas/__init__.py b/flexmeasures/data/schemas/__init__.py similarity index 100% rename from flexmeasures/api/common/schemas/__init__.py rename to flexmeasures/data/schemas/__init__.py diff --git a/flexmeasures/data/schemas/assets.py b/flexmeasures/data/schemas/assets.py new file mode 100644 index 000000000..3fbfed276 --- /dev/null +++ b/flexmeasures/data/schemas/assets.py @@ -0,0 +1,68 @@ +from marshmallow import validates, ValidationError, validates_schema, fields, validate + +from flexmeasures.data import ma +from flexmeasures.data.models.assets import Asset, AssetType +from flexmeasures.data.models.markets import Market +from flexmeasures.data.models.user import User +from flexmeasures.data.schemas.sensors import SensorSchemaMixin + + +class AssetSchema(SensorSchemaMixin, ma.SQLAlchemySchema): + """ + Asset schema, with validations. + """ + + class Meta: + model = Asset + + @validates("name") + def validate_name(self, name: str): + asset = Asset.query.filter(Asset.name == name).one_or_none() + if asset: + raise ValidationError(f"An asset with the name {name} already exists.") + + @validates("owner_id") + def validate_owner(self, owner_id: int): + owner = User.query.get(owner_id) + if not owner: + raise ValidationError(f"Owner with id {owner_id} doesn't exist.") + if "Prosumer" not in owner.flexmeasures_roles: + raise ValidationError( + "Asset owner must have role 'Prosumer'." + f" User {owner_id} has roles {[r.name for r in owner.flexmeasures_roles]}." + ) + + @validates("market_id") + def validate_market(self, market_id: int): + market = Market.query.get(market_id) + if not market: + raise ValidationError(f"Market with id {market_id} doesn't exist.") + + @validates("asset_type_name") + def validate_asset_type(self, asset_type_name: str): + asset_type = AssetType.query.get(asset_type_name) + if not asset_type: + raise ValidationError(f"Asset type {asset_type_name} doesn't exist.") + + @validates_schema(skip_on_field_errors=False) + def validate_soc_constraints(self, data, **kwargs): + if "max_soc_in_mwh" in data and "min_soc_in_mwh" in data: + if data["max_soc_in_mwh"] < data["min_soc_in_mwh"]: + errors = { + "max_soc_in_mwh": "This value must be equal or higher than the minimum soc." + } + raise ValidationError(errors) + + id = ma.auto_field() + display_name = fields.Str(validate=validate.Length(min=4)) + capacity_in_mw = fields.Float(required=True, validate=validate.Range(min=0.0001)) + min_soc_in_mwh = fields.Float(validate=validate.Range(min=0)) + max_soc_in_mwh = fields.Float(validate=validate.Range(min=0)) + soc_in_mwh = ma.auto_field() + soc_datetime = ma.auto_field() + soc_udi_event_id = ma.auto_field() + latitude = fields.Float(required=True, validate=validate.Range(min=-90, max=90)) + longitude = fields.Float(required=True, validate=validate.Range(min=-180, max=180)) + asset_type_name = ma.auto_field(required=True) + owner_id = ma.auto_field(required=True) + market_id = ma.auto_field(required=True) diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py new file mode 100644 index 000000000..80fb23ecd --- /dev/null +++ b/flexmeasures/data/schemas/sensors.py @@ -0,0 +1,33 @@ +from marshmallow import Schema, fields + +from flexmeasures.data import ma +from flexmeasures.data.models.time_series import Sensor + + +class SensorSchemaMixin(Schema): + """ + Base sensor schema. + + Here we include all fields which are implemented by timely_beliefs.SensorDBMixin + All classes inheriting from timely beliefs sensor don't need to repeat these. + In a while, this schema can represent our unified Sensor class. + + When subclassing, also subclass from `ma.SQLAlchemySchema` and add your own DB model class, e.g.: + + class Meta: + model = Asset + """ + + name = ma.auto_field(required=True) + unit = ma.auto_field(required=True) + timezone = ma.auto_field() + event_resolution = fields.TimeDelta(required=True, precision="minutes") + + +class SensorSchema(SensorSchemaMixin, ma.SQLAlchemySchema): + """ + Sensor schema, with validations. + """ + + class Meta: + model = Sensor diff --git a/flexmeasures/api/common/schemas/tests/__init__.py b/flexmeasures/data/schemas/tests/__init__.py similarity index 100% rename from flexmeasures/api/common/schemas/tests/__init__.py rename to flexmeasures/data/schemas/tests/__init__.py diff --git a/flexmeasures/api/common/schemas/tests/test_times.py b/flexmeasures/data/schemas/tests/test_times.py similarity index 97% rename from flexmeasures/api/common/schemas/tests/test_times.py rename to flexmeasures/data/schemas/tests/test_times.py index 475cd7a08..a834effc8 100644 --- a/flexmeasures/api/common/schemas/tests/test_times.py +++ b/flexmeasures/data/schemas/tests/test_times.py @@ -4,7 +4,7 @@ import pytz import isodate -from flexmeasures.api.common.schemas.times import DurationField, DurationValidationError +from flexmeasures.data.schemas.times import DurationField, DurationValidationError @pytest.mark.parametrize( diff --git a/flexmeasures/api/common/schemas/times.py b/flexmeasures/data/schemas/times.py similarity index 92% rename from flexmeasures/api/common/schemas/times.py rename to flexmeasures/data/schemas/times.py index a9bff0d07..32c9c3ae9 100644 --- a/flexmeasures/api/common/schemas/times.py +++ b/flexmeasures/data/schemas/times.py @@ -6,14 +6,14 @@ from isodate.isoerror import ISO8601Error import pandas as pd -from flexmeasures.api.common.utils.args_parsing import FMValidationError +from flexmeasures.data.schemas.utils import FMValidationError, MarshmallowClickMixin class DurationValidationError(FMValidationError): status = "INVALID_PERIOD" # USEF error status -class DurationField(fields.Str): +class DurationField(fields.Str, MarshmallowClickMixin): """Field that deserializes to a ISO8601 Duration and serializes back to a string.""" @@ -64,7 +64,7 @@ def ground_from( return duration -class AwareDateTimeField(fields.AwareDateTime): +class AwareDateTimeField(fields.AwareDateTime, MarshmallowClickMixin): """Field that deserializes to a timezone aware datetime and serializes back to a string.""" diff --git a/flexmeasures/data/schemas/users.py b/flexmeasures/data/schemas/users.py new file mode 100644 index 000000000..71883d67c --- /dev/null +++ b/flexmeasures/data/schemas/users.py @@ -0,0 +1,26 @@ +from marshmallow import validates, ValidationError, validate +from pytz import all_timezones + +from flexmeasures.data import ma +from flexmeasures.data.models.user import User as UserModel + + +class UserSchema(ma.SQLAlchemySchema): + """ + This schema lists fields we support through this API (e.g. no password). + """ + + class Meta: + model = UserModel + + @validates("timezone") + def validate_timezone(self, timezone): + if timezone not in all_timezones: + raise ValidationError(f"Timezone {timezone} doesn't exist.") + + id = ma.auto_field() + email = ma.auto_field(required=True, validate=validate.Email) + username = ma.auto_field(required=True) + active = ma.auto_field() + timezone = ma.auto_field() + flexmeasures_roles = ma.auto_field() diff --git a/flexmeasures/data/schemas/utils.py b/flexmeasures/data/schemas/utils.py new file mode 100644 index 000000000..42276d8e5 --- /dev/null +++ b/flexmeasures/data/schemas/utils.py @@ -0,0 +1,26 @@ +import click +import marshmallow as ma +from marshmallow import ValidationError + + +class MarshmallowClickMixin(click.ParamType): + def get_metavar(self, param): + return self.__class__.__name__ + + def convert(self, value, param, ctx, **kwargs): + try: + return self.deserialize(value, **kwargs) + except ma.exceptions.ValidationError as e: + raise click.exceptions.BadParameter(e, ctx=ctx, param=param) + + +class FMValidationError(ValidationError): + """ + Custom validation error class. + It differs from the classic validation error by having two + attributes, according to the USEF 2015 reference implementation. + Subclasses of this error might adjust the `status` attribute accordingly. + """ + + result = "Rejected" + status = "UNPROCESSABLE_ENTITY" diff --git a/flexmeasures/data/schemas/weather.py b/flexmeasures/data/schemas/weather.py new file mode 100644 index 000000000..5e81f63fc --- /dev/null +++ b/flexmeasures/data/schemas/weather.py @@ -0,0 +1,36 @@ +from marshmallow import validates, ValidationError, fields, validate + +from flexmeasures.data import ma +from flexmeasures.data.models.weather import WeatherSensor, WeatherSensorType +from flexmeasures.data.schemas.sensors import SensorSchemaMixin + + +class WeatherSensorSchema(SensorSchemaMixin, ma.SQLAlchemySchema): + """ + WeatherSensor schema, with validations. + """ + + class Meta: + model = WeatherSensor + + @validates("name") + def validate_name(self, name: str): + sensor = WeatherSensor.query.filter( + WeatherSensor.name == name.lower() + ).one_or_none() + if sensor: + raise ValidationError( + f"A weather sensor with the name {name} already exists." + ) + + @validates("weather_sensor_type_name") + def validate_weather_sensor_type(self, weather_sensor_type_name: str): + weather_sensor_type = WeatherSensorType.query.get(weather_sensor_type_name) + if not weather_sensor_type: + raise ValidationError( + f"Weather sensor type {weather_sensor_type_name} doesn't exist." + ) + + weather_sensor_type_name = ma.auto_field(required=True) + latitude = fields.Float(required=True, validate=validate.Range(min=-90, max=90)) + longitude = fields.Float(required=True, validate=validate.Range(min=-180, max=180)) diff --git a/flexmeasures/data/scripts/cli_tasks/data_add.py b/flexmeasures/data/scripts/cli_tasks/data_add.py index aab374aca..42278ebf1 100644 --- a/flexmeasures/data/scripts/cli_tasks/data_add.py +++ b/flexmeasures/data/scripts/cli_tasks/data_add.py @@ -16,10 +16,13 @@ from flexmeasures.data import db from flexmeasures.data.services.forecasting import create_forecasting_jobs from flexmeasures.data.services.users import create_user -from flexmeasures.data.models.time_series import Sensor, SensorSchema, TimedBelief -from flexmeasures.data.models.assets import Asset, AssetSchema +from flexmeasures.data.models.time_series import Sensor, TimedBelief +from flexmeasures.data.schemas.sensors import SensorSchema +from flexmeasures.data.models.assets import Asset +from flexmeasures.data.schemas.assets import AssetSchema from flexmeasures.data.models.markets import Market -from flexmeasures.data.models.weather import WeatherSensor, WeatherSensorSchema +from flexmeasures.data.models.weather import WeatherSensor +from flexmeasures.data.schemas.weather import WeatherSensorSchema from flexmeasures.data.models.data_sources import ( get_or_create_source, get_source_or_none, diff --git a/flexmeasures/ui/views/charts.py b/flexmeasures/ui/views/charts.py index fdfced2f6..47a63581e 100644 --- a/flexmeasures/ui/views/charts.py +++ b/flexmeasures/ui/views/charts.py @@ -6,7 +6,7 @@ from flexmeasures.api.v2_0 import flexmeasures_api as flexmeasures_api_v2_0 from flexmeasures.api.v2_0.routes import v2_0_service_listing -from flexmeasures.api.common.schemas.times import DurationField +from flexmeasures.data.schemas.times import DurationField from flexmeasures.data.queries.analytics import get_power_data from flexmeasures.ui.views.analytics import make_power_figure diff --git a/flexmeasures/ui/views/sensors.py b/flexmeasures/ui/views/sensors.py index c9e4eafd0..091c6d31f 100644 --- a/flexmeasures/ui/views/sensors.py +++ b/flexmeasures/ui/views/sensors.py @@ -5,7 +5,7 @@ from marshmallow import fields from webargs.flaskparser import use_kwargs -from flexmeasures.api.common.schemas.times import AwareDateTimeField +from flexmeasures.data.schemas.times import AwareDateTimeField from flexmeasures.api.dev.sensors import SensorAPI from flexmeasures.ui.utils.view_utils import render_flexmeasures_template