From 45a19ce2961682ada14bd68e7d5470b00aa46ef2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 14 Oct 2021 10:36:32 +0200 Subject: [PATCH 01/31] Add Annotation db model, including relationships Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 74 +++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 32267c04b..d567e94f6 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -682,3 +682,77 @@ def parse_source_arg( else: parsed_sources.append(source) return parsed_sources + + +class Annotation(db.Model): + """An annotation is a nominal value that applies to a specific time or time span. + + Examples of annotation types: + - user annotation: annotation.type == "label" and annotation.source.type == "user" + - unresolved alert: annotation.type == "alert" + - resolved alert: annotation.type == "label" and annotation.source.type == "alerting script" + - organisation holiday: annotation.type == "holiday" and annotation.source.type == "user" + - national, school or public holiday: annotation.type == "holiday" and annotation.source.type == "holiday script" + """ + + id = db.Column(db.Integer, nullable=False, primary_key=True) + name = db.Column(db.String(255), nullable=False) + start = db.Column(db.DateTime(timezone=True), nullable=False) + end = db.Column(db.DateTime(timezone=True), nullable=False) + source_id = db.Column( + db.Integer, db.ForeignKey(DataSource.__tablename__ + ".id"), primary_key=True + ) + source = db.relationship( + "DataSource", + foreign_keys=[source_id], + backref=db.backref("annotations", lazy=True), + ) + type = db.Column(db.Enum("alert", "holiday", "label")) + + @property + def duration(self) -> timedelta: + return self.end - self.start + + +class GenericAssetAnnotationRelationship(db.Model): + """Links annotations to generic assets.""" + + generic_asset_id = db.Column( + db.Integer, db.ForeignKey("generic_asset.id"), nullable=False, primary_key=True + ) + asset = db.relationship( + "GenericAsset", + foreign_keys=[generic_asset_id], + backref=db.backref("annotations", lazy=True), + ) + + annotation_id = db.Column( + db.Integer, db.ForeignKey("annotation.id"), nullable=False + ) + annotation = db.relationship( + "Annotation", + foreign_keys=[annotation_id], + backref=db.backref("generic_assets", lazy=True), + ) + + +class SensorAnnotationRelationship(db.Model): + """Links annotations to sensors.""" + + sensor_id = db.Column( + db.Integer, db.ForeignKey("sensor.id"), nullable=False, primary_key=True + ) + sensor = db.relationship( + "Sensor", + foreign_keys=[sensor_id], + backref=db.backref("annotations", lazy=True), + ) + + annotation_id = db.Column( + db.Integer, db.ForeignKey("annotation.id"), nullable=False + ) + annotation = db.relationship( + "Annotation", + foreign_keys=[annotation_id], + backref=db.backref("sensors", lazy=True), + ) From 32d27163ecb09340358ccdca2211564d0b4c8bb8 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 29 Jan 2022 20:21:25 +0100 Subject: [PATCH 02/31] Add representation of Annotation objects Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index d567e94f6..1b48488a1 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -713,6 +713,9 @@ class Annotation(db.Model): def duration(self) -> timedelta: return self.end - self.start + def __repr__(self) -> str: + return f"" + class GenericAssetAnnotationRelationship(db.Model): """Links annotations to generic assets.""" From f5fecbfd0e8b8615badf7f1b632ca51618ed9dd0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 29 Jan 2022 20:22:18 +0100 Subject: [PATCH 03/31] Add CLI command to add holidays Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 42 +++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 33471a2ac..5c072b8a8 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -12,12 +12,13 @@ import getpass from sqlalchemy.exc import IntegrityError import timely_beliefs as tb +from workalendar.registry import registry 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.user import Account, AccountRole, RolesAccounts -from flexmeasures.data.models.time_series import Sensor, TimedBelief +from flexmeasures.data.models.time_series import Sensor, TimedBelief, Annotation from flexmeasures.data.schemas.sensors import SensorSchema from flexmeasures.data.schemas.generic_assets import ( GenericAssetSchema, @@ -480,6 +481,45 @@ def add_beliefs( print("As a possible workaround, use the --allow-overwrite flag.") +@fm_add_data.command("holidays") +@with_appcontext +@click.option( + "--year", + type=click.INT, + help="The year for which to look up holidays", +) +@click.option( + "--country", + "countries", + type=click.STRING, + multiple=True, + help="The ISO 3166-1 country/region or ISO 3166-2 sub-region for which to look up holidays (such as US, BR and DE)", +) +def add_holidays( + year: int, + countries: List[str], +): + calendars = registry.get_calendars(countries) + _source = get_or_create_source("workalendar", source_type="CLI script") + num_holidays = {} + for country, calendar in calendars.items(): + holidays = calendar().holidays(year) + for holiday in holidays: + start = pd.Timestamp(holiday[0]) + end = start + pd.offsets.DateOffset(days=1) + annotation = Annotation( + name=holiday[1], + start=start, + end=end, + source=_source, + type="holiday", + ) + db.session.add(annotation) + num_holidays[country] = len(holidays) + db.session.commit() + print(f"Successfully added holidays:\n{num_holidays}") + + @fm_add_data.command("forecasts") @with_appcontext @click.option( From 245c83ef75a21ca276447ca53d696362cb0a6738 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 29 Jan 2022 21:28:18 +0100 Subject: [PATCH 04/31] Add migration Signed-off-by: F.N. Claessen --- .../7f8b8920355f_create_annotation_table.py | 46 +++++++++++++++++++ flexmeasures/data/models/time_series.py | 8 ++-- 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py new file mode 100644 index 000000000..23c599fe5 --- /dev/null +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -0,0 +1,46 @@ +"""create annotation table + +Revision ID: 7f8b8920355f +Revises: c1d316c60985 +Create Date: 2022-01-29 20:23:29.996133 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "7f8b8920355f" +down_revision = "c1d316c60985" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "annotation", + sa.Column( + "id", sa.Integer(), nullable=False, autoincrement=True, primary_key=True + ), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("start", sa.DateTime(timezone=True), nullable=False), + sa.Column("end", sa.DateTime(timezone=True), nullable=False), + sa.Column("source_id", sa.Integer(), nullable=False), + sa.Column( + "type", + sa.Enum("alert", "holiday", "label", name="annotation_type"), + nullable=False, + ), + sa.ForeignKeyConstraint(["source_id"], ["data_source.id"]), + ) + op.create_unique_constraint( + op.f("annotation_name_key"), + "annotation", + ["name", "start", "source_id", "type"], + ) + + +def downgrade(): + op.drop_constraint(op.f("annotation_name_key"), "annotation", type_="unique") + op.drop_table("annotation") + op.execute("DROP TYPE annotation_type;") diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 1b48488a1..fdb9cb80d 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -6,6 +6,7 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import Query, Session +from sqlalchemy import UniqueConstraint import timely_beliefs as tb from timely_beliefs.beliefs.probabilistic_utils import get_median_belief import timely_beliefs.utils as tb_utils @@ -695,19 +696,18 @@ class Annotation(db.Model): - national, school or public holiday: annotation.type == "holiday" and annotation.source.type == "holiday script" """ - id = db.Column(db.Integer, nullable=False, primary_key=True) + id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) name = db.Column(db.String(255), nullable=False) start = db.Column(db.DateTime(timezone=True), nullable=False) end = db.Column(db.DateTime(timezone=True), nullable=False) - source_id = db.Column( - db.Integer, db.ForeignKey(DataSource.__tablename__ + ".id"), primary_key=True - ) + source_id = db.Column(db.Integer, db.ForeignKey(DataSource.__tablename__ + ".id")) source = db.relationship( "DataSource", foreign_keys=[source_id], backref=db.backref("annotations", lazy=True), ) type = db.Column(db.Enum("alert", "holiday", "label")) + UniqueConstraint("name", "start", "source_id", "type") @property def duration(self) -> timedelta: From 1aadb48e456a6d13f59b796301d77ef64f218639 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 29 Jan 2022 21:29:12 +0100 Subject: [PATCH 05/31] One source per country Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 4 +++- flexmeasures/data/models/data_sources.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 5c072b8a8..515926bf6 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -500,9 +500,11 @@ def add_holidays( countries: List[str], ): calendars = registry.get_calendars(countries) - _source = get_or_create_source("workalendar", source_type="CLI script") num_holidays = {} for country, calendar in calendars.items(): + _source = get_or_create_source( + "workalendar", model=country, source_type="CLI script" + ) holidays = calendar().holidays(year) for holiday in holidays: start = pd.Timestamp(holiday[0]) diff --git a/flexmeasures/data/models/data_sources.py b/flexmeasures/data/models/data_sources.py index 90c0d0574..24236235f 100644 --- a/flexmeasures/data/models/data_sources.py +++ b/flexmeasures/data/models/data_sources.py @@ -87,11 +87,16 @@ def __str__(self): def get_or_create_source( - source: Union[User, str], source_type: Optional[str] = None, flush: bool = True + source: Union[User, str], + source_type: Optional[str] = None, + model: Optional[str] = None, + flush: bool = True, ) -> DataSource: if is_user(source): source_type = "user" query = DataSource.query.filter(DataSource.type == source_type) + if model is not None: + query = query.filter(DataSource.model == model) if is_user(source): query = query.filter(DataSource.user == source) elif isinstance(source, str): @@ -102,11 +107,11 @@ def get_or_create_source( if not _source: current_app.logger.info(f"Setting up '{source}' as new data source...") if is_user(source): - _source = DataSource(user=source) + _source = DataSource(user=source, model=model) else: if source_type is None: raise TypeError("Please specify a source type") - _source = DataSource(name=source, type=source_type) + _source = DataSource(name=source, model=model, type=source_type) db.session.add(_source) if flush: # assigns id so that we can reference the new object in the current db session From 14a88829ce1efa106b4a219686e0ba2faf713f77 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 29 Jan 2022 21:31:50 +0100 Subject: [PATCH 06/31] Refactor migration Signed-off-by: F.N. Claessen --- .../7f8b8920355f_create_annotation_table.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 23c599fe5..252635752 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -17,6 +17,18 @@ def upgrade(): + create_annotation_table() + # create_annotation_asset_relationship() + # create_annotation_sensor_relationship() + + +def downgrade(): + op.drop_constraint(op.f("annotation_name_key"), "annotation", type_="unique") + op.drop_table("annotation") + op.execute("DROP TYPE annotation_type;") + + +def create_annotation_table(): op.create_table( "annotation", sa.Column( @@ -38,9 +50,3 @@ def upgrade(): "annotation", ["name", "start", "source_id", "type"], ) - - -def downgrade(): - op.drop_constraint(op.f("annotation_name_key"), "annotation", type_="unique") - op.drop_table("annotation") - op.execute("DROP TYPE annotation_type;") From 7393c8548e796aeda011755aa1d94f7a00ef8c4d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 09:48:19 +0100 Subject: [PATCH 07/31] Add annotations_assets table Signed-off-by: F.N. Claessen --- .../7f8b8920355f_create_annotation_table.py | 14 +++++++++++++- flexmeasures/data/models/time_series.py | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 252635752..33cf2ee05 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -18,7 +18,7 @@ def upgrade(): create_annotation_table() - # create_annotation_asset_relationship() + create_annotation_asset_relationship_table() # create_annotation_sensor_relationship() @@ -28,6 +28,18 @@ def downgrade(): op.execute("DROP TYPE annotation_type;") +def create_annotation_asset_relationship_table(): + op.create_table( + "annotations_assets", + sa.Column( + "generic_asset_id", sa.Integer(), nullable=False, primary_key=True + ), + sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), + sa.ForeignKeyConstraint(["generic_asset_id"], ["generic_asset.id"]), + sa.ForeignKeyConstraint(["annotation_id"], ["annotation.id"]), + ) + + def create_annotation_table(): op.create_table( "annotation", diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index fdb9cb80d..b37414510 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -719,6 +719,7 @@ def __repr__(self) -> str: class GenericAssetAnnotationRelationship(db.Model): """Links annotations to generic assets.""" + __tablename__ = "annotations_assets" generic_asset_id = db.Column( db.Integer, db.ForeignKey("generic_asset.id"), nullable=False, primary_key=True @@ -741,6 +742,7 @@ class GenericAssetAnnotationRelationship(db.Model): class SensorAnnotationRelationship(db.Model): """Links annotations to sensors.""" + __tablename__ = "annotations_sensors" sensor_id = db.Column( db.Integer, db.ForeignKey("sensor.id"), nullable=False, primary_key=True From 4fc0f5b08111f68b16d3d106f054eeda0d305642 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:41:08 +0100 Subject: [PATCH 08/31] Improve inflection of plural Signed-off-by: F.N. Claessen --- flexmeasures/utils/flexmeasures_inflection.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flexmeasures/utils/flexmeasures_inflection.py b/flexmeasures/utils/flexmeasures_inflection.py index 0015b0e45..3c3d07cef 100644 --- a/flexmeasures/utils/flexmeasures_inflection.py +++ b/flexmeasures/utils/flexmeasures_inflection.py @@ -1,7 +1,10 @@ +from __future__ import annotations import re +import inflect import inflection +p = inflect.engine() # Give the inflection module some help for our domain inflection.UNCOUNTABLES.add("solar") @@ -30,10 +33,10 @@ def parameterize(word): return inflection.parameterize(word).replace("-", "_") -def pluralize(word): +def pluralize(word, count: str | int | None = None): if word.lower().split()[-1] in inflection.UNCOUNTABLES: return word - return inflection.pluralize(word) + return p.plural(word, count) def titleize(word): From 5233d799eed780403e2d8fc917746ec2e77fa02d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:44:27 +0100 Subject: [PATCH 09/31] Add a CLI option to relate new holidays annotations to assets Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 515926bf6..88d9d3e89 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -18,7 +18,12 @@ from flexmeasures.data.services.forecasting import create_forecasting_jobs from flexmeasures.data.services.users import create_user from flexmeasures.data.models.user import Account, AccountRole, RolesAccounts -from flexmeasures.data.models.time_series import Sensor, TimedBelief, Annotation +from flexmeasures.data.models.time_series import ( + Sensor, + TimedBelief, + Annotation, + GenericAssetAnnotationRelationship, +) from flexmeasures.data.schemas.sensors import SensorSchema from flexmeasures.data.schemas.generic_assets import ( GenericAssetSchema, @@ -31,6 +36,7 @@ get_or_create_source, get_source_or_none, ) +from flexmeasures.utils import flexmeasures_inflection from flexmeasures.utils.time_utils import server_now @@ -495,12 +501,26 @@ def add_beliefs( multiple=True, help="The ISO 3166-1 country/region or ISO 3166-2 sub-region for which to look up holidays (such as US, BR and DE)", ) +@click.option( + "--asset-id", + "generic_asset_ids", + type=click.INT, + multiple=True, + help="Add annotations to this asset. Follow up with the asset's ID.", +) def add_holidays( year: int, countries: List[str], + generic_asset_ids: List[int], ): + """Add holiday annotations to assets.""" calendars = registry.get_calendars(countries) num_holidays = {} + assets = ( + db.session.query(GenericAsset) + .filter(GenericAsset.id.in_(generic_asset_ids)) + .all() + ) for country, calendar in calendars.items(): _source = get_or_create_source( "workalendar", model=country, source_type="CLI script" @@ -517,9 +537,18 @@ def add_holidays( type="holiday", ) db.session.add(annotation) + db.session.flush() + for asset in assets: + relation = GenericAssetAnnotationRelationship( + annotation=annotation, + asset=asset, + ) + db.session.add(relation) num_holidays[country] = len(holidays) db.session.commit() - print(f"Successfully added holidays:\n{num_holidays}") + print( + f"Successfully added holidays to {len(assets)} {flexmeasures_inflection.pluralize('asset', len(assets))}:\n{num_holidays}" + ) @fm_add_data.command("forecasts") From 783d503fc9e4b88a58aa43ab0e9b6300cec70ac2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:48:38 +0100 Subject: [PATCH 10/31] Add a CLI option to relate new holidays annotations to all assets of an account Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 88d9d3e89..c84c6e61b 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -508,19 +508,28 @@ def add_beliefs( multiple=True, help="Add annotations to this asset. Follow up with the asset's ID.", ) +@click.option( + "--account-id", + "account_ids", + type=click.INT, + multiple=True, + help="Add annotations to all assets of this account. Follow up with the account's ID.", +) def add_holidays( year: int, countries: List[str], generic_asset_ids: List[int], + account_ids: List[int], ): """Add holiday annotations to assets.""" calendars = registry.get_calendars(countries) num_holidays = {} - assets = ( - db.session.query(GenericAsset) - .filter(GenericAsset.id.in_(generic_asset_ids)) - .all() - ) + asset_query = db.session.query(GenericAsset) + if generic_asset_ids: + asset_query = asset_query.filter(GenericAsset.id.in_(generic_asset_ids)) + if account_ids: + asset_query = asset_query.filter(GenericAsset.account_id.in_(account_ids)) + assets = asset_query.all() for country, calendar in calendars.items(): _source = get_or_create_source( "workalendar", model=country, source_type="CLI script" From 408275870bfb9caa620485e02d3200ece05d944d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:52:03 +0100 Subject: [PATCH 11/31] Add annotations_sensors table Signed-off-by: F.N. Claessen --- .../7f8b8920355f_create_annotation_table.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 33cf2ee05..611978fb8 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -19,7 +19,7 @@ def upgrade(): create_annotation_table() create_annotation_asset_relationship_table() - # create_annotation_sensor_relationship() + create_annotation_sensor_relationship_table() def downgrade(): @@ -28,6 +28,18 @@ def downgrade(): op.execute("DROP TYPE annotation_type;") +def create_annotation_sensor_relationship_table(): + op.create_table( + "annotations_sensor", + sa.Column( + "sensor_id", sa.Integer(), nullable=False, primary_key=True + ), + sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), + sa.ForeignKeyConstraint(["sensor_id"], ["sensor.id"]), + sa.ForeignKeyConstraint(["annotation_id"], ["annotation.id"]), + ) + + def create_annotation_asset_relationship_table(): op.create_table( "annotations_assets", From 99570c3eb731f61dcc6d0b04e52a491efc8f8ec7 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:52:34 +0100 Subject: [PATCH 12/31] black Signed-off-by: F.N. Claessen --- .../7f8b8920355f_create_annotation_table.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 611978fb8..2e4b08b76 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -31,24 +31,20 @@ def downgrade(): def create_annotation_sensor_relationship_table(): op.create_table( "annotations_sensor", - sa.Column( - "sensor_id", sa.Integer(), nullable=False, primary_key=True - ), + sa.Column("sensor_id", sa.Integer(), nullable=False, primary_key=True), sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), - sa.ForeignKeyConstraint(["sensor_id"], ["sensor.id"]), - sa.ForeignKeyConstraint(["annotation_id"], ["annotation.id"]), + sa.ForeignKeyConstraint(("sensor_id",), ["sensor.id"]), + sa.ForeignKeyConstraint(("annotation_id",), ["annotation.id"]), ) def create_annotation_asset_relationship_table(): op.create_table( "annotations_assets", - sa.Column( - "generic_asset_id", sa.Integer(), nullable=False, primary_key=True - ), + sa.Column("generic_asset_id", sa.Integer(), nullable=False, primary_key=True), sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), - sa.ForeignKeyConstraint(["generic_asset_id"], ["generic_asset.id"]), - sa.ForeignKeyConstraint(["annotation_id"], ["annotation.id"]), + sa.ForeignKeyConstraint(("generic_asset_id",), ["generic_asset.id"]), + sa.ForeignKeyConstraint(("annotation_id",), ["annotation.id"]), ) @@ -67,7 +63,7 @@ def create_annotation_table(): sa.Enum("alert", "holiday", "label", name="annotation_type"), nullable=False, ), - sa.ForeignKeyConstraint(["source_id"], ["data_source.id"]), + sa.ForeignKeyConstraint(("source_id",), ["data_source.id"]), ) op.create_unique_constraint( op.f("annotation_name_key"), From 518e70b3060458f298f6b72fa30a7bb6edba0c5f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:55:21 +0100 Subject: [PATCH 13/31] Fix downgrade Signed-off-by: F.N. Claessen --- .../migrations/versions/7f8b8920355f_create_annotation_table.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 2e4b08b76..2ad7ff43a 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -23,6 +23,8 @@ def upgrade(): def downgrade(): + op.drop_table("annotations_assets") + op.drop_table("annotations_sensors") op.drop_constraint(op.f("annotation_name_key"), "annotation", type_="unique") op.drop_table("annotation") op.execute("DROP TYPE annotation_type;") From 8e56e26b0f0a13fbec8424d995f7b06d50a7d20a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 10:59:21 +0100 Subject: [PATCH 14/31] Fix table name Signed-off-by: F.N. Claessen --- .../migrations/versions/7f8b8920355f_create_annotation_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 2ad7ff43a..e01d497f6 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -32,7 +32,7 @@ def downgrade(): def create_annotation_sensor_relationship_table(): op.create_table( - "annotations_sensor", + "annotations_sensors", sa.Column("sensor_id", sa.Integer(), nullable=False, primary_key=True), sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), sa.ForeignKeyConstraint(("sensor_id",), ["sensor.id"]), From 4e34115738790d4691993a839f182ab47718e57a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 11:02:28 +0100 Subject: [PATCH 15/31] Add downgrade prompt Signed-off-by: F.N. Claessen --- .../versions/7f8b8920355f_create_annotation_table.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index e01d497f6..8a8e5af35 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -6,6 +6,7 @@ """ from alembic import op +import click import sqlalchemy as sa @@ -23,6 +24,10 @@ def upgrade(): def downgrade(): + click.confirm( + "This downgrade drops the tables 'annotations_assets', 'annotations_sensors' and 'annotation'. Continue?", + abort=True, + ) op.drop_table("annotations_assets") op.drop_table("annotations_sensors") op.drop_constraint(op.f("annotation_name_key"), "annotation", type_="unique") From 1067666c84d4ff3fb61f63f3e3c131a59325899e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 11:18:30 +0100 Subject: [PATCH 16/31] black Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index b37414510..471a47383 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -719,6 +719,7 @@ def __repr__(self) -> str: class GenericAssetAnnotationRelationship(db.Model): """Links annotations to generic assets.""" + __tablename__ = "annotations_assets" generic_asset_id = db.Column( @@ -742,6 +743,7 @@ class GenericAssetAnnotationRelationship(db.Model): class SensorAnnotationRelationship(db.Model): """Links annotations to sensors.""" + __tablename__ = "annotations_sensors" sensor_id = db.Column( From 4ad6b3c8e5780392093783ed647d8e3c8ea4de21 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 11:20:02 +0100 Subject: [PATCH 17/31] Fix docstring Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 471a47383..c4b56c6cf 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -693,7 +693,7 @@ class Annotation(db.Model): - unresolved alert: annotation.type == "alert" - resolved alert: annotation.type == "label" and annotation.source.type == "alerting script" - organisation holiday: annotation.type == "holiday" and annotation.source.type == "user" - - national, school or public holiday: annotation.type == "holiday" and annotation.source.type == "holiday script" + - public holiday: annotation.type == "holiday" and annotation.source.name == "workalendar" """ id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) From ff036b893cab5fa84bc5292178fdd11dc322c45e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 11:26:59 +0100 Subject: [PATCH 18/31] Add dependency Signed-off-by: F.N. Claessen --- requirements/app.in | 1 + requirements/app.txt | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/requirements/app.in b/requirements/app.in index d3eb7745d..0a48d5185 100644 --- a/requirements/app.in +++ b/requirements/app.in @@ -10,6 +10,7 @@ pint py-moneyed iso8601 xlrd +workalendar inflection inflect humanize diff --git a/requirements/app.txt b/requirements/app.txt index 93aa6ad30..24e41c994 100644 --- a/requirements/app.txt +++ b/requirements/app.txt @@ -23,10 +23,6 @@ attrs==21.2.0 # trio babel==2.9.1 # via py-moneyed -backports.zoneinfo==0.2.1 - # via - # pytz-deprecation-shim - # tzlocal bcrypt==3.2.0 # via -r requirements/app.in beautifulsoup4==4.10.0 @@ -61,6 +57,8 @@ click==8.0.3 # rq colour==0.1.5 # via -r requirements/app.in +convertdate==2.4.0 + # via workalendar cryptography==35.0.0 # via # pyopenssl @@ -142,10 +140,7 @@ idna==3.3 importlib-metadata==4.8.1 # via # -r requirements/app.in - # alembic # timely-beliefs -importlib-resources==5.4.0 - # via alembic inflect==5.3.0 # via -r requirements/app.in inflection==0.5.1 @@ -172,6 +167,8 @@ jsonschema==4.1.2 # via altair kiwisolver==1.3.2 # via matplotlib +lunardate==0.2.0 + # via workalendar mako==1.1.5 # via alembic markupsafe==2.0.1 @@ -265,6 +262,10 @@ py-moneyed==2.0 # via -r requirements/app.in pycparser==2.20 # via cffi +pyluach==1.3.0 + # via workalendar +pymeeus==0.5.11 + # via convertdate pyomo==6.1.2 # via -r requirements/app.in pyopenssl==21.0.0 @@ -282,6 +283,7 @@ python-dateutil==2.8.2 # matplotlib # pandas # timetomodel + # workalendar python-dotenv==0.19.1 # via -r requirements/app.in pytz==2021.3 @@ -396,6 +398,8 @@ webargs==8.0.1 # via -r requirements/app.in werkzeug==2.0.2 # via flask +workalendar==16.2.0 + # via -r requirements/app.in wsproto==1.0.0 # via trio-websocket wtforms==2.3.3 @@ -403,9 +407,7 @@ wtforms==2.3.3 xlrd==2.0.1 # via -r requirements/app.in zipp==3.6.0 - # via - # importlib-metadata - # importlib-resources + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools From 5890aad48af32c813f3caa63cd03f1df343bbdd7 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 12:01:21 +0100 Subject: [PATCH 19/31] Update docstring Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index c84c6e61b..9a8f6d7bd 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -499,21 +499,21 @@ def add_beliefs( "countries", type=click.STRING, multiple=True, - help="The ISO 3166-1 country/region or ISO 3166-2 sub-region for which to look up holidays (such as US, BR and DE)", + help="The ISO 3166-1 country/region or ISO 3166-2 sub-region for which to look up holidays (such as US, BR and DE). This argument can be given multiple times.", ) @click.option( "--asset-id", "generic_asset_ids", type=click.INT, multiple=True, - help="Add annotations to this asset. Follow up with the asset's ID.", + help="Add annotations to this asset. Follow up with the asset's ID. This argument can be given multiple times.", ) @click.option( "--account-id", "account_ids", type=click.INT, multiple=True, - help="Add annotations to all assets of this account. Follow up with the account's ID.", + help="Add annotations to all assets of this account. Follow up with the account's ID. This argument can be given multiple times.", ) def add_holidays( year: int, From 3429557c539e3b56fd78c2affa2b63fab3ed3df8 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Jan 2022 16:42:23 +0100 Subject: [PATCH 20/31] Add missing enum name Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index c4b56c6cf..e4c6382f8 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -706,7 +706,7 @@ class Annotation(db.Model): foreign_keys=[source_id], backref=db.backref("annotations", lazy=True), ) - type = db.Column(db.Enum("alert", "holiday", "label")) + type = db.Column(db.Enum("alert", "holiday", "label", name="annotation_type")) UniqueConstraint("name", "start", "source_id", "type") @property From ffbb1841857292056b7d2a60e2159a81c5f6de7e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 07:05:36 +0100 Subject: [PATCH 21/31] Clarify use of workalendar registry Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 9a8f6d7bd..cf0f2da1b 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -12,7 +12,7 @@ import getpass from sqlalchemy.exc import IntegrityError import timely_beliefs as tb -from workalendar.registry import registry +from workalendar.registry import registry as workalendar_registry from flexmeasures.data import db from flexmeasures.data.services.forecasting import create_forecasting_jobs @@ -522,7 +522,7 @@ def add_holidays( account_ids: List[int], ): """Add holiday annotations to assets.""" - calendars = registry.get_calendars(countries) + calendars = workalendar_registry.get_calendars(countries) num_holidays = {} asset_query = db.session.query(GenericAsset) if generic_asset_ids: From f880ac274cd0eda56adc6d663740e5f42cf27631 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 07:13:56 +0100 Subject: [PATCH 22/31] Use recommended way of setting up UniqueConstraint Signed-off-by: F.N. Claessen --- flexmeasures/data/models/time_series.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index e4c6382f8..636521014 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -707,7 +707,15 @@ class Annotation(db.Model): backref=db.backref("annotations", lazy=True), ) type = db.Column(db.Enum("alert", "holiday", "label", name="annotation_type")) - UniqueConstraint("name", "start", "source_id", "type") + __table_args__ = ( + UniqueConstraint( + "name", + "start", + "source_id", + "type", + name="annotation_name_key", + ), + ) @property def duration(self) -> timedelta: From 17807b3beefec03e03d1773b6bfb6bcfcc769bb6 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 09:33:27 +0100 Subject: [PATCH 23/31] Fix setup of many-to-many relationships between annotations, generic assets and sensors Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 7 +-- .../7f8b8920355f_create_annotation_table.py | 10 +++-- flexmeasures/data/models/generic_assets.py | 5 +++ flexmeasures/data/models/time_series.py | 45 +++++-------------- 4 files changed, 23 insertions(+), 44 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index cf0f2da1b..415f8e708 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -546,13 +546,8 @@ def add_holidays( type="holiday", ) db.session.add(annotation) - db.session.flush() for asset in assets: - relation = GenericAssetAnnotationRelationship( - annotation=annotation, - asset=asset, - ) - db.session.add(relation) + asset.annotations.append(annotation) num_holidays[country] = len(holidays) db.session.commit() print( diff --git a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py index 8a8e5af35..674e58f5f 100644 --- a/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py +++ b/flexmeasures/data/migrations/versions/7f8b8920355f_create_annotation_table.py @@ -38,8 +38,9 @@ def downgrade(): def create_annotation_sensor_relationship_table(): op.create_table( "annotations_sensors", - sa.Column("sensor_id", sa.Integer(), nullable=False, primary_key=True), - sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("sensor_id", sa.Integer()), + sa.Column("annotation_id", sa.Integer()), sa.ForeignKeyConstraint(("sensor_id",), ["sensor.id"]), sa.ForeignKeyConstraint(("annotation_id",), ["annotation.id"]), ) @@ -48,8 +49,9 @@ def create_annotation_sensor_relationship_table(): def create_annotation_asset_relationship_table(): op.create_table( "annotations_assets", - sa.Column("generic_asset_id", sa.Integer(), nullable=False, primary_key=True), - sa.Column("annotation_id", sa.Integer(), nullable=False, primary_key=True), + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("generic_asset_id", sa.Integer()), + sa.Column("annotation_id", sa.Integer()), sa.ForeignKeyConstraint(("generic_asset_id",), ["generic_asset.id"]), sa.ForeignKeyConstraint(("annotation_id",), ["annotation.id"]), ) diff --git a/flexmeasures/data/models/generic_assets.py b/flexmeasures/data/models/generic_assets.py index 60af5e119..9fc33e2aa 100644 --- a/flexmeasures/data/models/generic_assets.py +++ b/flexmeasures/data/models/generic_assets.py @@ -47,6 +47,11 @@ class GenericAsset(db.Model, AuthModelMixin): foreign_keys=[generic_asset_type_id], backref=db.backref("generic_assets", lazy=True), ) + annotations = db.relationship( + "Annotation", + secondary="annotations_assets", + backref=db.backref("assets", lazy="dynamic"), + ) __table_args__ = ( UniqueConstraint( diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 636521014..63df8994c 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -48,6 +48,11 @@ class Sensor(db.Model, tb.SensorDBMixin, AuthModelMixin): "sensors", lazy=True, cascade="all, delete-orphan", passive_deletes=True ), ) + annotations = db.relationship( + "Annotation", + secondary="annotations_sensors", + backref=db.backref("sensors", lazy="dynamic"), + ) def __init__( self, @@ -730,23 +735,9 @@ class GenericAssetAnnotationRelationship(db.Model): __tablename__ = "annotations_assets" - generic_asset_id = db.Column( - db.Integer, db.ForeignKey("generic_asset.id"), nullable=False, primary_key=True - ) - asset = db.relationship( - "GenericAsset", - foreign_keys=[generic_asset_id], - backref=db.backref("annotations", lazy=True), - ) - - annotation_id = db.Column( - db.Integer, db.ForeignKey("annotation.id"), nullable=False - ) - annotation = db.relationship( - "Annotation", - foreign_keys=[annotation_id], - backref=db.backref("generic_assets", lazy=True), - ) + id = db.Column(db.Integer(), primary_key=True) + generic_asset_id = db.Column(db.Integer, db.ForeignKey("generic_asset.id")) + annotation_id = db.Column(db.Integer, db.ForeignKey("annotation.id")) class SensorAnnotationRelationship(db.Model): @@ -754,20 +745,6 @@ class SensorAnnotationRelationship(db.Model): __tablename__ = "annotations_sensors" - sensor_id = db.Column( - db.Integer, db.ForeignKey("sensor.id"), nullable=False, primary_key=True - ) - sensor = db.relationship( - "Sensor", - foreign_keys=[sensor_id], - backref=db.backref("annotations", lazy=True), - ) - - annotation_id = db.Column( - db.Integer, db.ForeignKey("annotation.id"), nullable=False - ) - annotation = db.relationship( - "Annotation", - foreign_keys=[annotation_id], - backref=db.backref("sensors", lazy=True), - ) + id = db.Column(db.Integer(), primary_key=True) + sensor_id = db.Column(db.Integer, db.ForeignKey("sensor.id")) + annotation_id = db.Column(db.Integer, db.ForeignKey("annotation.id")) From 2b570fc4eef1ef4c494affd43cc853816eb0ffdc Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 09:39:23 +0100 Subject: [PATCH 24/31] Refactor loop Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 415f8e708..7bc7a7956 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -530,6 +530,7 @@ def add_holidays( if account_ids: asset_query = asset_query.filter(GenericAsset.account_id.in_(account_ids)) assets = asset_query.all() + annotations = [] for country, calendar in calendars.items(): _source = get_or_create_source( "workalendar", model=country, source_type="CLI script" @@ -538,17 +539,19 @@ def add_holidays( for holiday in holidays: start = pd.Timestamp(holiday[0]) end = start + pd.offsets.DateOffset(days=1) - annotation = Annotation( - name=holiday[1], - start=start, - end=end, - source=_source, - type="holiday", + annotations.append( + Annotation( + name=holiday[1], + start=start, + end=end, + source=_source, + type="holiday", + ) ) - db.session.add(annotation) - for asset in assets: - asset.annotations.append(annotation) num_holidays[country] = len(holidays) + db.session.add_all(annotations) + for asset in assets: + asset.annotations += annotations db.session.commit() print( f"Successfully added holidays to {len(assets)} {flexmeasures_inflection.pluralize('asset', len(assets))}:\n{num_holidays}" From a95e2cacab9ff374ac6e76415e268c9ef5e1ff30 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 09:46:24 +0100 Subject: [PATCH 25/31] Log the string representation of the newly created Source object, which print its description instead of just its name Signed-off-by: F.N. Claessen --- flexmeasures/data/models/data_sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/data_sources.py b/flexmeasures/data/models/data_sources.py index 24236235f..d1ffa9bd0 100644 --- a/flexmeasures/data/models/data_sources.py +++ b/flexmeasures/data/models/data_sources.py @@ -105,13 +105,13 @@ def get_or_create_source( raise TypeError("source should be of type User or str") _source = query.one_or_none() if not _source: - current_app.logger.info(f"Setting up '{source}' as new data source...") if is_user(source): _source = DataSource(user=source, model=model) else: if source_type is None: raise TypeError("Please specify a source type") _source = DataSource(name=source, model=model, type=source_type) + current_app.logger.info(f"Setting up {_source} as new data source...") db.session.add(_source) if flush: # assigns id so that we can reference the new object in the current db session From a237ee9bef9c0b9936bc5ab6a3d9927aa76ebac2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 09:48:41 +0100 Subject: [PATCH 26/31] flake8 Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 7bc7a7956..f58cf7ef2 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -22,7 +22,6 @@ Sensor, TimedBelief, Annotation, - GenericAssetAnnotationRelationship, ) from flexmeasures.data.schemas.sensors import SensorSchema from flexmeasures.data.schemas.generic_assets import ( From b1b9f57d1b3064447f1d762b4935f3ae6a7ce160 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 10:28:22 +0100 Subject: [PATCH 27/31] Add changelog warning to upgrade the database Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 85a4b9afb..5e1acefd3 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -5,6 +5,8 @@ FlexMeasures Changelog v0.9.0 | February XX, 2022 =========================== +.. warning:: Upgrading to this version requires running ``flexmeasures db upgrade`` (you can create a backup first with ``flexmeasures db-ops dump``). + New features ----------- * Three new CLI commands for cleaning up your database: delete 1) unchanged beliefs, 2) NaN values or 3) a sensor and all of its time series data [see `PR #328 `_] From 22fb920a93edb8f483314657225670f0cb8e442a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 10:29:25 +0100 Subject: [PATCH 28/31] Typos Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 5e1acefd3..448f7fa50 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -10,10 +10,8 @@ v0.9.0 | February XX, 2022 New features ----------- * Three new CLI commands for cleaning up your database: delete 1) unchanged beliefs, 2) NaN values or 3) a sensor and all of its time series data [see `PR #328 `_] - * Add CLI-commands ``flexmeasures add sensor``, ``flexmeasures add asset-type``, ``flexmeasures add beliefs`` (which were experimental features before). [see `PR #337 `_] - -* add CLI comands for showing data [see `PR #339 `_] +* Add CLI commands for showing data [see `PR #339 `_] Bugfixes ----------- From afd18f8f8447c218a9748686e9cf7207dc0e50a2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 10:33:09 +0100 Subject: [PATCH 29/31] Changelog entry Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 448f7fa50..c2e9d9547 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -12,6 +12,7 @@ New features * Three new CLI commands for cleaning up your database: delete 1) unchanged beliefs, 2) NaN values or 3) a sensor and all of its time series data [see `PR #328 `_] * Add CLI-commands ``flexmeasures add sensor``, ``flexmeasures add asset-type``, ``flexmeasures add beliefs`` (which were experimental features before). [see `PR #337 `_] * Add CLI commands for showing data [see `PR #339 `_] +* Add CLI command for attaching annotations to assets: ``flexmeasures add holidays`` adds public holidays [see `PR #343 `_] Bugfixes ----------- From 619f98612fb151212be65ce77d47a35c3f7d811e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 10:52:20 +0100 Subject: [PATCH 30/31] Refactor: move new annotation classes to dedicated module Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 2 +- flexmeasures/data/models/annotations.py | 66 +++++++++++++++++++++++++ flexmeasures/data/models/time_series.py | 61 ----------------------- 3 files changed, 67 insertions(+), 62 deletions(-) create mode 100644 flexmeasures/data/models/annotations.py diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index f58cf7ef2..f046e2494 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -21,8 +21,8 @@ from flexmeasures.data.models.time_series import ( Sensor, TimedBelief, - Annotation, ) +from flexmeasures.data.models.annotations import Annotation from flexmeasures.data.schemas.sensors import SensorSchema from flexmeasures.data.schemas.generic_assets import ( GenericAssetSchema, diff --git a/flexmeasures/data/models/annotations.py b/flexmeasures/data/models/annotations.py new file mode 100644 index 000000000..72f7fd843 --- /dev/null +++ b/flexmeasures/data/models/annotations.py @@ -0,0 +1,66 @@ +from datetime import timedelta + +from sqlalchemy import UniqueConstraint + +from flexmeasures.data import db +from flexmeasures.data.models.data_sources import DataSource + + +class Annotation(db.Model): + """An annotation is a nominal value that applies to a specific time or time span. + + Examples of annotation types: + - user annotation: annotation.type == "label" and annotation.source.type == "user" + - unresolved alert: annotation.type == "alert" + - resolved alert: annotation.type == "label" and annotation.source.type == "alerting script" + - organisation holiday: annotation.type == "holiday" and annotation.source.type == "user" + - public holiday: annotation.type == "holiday" and annotation.source.name == "workalendar" + """ + + id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) + name = db.Column(db.String(255), nullable=False) + start = db.Column(db.DateTime(timezone=True), nullable=False) + end = db.Column(db.DateTime(timezone=True), nullable=False) + source_id = db.Column(db.Integer, db.ForeignKey(DataSource.__tablename__ + ".id")) + source = db.relationship( + "DataSource", + foreign_keys=[source_id], + backref=db.backref("annotations", lazy=True), + ) + type = db.Column(db.Enum("alert", "holiday", "label", name="annotation_type")) + __table_args__ = ( + UniqueConstraint( + "name", + "start", + "source_id", + "type", + name="annotation_name_key", + ), + ) + + @property + def duration(self) -> timedelta: + return self.end - self.start + + def __repr__(self) -> str: + return f"" + + +class GenericAssetAnnotationRelationship(db.Model): + """Links annotations to generic assets.""" + + __tablename__ = "annotations_assets" + + id = db.Column(db.Integer(), primary_key=True) + generic_asset_id = db.Column(db.Integer, db.ForeignKey("generic_asset.id")) + annotation_id = db.Column(db.Integer, db.ForeignKey("annotation.id")) + + +class SensorAnnotationRelationship(db.Model): + """Links annotations to sensors.""" + + __tablename__ = "annotations_sensors" + + id = db.Column(db.Integer(), primary_key=True) + sensor_id = db.Column(db.Integer, db.ForeignKey("sensor.id")) + annotation_id = db.Column(db.Integer, db.ForeignKey("annotation.id")) diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py index 63df8994c..74841e8bb 100644 --- a/flexmeasures/data/models/time_series.py +++ b/flexmeasures/data/models/time_series.py @@ -6,7 +6,6 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import Query, Session -from sqlalchemy import UniqueConstraint import timely_beliefs as tb from timely_beliefs.beliefs.probabilistic_utils import get_median_belief import timely_beliefs.utils as tb_utils @@ -688,63 +687,3 @@ def parse_source_arg( else: parsed_sources.append(source) return parsed_sources - - -class Annotation(db.Model): - """An annotation is a nominal value that applies to a specific time or time span. - - Examples of annotation types: - - user annotation: annotation.type == "label" and annotation.source.type == "user" - - unresolved alert: annotation.type == "alert" - - resolved alert: annotation.type == "label" and annotation.source.type == "alerting script" - - organisation holiday: annotation.type == "holiday" and annotation.source.type == "user" - - public holiday: annotation.type == "holiday" and annotation.source.name == "workalendar" - """ - - id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) - name = db.Column(db.String(255), nullable=False) - start = db.Column(db.DateTime(timezone=True), nullable=False) - end = db.Column(db.DateTime(timezone=True), nullable=False) - source_id = db.Column(db.Integer, db.ForeignKey(DataSource.__tablename__ + ".id")) - source = db.relationship( - "DataSource", - foreign_keys=[source_id], - backref=db.backref("annotations", lazy=True), - ) - type = db.Column(db.Enum("alert", "holiday", "label", name="annotation_type")) - __table_args__ = ( - UniqueConstraint( - "name", - "start", - "source_id", - "type", - name="annotation_name_key", - ), - ) - - @property - def duration(self) -> timedelta: - return self.end - self.start - - def __repr__(self) -> str: - return f"" - - -class GenericAssetAnnotationRelationship(db.Model): - """Links annotations to generic assets.""" - - __tablename__ = "annotations_assets" - - id = db.Column(db.Integer(), primary_key=True) - generic_asset_id = db.Column(db.Integer, db.ForeignKey("generic_asset.id")) - annotation_id = db.Column(db.Integer, db.ForeignKey("annotation.id")) - - -class SensorAnnotationRelationship(db.Model): - """Links annotations to sensors.""" - - __tablename__ = "annotations_sensors" - - id = db.Column(db.Integer(), primary_key=True) - sensor_id = db.Column(db.Integer, db.ForeignKey("sensor.id")) - annotation_id = db.Column(db.Integer, db.ForeignKey("annotation.id")) From 2afce57d15ef45c154602bcfd8eb18fda19a520a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Feb 2022 10:55:05 +0100 Subject: [PATCH 31/31] Refactor: factor out import statement Signed-off-by: F.N. Claessen --- flexmeasures/data/models/annotations.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flexmeasures/data/models/annotations.py b/flexmeasures/data/models/annotations.py index 72f7fd843..193ea755e 100644 --- a/flexmeasures/data/models/annotations.py +++ b/flexmeasures/data/models/annotations.py @@ -1,7 +1,5 @@ from datetime import timedelta -from sqlalchemy import UniqueConstraint - from flexmeasures.data import db from flexmeasures.data.models.data_sources import DataSource @@ -29,7 +27,7 @@ class Annotation(db.Model): ) type = db.Column(db.Enum("alert", "holiday", "label", name="annotation_type")) __table_args__ = ( - UniqueConstraint( + db.UniqueConstraint( "name", "start", "source_id",