Repository: FlexMeasures/flexmeasures (language: python)

Issue:
Support SQLAlchemy 2.0.x
## Description

This PR updates SQLAlchemy dependency from version 1.4 to 2.0. The update underwent thorough testing to ensure compatibility, and all the tests passed successfully.

## How to test

Run `make install-for-dev` to install the required packages. Do the testing using the UI and CLI. 



Discussion / hints:

Fix patch:
diff --git a/documentation/changelog.rst b/documentation/changelog.rst
index d9b8265fe..abbe09878 100644
--- a/documentation/changelog.rst
+++ b/documentation/changelog.rst
@@ -24,6 +24,7 @@ Infrastructure / Support
 
 * Deprecate use of flask's ``FLASK_ENV`` variable and replace it with ``FLEXMEASURES_ENV`` [see `PR #907 <https://github.com/FlexMeasures/flexmeasures/pull/907>`_]
 * Streamline CLI option naming by favoring ``--<entity>`` over ``--<entity>-id`` [see `PR #946 <https://github.com/FlexMeasures/flexmeasures/pull/946>`_]
+* Faster database queries of time series data by upgrading SQLAlchemy and timely-beliefs [see `PR #938 <https://github.com/FlexMeasures/flexmeasures/pull/938>`_]
 
 
 Bugfixes
diff --git a/flexmeasures/api/__init__.py b/flexmeasures/api/__init__.py
index cd536fd29..0f71311ea 100644
--- a/flexmeasures/api/__init__.py
+++ b/flexmeasures/api/__init__.py
@@ -7,7 +7,9 @@
 from flask_json import as_json
 from flask_login import current_user
 from sqlalchemy.exc import IntegrityError
+from sqlalchemy import select
 
+from flexmeasures.data import db
 from flexmeasures import __version__ as flexmeasures_version
 from flexmeasures.api.common.utils.api_utils import catch_timed_belief_replacements
 from flexmeasures.data.models.user import User
@@ -50,7 +52,9 @@ def request_auth_token():
         if current_user.is_authenticated and current_user.email == email:
             user = current_user
         else:
-            user = User.query.filter_by(email=email).one_or_none()
+            user = db.session.execute(
+                select(User).filter_by(email=email)
+            ).scalar_one_or_none()
             if not user:
                 return (
                     {"errors": ["User with email '%s' does not exist" % email]},
diff --git a/flexmeasures/api/common/implementations.py b/flexmeasures/api/common/implementations.py
index c706f35d7..e7250dca0 100644
--- a/flexmeasures/api/common/implementations.py
+++ b/flexmeasures/api/common/implementations.py
@@ -4,7 +4,7 @@
 import pytz
 from flask import request, current_app
 from flask_json import as_json
-from sqlalchemy import exc as sqla_exc
+from sqlalchemy import exc as sqla_exc, select
 
 from flexmeasures.data import db
 from flexmeasures.data.models.task_runs import LatestTaskRun
@@ -58,14 +58,14 @@ def make_response(status: str, reason: str, last_run: datetime) -> dict:
         return make_response("ERROR", "No task name given.", datetime(1970, 1, 1)), 400
 
     try:
-        last_known_run = LatestTaskRun.query.filter(
-            LatestTaskRun.name == task_name
+        last_known_run = db.session.scalars(
+            select(LatestTaskRun).filter(LatestTaskRun.name == task_name).limit(1)
         ).first()
     except (sqla_exc.ResourceClosedError, sqla_exc.DatabaseError):
         # This is an attempt to make this more stable against some rare condition we encounter. Let's try once more.
         time.sleep(2)
-        last_known_run = LatestTaskRun.query.filter(
-            LatestTaskRun.name == task_name
+        last_known_run = db.session.scalars(
+            select(LatestTaskRun).filter(LatestTaskRun.name == task_name).limit(1)
         ).first()
 
     if not last_known_run:
@@ -94,9 +94,9 @@ def post_task_run():
     date_time = request.form.get("datetime", datetime.utcnow().replace(tzinfo=pytz.utc))
     status = request.form.get("status", "True") == "True"
     try:
-        task_run = LatestTaskRun.query.filter(
-            LatestTaskRun.name == task_name
-        ).one_or_none()
+        task_run = db.session.execute(
+            select(LatestTaskRun).filter(LatestTaskRun.name == task_name)
+        ).scalar_one_or_none()
         if task_run is None:
             task_run = LatestTaskRun(name=task_name)
             db.session.add(task_run)
diff --git a/flexmeasures/api/common/schemas/generic_assets.py b/flexmeasures/api/common/schemas/generic_assets.py
index 6bdab3249..dfda954c5 100644
--- a/flexmeasures/api/common/schemas/generic_assets.py
+++ b/flexmeasures/api/common/schemas/generic_assets.py
@@ -1,6 +1,8 @@
 from flask import abort
 from marshmallow import fields
+from sqlalchemy import select
 
+from flexmeasures.data import db
 from flexmeasures.data.models.generic_assets import GenericAsset
 
 
@@ -10,9 +12,9 @@ class AssetIdField(fields.Integer):
     """
 
     def _deserialize(self, asset_id: int, attr, obj, **kwargs) -> GenericAsset:
-        asset: GenericAsset = GenericAsset.query.filter_by(
-            id=int(asset_id)
-        ).one_or_none()
+        asset: GenericAsset = db.session.execute(
+            select(GenericAsset).filter_by(id=int(asset_id))
+        ).scalar_one_or_none()
         if asset is None:
             raise abort(404, f"GenericAsset {asset_id} not found")
         return asset
diff --git a/flexmeasures/api/common/schemas/sensors.py b/flexmeasures/api/common/schemas/sensors.py
index d33eaecb7..dccc23c4d 100644
--- a/flexmeasures/api/common/schemas/sensors.py
+++ b/flexmeasures/api/common/schemas/sensors.py
@@ -1,6 +1,8 @@
 from flask import abort
 from marshmallow import fields
+from sqlalchemy import select
 
+from flexmeasures.data import db
 from flexmeasures.api import FMValidationError
 from flexmeasures.utils.entity_address_utils import (
     parse_entity_address,
@@ -19,7 +21,9 @@ class SensorIdField(fields.Integer):
     """
 
     def _deserialize(self, sensor_id: int, attr, obj, **kwargs) -> Sensor:
-        sensor: Sensor = Sensor.query.filter_by(id=int(sensor_id)).one_or_none()
+        sensor: Sensor = db.session.execute(
+            select(Sensor).filter_by(id=int(sensor_id))
+        ).scalar_one_or_none()
         if sensor is None:
             raise abort(404, f"Sensor {sensor_id} not found")
         return sensor
@@ -58,7 +62,9 @@ def _deserialize(self, value, attr, obj, **kwargs) -> Sensor:
             if self.fm_scheme == "fm0":
                 raise EntityAddressException("The fm0 scheme is no longer supported.")
             else:
-                sensor = Sensor.query.filter(Sensor.id == ea["sensor_id"]).one_or_none()
+                sensor = db.session.execute(
+                    select(Sensor).filter_by(id=ea["sensor_id"])
+                ).scalar_one_or_none()
             if sensor is not None:
                 return sensor
             else:
diff --git a/flexmeasures/api/common/schemas/users.py b/flexmeasures/api/common/schemas/users.py
index 4470e9968..db50b9d1d 100644
--- a/flexmeasures/api/common/schemas/users.py
+++ b/flexmeasures/api/common/schemas/users.py
@@ -1,7 +1,9 @@
 from flask import abort
 from flask_security import current_user
 from marshmallow import fields
+from sqlalchemy import select
 
+from flexmeasures.data import db
 from flexmeasures.data.models.user import User, Account
 
 
@@ -11,7 +13,9 @@ class AccountIdField(fields.Integer):
     """
 
     def _deserialize(self, account_id: str, attr, obj, **kwargs) -> Account:
-        account: Account = Account.query.filter_by(id=int(account_id)).one_or_none()
+        account: Account = db.session.execute(
+            select(Account).filter_by(id=int(account_id))
+        ).scalar_one_or_none()
         if account is None:
             raise abort(404, f"Account {account_id} not found")
         return account
@@ -40,7 +44,9 @@ def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
     def _deserialize(self, user_id: int, attr, obj, **kwargs) -> User:
-        user: User = User.query.filter_by(id=int(user_id)).one_or_none()
+        user: User = db.session.execute(
+            select(User).filter_by(id=int(user_id))
+        ).scalar_one_or_none()
         if user is None:
             raise abort(404, f"User {user_id} not found")
         return user
diff --git a/flexmeasures/api/dev/sensors.py b/flexmeasures/api/dev/sensors.py
index b78ca0f8a..5c3596623 100644
--- a/flexmeasures/api/dev/sensors.py
+++ b/flexmeasures/api/dev/sensors.py
@@ -7,6 +7,7 @@
 from webargs.flaskparser import use_kwargs
 from werkzeug.exceptions import abort
 
+from flexmeasures.data import db
 from flexmeasures.auth.policy import ADMIN_ROLE, ADMIN_READER_ROLE
 from flexmeasures.auth.decorators import permission_required_for_context
 from flexmeasures.data.schemas import (
@@ -191,7 +192,7 @@ def get_sensor_or_abort(id: int) -> Sensor:
         "Util function will be deprecated. Switch to using SensorIdField to suppress this warning.",
         FutureWarning,
     )
-    sensor = Sensor.query.filter(Sensor.id == id).one_or_none()
+    sensor = db.session.get(Sensor, id)
     if sensor is None:
         raise abort(404, f"Sensor {id} not found")
     if not (
diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py
index 17cd1c158..a9b7408fd 100644
--- a/flexmeasures/api/v3_0/assets.py
+++ b/flexmeasures/api/v3_0/assets.py
@@ -6,6 +6,7 @@
 from flask_json import as_json
 from marshmallow import fields
 from webargs.flaskparser import use_kwargs, use_args
+from sqlalchemy import select, delete
 
 from flexmeasures.auth.decorators import permission_required_for_context
 from flexmeasures.data import db
@@ -98,7 +99,9 @@ def public(self):
         :status 401: UNAUTHORIZED
         :status 422: UNPROCESSABLE_ENTITY
         """
-        assets = GenericAsset.query.filter(GenericAsset.account_id.is_(None)).all()
+        assets = db.session.scalars(
+            select(GenericAsset).filter(GenericAsset.account_id.is_(None))
+        ).all()
         return assets_schema.dump(assets), 200
 
     @route("", methods=["POST"])
@@ -255,7 +258,7 @@ def delete(self, id: int, asset: GenericAsset):
         :status 422: UNPROCESSABLE_ENTITY
         """
         asset_name = asset.name
-        db.session.delete(asset)
+        db.session.execute(delete(GenericAsset).filter_by(id=asset.id))
         db.session.commit()
         current_app.logger.info("Deleted asset '%s'." % asset_name)
         return {}, 204
diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py
index c975cd6d2..9cb262568 100644
--- a/flexmeasures/api/v3_0/sensors.py
+++ b/flexmeasures/api/v3_0/sensors.py
@@ -12,6 +12,7 @@
 from rq.job import Job, NoSuchJobError
 from timely_beliefs import BeliefsDataFrame
 from webargs.flaskparser import use_args, use_kwargs
+from sqlalchemy import delete
 
 from flexmeasures.api.common.responses import (
     request_processed,
@@ -747,10 +748,10 @@ def delete(self, id: int, sensor: Sensor):
         """
 
         """Delete time series data."""
-        TimedBelief.query.filter(TimedBelief.sensor_id == sensor.id).delete()
+        db.session.execute(delete(TimedBelief).filter_by(sensor_id=sensor.id))
 
         sensor_name = sensor.name
-        db.session.delete(sensor)
+        db.session.execute(delete(Sensor).filter_by(id=sensor.id))
         db.session.commit()
         current_app.logger.info("Deleted sensor '%s'." % sensor_name)
         return {}, 204
diff --git a/flexmeasures/auth/decorators.py b/flexmeasures/auth/decorators.py
index 79f42a369..2655bb42a 100644
--- a/flexmeasures/auth/decorators.py
+++ b/flexmeasures/auth/decorators.py
@@ -17,7 +17,7 @@
 )
 from werkzeug.local import LocalProxy
 from werkzeug.exceptions import Forbidden
-
+from flexmeasures.data import db
 from flexmeasures.auth.policy import ADMIN_ROLE, AuthModelMixin, check_access
 
 
@@ -188,7 +188,7 @@ def decorated_view(*args, **kwargs):
                     if inspect.isclass(ctx_loader) and issubclass(
                         ctx_loader, AuthModelMixin
                     ):
-                        context = ctx_loader.query.get(context_from_args)
+                        context = db.session.get(ctx_loader, context_from_args)
                     else:
                         context = ctx_loader(context_from_args)
                 else:
diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py
index 324627a77..d345a7486 100755
--- a/flexmeasures/cli/data_add.py
+++ b/flexmeasures/cli/data_add.py
@@ -21,7 +21,7 @@
 import click
 import getpass
 from sqlalchemy.exc import IntegrityError
-from sqlalchemy import func
+from sqlalchemy import func, select
 from timely_beliefs.sensors.func_store.knowledge_horizons import x_days_ago_at_y_oclock
 import timely_beliefs as tb
 import timely_beliefs.utils as tb_utils
@@ -141,7 +141,9 @@ def new_account_role(name: str, description: str):
     """
     Create an account role.
     """
-    role = AccountRole.query.filter_by(name=name).one_or_none()
+    role = db.session.execute(
+        select(AccountRole).filter_by(name=name)
+    ).scalar_one_or_none()
     if role is not None:
         click.secho(f"Account role '{name}' already exists.", **MsgStyle.ERROR)
         raise click.Abort()
@@ -168,7 +170,9 @@ def new_account(name: str, roles: str, consultancy_account: Account | None):
     """
     Create an account for a tenant in the FlexMeasures platform.
     """
-    account = db.session.query(Account).filter_by(name=name).one_or_none()
+    account = db.session.execute(
+        select(Account).filter_by(name=name)
+    ).scalar_one_or_none()
     if account is not None:
         click.secho(f"Account '{name}' already exists.", **MsgStyle.ERROR)
         raise click.Abort()
@@ -176,7 +180,9 @@ def new_account(name: str, roles: str, consultancy_account: Account | None):
     db.session.add(account)
     if roles:
         for role_name in roles.split(","):
-            role = AccountRole.query.filter_by(name=role_name).one_or_none()
+            role = db.session.execute(
+                select(AccountRole).filter_by(name=role_name)
+            ).scalar_one_or_none()
             if role is None:
                 click.secho(f"Adding account role {role_name} ...", **MsgStyle.ERROR)
                 role = AccountRole(name=role_name)
@@ -237,7 +243,7 @@ def new_user(
     except pytz.UnknownTimeZoneError:
         click.secho(f"Timezone {timezone} is unknown!", **MsgStyle.ERROR)
         raise click.Abort()
-    account = db.session.query(Account).get(account_id)
+    account = db.session.get(Account, account_id)
     if account is None:
         click.secho(f"No account with ID {account_id} found!", **MsgStyle.ERROR)
         raise click.Abort()
@@ -810,23 +816,23 @@ def add_annotation(
         else start + pd.offsets.DateOffset(days=1)
     )
     accounts = (
-        db.session.query(Account).filter(Account.id.in_(account_ids)).all()
+        db.session.scalars(select(Account).filter(Account.id.in_(account_ids))).all()
         if account_ids
         else []
     )
     assets = (
-        db.session.query(GenericAsset)
-        .filter(GenericAsset.id.in_(generic_asset_ids))
-        .all()
+        db.session.scalars(
+            select(GenericAsset).filter(GenericAsset.id.in_(generic_asset_ids))
+        ).all()
         if generic_asset_ids
         else []
     )
     sensors = (
-        db.session.query(Sensor).filter(Sensor.id.in_(sensor_ids)).all()
+        db.session.scalars(select(Sensor).filter(Sensor.id.in_(sensor_ids))).all()
         if sensor_ids
         else []
     )
-    user = db.session.query(User).get(user_id)
+    user = db.session.get(User, user_id)
     _source = get_or_create_source(user)
 
     # Create annotation
@@ -896,14 +902,14 @@ def add_holidays(
     num_holidays = {}
 
     accounts = (
-        db.session.query(Account).filter(Account.id.in_(account_ids)).all()
+        db.session.scalars(select(Account).filter(Account.id.in_(account_ids))).all()
         if account_ids
         else []
     )
     assets = (
-        db.session.query(GenericAsset)
-        .filter(GenericAsset.id.in_(generic_asset_ids))
-        .all()
+        db.session.scalars(
+            select(GenericAsset).filter(GenericAsset.id.in_(generic_asset_ids))
+        ).all()
         if generic_asset_ids
         else []
     )
@@ -1714,15 +1720,14 @@ def add_report(  # noqa: C901
         )
 
         # todo: get the oldest last_value among all the sensors
-        last_value_datetime = (
-            db.session.query(func.max(TimedBelief.event_start))
-            .filter(TimedBelief.sensor_id == output[0]["sensor"].id)
-            .one_or_none()
-        )
-
+        last_value_datetime = db.session.execute(
+            select(func.max(TimedBelief.event_start))
+            .select_from(TimedBelief)
+            .filter_by(sensor_id=output[0]["sensor"].id)
+        ).scalar_one_or_none()
         # If there's data saved to the reporter sensors
-        if last_value_datetime[0] is not None:
-            start = last_value_datetime[0]
+        if last_value_datetime is not None:
+            start = last_value_datetime
         else:
             click.secho(
                 "Could not find any data for the output sensors provided. Such data is needed to compute"
@@ -1900,7 +1905,9 @@ def add_toy_account(kind: str, name: str):
     location = (52.374, 4.88969)  # Amsterdam
 
     # make an account (if not exist)
-    account = Account.query.filter(Account.name == name).one_or_none()
+    account = db.session.execute(
+        select(Account).filter_by(name=name)
+    ).scalar_one_or_none()
     if account:
         click.secho(
             f"Account '{account}' already exists. Skipping account creation. Use `flexmeasures delete account --id {account.id}` if you need to remove it.",
@@ -1909,7 +1916,7 @@ def add_toy_account(kind: str, name: str):
 
     # make an account user (account-admin?)
     email = "toy-user@flexmeasures.io"
-    user = User.query.filter_by(email=email).one_or_none()
+    user = db.session.execute(select(User).filter_by(email=email)).scalar_one_or_none()
     if user is not None:
         click.secho(
             f"User with email {email} already exists in account {user.account.name}.",
@@ -1963,7 +1970,7 @@ def create_asset_with_one_sensor(
             GenericAsset,
             name=asset_name,
             generic_asset_type=asset_types[asset_type],
-            owner=Account.query.get(account_id),
+            owner=db.session.get(Account, account_id),
             latitude=location[0],
             longitude=location[1],
         )
@@ -2090,7 +2097,7 @@ def create_asset_with_one_sensor(
             event_start=tz.localize(start_year),
             belief_time=tz.localize(datetime.now()),
             event_value=0.5,
-            source=DataSource.query.get(1),
+            source=db.session.get(DataSource, 1),
             sensor=grid_connection_capacity,
         )
 
diff --git a/flexmeasures/cli/data_delete.py b/flexmeasures/cli/data_delete.py
index b00470f0b..b9ba1d9a0 100644
--- a/flexmeasures/cli/data_delete.py
+++ b/flexmeasures/cli/data_delete.py
@@ -11,6 +11,8 @@
 from flask import current_app as app
 from flask.cli import with_appcontext
 from timely_beliefs.beliefs.queries import query_unchanged_beliefs
+from sqlalchemy import select, func, delete
+
 
 from flexmeasures.data import db
 from flexmeasures.data.models.user import Account, AccountRole, RolesAccounts, User
@@ -35,7 +37,9 @@ def delete_account_role(name: str):
     Delete an account role.
     If it has accounts connected, print them before deleting the connection.
     """
-    role: AccountRole = AccountRole.query.filter_by(name=name).one_or_none()
+    role: AccountRole = db.session.execute(
+        select(AccountRole).filter_by(name=name)
+    ).scalar_one_or_none()
     if role is None:
         click.secho(f"Account role '{name}' does not exist.", **MsgStyle.ERROR)
         raise click.Abort()
@@ -46,7 +50,7 @@ def delete_account_role(name: str):
         )
         for account in accounts:
             account.account_roles.remove(role)
-    db.session.delete(role)
+    db.session.execute(delete(AccountRole).filter_by(id=role.id))
     db.session.commit()
     click.secho(f"Account role '{name}' has been deleted.", **MsgStyle.SUCCESS)
 
@@ -61,16 +65,18 @@ def delete_account(id: int, force: bool):
     """
     Delete an account, including their users & data.
     """
-    account: Account = db.session.query(Account).get(id)
+    account: Account = db.session.get(Account, id)
     if account is None:
         click.secho(f"Account with ID '{id}' does not exist.", **MsgStyle.ERROR)
         raise click.Abort()
     if not force:
         prompt = f"Delete account '{account.name}', including generic assets, users and all their data?\n"
-        users = User.query.filter(User.account_id == id).all()
+        users = db.session.scalars(select(User).filter_by(account_id=id)).all()
         if users:
             prompt += "Affected users: " + ",".join([u.username for u in users]) + "\n"
-        generic_assets = GenericAsset.query.filter(GenericAsset.account_id == id).all()
+        generic_assets = db.session.scalars(
+            select(GenericAsset).filter_by(account_id=id)
+        ).all()
         if generic_assets:
             prompt += (
                 "Affected generic assets: "
@@ -81,19 +87,23 @@ def delete_account(id: int, force: bool):
     for user in account.users:
         click.secho(f"Deleting user {user} ...")
         delete_user(user)
-    for role_account_association in RolesAccounts.query.filter_by(
-        account_id=account.id
+    for role_account_association in db.session.scalars(
+        select(RolesAccounts).filter_by(account_id=account.id)
     ).all():
-        role = AccountRole.query.get(role_account_association.role_id)
+        role = db.session.get(AccountRole, role_account_association.role_id)
         click.echo(
             f"Deleting association of account {account.name} and role {role.name} ...",
         )
-        db.session.delete(role_account_association)
+        db.session.execute(
+            delete(RolesAccounts).filter_by(
+                account_id=role_account_association.account_id
+            )
+        )
     for asset in account.generic_assets:
         click.echo(f"Deleting generic asset {asset} (and sensors & beliefs) ...")
-        db.session.delete(asset)
+        db.session.execute(delete(GenericAsset).filter_by(id=asset.id))
     account_name = account.name
-    db.session.delete(account)
+    db.session.execute(delete(Account).filter_by(id=account.id))
     db.session.commit()
     click.secho(f"Account {account_name} has been deleted.", **MsgStyle.SUCCESS)
 
@@ -136,7 +146,7 @@ def delete_asset_and_data(asset: GenericAsset, force: bool):
             f"Delete {asset.__repr__()}, including all its sensors, data and children?"
         )
         click.confirm(prompt, abort=True)
-    db.session.delete(asset)
+    db.session.execute(delete(GenericAsset).filter_by(id=asset.id))
     db.session.commit()
 
 
@@ -244,18 +254,17 @@ def delete_unchanged_beliefs(
     delete_unchanged_measurements: bool = True,
 ):
     """Delete unchanged beliefs (i.e. updated beliefs with a later belief time, but with the same event value)."""
-    q = db.session.query(TimedBelief)
+    q = select(TimedBelief)
     if sensor_id:
-        sensor = Sensor.query.filter(Sensor.id == sensor_id).one_or_none()
+        sensor = db.session.get(Sensor, sensor_id)
         if sensor is None:
             click.secho(
                 f"Failed to delete any beliefs: no sensor found with id {sensor_id}.",
                 **MsgStyle.ERROR,
             )
             raise click.Abort()
-        q = q.filter(TimedBelief.sensor_id == sensor.id)
-    num_beliefs_before = q.count()
-
+        q = q.filter_by(sensor_id=sensor.id)
+    num_beliefs_before = db.session.scalar(select(func.count()).select_from(q))
     unchanged_queries = []
     num_forecasts_up_for_deletion = 0
     num_measurements_up_for_deletion = 0
@@ -269,7 +278,9 @@ def delete_unchanged_beliefs(
             include_non_positive_horizons=False,
         )
         unchanged_queries.append(q_unchanged_forecasts)
-        num_forecasts_up_for_deletion = q_unchanged_forecasts.count()
+        num_forecasts_up_for_deletion = db.session.scalar(
+            select(func.count()).select_from(q_unchanged_forecasts)
+        )
     if delete_unchanged_measurements:
         q_unchanged_measurements = query_unchanged_beliefs(
             db.session,
@@ -280,7 +291,9 @@ def delete_unchanged_beliefs(
             include_positive_horizons=False,
         )
         unchanged_queries.append(q_unchanged_measurements)
-        num_measurements_up_for_deletion = q_unchanged_measurements.count()
+        num_measurements_up_for_deletion = db.session.scalar(
+            select(func.count()).select_from(q_unchanged_measurements)
+        )
 
     num_beliefs_up_for_deletion = (
         num_forecasts_up_for_deletion + num_measurements_up_for_deletion
@@ -288,7 +301,9 @@ def delete_unchanged_beliefs(
     prompt = f"Delete {num_beliefs_up_for_deletion} unchanged beliefs ({num_measurements_up_for_deletion} measurements and {num_forecasts_up_for_deletion} forecasts) out of {num_beliefs_before} beliefs?"
     click.confirm(prompt, abort=True)
 
-    beliefs_up_for_deletion = list(chain(*[q.all() for q in unchanged_queries]))
+    beliefs_up_for_deletion = list(
+        chain(*[db.session.scalars(q).all() for q in unchanged_queries])
+    )
     batch_size = 10000
     for i, b in enumerate(beliefs_up_for_deletion, start=1):
         if i % batch_size == 0 or i == num_beliefs_up_for_deletion:
@@ -296,7 +311,7 @@ def delete_unchanged_beliefs(
         db.session.delete(b)
     click.secho(f"Removing {num_beliefs_up_for_deletion} beliefs ...")
     db.session.commit()
-    num_beliefs_after = q.count()
+    num_beliefs_after = db.session.scalar(select(func.count()).select_from(q))
     click.secho(f"Done! {num_beliefs_after} beliefs left", **MsgStyle.SUCCESS)
 
 
@@ -340,15 +355,18 @@ def delete_sensor(
     sensors: list[Sensor],
 ):
     """Delete sensors and their (time series) data."""
-    n = TimedBelief.query.filter(
+    n = delete(TimedBelief).where(
         TimedBelief.sensor_id.in_(sensor.id for sensor in sensors)
-    ).delete()
+    )
+    statements = []
     for sensor in sensors:
-        db.session.delete(sensor)
+        statements.append(delete(Sensor).filter_by(id=sensor.id))
     click.confirm(
         f"Delete {', '.join(sensor.__repr__() for sensor in sensors)}, along with {n} beliefs?",
         abort=True,
     )
+    for statement in statements:
+        db.session.execute(statement)
     db.session.commit()
 
 
diff --git a/flexmeasures/cli/data_edit.py b/flexmeasures/cli/data_edit.py
index bd3b55e0e..78b43da6e 100644
--- a/flexmeasures/cli/data_edit.py
+++ b/flexmeasures/cli/data_edit.py
@@ -11,6 +11,7 @@
 from flask import current_app as app
 from flask.cli import with_appcontext
 import json
+from sqlalchemy import delete
 
 from flexmeasures import Sensor
 from flexmeasures.data import db
@@ -203,7 +204,7 @@ def resample_sensor_data(
     event_starts_after = pd.Timestamp(start_str)  # note that "" or None becomes NaT
     event_ends_before = pd.Timestamp(end_str)
     for sensor_id in sensor_ids:
-        sensor = Sensor.query.get(sensor_id)
+        sensor = db.session.get(Sensor, sensor_id)
         if sensor.event_resolution == event_resolution:
             click.echo(f"{sensor} already has the desired event resolution.")
             continue
@@ -230,14 +231,14 @@ def resample_sensor_data(
         db.session.add(sensor)
 
         # Update sensor data
-        query = TimedBelief.query.filter(TimedBelief.sensor == sensor)
+        query = delete(TimedBelief).filter_by(sensor=sensor)
         if not pd.isnull(event_starts_after):
             query = query.filter(TimedBelief.event_start >= event_starts_after)
         if not pd.isnull(event_ends_before):
             query = query.filter(
                 TimedBelief.event_start + sensor.event_resolution <= event_ends_before
             )
-        query.delete()
+        db.session.execute(query)
         save_to_db(df_resampled, bulk_save_objects=True)
     db.session.commit()
     click.secho("Successfully resampled sensor data.", **MsgStyle.SUCCESS)
diff --git a/flexmeasures/cli/data_show.py b/flexmeasures/cli/data_show.py
index 3bf8d9c75..48859b1eb 100644
--- a/flexmeasures/cli/data_show.py
+++ b/flexmeasures/cli/data_show.py
@@ -17,7 +17,9 @@
 from string import Template
 import pytz
 import json
+from sqlalchemy import select, func
 
+from flexmeasures.data import db
 from flexmeasures.data.models.user import Account, AccountRole, User, Role
 from flexmeasures.data.models.data_sources import DataSource
 from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
@@ -45,7 +47,7 @@ def list_accounts():
     """
     List all accounts on this FlexMeasures instance.
     """
-    accounts = Account.query.order_by(Account.name).all()
+    accounts = db.session.scalars(select(Account).order_by(Account.name)).all()
     if not accounts:
         click.secho("No accounts created yet.", **MsgStyle.WARN)
         raise click.Abort()
@@ -54,7 +56,11 @@ def list_accounts():
         (
             account.id,
             account.name,
-            GenericAsset.query.filter(GenericAsset.account_id == account.id).count(),
+            db.session.scalar(
+                select(func.count())
+                .select_from(GenericAsset)
+                .filter_by(account_id=account.id)
+            ),
         )
         for account in accounts
     ]
@@ -67,7 +73,9 @@ def list_roles():
     """
     Show available account and user roles
     """
-    account_roles = AccountRole.query.order_by(AccountRole.name).all()
+    account_roles = db.session.scalars(
+        select(AccountRole).order_by(AccountRole.name)
+    ).all()
     if not account_roles:
         click.secho("No account roles created yet.", **MsgStyle.WARN)
         raise click.Abort()
@@ -79,7 +87,7 @@ def list_roles():
         )
     )
     click.echo()
-    user_roles = Role.query.order_by(Role.name).all()
+    user_roles = db.session.scalars(select(Role).order_by(Role.name)).all()
     if not user_roles:
         click.secho("No user roles created yet, not even admin.", **MsgStyle.WARN)
         raise click.Abort()
@@ -111,7 +119,9 @@ def show_account(account):
         click.secho("Account has no roles.", **MsgStyle.WARN)
     click.echo()
 
-    users = User.query.filter_by(account_id=account.id).order_by(User.username).all()
+    users = db.session.scalars(
+        select(User).filter_by(account_id=account.id).order_by(User.username)
+    ).all()
     if not users:
         click.secho("No users in account ...", **MsgStyle.WARN)
     else:
@@ -135,11 +145,11 @@ def show_account(account):
         )
 
     click.echo()
-    assets = (
-        GenericAsset.query.filter_by(account_id=account.id)
+    assets = db.session.scalars(
+        select(GenericAsset)
+        .filter_by(account_id=account.id)
         .order_by(GenericAsset.name)
-        .all()
-    )
+    ).all()
     if not assets:
         click.secho("No assets in account ...", **MsgStyle.WARN)
     else:
@@ -157,7 +167,9 @@ def list_asset_types():
     """
     Show available asset types
     """
-    asset_types = GenericAssetType.query.order_by(GenericAssetType.name).all()
+    asset_types = db.session.scalars(
+        select(GenericAssetType).order_by(GenericAssetType.name)
+    ).all()
     if not asset_types:
         click.secho("No asset types created yet.", **MsgStyle.WARN)
         raise click.Abort()
@@ -190,9 +202,9 @@ def show_generic_asset(asset):
     click.echo(tabulate(asset_data, headers=["Type", "Location", "Attributes"]))
 
     click.echo()
-    sensors = (
-        Sensor.query.filter_by(generic_asset_id=asset.id).order_by(Sensor.name).all()
-    )
+    sensors = db.session.scalars(
+        select(Sensor).filter_by(generic_asset_id=asset.id).order_by(Sensor.name)
+    ).all()
     if not sensors:
         click.secho("No sensors in asset ...", **MsgStyle.WARN)
         raise click.Abort()
@@ -238,13 +250,13 @@ def list_data_sources(source: DataSource | None = None, show_attributes: bool =
     Show available data sources
     """
     if source is None:
-        sources = (
-            DataSource.query.order_by(DataSource.type)
+        sources = db.session.scalars(
+            select(DataSource)
+            .order_by(DataSource.type)
             .order_by(DataSource.name)
             .order_by(DataSource.model)
             .order_by(DataSource.version)
-            .all()
-        )
+        ).all()
     else:
         sources = [source]
 
@@ -405,9 +417,9 @@ def chart(
         # need to fetch the entities as they get detached
         # and we get the (in)famous detached instance error.
         if entity_type == "asset":
-            entity = GenericAsset.query.get(entity.id)
+            entity = db.session.get(GenericAsset, entity.id)
         else:
-            entity = Sensor.query.get(entity.id)
+            entity = db.session.get(Sensor, entity.id)
 
         chart_description = entity.chart(
             event_starts_after=start,
diff --git a/flexmeasures/cli/monitor.py b/flexmeasures/cli/monitor.py
index 55430b7cf..a9d9b2dec 100644
--- a/flexmeasures/cli/monitor.py
+++ b/flexmeasures/cli/monitor.py
@@ -14,7 +14,9 @@
     capture_message as capture_message_for_sentry,
     set_context as set_sentry_context,
 )
+from sqlalchemy import select
 
+from flexmeasures.data import db
 from flexmeasures.data.models.task_runs import LatestTaskRun
 from flexmeasures.data.models.user import User
 from flexmeasures.utils.time_utils import server_now
@@ -115,7 +117,7 @@ def monitor_latest_run(task, custom_message):
     for t in task:
         task_name = t[0]
         app.logger.info(f"Checking latest run of task {task_name} ...")
-        latest_run: LatestTaskRun = LatestTaskRun.query.get(task_name)
+        latest_run: LatestTaskRun = db.session.get(LatestTaskRun, task_name)
         if latest_run is None:
             msg = f"Task {task_name} has no last run and thus cannot be monitored. Is it configured properly?"
             send_task_monitoring_alert(task_name, msg, custom_msg=custom_message)
@@ -247,10 +249,9 @@ def monitor_last_seen(
     last_seen_delta = timedelta(minutes=maximum_minutes_since_last_seen)
 
     # find users we haven't seen in the given time window
-    users: list[User] = User.query.filter(
-        User.last_seen_at < datetime.utcnow() - last_seen_delta
+    users: list[User] = db.session.scalars(
+        select(User).filter(User.last_seen_at < datetime.utcnow() - last_seen_delta)
     ).all()
-
     # role filters
     if account_role is not None:
         users = [user for user in users if user.account.has_role(account_role)]
diff --git a/flexmeasures/data/config.py b/flexmeasures/data/config.py
index 8209b0243..246c233e2 100644
--- a/flexmeasures/data/config.py
+++ b/flexmeasures/data/config.py
@@ -2,7 +2,7 @@
 Database configuration utils
 """
 
-from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import declarative_base
 from sqlalchemy import MetaData
 import sqlalchemy as sa
 from flask_sqlalchemy import SQLAlchemy
diff --git a/flexmeasures/data/migrations/versions/565e092a6c5e_introduce_the_GenericAssetType_table.py b/flexmeasures/data/migrations/versions/565e092a6c5e_introduce_the_GenericAssetType_table.py
index 225f3102e..b57af2032 100644
--- a/flexmeasures/data/migrations/versions/565e092a6c5e_introduce_the_GenericAssetType_table.py
+++ b/flexmeasures/data/migrations/versions/565e092a6c5e_introduce_the_GenericAssetType_table.py
@@ -74,7 +74,7 @@ def upgrade_data():
     # Select all existing ids that need migrating, while keeping names intact
     asset_type_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_asset_types.c.name,
                 t_asset_types.c.display_name,
             ]
@@ -82,7 +82,7 @@ def upgrade_data():
     ).fetchall()
     market_type_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_market_types.c.name,
                 t_market_types.c.display_name,
             ]
@@ -90,7 +90,7 @@ def upgrade_data():
     ).fetchall()
     weather_sensor_type_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_weather_sensor_types.c.name,
                 t_weather_sensor_types.c.display_name,
             ]
diff --git a/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py b/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py
index 3612bd57a..ba3c360f3 100644
--- a/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py
+++ b/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py
@@ -320,7 +320,7 @@ def copy_location_columns_to_generic_asset(
     ]
     # Get columns from old model
     results = connection.execute(
-        sa.select([getattr(t_old_model.c, a) for a in old_model_attributes])
+        sa.select(*[getattr(t_old_model.c, a) for a in old_model_attributes])
     ).fetchall()
 
     for sensor_id, *args in results:
@@ -353,7 +353,7 @@ def copy_sensor_columns(connection, t_old_model, t_sensor):
 
     # Get columns from old model
     results = connection.execute(
-        sa.select([getattr(t_old_model.c, a) for a in old_model_attributes])
+        sa.select(*[getattr(t_old_model.c, a) for a in old_model_attributes])
     ).fetchall()
 
     for sensor_id, *args in results:
@@ -396,7 +396,7 @@ def copy_attributes(
     """
     # Get attributes from old model
     results = connection.execute(
-        sa.select([getattr(t_old_model.c, a) for a in old_model_attributes])
+        sa.select(*[getattr(t_old_model.c, a) for a in old_model_attributes])
     ).fetchall()
 
     for _id, type_name, *args in results:
@@ -483,7 +483,7 @@ def get_generic_asset_id(connection, old_model_id: int, t_sensors) -> int:
     """Get the Sensor with the same id as the OldModel, and then get the id of the GenericAsset of that Sensor."""
     (generic_asset_id,) = connection.execute(
         sa.select(
-            [
+            *[
                 t_sensors.c.generic_asset_id,
             ]
         ).filter(t_sensors.c.id == old_model_id)
@@ -498,7 +498,7 @@ def get_old_model_type_attributes(
     """Get the attributes from the OldModelType."""
     values = connection.execute(
         sa.select(
-            [getattr(t_old_model_types.c, a) for a in old_model_type_attributes]
+            *[getattr(t_old_model_types.c, a) for a in old_model_type_attributes]
         ).filter(t_old_model_types.c.name == old_model_type_name)
     ).one_or_none()
     assert values is not None
diff --git a/flexmeasures/data/migrations/versions/994170c26bc6_add_account_table.py b/flexmeasures/data/migrations/versions/994170c26bc6_add_account_table.py
index 02e9699a0..c36c90be8 100644
--- a/flexmeasures/data/migrations/versions/994170c26bc6_add_account_table.py
+++ b/flexmeasures/data/migrations/versions/994170c26bc6_add_account_table.py
@@ -111,9 +111,9 @@ def upgrade_data():
             print(
                 f"Linking user {user_id} to account {account_name} (as from custom param) ..."
             )
-            account_results = (
-                session.query(Account.id).filter_by(name=account_name).one_or_none()
-            )
+            account_results = session.execute(
+                sa.select(Account.id).filter_by(name=account_name)
+            ).scalar_one_or_none()
             if account_results is None:
                 print(f"need to create account {account_name} ...")
                 account = Account(name=account_name)
@@ -121,8 +121,10 @@ def upgrade_data():
                 session.flush()
                 account_id = account.id
             else:
-                account_id = account_results[0]
-            user_results = session.query(User.id).filter_by(id=user_id).one_or_none()
+                account_id = account_results
+            user_results = session.execute(
+                sa.select(User.id).filter_by(id=user_id)
+            ).scalar_one_or_none()
             if not user_results:
                 raise ValueError(f"User with ID {user_id} does not exist!")
             connection.execute(
@@ -130,7 +132,9 @@ def upgrade_data():
             )
 
     # Make sure each existing user has an account
-    for user_results in session.query(User.id, User.email, User.account_id).all():
+    for user_results in session.execute(
+        sa.select(User.id, User.email, User.account_id)
+    ).all():
         user_id = user_results[0]
         user_email = user_results[1]
         user_account_id = user_results[2]
@@ -159,7 +163,7 @@ def upgrade_data():
     asset_ownership_db = _generic_asset_ownership()
     generic_asset_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_generic_assets.c.id,
                 t_generic_assets.c.name,
                 t_generic_assets.c.generic_asset_type_id,
@@ -187,7 +191,7 @@ def upgrade_data():
                 )
                 continue
             asset_results = connection.execute(
-                sa.select([t_assets.c.owner_id]).where(
+                sa.select(*[t_assets.c.owner_id]).where(
                     t_assets.c.id == sensor_results[0]
                 )
             ).one_or_none()
@@ -286,7 +290,7 @@ def _generic_asset_ownership() -> List[Tuple[int, int]]:
     # Select all existing ids that need migrating, while keeping names intact
     asset_ownership_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_asset_owners.c.id,
                 t_asset_owners.c.owner_id,
             ]
diff --git a/flexmeasures/data/migrations/versions/a528c3c81506_unique_generic_sensor_ids.py b/flexmeasures/data/migrations/versions/a528c3c81506_unique_generic_sensor_ids.py
index b83560d83..5b51ddf37 100644
--- a/flexmeasures/data/migrations/versions/a528c3c81506_unique_generic_sensor_ids.py
+++ b/flexmeasures/data/migrations/versions/a528c3c81506_unique_generic_sensor_ids.py
@@ -123,7 +123,7 @@ def upgrade_data():
     # Select all existing ids that need migrating, while keeping names intact
     asset_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_assets.c.id,
                 t_assets.c.name,
             ]
@@ -131,7 +131,7 @@ def upgrade_data():
     ).fetchall()
     market_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_markets.c.id,
                 t_markets.c.name,
             ]
@@ -139,7 +139,7 @@ def upgrade_data():
     ).fetchall()
     weather_sensor_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_weather_sensors.c.id,
                 t_weather_sensors.c.name,
             ]
@@ -203,8 +203,10 @@ def upgrade_data():
     sequence_name = "%s_id_seq" % t_sensors.name
     # Set next id for table seq to just after max id of all old sensors combined
     connection.execute(
-        "SELECT setval('%s', %s, false);"  # is_called = False
-        % (sequence_name, max_asset_id + max_market_id + max_weather_sensor_id + 1)
+        sa.sql.expression.text("SELECT setval(:seq_name, :next_id, false);").bindparams(
+            seq_name=sequence_name,
+            next_id=max_asset_id + max_market_id + max_weather_sensor_id + 1,
+        )
     )
 
 
@@ -278,7 +280,7 @@ def downgrade_data():
     # Select all existing ids that need migrating
     market_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_markets.c.id,
                 t_markets.c.name,
             ]
@@ -302,7 +304,7 @@ def downgrade_data():
     )
     weather_sensor_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_weather_sensors.c.id,
                 t_weather_sensors.c.name,
             ]
@@ -372,7 +374,7 @@ def get_max_id(connection, generic_sensor_type: str) -> int:
     )
     max_id = connection.execute(
         sa.select(
-            [
+            *[
                 sa.sql.expression.func.max(
                     t_generic_sensor.c.id,
                 )
diff --git a/flexmeasures/data/migrations/versions/ad98460751d9_remove_obsolete_tables.py b/flexmeasures/data/migrations/versions/ad98460751d9_remove_obsolete_tables.py
index af2875fa0..769af6f35 100644
--- a/flexmeasures/data/migrations/versions/ad98460751d9_remove_obsolete_tables.py
+++ b/flexmeasures/data/migrations/versions/ad98460751d9_remove_obsolete_tables.py
@@ -33,13 +33,16 @@ def upgrade():
         "weather_sensor",
     ]:
         try:
-            result = db.engine.execute(
-                f"SELECT EXISTS (SELECT 1 FROM {table});"
-            ).one_or_none()
-            if result[0]:
-                tables_with_data += [table]
+            inspect = sa.inspect(db.engine)
+            if inspect.has_table(table):
+                result = db.session.execute(
+                    sa.text(f"SELECT 1 FROM {table};")
+                ).scalar_one_or_none()
+                if result:
+                    tables_with_data.append(table)
+            else:
+                print(f"Table {table} not found, continuing...")
         except ProgrammingError as exception:
-            print(f"Table {table} not found, continuing...")
             print(exception)
     db.session.close()  # https://stackoverflow.com/a/26346280/13775459
 
diff --git a/flexmeasures/data/migrations/versions/b6d49ed7cceb_introduce_the_GenericAsset_table.py b/flexmeasures/data/migrations/versions/b6d49ed7cceb_introduce_the_GenericAsset_table.py
index b3e8cafd7..3adf80079 100644
--- a/flexmeasures/data/migrations/versions/b6d49ed7cceb_introduce_the_GenericAsset_table.py
+++ b/flexmeasures/data/migrations/versions/b6d49ed7cceb_introduce_the_GenericAsset_table.py
@@ -107,7 +107,7 @@ def upgrade_data():
     # Select all existing ids that need migrating, while keeping names intact
     sensor_results = connection.execute(
         sa.select(
-            [
+            *[
                 t_sensors.c.id,
                 t_sensors.c.name,
             ]
@@ -127,7 +127,7 @@ def upgrade_data():
                 f"At least some of these sensor ids {sensor_group_dict['sensor_ids']} do not exist."
             )
         generic_asset_type_results = connection.execute(
-            sa.select([t_generic_asset_types.c.id]).where(
+            sa.select(*[t_generic_asset_types.c.id]).where(
                 t_generic_asset_types.c.name == sensor_group_dict["asset_type_name"]
             )
         ).one_or_none()
@@ -171,7 +171,7 @@ def upgrade_data():
 
         asset_results = connection.execute(
             sa.select(
-                [
+                *[
                     t_assets.c.asset_type_name,
                 ]
             ).where(t_assets.c.id == id_)
@@ -181,7 +181,7 @@ def upgrade_data():
         else:
             market_results = connection.execute(
                 sa.select(
-                    [
+                    *[
                         t_markets.c.market_type_name,
                     ]
                 ).where(t_markets.c.id == id_)
@@ -191,7 +191,7 @@ def upgrade_data():
             else:
                 weather_sensor_results = connection.execute(
                     sa.select(
-                        [
+                        *[
                             t_weather_sensors.c.weather_sensor_type_name,
                         ]
                     ).where(t_weather_sensors.c.id == id_)
@@ -205,7 +205,7 @@ def upgrade_data():
 
         generic_asset_type_results = connection.execute(
             sa.select(
-                [
+                *[
                     t_generic_asset_types.c.id,
                 ]
             ).where(t_generic_asset_types.c.name == asset_type_name)
@@ -276,5 +276,5 @@ def get_latest_generic_asset_id(connection) -> int:
     https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.LegacyCursorResult.inserted_primary_key)
     """
     return connection.execute(
-        sa.select([t_generic_assets.c.id]).order_by(t_generic_assets.c.id)
+        sa.select(*[t_generic_assets.c.id]).order_by(t_generic_assets.c.id)
     ).fetchall()[-1][0]
diff --git a/flexmeasures/data/migrations/versions/b797328ac32d_add_user_fs_uniquifier_for_faster_auth_.py b/flexmeasures/data/migrations/versions/b797328ac32d_add_user_fs_uniquifier_for_faster_auth_.py
index fde67f095..31e11f5be 100644
--- a/flexmeasures/data/migrations/versions/b797328ac32d_add_user_fs_uniquifier_for_faster_auth_.py
+++ b/flexmeasures/data/migrations/versions/b797328ac32d_add_user_fs_uniquifier_for_faster_auth_.py
@@ -42,7 +42,7 @@ def upgrade():
         sa.Column("fs_uniquifier", sa.String),
     )
     conn = op.get_bind()
-    for row in conn.execute(sa.select([user_table.c.id])):
+    for row in conn.execute(sa.select(*[user_table.c.id])):
         conn.execute(
             user_table.update()
             .values(fs_uniquifier=uuid.uuid4().hex)
diff --git a/flexmeasures/data/migrations/versions/e690d373a3d9_copy_Power_Price_Weather_time_series_data_to_TimedBeliefs_table.py b/flexmeasures/data/migrations/versions/e690d373a3d9_copy_Power_Price_Weather_time_series_data_to_TimedBeliefs_table.py
index 48def8a3b..cdb055b7e 100644
--- a/flexmeasures/data/migrations/versions/e690d373a3d9_copy_Power_Price_Weather_time_series_data_to_TimedBeliefs_table.py
+++ b/flexmeasures/data/migrations/versions/e690d373a3d9_copy_Power_Price_Weather_time_series_data_to_TimedBeliefs_table.py
@@ -106,7 +106,7 @@ def copy_time_series_data(
 
     # Get data from old data model
     results = connection.execute(
-        sa.select([getattr(t_old_data_model.c, a) for a in mapping.keys()])
+        sa.select(*[getattr(t_old_data_model.c, a) for a in mapping.keys()])
     ).fetchall()
 
     if len(results) > 0:
diff --git a/flexmeasures/data/models/annotations.py b/flexmeasures/data/models/annotations.py
index fe19c5223..0c6a13441 100644
--- a/flexmeasures/data/models/annotations.py
+++ b/flexmeasures/data/models/annotations.py
@@ -3,6 +3,7 @@
 from datetime import timedelta
 
 import pandas as pd
+from sqlalchemy import select
 
 from flexmeasures.data import db
 
@@ -186,17 +187,15 @@ def get_or_create_annotation(
     Return the old annotation object if it exists (and expunge the new one). Otherwise, return the new one.
     """
     with db.session.no_autoflush:
-        existing_annotation = (
-            db.session.query(Annotation)
-            .filter(
+        existing_annotation = db.session.execute(
+            select(Annotation).filter(
                 Annotation.content == annotation.content,
                 Annotation.start == annotation.start,
                 Annotation.end == annotation.end,
                 Annotation.source == annotation.source,
                 Annotation.type == annotation.type,
             )
-            .one_or_none()
-        )
+        ).scalar_one_or_none()
     if existing_annotation is None:
         db.session.add(annotation)
         return annotation
diff --git a/flexmeasures/data/models/data_sources.py b/flexmeasures/data/models/data_sources.py
index c0b1c669b..679d55369 100644
--- a/flexmeasures/data/models/data_sources.py
+++ b/flexmeasures/data/models/data_sources.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 import json
-from typing import TYPE_CHECKING, Any, List, Dict
+from typing import TYPE_CHECKING, Any, List, Dict, ClassVar
 from sqlalchemy.ext.mutable import MutableDict
 
 import timely_beliefs as tb
@@ -226,7 +226,7 @@ class DataSource(db.Model, tb.BeliefSourceDBMixin):
         viewonly=True,
     )
 
-    _data_generator: DataGenerator | None = None
+    _data_generator: ClassVar[DataGenerator | None] = None
 
     def __init__(
         self,
diff --git a/flexmeasures/data/models/forecasting/utils.py b/flexmeasures/data/models/forecasting/utils.py
index 17273b3aa..89e942a4d 100644
--- a/flexmeasures/data/models/forecasting/utils.py
+++ b/flexmeasures/data/models/forecasting/utils.py
@@ -1,6 +1,8 @@
 from typing import Tuple, List, Union
 from datetime import datetime, timedelta
+from sqlalchemy import select
 
+from flexmeasures.data import db
 from flexmeasures.data.models.forecasting.exceptions import NotEnoughDataException
 from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.utils.time_utils import as_server_time
@@ -18,11 +20,17 @@ def check_data_availability(
     for training window and lagged variables. Otherwise, suggest new forecast period.
     TODO: we could also check regressor data, if we get regressor specs passed in here.
     """
-    q = old_time_series_data_model.query.join(old_sensor_model.__class__).filter(
-        old_sensor_model.__class__.name == old_sensor_model.name
+    q = (
+        select(old_time_series_data_model)
+        .join(old_sensor_model.__class__)
+        .filter(old_sensor_model.__class__.name == old_sensor_model.name)
     )
-    first_value = q.order_by(old_time_series_data_model.event_start.asc()).first()
-    last_value = q.order_by(old_time_series_data_model.event_start.desc()).first()
+    first_value = db.session.scalars(
+        q.order_by(old_time_series_data_model.event_start.asc()).limit(1)
+    ).first()
+    last_value = db.session.scalars(
+        q.order_by(old_time_series_data_model.event_start.desc()).limit(1)
+    ).first()
     if first_value is None:
         raise NotEnoughDataException(
             "No data available at all. Forecasting impossible."
diff --git a/flexmeasures/data/models/generic_assets.py b/flexmeasures/data/models/generic_assets.py
index 721ebdf6b..ce04ba89d 100644
--- a/flexmeasures/data/models/generic_assets.py
+++ b/flexmeasures/data/models/generic_assets.py
@@ -7,9 +7,10 @@
 from flask import current_app
 from flask_security import current_user
 import pandas as pd
+from sqlalchemy import select
 from sqlalchemy.engine import Row
 from sqlalchemy.ext.hybrid import hybrid_method
-from sqlalchemy.sql.expression import func
+from sqlalchemy.sql.expression import func, text
 from sqlalchemy.ext.mutable import MutableDict
 from timely_beliefs import BeliefsDataFrame, utils as tb_utils
 
@@ -238,12 +239,14 @@ def search_annotations(
         :param annotations_before: only return annotations that start before this datetime (exclusive)
         """
         parsed_sources = parse_source_arg(source)
-        annotations = query_asset_annotations(
-            asset_id=self.id,
-            annotations_after=annotations_after,
-            annotations_before=annotations_before,
-            sources=parsed_sources,
-            annotation_type=annotation_type,
+        annotations = db.session.scalars(
+            query_asset_annotations(
+                asset_id=self.id,
+                annotations_after=annotations_after,
+                annotations_before=annotations_before,
+                sources=parsed_sources,
+                annotation_type=annotation_type,
+            )
         ).all()
         if include_account_annotations and self.owner is not None:
             annotations += self.owner.search_annotations(
@@ -483,7 +486,7 @@ def sensors_to_show(self) -> list["Sensor" | list["Sensor"]]:  # noqa F821
         if current_app.config.get("FLEXMEASURES_MODE") == "play":
             from flexmeasures.data.models.user import Account
 
-            accounts = Account.query.all()
+            accounts = db.session.scalars(select(Account)).all()
 
         from flexmeasures.data.services.sensors import get_sensors
 
@@ -596,9 +599,9 @@ def create_generic_asset(generic_asset_type: str, **kwargs) -> GenericAsset:
             asset_type_name = kwargs.pop(f"{generic_asset_type}_type").name
         else:
             asset_type_name = kwargs.pop("sensor_type").name
-    generic_asset_type = GenericAssetType.query.filter_by(
-        name=asset_type_name
-    ).one_or_none()
+    generic_asset_type = db.session.execute(
+        select(GenericAssetType).filter_by(name=asset_type_name)
+    ).scalar_one_or_none()
     if generic_asset_type is None:
         raise ValueError(f"Cannot find GenericAssetType {asset_type_name} in database.")
 
@@ -638,7 +641,7 @@ def get_center_location_of_assets(user: Optional[User]) -> Tuple[float, float]:
     if user is None:
         user = current_user
     query += f" where generic_asset.account_id = {user.account_id}"
-    locations: List[Row] = db.session.execute(query + ";").fetchall()
+    locations: List[Row] = db.session.execute(text(query + ";")).fetchall()
     if (
         len(locations) == 0
         or locations[0].latitude is None
diff --git a/flexmeasures/data/models/legacy_migration_utils.py b/flexmeasures/data/models/legacy_migration_utils.py
index f2360a427..984617f8e 100644
--- a/flexmeasures/data/models/legacy_migration_utils.py
+++ b/flexmeasures/data/models/legacy_migration_utils.py
@@ -54,8 +54,8 @@ def get_old_model_type(
     :returns: the old sensor's type
     """
     if old_sensor_type_name_key in kwargs:
-        old_sensor_type = db.session.query(old_sensor_type_class).get(
-            kwargs[old_sensor_type_name_key]
+        old_sensor_type = db.session.get(
+            old_sensor_type_class, kwargs[old_sensor_type_name_key]
         )
     else:
         old_sensor_type = kwargs[old_sensor_type_key]
diff --git a/flexmeasures/data/models/parsing_utils.py b/flexmeasures/data/models/parsing_utils.py
index 19bcb03a1..f36914559 100644
--- a/flexmeasures/data/models/parsing_utils.py
+++ b/flexmeasures/data/models/parsing_utils.py
@@ -2,6 +2,7 @@
 
 from collections.abc import Sequence
 
+from sqlalchemy import select
 from flask import current_app
 from flexmeasures.data import db
 
@@ -31,9 +32,7 @@ def parse_source_arg(
     parsed_sources: list[DataSource] = []
     for source in sources:
         if isinstance(source, int):
-            parsed_source = (
-                db.session.query(DataSource).filter_by(id=source).one_or_none()
-            )
+            parsed_source = db.session.get(DataSource, source)
             if parsed_source is None:
                 current_app.logger.warning(
                     f"Beliefs searched for unknown source {source}"
@@ -41,7 +40,9 @@ def parse_source_arg(
             else:
                 parsed_sources.append(parsed_source)
         elif isinstance(source, str):
-            _parsed_sources = db.session.query(DataSource).filter_by(name=source).all()
+            _parsed_sources = db.session.scalars(
+                select(DataSource).filter_by(name=source)
+            ).all()
             if _parsed_sources is []:
                 current_app.logger.warning(
                     f"Beliefs searched for unknown source {source}"
diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py
index ba5541504..efbf16ba0 100644
--- a/flexmeasures/data/models/planning/utils.py
+++ b/flexmeasures/data/models/planning/utils.py
@@ -10,6 +10,7 @@
 import numpy as np
 import timely_beliefs as tb
 
+from flexmeasures.data import db
 from flexmeasures.data.models.time_series import Sensor, TimedBelief
 from flexmeasures.data.models.planning.exceptions import (
     UnknownMarketException,
@@ -90,7 +91,7 @@ def add_tiny_price_slope(
 
 def get_market(sensor: Sensor) -> Sensor:
     """Get market sensor from the sensor's attributes."""
-    price_sensor = Sensor.query.get(sensor.get_attribute("market_id"))
+    price_sensor = db.session.get(Sensor, sensor.get_attribute("market_id"))
     if price_sensor is None:
         raise UnknownMarketException
     return price_sensor
diff --git a/flexmeasures/data/models/task_runs.py b/flexmeasures/data/models/task_runs.py
index 3e63889d9..1c3f0d20b 100644
--- a/flexmeasures/data/models/task_runs.py
+++ b/flexmeasures/data/models/task_runs.py
@@ -1,6 +1,8 @@
 from datetime import datetime
 import pytz
 
+from sqlalchemy import select
+
 from flexmeasures.data import db
 
 
@@ -31,9 +33,9 @@ def record_run(task_name: str, status: bool):
         If the row is not yet in the table, create it first.
         Does not commit.
         """
-        task_run = LatestTaskRun.query.filter(
-            LatestTaskRun.name == task_name
-        ).one_or_none()
+        task_run = db.session.execute(
+            select(LatestTaskRun).filter(LatestTaskRun.name == task_name)
+        ).scalar_one_or_none()
         if task_run is None:
             task_run = LatestTaskRun(name=task_name)
             db.session.add(task_run)
diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py
index 147bf6902..cae6d8259 100644
--- a/flexmeasures/data/models/time_series.py
+++ b/flexmeasures/data/models/time_series.py
@@ -6,6 +6,7 @@
 from flask import current_app
 
 import pandas as pd
+from sqlalchemy import select
 from sqlalchemy.ext.declarative import declared_attr
 from sqlalchemy.ext.mutable import MutableDict
 from sqlalchemy.schema import UniqueConstraint
@@ -257,9 +258,13 @@ def search_annotations(
         )
 
         parsed_sources = parse_source_arg(source)
-        query = Annotation.query.join(SensorAnnotationRelationship).filter(
-            SensorAnnotationRelationship.sensor_id == self.id,
-            SensorAnnotationRelationship.annotation_id == Annotation.id,
+        query = (
+            select(Annotation)
+            .join(SensorAnnotationRelationship)
+            .filter(
+                SensorAnnotationRelationship.sensor_id == self.id,
+                SensorAnnotationRelationship.annotation_id == Annotation.id,
+            )
         )
         if annotations_after is not None:
             query = query.filter(
@@ -273,7 +278,7 @@ def search_annotations(
             query = query.filter(
                 Annotation.source.in_(parsed_sources),
             )
-        annotations = query.all()
+        annotations = db.session.scalars(query).all()
         if include_asset_annotations:
             annotations += self.generic_asset.search_annotations(
                 annotations_after=annotations_after,
@@ -526,9 +531,9 @@ def find_closest(
             account_id=account_id_filter,
         )
         if n == 1:
-            return query.first()
+            return db.session.scalar(query.limit(1)).first()
         else:
-            return query.limit(n).all()
+            return db.session.scalars(query.limit(n)).all()
 
     def make_hashable(self) -> tuple:
         """Returns a tuple with the properties subject to change
@@ -577,7 +582,7 @@ def __init__(
         if (
             inspection_obj and inspection_obj.detached
         ):  # fetch Sensor only when it is detached
-            sensor = Sensor.query.get(sensor.id)
+            sensor = db.session.get(Sensor, sensor.id)
 
         tb.TimedBeliefDBMixin.__init__(self, sensor, source, **kwargs)
         tb_utils.remove_class_init_kwargs(tb.TimedBeliefDBMixin, kwargs)
@@ -664,8 +669,8 @@ def search(
         sensor_names = [s for s in sensors if isinstance(s, str)]
         if sensor_names:
             sensors = [s for s in sensors if not isinstance(s, str)]
-            sensors_from_names = Sensor.query.filter(
-                Sensor.name.in_(sensor_names)
+            sensors_from_names = db.session.scalars(
+                select(Sensor).filter(Sensor.name.in_(sensor_names))
             ).all()
             sensors.extend(sensors_from_names)
 
diff --git a/flexmeasures/data/models/user.py b/flexmeasures/data/models/user.py
index 1119d1fd5..a18cf8990 100644
--- a/flexmeasures/data/models/user.py
+++ b/flexmeasures/data/models/user.py
@@ -4,6 +4,7 @@
 
 from flask_security import UserMixin, RoleMixin
 import pandas as pd
+from sqlalchemy import select
 from sqlalchemy.orm import relationship, backref
 from sqlalchemy import Boolean, DateTime, Column, Integer, String, ForeignKey
 from sqlalchemy.ext.hybrid import hybrid_property
@@ -145,9 +146,13 @@ def search_annotations(
         )
 
         parsed_sources = parse_source_arg(source)
-        query = Annotation.query.join(AccountAnnotationRelationship).filter(
-            AccountAnnotationRelationship.account_id == self.id,
-            AccountAnnotationRelationship.annotation_id == Annotation.id,
+        query = (
+            select(Annotation)
+            .join(AccountAnnotationRelationship)
+            .filter(
+                AccountAnnotationRelationship.account_id == self.id,
+                AccountAnnotationRelationship.annotation_id == Annotation.id,
+            )
         )
         if annotations_after is not None:
             query = query.filter(
@@ -161,7 +166,7 @@ def search_annotations(
             query = query.filter(
                 Annotation.source.in_(parsed_sources),
             )
-        annotations = query.all()
+        annotations = db.session.scalars(query).all()
 
         return to_annotation_frame(annotations) if as_frame else annotations
 
diff --git a/flexmeasures/data/queries/annotations.py b/flexmeasures/data/queries/annotations.py
index d41854c56..0a56e571c 100644
--- a/flexmeasures/data/queries/annotations.py
+++ b/flexmeasures/data/queries/annotations.py
@@ -2,6 +2,7 @@
 
 from datetime import datetime
 
+from sqlalchemy import select
 from sqlalchemy.orm import Query
 
 from flexmeasures.data.models.annotations import (
@@ -19,10 +20,15 @@ def query_asset_annotations(
     annotation_type: str | None = None,
 ) -> Query:
     """Match annotations assigned to the given asset."""
-    query = Annotation.query.join(GenericAssetAnnotationRelationship).filter(
-        GenericAssetAnnotationRelationship.generic_asset_id == asset_id,
-        GenericAssetAnnotationRelationship.annotation_id == Annotation.id,
+    query = (
+        select(Annotation)
+        .join(GenericAssetAnnotationRelationship)
+        .filter(
+            GenericAssetAnnotationRelationship.generic_asset_id == asset_id,
+            GenericAssetAnnotationRelationship.annotation_id == Annotation.id,
+        )
     )
+
     if annotations_after is not None:
         query = query.filter(
             Annotation.end > annotations_after,
diff --git a/flexmeasures/data/queries/generic_assets.py b/flexmeasures/data/queries/generic_assets.py
index 4e526a2c7..62517bc87 100644
--- a/flexmeasures/data/queries/generic_assets.py
+++ b/flexmeasures/data/queries/generic_assets.py
@@ -1,8 +1,10 @@
-from typing import List, Union, Optional, Dict
+from __future__ import annotations
+
 from itertools import groupby
 from flask_login import current_user
 
-from sqlalchemy.orm import Query
+from sqlalchemy import select, Select
+from flexmeasures.data import db
 from flexmeasures.auth.policy import user_has_admin_access
 
 from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
@@ -12,10 +14,10 @@
 
 
 def query_assets_by_type(
-    type_names: Union[List[str], str],
-    account_id: Optional[int] = None,
-    query: Optional[Query] = None,
-) -> Query:
+    type_names: list[str] | str,
+    account_id: int | None = None,
+    query: Select | None = None,
+) -> Select:
     """
     Return a query which looks for GenericAssets by their type.
 
@@ -24,7 +26,7 @@ def query_assets_by_type(
     :param query: Pass in an existing Query object if you have one.
     """
     if not query:
-        query = GenericAsset.query
+        query = select(GenericAsset)
     query = query.join(GenericAssetType).filter(
         GenericAsset.generic_asset_type_id == GenericAssetType.id
     )
@@ -36,7 +38,9 @@ def query_assets_by_type(
     return query
 
 
-def get_location_queries(account_id: Optional[int] = None) -> Dict[str, Query]:
+def get_location_queries(
+    account_id: int | None = None,
+) -> dict[str, Select[tuple[GenericAsset]]]:
     """
     Make queries for grouping assets by location.
 
@@ -56,8 +60,8 @@ def get_location_queries(account_id: Optional[int] = None) -> Dict[str, Query]:
     :param account_id: Pass in an account ID if you want to query an account other than your own. This only works for admins. Public assets are always queried.
     """
     asset_queries = {}
-    all_assets = potentially_limit_assets_query_to_account(
-        GenericAsset.query, account_id
+    all_assets = db.session.scalars(
+        potentially_limit_assets_query_to_account(select(GenericAsset), account_id)
     ).all()
     loc_groups = group_assets_by_location(all_assets)
     for loc_group in loc_groups:
@@ -72,7 +76,7 @@ def get_location_queries(account_id: Optional[int] = None) -> Dict[str, Query]:
         ):
             location_type = "(Charge Point)"
         location_name = f"{loc_group[0].name.split(' -')[0]} {location_type}"
-        location_query = GenericAsset.query.filter(
+        location_query = select(GenericAsset).filter(
             GenericAsset.name.in_([asset.name for asset in loc_group])
         )
         asset_queries[location_name] = potentially_limit_assets_query_to_account(
@@ -82,8 +86,8 @@ def get_location_queries(account_id: Optional[int] = None) -> Dict[str, Query]:
 
 
 def group_assets_by_location(
-    asset_list: List[GenericAsset],
-) -> List[List[GenericAsset]]:
+    asset_list: list[GenericAsset],
+) -> list[list[GenericAsset]]:
     groups = []
 
     def key_function(x):
@@ -99,8 +103,8 @@ def get_asset_group_queries(
     group_by_type: bool = True,
     group_by_account: bool = False,
     group_by_location: bool = False,
-    custom_aggregate_type_groups: Optional[Dict[str, List[str]]] = None,
-) -> Dict[str, Query]:
+    custom_aggregate_type_groups: dict[str, list[str]] | None = None,
+) -> dict[str, Select]:
     """
     An asset group is defined by Asset queries, which this function can generate.
     Each query has a name (for the asset group it represents).
@@ -122,16 +126,16 @@ def get_asset_group_queries(
 
     # 2. Include a group per asset type - using the pluralised asset type name
     if group_by_type:
-        for asset_type in GenericAssetType.query.all():
+        for asset_type in db.session.scalars(select(GenericAssetType)).all():
             asset_queries[pluralize(asset_type.name)] = query_assets_by_type(
                 asset_type.name
             )
 
     # 3. Include a group per account (admins only)  # TODO: we can later adjust this for accounts who admin certain others, not all
     if group_by_account and user_has_admin_access(current_user, "read"):
-        for account in Account.query.all():
-            asset_queries[account.name] = GenericAsset.query.filter(
-                GenericAsset.account_id == account.id
+        for account in db.session.scalars(select(Account)).all():
+            asset_queries[account.name] = select(GenericAsset).filter_by(
+                account_id=account.id
             )
 
     # 4. Finally, we can group assets by location
diff --git a/flexmeasures/data/queries/sensors.py b/flexmeasures/data/queries/sensors.py
index 30650b271..731f02975 100644
--- a/flexmeasures/data/queries/sensors.py
+++ b/flexmeasures/data/queries/sensors.py
@@ -1,6 +1,6 @@
 from typing import List, Optional
 
-from sqlalchemy.orm import Query
+from sqlalchemy.sql import Select, select
 
 from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
 from flexmeasures.data.queries.utils import potentially_limit_assets_query_to_account
@@ -10,7 +10,7 @@ def query_sensor_by_name_and_generic_asset_type_name(
     sensor_name: Optional[str] = None,
     generic_asset_type_names: Optional[List[str]] = None,
     account_id: Optional[int] = None,
-) -> Query:
+) -> Select:
     """Match a sensor by its own name and that of its generic asset type.
 
     :param sensor_name: should match (if None, no match is needed)
@@ -19,7 +19,7 @@ def query_sensor_by_name_and_generic_asset_type_name(
     """
     from flexmeasures.data.models.time_series import Sensor
 
-    query = Sensor.query
+    query = select(Sensor)
     if sensor_name is not None:
         query = query.filter(Sensor.name == sensor_name)
     if generic_asset_type_names is not None:
@@ -40,12 +40,14 @@ def query_sensors_by_proximity(
     generic_asset_type_name: Optional[str],
     sensor_name: Optional[str],
     account_id: Optional[int] = None,
-) -> Query:
+) -> Select:
     """Order them by proximity of their asset's location to the target."""
     from flexmeasures.data.models.time_series import Sensor
 
-    closest_sensor_query = Sensor.query.join(GenericAsset).filter(
-        Sensor.generic_asset_id == GenericAsset.id
+    closest_sensor_query = (
+        select(Sensor)
+        .join(GenericAsset)
+        .filter(Sensor.generic_asset_id == GenericAsset.id)
     )
     if generic_asset_type_name:
         closest_sensor_query = closest_sensor_query.join(GenericAssetType)
diff --git a/flexmeasures/data/queries/utils.py b/flexmeasures/data/queries/utils.py
index 5e769a48b..3044240d1 100644
--- a/flexmeasures/data/queries/utils.py
+++ b/flexmeasures/data/queries/utils.py
@@ -1,13 +1,16 @@
-from typing import List, Optional, Type, Tuple, Union
+from __future__ import annotations
+
+from typing import Type
 from datetime import datetime, timedelta
 
 from flask_security import current_user
 from werkzeug.exceptions import Forbidden
 import pandas as pd
 import timely_beliefs as tb
-from sqlalchemy.orm import Query, Session
+from sqlalchemy.orm import Session
 from sqlalchemy.sql.elements import BinaryExpression, or_
 from sqlalchemy.sql.expression import null
+from sqlalchemy import select, Select
 
 from flexmeasures.data.config import db
 from flexmeasures.data.models.generic_assets import GenericAsset
@@ -22,14 +25,12 @@ def create_beliefs_query(
     cls: "Type[ts.TimedValue]",
     session: Session,
     old_sensor_class: db.Model,
-    old_sensor_names: Tuple[str],
-    start: Optional[datetime],
-    end: Optional[datetime],
-) -> Query:
+    old_sensor_names: tuple[str],
+    start: datetime | None,
+    end: datetime | None,
+) -> Select:
     query = (
-        session.query(
-            old_sensor_class.name, cls.datetime, cls.value, cls.horizon, DataSource
-        )
+        select(old_sensor_class.name, cls.datetime, cls.value, cls.horizon, DataSource)
         .join(DataSource)
         .filter(cls.data_source_id == DataSource.id)
         .join(old_sensor_class)
@@ -43,9 +44,9 @@ def create_beliefs_query(
 
 
 def potentially_limit_assets_query_to_account(
-    query: Query,
-    account_id: Optional[int] = None,
-) -> Query:
+    query: Select[tuple[GenericAsset]],
+    account_id: int | None = None,
+) -> Select[tuple[GenericAsset]]:
     """Filter out all assets that are not in the current user's account.
     For admins and CLI users, no assets are filtered out, unless an account_id is set.
 
@@ -54,7 +55,7 @@ def potentially_limit_assets_query_to_account(
     if not running_as_cli() and not current_user.is_authenticated:
         raise Forbidden("Unauthenticated user cannot list assets.")
     user_is_admin = running_as_cli() or user_has_admin_access(
-        current_user, permission="read" if query.statement.is_select else "update"
+        current_user, permission="read" if query.is_select else "update"
     )
     if account_id is None and user_is_admin:
         return query  # allow admins to query assets across all accounts
@@ -76,12 +77,12 @@ def potentially_limit_assets_query_to_account(
 
 
 def get_source_criteria(
-    cls: "Union[Type[ts.TimedValue], Type[ts.TimedBelief]]",
-    user_source_ids: Union[int, List[int]],
-    source_types: List[str],
-    exclude_source_types: List[str],
-) -> List[BinaryExpression]:
-    source_criteria: List[BinaryExpression] = []
+    cls: "Type[ts.TimedValue] | Type[ts.TimedBelief]",
+    user_source_ids: int | list[int],
+    source_types: list[str],
+    exclude_source_types: list[str],
+) -> list[BinaryExpression]:
+    source_criteria: list[BinaryExpression] = []
     if user_source_ids is not None:
         source_criteria.append(user_source_criterion(cls, user_source_ids))
     if source_types is not None:
@@ -96,8 +97,8 @@ def get_source_criteria(
 
 
 def user_source_criterion(
-    cls: "Union[Type[ts.TimedValue], Type[ts.TimedBelief]]",
-    user_source_ids: Union[int, List[int]],
+    cls: "Type[ts.TimedValue] | Type[ts.TimedBelief]",
+    user_source_ids: int | list[int],
 ) -> BinaryExpression:
     """Criterion to search only through user data from the specified user sources.
 
@@ -109,11 +110,11 @@ def user_source_criterion(
     """
     if user_source_ids is not None and not isinstance(user_source_ids, list):
         user_source_ids = [user_source_ids]  # ensure user_source_ids is a list
-    ignorable_user_sources = (
-        DataSource.query.filter(DataSource.type == "user")
+    ignorable_user_sources = db.session.scalars(
+        select(DataSource)
+        .filter(DataSource.type == "user")
         .filter(DataSource.id.not_in(user_source_ids))
-        .all()
-    )
+    ).all()
     ignorable_user_source_ids = [
         user_source.id for user_source in ignorable_user_sources
     ]
@@ -124,12 +125,12 @@ def user_source_criterion(
     return cls.source_id.not_in(ignorable_user_source_ids)
 
 
-def source_type_criterion(source_types: List[str]) -> BinaryExpression:
+def source_type_criterion(source_types: list[str]) -> BinaryExpression:
     """Criterion to collect only data from sources that are of the given type."""
     return DataSource.type.in_(source_types)
 
 
-def source_type_exclusion_criterion(source_types: List[str]) -> BinaryExpression:
+def source_type_exclusion_criterion(source_types: list[str]) -> BinaryExpression:
     """Criterion to exclude sources that are of the given type."""
     return DataSource.type.not_in(source_types)
 
@@ -137,9 +138,9 @@ def source_type_exclusion_criterion(source_types: List[str]) -> BinaryExpression
 def get_belief_timing_criteria(
     cls: "Type[ts.TimedValue]",
     asset_class: db.Model,
-    belief_horizon_window: Tuple[Optional[timedelta], Optional[timedelta]],
-    belief_time_window: Tuple[Optional[datetime], Optional[datetime]],
-) -> List[BinaryExpression]:
+    belief_horizon_window: tuple[timedelta | None, timedelta | None],
+    belief_time_window: tuple[datetime | None, datetime | None],
+) -> list[BinaryExpression]:
     """Get filter criteria for the desired windows with relevant belief times and belief horizons.
 
     # todo: interpret belief horizons with respect to knowledge time rather than event end.
@@ -178,7 +179,7 @@ def get_belief_timing_criteria(
         belief_time_window = (None, datetime(2020, 5, 13))
 
     """
-    criteria: List[BinaryExpression] = []
+    criteria: list[BinaryExpression] = []
     earliest_belief_time, latest_belief_time = belief_time_window
     if (
         earliest_belief_time is not None
@@ -216,7 +217,7 @@ def get_belief_timing_criteria(
 
 
 def simplify_index(
-    bdf: tb.BeliefsDataFrame, index_levels_to_columns: Optional[List[str]] = None
+    bdf: tb.BeliefsDataFrame, index_levels_to_columns: list[str] | None = None
 ) -> pd.DataFrame:
     """Drops indices other than event_start.
     Optionally, salvage index levels as new columns.
@@ -247,7 +248,7 @@ def multiply_dataframe_with_deterministic_beliefs(
     df1: pd.DataFrame,
     df2: pd.DataFrame,
     multiplication_factor: float = 1,
-    result_source: Optional[str] = None,
+    result_source: str | None = None,
 ) -> pd.DataFrame:
     """
     Create new DataFrame where the event_value columns of df1 and df2 are multiplied.
diff --git a/flexmeasures/data/schemas/account.py b/flexmeasures/data/schemas/account.py
index 1da4a4993..a1c25ce2c 100644
--- a/flexmeasures/data/schemas/account.py
+++ b/flexmeasures/data/schemas/account.py
@@ -2,6 +2,7 @@
 from flexmeasures.data import ma
 from marshmallow import fields
 
+from flexmeasures.data import db
 from flexmeasures.data.models.user import (
     Account as AccountModel,
     AccountRole as AccountRoleModel,
@@ -38,7 +39,7 @@ class AccountIdField(fields.Int, MarshmallowClickMixin):
     @with_appcontext
     def _deserialize(self, value, attr, obj, **kwargs) -> AccountModel:
         """Turn an account id into an Account."""
-        account = AccountModel.query.get(value)
+        account = db.session.get(AccountModel, value)
         if account is None:
             raise FMValidationError(f"No account found with id {value}.")
         # lazy loading now (account somehow is not in the session after this)
diff --git a/flexmeasures/data/schemas/generic_assets.py b/flexmeasures/data/schemas/generic_assets.py
index 8df5d38fe..745020556 100644
--- a/flexmeasures/data/schemas/generic_assets.py
+++ b/flexmeasures/data/schemas/generic_assets.py
@@ -4,8 +4,9 @@
 
 from marshmallow import validates, ValidationError, fields, validates_schema
 from flask_security import current_user
+from sqlalchemy import select
 
-from flexmeasures.data import ma
+from flexmeasures.data import ma, db
 from flexmeasures.data.models.user import Account
 from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
 from flexmeasures.data.schemas.locations import LatitudeField, LongitudeField
@@ -52,10 +53,14 @@ class Meta:
     def validate_name_is_unique_under_parent(self, data, **kwargs):
         if "name" in data:
 
-            asset = GenericAsset.query.filter(
-                GenericAsset.name == data["name"],
-                GenericAsset.parent_asset_id == data.get("parent_asset_id"),
-                GenericAsset.account_id == data.get("account_id"),
+            asset = db.session.scalars(
+                select(GenericAsset)
+                .filter_by(
+                    name=data["name"],
+                    parent_asset_id=data.get("parent_asset_id"),
+                    account_id=data.get("account_id"),
+                )
+                .limit(1)
             ).first()
 
             if asset:
@@ -66,7 +71,7 @@ def validate_name_is_unique_under_parent(self, data, **kwargs):
 
     @validates("generic_asset_type_id")
     def validate_generic_asset_type(self, generic_asset_type_id: int):
-        generic_asset_type = GenericAssetType.query.get(generic_asset_type_id)
+        generic_asset_type = db.session.get(GenericAssetType, generic_asset_type_id)
         if not generic_asset_type:
             raise ValidationError(
                 f"GenericAssetType with id {generic_asset_type_id} doesn't exist."
@@ -75,7 +80,7 @@ def validate_generic_asset_type(self, generic_asset_type_id: int):
     @validates("parent_asset_id")
     def validate_parent_asset(self, parent_asset_id: int | None):
         if parent_asset_id is not None:
-            parent_asset = GenericAsset.query.get(parent_asset_id)
+            parent_asset = db.session.get(GenericAsset, parent_asset_id)
             if not parent_asset:
                 raise ValidationError(
                     f"Parent GenericAsset with id {parent_asset_id} doesn't exist."
@@ -87,7 +92,7 @@ def validate_account(self, account_id: int | None):
             running_as_cli() or user_has_admin_access(current_user, "update")
         ):
             return
-        account = Account.query.get(account_id)
+        account = db.session.get(Account, account_id)
         if not account:
             raise ValidationError(f"Account with Id {account_id} doesn't exist.")
         if not running_as_cli() and (
@@ -144,7 +149,7 @@ class GenericAssetIdField(MarshmallowClickMixin, fields.Int):
     @with_appcontext_if_needed()
     def _deserialize(self, value, attr, obj, **kwargs) -> GenericAsset:
         """Turn a generic asset id into a GenericAsset."""
-        generic_asset = GenericAsset.query.get(value)
+        generic_asset = db.session.get(GenericAsset, value)
         if generic_asset is None:
             raise FMValidationError(f"No asset found with id {value}.")
         # lazy loading now (asset is somehow not in session after this)
diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py
index da597b2af..2f59fcf4e 100644
--- a/flexmeasures/data/schemas/sensors.py
+++ b/flexmeasures/data/schemas/sensors.py
@@ -5,7 +5,7 @@
 import json
 import re
 
-from flexmeasures.data import ma
+from flexmeasures.data import ma, db
 from flexmeasures.data.models.generic_assets import GenericAsset
 from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.schemas.utils import (
@@ -65,7 +65,7 @@ class SensorSchema(SensorSchemaMixin, ma.SQLAlchemySchema):
 
     @validates("generic_asset_id")
     def validate_generic_asset(self, generic_asset_id: int):
-        generic_asset = GenericAsset.query.get(generic_asset_id)
+        generic_asset = db.session.get(GenericAsset, generic_asset_id)
         if not generic_asset:
             raise ValidationError(
                 f"Generic asset with id {generic_asset_id} doesn't exist."
@@ -81,7 +81,7 @@ class SensorIdField(MarshmallowClickMixin, fields.Int):
     @with_appcontext_if_needed()
     def _deserialize(self, value: int, attr, obj, **kwargs) -> Sensor:
         """Turn a sensor id into a Sensor."""
-        sensor = Sensor.query.get(value)
+        sensor = db.session.get(Sensor, value)
         if sensor is None:
             raise FMValidationError(f"No sensor found with id {value}.")
         # lazy loading now (sensor is somehow not in session after this)
@@ -117,7 +117,7 @@ def _deserialize(
                     "Dictionary provided but `sensor` key not found."
                 )
 
-            sensor = Sensor.query.get(value["sensor"])
+            sensor = db.session.get(Sensor, value["sensor"])
 
             if sensor is None:
                 raise FMValidationError(f"No sensor found with id {value['sensor']}.")
diff --git a/flexmeasures/data/schemas/sources.py b/flexmeasures/data/schemas/sources.py
index 993f5f716..423b2e628 100644
--- a/flexmeasures/data/schemas/sources.py
+++ b/flexmeasures/data/schemas/sources.py
@@ -1,5 +1,6 @@
 from marshmallow import fields
 
+from flexmeasures.data import db
 from flexmeasures.data.models.data_sources import DataSource
 from flexmeasures.data.schemas.utils import (
     with_appcontext_if_needed,
@@ -14,7 +15,7 @@ class DataSourceIdField(fields.Int, MarshmallowClickMixin):
     @with_appcontext_if_needed()
     def _deserialize(self, value, attr, obj, **kwargs) -> DataSource:
         """Turn a source id into a DataSource."""
-        source = DataSource.query.get(value)
+        source = db.session.get(DataSource, value)
         if source is None:
             raise FMValidationError(f"No data source found with id {value}.")
         return source
diff --git a/flexmeasures/data/scripts/data_gen.py b/flexmeasures/data/scripts/data_gen.py
index dc64db0b5..8563b1551 100644
--- a/flexmeasures/data/scripts/data_gen.py
+++ b/flexmeasures/data/scripts/data_gen.py
@@ -10,7 +10,7 @@
 from flask import current_app as app
 from flask_sqlalchemy import SQLAlchemy
 import click
-from sqlalchemy import func, and_
+from sqlalchemy import func, and_, select, delete
 from sqlalchemy.exc import SQLAlchemyError
 from sqlalchemy.ext.serializer import loads, dumps
 from timetomodel.forecasting import make_rolling_forecasts
@@ -41,9 +41,11 @@ def add_default_data_sources(db: SQLAlchemy):
         ("Seita", "forecaster"),
         ("Seita", "scheduler"),
     ):
-        source = DataSource.query.filter(
-            and_(DataSource.name == source_name, DataSource.type == source_type)
-        ).one_or_none()
+        source = db.session.execute(
+            select(DataSource).filter(
+                and_(DataSource.name == source_name, DataSource.type == source_type)
+            )
+        ).scalar_one_or_none()
         if source:
             click.echo(f"Source {source_name} ({source_type}) already exists.")
         else:
@@ -64,9 +66,9 @@ def add_default_asset_types(db: SQLAlchemy) -> Dict[str, GenericAssetType]:
         ("building", "building"),
         ("process", "process"),
     ):
-        _type = GenericAssetType.query.filter(
-            GenericAssetType.name == type_name
-        ).one_or_none()
+        _type = db.session.execute(
+            select(GenericAssetType).filter_by(name=type_name)
+        ).scalar_one_or_none()
         if _type is None:
             _type = GenericAssetType(name=type_name, description=type_description)
             db.session.add(_type)
@@ -89,7 +91,9 @@ def add_default_user_roles(db: SQLAlchemy):
         ("account-admin", "Can post and edit sensors and assets in their account"),
         ("consultant", "Can read everything in consultancy client accounts"),
     ):
-        role = Role.query.filter(Role.name == role_name).one_or_none()
+        role = db.session.execute(
+            select(Role).filter_by(name=role_name)
+        ).scalar_one_or_none()
         if role:
             click.echo(f"Role {role_name} already exists.")
         else:
@@ -107,7 +111,9 @@ def add_default_account_roles(db: SQLAlchemy):
         ("Aggregator", "Aggregator of energy flexibility"),
         ("ESCO", "Energy Service Company"),
     ):
-        role = AccountRole.query.filter(AccountRole.name == role_name).one_or_none()
+        role = db.session.execute(
+            select(AccountRole).filter_by(name=role_name)
+        ).scalar_one_or_none()
         if role:
             click.echo(f"Account role {role_name} already exists.")
         else:
@@ -118,9 +124,9 @@ def add_transmission_zone_asset(country_code: str, db: SQLAlchemy) -> GenericAss
     """
     Ensure a GenericAsset exists to model a transmission zone for a country.
     """
-    transmission_zone_type = GenericAssetType.query.filter(
-        GenericAssetType.name == "transmission zone"
-    ).one_or_none()
+    transmission_zone_type = db.session.execute(
+        select(GenericAssetType).filter_by(name="transmission zone")
+    ).scalar_one_or_none()
     if not transmission_zone_type:
         click.echo("Adding transmission zone type ...")
         transmission_zone_type = GenericAssetType(
@@ -129,9 +135,9 @@ def add_transmission_zone_asset(country_code: str, db: SQLAlchemy) -> GenericAss
         )
         db.session.add(transmission_zone_type)
     ga_name = f"{country_code} transmission zone"
-    transmission_zone = GenericAsset.query.filter(
-        GenericAsset.name == ga_name
-    ).one_or_none()
+    transmission_zone = db.session.execute(
+        select(GenericAsset).filter_by(name=ga_name)
+    ).scalar_one_or_none()
     if not transmission_zone:
         click.echo(f"Adding {ga_name} ...")
         transmission_zone = GenericAsset(
@@ -139,6 +145,7 @@ def add_transmission_zone_asset(country_code: str, db: SQLAlchemy) -> GenericAss
             generic_asset_type=transmission_zone_type,
             account_id=None,  # public
         )
+        db.session.add(transmission_zone)
     return transmission_zone
 
 
@@ -156,12 +163,23 @@ def populate_initial_structure(db: SQLAlchemy):
     add_default_user_roles(db)
     add_default_account_roles(db)
     add_default_asset_types(db)
-    click.echo("DB now has %d DataSource(s)" % db.session.query(DataSource).count())
     click.echo(
-        "DB now has %d AssetType(s)" % db.session.query(GenericAssetType).count()
+        "DB now has %d DataSource(s)"
+        % db.session.scalar(select(func.count()).select_from(DataSource))
+    )
+
+    click.echo(
+        "DB now has %d AssetType(s)"
+        % db.session.scalar(select(func.count()).select_from(GenericAssetType))
+    )
+    click.echo(
+        "DB now has %d Role(s) for users"
+        % db.session.scalar(select(func.count()).select_from(Role))
+    )
+    click.echo(
+        "DB now has %d AccountRole(s)"
+        % db.session.scalar(select(func.count()).select_from(AccountRole))
     )
-    click.echo("DB now has %d Role(s) for users" % db.session.query(Role).count())
-    click.echo("DB now has %d AccountRole(s)" % db.session.query(AccountRole).count())
 
 
 @as_transaction  # noqa: C901
@@ -181,12 +199,15 @@ def populate_time_series_forecasts(  # noqa: C901
     )
 
     # Set a data source for the forecasts
-    data_source = DataSource.query.filter_by(
-        name="Seita", type="demo script"
-    ).one_or_none()
-
+    data_source = db.session.execute(
+        select(DataSource).filter_by(name="Seita", type="demo script")
+    ).scalar_one_or_none()
     # List all sensors for which to forecast.
-    sensors = [Sensor.query.filter(Sensor.id.in_(sensor_ids)).one_or_none()]
+    sensors = [
+        db.session.execute(
+            select(Sensor).filter(Sensor.id.in_(sensor_ids))
+        ).scalar_one_or_none()
+    ]
     if not sensors:
         click.echo("No such sensors in db, so I will not add any forecasts.")
         return
@@ -254,28 +275,23 @@ def populate_time_series_forecasts(  # noqa: C901
 
     click.echo(
         "DB now has %d forecasts"
-        % db.session.query(TimedBelief)
-        .filter(TimedBelief.belief_horizon > timedelta(hours=0))
-        .count()
+        % db.session.scalar(
+            select(func.count())
+            .select_from(TimedBelief)
+            .filter(TimedBelief.belief_horizon > timedelta(hours=0))
+        )
     )
 
 
 @as_transaction
 def depopulate_structure(db: SQLAlchemy):
     click.echo("Depopulating structural data from the database %s ..." % db.engine)
-    num_assets_deleted = db.session.query(GenericAsset).delete()
-    num_asset_types_deleted = db.session.query(GenericAssetType).delete()
-    num_data_sources_deleted = db.session.query(DataSource).delete()
-    roles = db.session.query(Role).all()
-    num_roles_deleted = 0
-    for role in roles:
-        db.session.delete(role)
-        num_roles_deleted += 1
-    users = db.session.query(User).all()
-    num_users_deleted = 0
-    for user in users:
-        db.session.delete(user)
-        num_users_deleted += 1
+    num_assets_deleted = db.session.execute(delete(GenericAsset))
+    num_asset_types_deleted = db.session.execute(delete(GenericAssetType))
+
+    num_data_sources_deleted = db.session.execute(delete(DataSource))
+    num_roles_deleted = db.session.execute(delete(Role))
+    num_users_deleted = db.session.execute(delete(User))
     click.echo("Deleted %d AssetTypes" % num_asset_types_deleted)
     click.echo("Deleted %d Assets" % num_assets_deleted)
     click.echo("Deleted %d DataSources" % num_data_sources_deleted)
@@ -290,12 +306,10 @@ def depopulate_measurements(
 ):
     click.echo("Deleting (time series) data from the database %s ..." % db.engine)
 
-    query = db.session.query(TimedBelief).filter(
-        TimedBelief.belief_horizon <= timedelta(hours=0)
-    )
+    query = delete(TimedBelief).filter(TimedBelief.belief_horizon <= timedelta(hours=0))
     if sensor_id is not None:
         query = query.filter(TimedBelief.sensor_id == sensor_id)
-    num_measurements_deleted = query.delete()
+    num_measurements_deleted = db.session.execute(query)
 
     click.echo("Deleted %d measurements (ex-post beliefs)" % num_measurements_deleted)
 
@@ -324,12 +338,11 @@ def depopulate_prognoses(
         num_scheduling_jobs_deleted = app.queues["scheduling"].empty()
 
     # Clear all forecasts (data with positive horizon)
-    query = db.session.query(TimedBelief).filter(
-        TimedBelief.belief_horizon > timedelta(hours=0)
-    )
+    query = delete(TimedBelief).filter(TimedBelief.belief_horizon > timedelta(hours=0))
+
     if sensor_id is not None:
         query = query.filter(TimedBelief.sensor_id == sensor_id)
-    num_forecasts_deleted = query.delete()
+    num_forecasts_deleted = db.session.execute(query)
 
     if not sensor_id:
         click.echo("Deleted %d Forecast Jobs" % num_forecasting_jobs_deleted)
@@ -373,7 +386,7 @@ def save_tables(
             file_path = "%s/%s/%s.obj" % (backup_path, backup_name, c.__tablename__)
 
             with open(file_path, "xb") as file_handler:
-                file_handler.write(dumps(db.session.query(c).all()))
+                file_handler.write(dumps(db.session.scalars(select(c))).all())
             click.echo("Successfully saved %s/%s." % (backup_name, c.__tablename__))
     except SQLAlchemyError as e:
         click.echo(
@@ -410,7 +423,9 @@ def load_tables(
                 if sequence_name in sequence_names:
 
                     # Get max id
-                    max_id = db.session.query(func.max(c.id)).one_or_none()[0]
+                    max_id = db.session.execute(
+                        select(func.max(c.id)).select_from(c)
+                    ).scalar_one_or_none()
                     max_id = 1 if max_id is None else max_id
 
                     # Set table seq to max id
diff --git a/flexmeasures/data/services/accounts.py b/flexmeasures/data/services/accounts.py
index a1cce7ee5..2c5cee867 100644
--- a/flexmeasures/data/services/accounts.py
+++ b/flexmeasures/data/services/accounts.py
@@ -1,5 +1,8 @@
 from __future__ import annotations
 
+from sqlalchemy import select, func
+
+from flexmeasures.data import db
 from flexmeasures.data.models.user import Account, AccountRole
 from flexmeasures.data.models.generic_assets import GenericAsset
 
@@ -11,27 +14,31 @@ def get_accounts(
     The role_name parameter allows to filter by role.
     """
     if role_name is not None:
-        role = AccountRole.query.filter(AccountRole.name == role_name).one_or_none()
+        role = db.session.execute(
+            select(AccountRole).filter_by(name=role_name)
+        ).scalar_one_or_none()
         if role:
-            accounts = Account.query.filter(Account.account_roles.contains(role)).all()
+            accounts = db.session.scalars(
+                select(Account).filter(Account.account_roles.contains(role))
+            ).all()
         else:
             return []
     else:
-        accounts = Account.query.all()
+        accounts = db.session.scalars(select(Account)).all()
 
     return accounts
 
 
 def get_number_of_assets_in_account(account_id: int) -> int:
     """Get the number of assets in an account."""
-    number_of_assets_in_account = GenericAsset.query.filter(
-        GenericAsset.account_id == account_id
-    ).count()
+    number_of_assets_in_account = db.session.scalar(
+        select(func.count()).select_from(GenericAsset).filter_by(account_id=account_id)
+    )
     return number_of_assets_in_account
 
 
 def get_account_roles(account_id: int) -> list[AccountRole]:
-    account = Account.query.filter_by(id=account_id).one_or_none()
+    account = db.session.get(Account, account_id)
     if account is None:
         return []
     return account.account_roles
diff --git a/flexmeasures/data/services/asset_grouping.py b/flexmeasures/data/services/asset_grouping.py
index 2612f5676..714eb7eaa 100644
--- a/flexmeasures/data/services/asset_grouping.py
+++ b/flexmeasures/data/services/asset_grouping.py
@@ -7,8 +7,10 @@
 from typing import List, Optional, Dict
 import inflect
 
+from sqlalchemy import select
 from sqlalchemy.orm import Query
 
+from flexmeasures.data import db
 from flexmeasures.data.queries.generic_assets import (
     get_asset_group_queries as get_asset_group_queries_new,
 )
@@ -81,11 +83,11 @@ def __init__(self, name: str, asset_query: Optional[Query] = None):
             raise Exception("Empty asset (group) name passed (%s)" % name)
         self.name = name
 
-        if not asset_query:
-            asset_query = GenericAsset.query.filter_by(name=self.name)
+        if asset_query is None:
+            asset_query = select(GenericAsset).filter_by(name=self.name)
 
         # List unique asset types and asset type names represented by this group
-        self.assets = asset_query.all()
+        self.assets = db.session.scalars(asset_query).all()
         self.unique_asset_types = list(set([a.asset_type for a in self.assets]))
         self.unique_asset_type_names = list(
             set([a.asset_type.name for a in self.assets])
diff --git a/flexmeasures/data/services/data_sources.py b/flexmeasures/data/services/data_sources.py
index 74eae3f56..87c866416 100644
--- a/flexmeasures/data/services/data_sources.py
+++ b/flexmeasures/data/services/data_sources.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from flask import current_app
+from sqlalchemy import select
 
 from flexmeasures import User
 from flexmeasures.data import db
@@ -18,7 +19,7 @@ def get_or_create_source(
 ) -> DataSource:
     if is_user(source):
         source_type = "user"
-    query = DataSource.query.filter(DataSource.type == source_type)
+    query = select(DataSource).filter(DataSource.type == source_type)
     if model is not None:
         query = query.filter(DataSource.model == model)
     if version is not None:
@@ -33,7 +34,7 @@ def get_or_create_source(
         query = query.filter(DataSource.name == source)
     else:
         raise TypeError("source should be of type User or str")
-    _source = query.one_or_none()
+    _source = db.session.execute(query).scalar_one_or_none()
     if not _source:
         if is_user(source):
             _source = DataSource(user=source, model=model, version=version)
@@ -62,8 +63,8 @@ def get_source_or_none(
     :param source:      source id
     :param source_type: optionally, filter by source type
     """
-    query = DataSource.query
+    query = select(DataSource)
     if source_type is not None:
         query = query.filter(DataSource.type == source_type)
     query = query.filter(DataSource.id == int(source))
-    return query.one_or_none()
+    return db.session.execute(query).scalar_one_or_none()
diff --git a/flexmeasures/data/services/forecasting.py b/flexmeasures/data/services/forecasting.py
index cb33bef68..1e1a6e0f4 100644
--- a/flexmeasures/data/services/forecasting.py
+++ b/flexmeasures/data/services/forecasting.py
@@ -178,7 +178,7 @@ def make_rolling_viewpoint_forecasts(
     model_search_term = rq_job.meta.get("model_search_term", "linear-OLS")
 
     # find sensor
-    sensor = Sensor.query.filter_by(id=sensor_id).one_or_none()
+    sensor = db.session.get(Sensor, sensor_id)
 
     click.echo(
         "Running Forecasting Job %s: %s for %s on model '%s', from %s to %s"
diff --git a/flexmeasures/data/services/scheduling.py b/flexmeasures/data/services/scheduling.py
index 19a1d8d95..c3b4a3c4d 100644
--- a/flexmeasures/data/services/scheduling.py
+++ b/flexmeasures/data/services/scheduling.py
@@ -19,6 +19,7 @@
 from rq.job import Job
 import timely_beliefs as tb
 import pandas as pd
+from sqlalchemy import select
 
 from flexmeasures.data import db
 from flexmeasures.data.models.planning import Scheduler, SchedulerOutputType
@@ -426,19 +427,19 @@ def get_data_source_for_job(job: Job) -> DataSource | None:
     data_source_info = job.meta.get("data_source_info")
     if data_source_info and "id" in data_source_info:
         # this is the expected outcome
-        return DataSource.query.get(data_source_info["id"])
+        return db.session.get(DataSource, data_source_info["id"])
     if data_source_info is None:
         raise ValueError(
             "Cannot look up scheduling data without knowing the full data_source_info (version)."
         )
-    scheduler_sources = (
-        DataSource.query.filter_by(
+    scheduler_sources = db.session.scalars(
+        select(DataSource)
+        .filter_by(
             type="scheduler",
             **data_source_info,
         )
         .order_by(DataSource.version.desc())
-        .all()
-    )  # Might still be more than one, e.g. per user
+    ).all()  # Might still be more than one, e.g. per user
     if len(scheduler_sources) == 0:
         return None
     return scheduler_sources[0]
diff --git a/flexmeasures/data/services/sensors.py b/flexmeasures/data/services/sensors.py
index 70c37b71e..802a06e30 100644
--- a/flexmeasures/data/services/sensors.py
+++ b/flexmeasures/data/services/sensors.py
@@ -2,6 +2,7 @@
 
 import sqlalchemy as sa
 
+from flexmeasures.data import db
 from flexmeasures import Sensor, Account
 from flexmeasures.data.models.generic_assets import GenericAsset
 
@@ -19,7 +20,7 @@ def get_sensors(
     :param sensor_id_allowlist:     optionally, allow only sensors whose id is in this list
     :param sensor_name_allowlist:   optionally, allow only sensors whose name is in this list
     """
-    sensor_query = Sensor.query
+    sensor_query = sa.select(Sensor)
     if account is None:
         account_ids = []
     elif isinstance(account, list):
@@ -42,4 +43,4 @@ def get_sensors(
         sensor_query = sensor_query.filter(Sensor.id.in_(sensor_id_allowlist))
     if sensor_name_allowlist:
         sensor_query = sensor_query.filter(Sensor.name.in_(sensor_name_allowlist))
-    return sensor_query.all()
+    return db.session.scalars(sensor_query).all()
diff --git a/flexmeasures/data/services/timerange.py b/flexmeasures/data/services/timerange.py
index d894ed4bf..37d6d6b75 100644
--- a/flexmeasures/data/services/timerange.py
+++ b/flexmeasures/data/services/timerange.py
@@ -2,8 +2,9 @@
 
 from datetime import datetime
 
-from sqlalchemy import func
+from sqlalchemy import func, select
 
+from flexmeasures.data import db
 from flexmeasures.utils import time_utils
 
 
@@ -14,13 +15,12 @@ def get_timerange(sensor_ids: list[int]) -> tuple[datetime, datetime]:
     """
     from flexmeasures.data.models.time_series import Sensor, TimedBelief
 
-    least_recent_event_start_and_most_recent_event_end = (
-        TimedBelief.query.with_entities(
-            # least recent event start
+    least_recent_event_start_and_most_recent_event_end = db.session.execute(
+        select(
             func.min(TimedBelief.event_start),
-            # most recent event end
             func.max(TimedBelief.event_start + Sensor.event_resolution),
         )
+        .select_from(TimedBelief)
         .join(Sensor, TimedBelief.sensor_id == Sensor.id)
         .filter(TimedBelief.sensor_id.in_(sensor_ids))
     ).one_or_none()
diff --git a/flexmeasures/data/services/users.py b/flexmeasures/data/services/users.py
index ed786d0e4..c718387a3 100644
--- a/flexmeasures/data/services/users.py
+++ b/flexmeasures/data/services/users.py
@@ -14,6 +14,7 @@
 from email_validator.deliverability import validate_email_deliverability
 from flask_security.utils import hash_password
 from werkzeug.exceptions import NotFound
+from sqlalchemy import select, delete
 
 from flexmeasures.data import db
 from flexmeasures.data.models.data_sources import DataSource
@@ -26,7 +27,7 @@ class InvalidFlexMeasuresUser(Exception):
 
 def get_user(id: str) -> User:
     """Get a user, raise if not found."""
-    user: User = User.query.filter_by(id=int(id)).one_or_none()
+    user: User = db.session.get(User, int(id))
     if user is None:
         raise NotFound
     return user
@@ -42,24 +43,27 @@ def get_users(
     The role_name parameter allows to filter by role.
     Set only_active to False if you also want non-active users.
     """
-    user_query = User.query
+    user_query = select(User)
 
     if account_name is not None:
-        account = Account.query.filter(Account.name == account_name).one_or_none()
+        account = db.session.execute(
+            select(Account).filter_by(name=account_name)
+        ).scalar_one_or_none()
         if not account:
             raise NotFound(f"There is no account named {account_name}!")
-        user_query = user_query.filter(User.account == account)
+        user_query = user_query.filter_by(account=account)
 
     if only_active:
         user_query = user_query.filter(User.active.is_(True))
 
     if role_name is not None:
-        role = Role.query.filter(Role.name == role_name).one_or_none()
+        role = db.session.execute(
+            select(Role).filter_by(name=role_name)
+        ).scalar_one_or_none()
         if role:
             user_query = user_query.filter(User.flexmeasures_roles.contains(role))
 
-    users = user_query.all()
-
+    users = db.session.scalars(user_query).all()
     if account_role_name is not None:
         users = [u for u in users if u.account.has_role(account_role_name)]
 
@@ -125,10 +129,14 @@ def create_user(  # noqa: C901
         username = kwargs.pop("username").strip()
 
     # Check integrity explicitly before anything happens
-    existing_user_by_email = User.query.filter_by(email=email).one_or_none()
+    existing_user_by_email = db.session.execute(
+        select(User).filter_by(email=email)
+    ).scalar_one_or_none()
     if existing_user_by_email is not None:
         raise InvalidFlexMeasuresUser("User with email %s already exists." % email)
-    existing_user_by_username = User.query.filter_by(username=username).one_or_none()
+    existing_user_by_username = db.session.execute(
+        select(User).filter_by(username=username)
+    ).scalar_one_or_none()
     if existing_user_by_username is not None:
         raise InvalidFlexMeasuresUser(
             "User with username %s already exists." % username
@@ -139,7 +147,9 @@ def create_user(  # noqa: C901
         raise InvalidFlexMeasuresUser(
             "Cannot create user without knowing the name of the account which this user is associated with."
         )
-    account = db.session.query(Account).filter_by(name=account_name).one_or_none()
+    account = db.session.execute(
+        select(Account).filter_by(name=account_name)
+    ).scalar_one_or_none()
     if account is None:
         print(f"Creating account {account_name} ...")
         account = Account(name=account_name)
@@ -209,5 +219,5 @@ def delete_user(user: User):
         raise Exception("You cannot delete yourself.")
     user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
     user_datastore.delete_user(user)
-    db.session.delete(user)
+    db.session.execute(delete(User).filter_by(id=user.id))
     current_app.logger.info("Deleted %s." % user)
diff --git a/flexmeasures/data/services/utils.py b/flexmeasures/data/services/utils.py
index f8fd55f2d..43e6e933d 100644
--- a/flexmeasures/data/services/utils.py
+++ b/flexmeasures/data/services/utils.py
@@ -11,6 +11,7 @@
 from sqlalchemy import JSON, String, cast, literal
 from flask import current_app
 from rq.job import Job
+from sqlalchemy import select
 
 from flexmeasures import Sensor, Asset
 from flexmeasures.data import db
@@ -68,7 +69,7 @@ def get_asset_or_sensor_from_ref(asset_or_sensor: dict):
             f"Unrecognized class `{asset_or_sensor['class']}`. Please, consider using GenericAsset or Sensor."
         )
 
-    return klass.query.get(asset_or_sensor["id"])
+    return db.session.get(klass, asset_or_sensor["id"])
 
 
 def get_or_create_model(
@@ -112,12 +113,12 @@ def get_or_create_model(
             pass
 
     # See if the model already exists as a db row
-    model_query = model_class.query.filter_by(**filter_by_kwargs)
+    model_query = select(model_class).filter_by(**filter_by_kwargs)
     for kw, arg in filter_json_kwargs.items():
         model_query = model_query.filter(
             cast(getattr(model_class, kw), String) == cast(literal(arg, JSON()), String)
         )
-    model = model_query.one_or_none()
+    model = db.session.execute(model_query).scalar_one_or_none()
 
     # Create the model and add it to the database if it didn't already exist
     if model is None:
diff --git a/flexmeasures/data/utils.py b/flexmeasures/data/utils.py
index 4985ab421..70ac77804 100644
--- a/flexmeasures/data/utils.py
+++ b/flexmeasures/data/utils.py
@@ -6,6 +6,7 @@
 
 from flask import current_app
 from timely_beliefs import BeliefsDataFrame, BeliefsSeries
+from sqlalchemy import select
 
 from flexmeasures.data import db
 from flexmeasures.data.models.data_sources import DataSource
@@ -32,12 +33,14 @@ def get_data_source(
     Meant for scripts that may run for the first time.
     """
 
-    data_source = DataSource.query.filter_by(
-        name=data_source_name,
-        model=data_source_model,
-        version=data_source_version,
-        type=data_source_type,
-    ).one_or_none()
+    data_source = db.session.execute(
+        select(DataSource).filter_by(
+            name=data_source_name,
+            model=data_source_model,
+            version=data_source_version,
+            type=data_source_type,
+        )
+    ).scalar_one_or_none()
     if data_source is None:
         data_source = DataSource(
             name=data_source_name,
diff --git a/flexmeasures/ui/crud/accounts.py b/flexmeasures/ui/crud/accounts.py
index 57280a1a2..461ca54b8 100644
--- a/flexmeasures/ui/crud/accounts.py
+++ b/flexmeasures/ui/crud/accounts.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+from sqlalchemy import select
 from flask import request, url_for
 from flask_classful import FlaskView
 from flask_security import login_required
@@ -8,6 +9,7 @@
 from flexmeasures.ui.crud.assets import get_assets_by_account
 from flexmeasures.ui.crud.users import get_users_by_account
 from flexmeasures.data.models.user import Account
+from flexmeasures.data import db
 
 
 def get_accounts() -> list[dict]:
@@ -48,9 +50,9 @@ def get(self, account_id: str):
         include_inactive = request.args.get("include_inactive", "0") != "0"
         account = get_account(account_id)
         if account["consultancy_account_id"]:
-            consultancy_account = Account.query.filter_by(
-                id=account["consultancy_account_id"]
-            ).one_or_none()
+            consultancy_account = db.session.execute(
+                select(Account).filter_by(id=account["consultancy_account_id"])
+            ).scalar_one_or_none()
             if consultancy_account:
                 account["consultancy_account_name"] = consultancy_account.name
         assets = get_assets_by_account(account_id)
diff --git a/flexmeasures/ui/crud/assets.py b/flexmeasures/ui/crud/assets.py
index 834252cca..7679bd3f6 100644
--- a/flexmeasures/ui/crud/assets.py
+++ b/flexmeasures/ui/crud/assets.py
@@ -9,6 +9,7 @@
 from flask_security import login_required, current_user
 from wtforms import StringField, DecimalField, SelectField
 from wtforms.validators import DataRequired, optional
+from sqlalchemy import select
 from flexmeasures.auth.policy import user_has_admin_access
 
 from flexmeasures.data import db
@@ -107,11 +108,13 @@ class NewAssetForm(AssetForm):
 def with_options(form: AssetForm | NewAssetForm) -> AssetForm | NewAssetForm:
     if "generic_asset_type_id" in form:
         form.generic_asset_type_id.choices = [(-1, "--Select type--")] + [
-            (atype.id, atype.name) for atype in GenericAssetType.query.all()
+            (atype.id, atype.name)
+            for atype in db.session.scalars(select(GenericAssetType)).all()
         ]
     if "account_id" in form:
         form.account_id.choices = [(-1, "--Select account--")] + [
-            (account.id, account.name) for account in Account.query.all()
+            (account.id, account.name)
+            for account in db.session.scalars(select(Account)).all()
         ]
     return form
 
@@ -143,21 +146,24 @@ def expunge_asset():
                 **{"attributes": json.loads(asset_data.get("attributes", "{}"))},
             }
         )  # TODO: use schema?
-        asset.generic_asset_type = GenericAssetType.query.get(
-            asset.generic_asset_type_id
+        asset.generic_asset_type = db.session.get(
+            GenericAssetType, asset.generic_asset_type_id
         )
         expunge_asset()
-        asset.owner = Account.query.get(asset_data["account_id"])
+        asset.owner = db.session.get(Account, asset_data["account_id"])
         expunge_asset()
+        db.session.flush()
         if "id" in asset_data:
-            asset.sensors = Sensor.query.filter(
-                Sensor.generic_asset_id == asset_data["id"]
+            asset.sensors = db.session.scalars(
+                select(Sensor).filter_by(generic_asset_id=asset_data["id"])
             ).all()
             expunge_asset()
         if asset_data.get("parent_asset_id", None) is not None:
-            asset.parent_asset = GenericAsset.query.filter(
-                GenericAsset.id == asset_data["parent_asset_id"]
-            ).one_or_none()
+            asset.parent_asset = db.session.execute(
+                select(GenericAsset).filter(
+                    GenericAsset.id == asset_data["parent_asset_id"]
+                )
+            ).scalar_one_or_none()
             expunge_asset()
 
         child_assets = []
@@ -220,7 +226,7 @@ def index(self, msg=""):
         assets = []
 
         if user_has_admin_access(current_user, "read"):
-            for account in Account.query.all():
+            for account in db.session.scalars(select(Account)).all():
                 assets += get_assets_by_account(account.id)
             assets += get_assets_by_account(account_id=None)
         else:
@@ -250,9 +256,10 @@ def owned_by(self, account_id: str):
                 process_internal_api_response(ad, make_obj=True)
                 for ad in get_assets_response.json()
             ]
+        db.session.flush()
         return render_flexmeasures_template(
             "crud/assets.html",
-            account=Account.query.get(account_id),
+            account=db.session.get(Account, account_id),
             assets=assets,
             msg=msg,
             user_can_create_assets=user_can_create_assets(),
@@ -357,7 +364,7 @@ def post(self, id: str):
         else:
             asset_form = with_options(AssetForm())
             if not asset_form.validate_on_submit():
-                asset = GenericAsset.query.get(id)
+                asset = db.session.get(GenericAsset, id)
                 # Display the form data, but set some extra data which the page wants to show.
                 asset_info = asset_form.to_json()
                 asset_info["id"] = id
@@ -394,7 +401,7 @@ def post(self, id: str):
                 asset_form.process_api_validation_errors(
                     patch_asset_response.json().get("message")
                 )
-                asset = GenericAsset.query.get(id)
+                asset = db.session.get(GenericAsset, id)
 
         return render_flexmeasures_template(
             "crud/asset.html",
@@ -426,7 +433,9 @@ def _set_account(asset_form: NewAssetForm) -> tuple[Account | None, str | None]:
         else:
             account_error = "Please pick an existing account."
 
-    account = Account.query.filter_by(id=int(asset_form.account_id.data)).one_or_none()
+    account = db.session.execute(
+        select(Account).filter_by(id=int(asset_form.account_id.data))
+    ).scalar_one_or_none()
 
     if account:
         asset_form.account_id.data = account.id
@@ -446,9 +455,11 @@ def _set_asset_type(
     if int(asset_form.generic_asset_type_id.data) == -1:
         asset_type_error = "Pick an existing asset type."
     else:
-        asset_type = GenericAssetType.query.filter_by(
-            id=int(asset_form.generic_asset_type_id.data)
-        ).one_or_none()
+        asset_type = db.session.execute(
+            select(GenericAssetType).filter_by(
+                id=int(asset_form.generic_asset_type_id.data)
+            )
+        ).scalar_one_or_none()
 
     if asset_type:
         asset_form.generic_asset_type_id.data = asset_type.id
diff --git a/flexmeasures/ui/crud/users.py b/flexmeasures/ui/crud/users.py
index af2359273..643bde9a4 100644
--- a/flexmeasures/ui/crud/users.py
+++ b/flexmeasures/ui/crud/users.py
@@ -9,6 +9,7 @@
 from flask_wtf import FlaskForm
 from wtforms import StringField, FloatField, DateTimeField, BooleanField
 from wtforms.validators import DataRequired
+from sqlalchemy import select
 
 from flexmeasures.auth.policy import ADMIN_READER_ROLE, ADMIN_ROLE
 from flexmeasures.auth.decorators import roles_required, roles_accepted
@@ -56,7 +57,9 @@ def process_internal_api_response(
     """
     with db.session.no_autoflush:
         role_ids = tuple(user_data.get("flexmeasures_roles", []))
-        user_data["flexmeasures_roles"] = Role.query.filter(Role.id.in_(role_ids)).all()
+        user_data["flexmeasures_roles"] = db.session.scalars(
+            select(Role).filter(Role.id.in_(role_ids))
+        ).all()
         user_data.pop("status", None)  # might have come from requests.response
         for date_field in ("last_login_at", "last_seen_at"):
             if date_field in user_data and user_data[date_field] is not None:
@@ -65,7 +68,7 @@ def process_internal_api_response(
             user_data["id"] = user_id
         if make_obj:
             user = User(**user_data)
-            user.account = Account.query.get(user_data.get("account_id", -1))
+            user.account = db.session.get(Account, user_data.get("account_id", -1))
             if user in db.session:
                 db.session.expunge(user)
             return user
@@ -101,7 +104,7 @@ def index(self):
         if current_user.has_role(ADMIN_ROLE) or current_user.has_role(
             ADMIN_READER_ROLE
         ):
-            accounts = Account.query.all()
+            accounts = db.session.scalars(select(Account)).all()
         else:
             accounts = [current_user.account]
         for account in accounts:
diff --git a/flexmeasures/ui/utils/view_utils.py b/flexmeasures/ui/utils/view_utils.py
index dfe5a89c8..ecfa1c8f5 100644
--- a/flexmeasures/ui/utils/view_utils.py
+++ b/flexmeasures/ui/utils/view_utils.py
@@ -8,6 +8,7 @@
 from flask import render_template, request, session, current_app
 from flask_security.core import current_user
 
+from flexmeasures.data import db
 from flexmeasures import __version__ as flexmeasures_version
 from flexmeasures.auth.policy import user_has_admin_access
 from flexmeasures.ui.utils.breadcrumb_utils import get_breadcrumb_info
@@ -185,7 +186,7 @@ def asset_icon_name(asset_type_name: str) -> str:
 
 
 def username(user_id) -> str:
-    user = User.query.get(user_id)
+    user = db.session.get(User, user_id)
     if user is None:
         current_app.logger.warning(f"Could not find user with id {user_id}")
         return ""
@@ -194,7 +195,7 @@ def username(user_id) -> str:
 
 
 def accountname(account_id) -> str:
-    account = Account.query.get(account_id)
+    account = db.session.get(Account, account_id)
     if account is None:
         current_app.logger.warning(f"Could not find account with id {account_id}")
         return ""
diff --git a/flexmeasures/ui/views/new_dashboard.py b/flexmeasures/ui/views/new_dashboard.py
index 00c895781..e8b873b04 100644
--- a/flexmeasures/ui/views/new_dashboard.py
+++ b/flexmeasures/ui/views/new_dashboard.py
@@ -1,9 +1,11 @@
 from flask import request, current_app
 from flask_security import login_required
 from flask_security.core import current_user
+from sqlalchemy import select
+
 from flexmeasures.auth.policy import user_has_admin_access
 from flexmeasures.data.queries.generic_assets import get_asset_group_queries
-
+from flexmeasures.data import db
 from flexmeasures.ui.views import flexmeasures_ui
 from flexmeasures.ui.utils.view_utils import render_flexmeasures_template, clear_session
 from flexmeasures.data.models.generic_assets import (
@@ -50,7 +52,9 @@ def dashboard_view():
         if any([a.location for a in asset_group.assets]):
             map_asset_groups[asset_group_name] = asset_group
 
-    known_asset_types = [gat.name for gat in GenericAssetType.query.all()]
+    known_asset_types = [
+        gat.name for gat in db.session.scalars(select(GenericAssetType)).all()
+    ]
 
     return render_flexmeasures_template(
         "views/new_dashboard.html",
diff --git a/flexmeasures/ui/views/sensors.py b/flexmeasures/ui/views/sensors.py
index 7b6dc235e..e1f45cf16 100644
--- a/flexmeasures/ui/views/sensors.py
+++ b/flexmeasures/ui/views/sensors.py
@@ -7,6 +7,7 @@
 from marshmallow import fields
 from webargs.flaskparser import use_kwargs
 
+from flexmeasures.data import db
 from flexmeasures.data.schemas.times import AwareDateTimeField
 from flexmeasures.api.dev.sensors import SensorAPI
 from flexmeasures import Sensor
@@ -72,5 +73,5 @@ def get(self, id: int):
             "views/sensors.html",
             sensor_id=id,
             msg="",
-            breadcrumb_info=get_breadcrumb_info(Sensor.query.get(id)),
+            breadcrumb_info=get_breadcrumb_info(db.session.get(Sensor, id)),
         )
diff --git a/requirements/3.10/app.txt b/requirements/3.10/app.txt
index 9744a42a1..2d19b43b8 100644
--- a/requirements/3.10/app.txt
+++ b/requirements/3.10/app.txt
@@ -4,33 +4,33 @@
 #
 #    pip-compile --output-file=requirements/3.10/app.txt requirements/app.in
 #
-alembic==1.12.0
+alembic==1.13.1
     # via flask-migrate
-altair==5.1.1
+altair==5.2.0
     # via -r requirements/app.in
-arrow==1.2.3
+arrow==1.3.0
     # via rq-dashboard
 async-timeout==4.0.3
     # via redis
-attrs==23.1.0
+attrs==23.2.0
     # via
     #   jsonschema
     #   referencing
-babel==2.12.1
+babel==2.14.0
     # via py-moneyed
-bcrypt==4.0.1
+bcrypt==4.1.2
     # via -r requirements/app.in
-blinker==1.6.2
+blinker==1.7.0
     # via
     #   flask-mail
     #   flask-principal
     #   flask-security-too
     #   sentry-sdk
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   requests
     #   sentry-sdk
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via requests
 click==8.1.7
     # via
@@ -42,21 +42,21 @@ click-default-group==1.2.4
     # via -r requirements/app.in
 colour==0.1.5
     # via -r requirements/app.in
-contourpy==1.1.0
+contourpy==1.2.0
     # via matplotlib
 convertdate==2.4.0
     # via workalendar
-cycler==0.11.0
+cycler==0.12.1
     # via matplotlib
-dill==0.3.7
+dill==0.3.8
     # via openturns
-dnspython==2.4.2
+dnspython==2.5.0
     # via email-validator
-email-validator==2.0.0.post2
+email-validator==2.1.0.post1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-filelock==3.12.3
+filelock==3.13.1
     # via tldextract
 flask==2.2.5
     # via
@@ -81,42 +81,42 @@ flask-cors==4.0.0
     # via -r requirements/app.in
 flask-json==0.4.0
     # via -r requirements/app.in
-flask-login==0.6.2
+flask-login==0.6.3
     # via
     #   -r requirements/app.in
     #   flask-security-too
 flask-mail==0.9.1
     # via -r requirements/app.in
-flask-marshmallow==0.15.0
+flask-marshmallow==1.2.0
     # via -r requirements/app.in
-flask-migrate==4.0.4
+flask-migrate==4.0.5
     # via -r requirements/app.in
 flask-principal==0.4.0
     # via flask-security-too
 flask-security-too==5.1.2
     # via -r requirements/app.in
-flask-sqlalchemy==3.0.5
+flask-sqlalchemy==3.1.1
     # via
     #   -r requirements/app.in
     #   flask-migrate
 flask-sslify==0.1.5
     # via -r requirements/app.in
-flask-wtf==1.1.1
+flask-wtf==1.2.1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-fonttools==4.42.1
+fonttools==4.48.1
     # via matplotlib
-greenlet==2.0.2
+greenlet==3.0.3
     # via sqlalchemy
-humanize==4.8.0
+humanize==4.9.0
     # via -r requirements/app.in
-idna==3.4
+idna==3.6
     # via
     #   email-validator
     #   requests
     #   tldextract
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -r requirements/app.in
     #   timely-beliefs
@@ -124,7 +124,7 @@ inflect==6.0.2
     # via -r requirements/app.in
 inflection==0.5.1
     # via -r requirements/app.in
-iso8601==2.0.0
+iso8601==2.1.0
     # via -r requirements/app.in
 isodate==0.6.1
     # via
@@ -135,30 +135,31 @@ itsdangerous==2.1.2
     #   flask
     #   flask-security-too
     #   flask-wtf
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   altair
     #   flask
 joblib==1.3.2
     # via scikit-learn
-jsonschema==4.19.0
+jsonschema==4.21.1
     # via altair
-jsonschema-specifications==2023.7.1
+jsonschema-specifications==2023.12.1
     # via jsonschema
 kiwisolver==1.4.5
     # via matplotlib
-lunardate==0.2.0
+lunardate==0.2.2
     # via workalendar
-mako==1.2.4
+mako==1.3.2
     # via alembic
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
+    #   flask-security-too
     #   jinja2
     #   mako
     #   sentry-sdk
     #   werkzeug
     #   wtforms
-marshmallow==3.20.1
+marshmallow==3.20.2
     # via
     #   -r requirements/app.in
     #   flask-marshmallow
@@ -167,9 +168,9 @@ marshmallow==3.20.1
     #   webargs
 marshmallow-polyfield==5.11
     # via -r requirements/app.in
-marshmallow-sqlalchemy==0.29.0
+marshmallow-sqlalchemy==1.0.0
     # via -r requirements/app.in
-matplotlib==3.7.3
+matplotlib==3.8.2
     # via timetomodel
 numpy==1.24.4
     # via
@@ -187,19 +188,17 @@ numpy==1.24.4
     #   timely-beliefs
     #   timetomodel
     #   uniplot
-openturns==1.21
+openturns==1.22
     # via timely-beliefs
-packaging==23.1
+packaging==23.2
     # via
     #   altair
-    #   flask-marshmallow
     #   marshmallow
-    #   marshmallow-sqlalchemy
     #   matplotlib
     #   sktime
     #   statsmodels
     #   webargs
-pandas==2.0.3
+pandas==2.1.4
     # via
     #   -r requirements/app.in
     #   altair
@@ -209,13 +208,13 @@ pandas==2.0.3
     #   timetomodel
 passlib==1.7.4
     # via flask-security-too
-patsy==0.5.3
+patsy==0.5.6
     # via statsmodels
-pillow==10.0.1
+pillow==10.2.0
     # via
     #   -r requirements/app.in
     #   matplotlib
-pint==0.22
+pint==0.23
     # via -r requirements/app.in
 ply==3.11
     # via pyomo
@@ -223,15 +222,15 @@ properscoring==0.1
     # via timely-beliefs
 pscript==0.7.7
     # via -r requirements/app.in
-psutil==5.9.5
+psutil==5.9.8
     # via openturns
-psycopg2-binary==2.9.7
+psycopg2-binary==2.9.9
     # via
     #   -r requirements/app.in
     #   timely-beliefs
 py-moneyed==3.0
     # via -r requirements/app.in
-pydantic==1.10.12
+pydantic==1.10.14
     # via
     #   -r requirements/app.in
     #   inflect
@@ -239,7 +238,7 @@ pyluach==2.2.0
     # via workalendar
 pymeeus==0.5.12
     # via convertdate
-pyomo==6.6.2
+pyomo==6.7.0
     # via -r requirements/app.in
 pyparsing==3.1.1
     # via matplotlib
@@ -250,9 +249,9 @@ python-dateutil==2.8.2
     #   pandas
     #   timetomodel
     #   workalendar
-python-dotenv==1.0.0
+python-dotenv==1.0.1
     # via -r requirements/app.in
-pytz==2023.3.post1
+pytz==2024.1
     # via
     #   -r requirements/app.in
     #   pandas
@@ -268,7 +267,7 @@ redis==4.6.0
     #   rq-dashboard
 redis-sentinel-url==1.0.1
     # via rq-dashboard
-referencing==0.30.2
+referencing==0.33.0
     # via
     #   jsonschema
     #   jsonschema-specifications
@@ -276,9 +275,9 @@ requests==2.31.0
     # via
     #   requests-file
     #   tldextract
-requests-file==1.5.1
+requests-file==2.0.0
     # via tldextract
-rpds-py==0.10.2
+rpds-py==0.17.1
     # via
     #   jsonschema
     #   referencing
@@ -288,13 +287,13 @@ rq==1.15.1
     #   rq-dashboard
 rq-dashboard==0.6.7
     # via -r requirements/app.in
-scikit-base==0.5.1
+scikit-base==0.7.2
     # via sktime
-scikit-learn==1.3.0
+scikit-learn==1.4.0
     # via
     #   sktime
     #   timetomodel
-scipy==1.11.2
+scipy==1.12.0
     # via
     #   properscoring
     #   scikit-learn
@@ -302,17 +301,16 @@ scipy==1.11.2
     #   statsmodels
     #   timely-beliefs
     #   timetomodel
-sentry-sdk[flask]==1.30.0
+sentry-sdk[flask]==1.40.1
     # via -r requirements/app.in
 six==1.16.0
     # via
     #   isodate
     #   patsy
     #   python-dateutil
-    #   requests-file
-sktime==0.22.0
+sktime==0.26.0
     # via timely-beliefs
-sqlalchemy==1.4.49
+sqlalchemy==2.0.25
     # via
     #   -r requirements/app.in
     #   alembic
@@ -320,39 +318,41 @@ sqlalchemy==1.4.49
     #   marshmallow-sqlalchemy
     #   timely-beliefs
     #   timetomodel
-statsmodels==0.14.0
+statsmodels==0.14.1
     # via timetomodel
 tabulate==0.9.0
     # via -r requirements/app.in
 threadpoolctl==3.2.0
     # via scikit-learn
-timely-beliefs[forecast]==1.25.1
+timely-beliefs[forecast]==2.0.0
     # via -r requirements/app.in
 timetomodel==0.7.3
     # via -r requirements/app.in
-tldextract==3.5.0
+tldextract==5.1.1
     # via -r requirements/app.in
-toolz==0.12.0
+toolz==0.12.1
     # via altair
-typing-extensions==4.7.1
+types-python-dateutil==2.8.19.20240106
+    # via arrow
+typing-extensions==4.9.0
     # via
     #   alembic
     #   altair
-    #   filelock
     #   pint
     #   py-moneyed
     #   pydantic
-tzdata==2023.3
+    #   sqlalchemy
+tzdata==2023.4
     # via pandas
-uniplot==0.10.2
+uniplot==0.11.0
     # via -r requirements/app.in
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   requests
     #   sentry-sdk
-vl-convert-python==0.13.1
+vl-convert-python==1.2.3
     # via -r requirements/app.in
-webargs==8.3.0
+webargs==8.4.0
     # via -r requirements/app.in
 werkzeug==2.2.3
     # via
@@ -361,13 +361,13 @@ werkzeug==2.2.3
     #   flask-login
 workalendar==17.0.0
     # via -r requirements/app.in
-wtforms==3.0.1
+wtforms==3.1.2
     # via
     #   flask-security-too
     #   flask-wtf
 xlrd==2.0.1
     # via -r requirements/app.in
-zipp==3.16.2
+zipp==3.17.0
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/3.10/dev.txt b/requirements/3.10/dev.txt
index eeefa82c8..c18de0f2b 100644
--- a/requirements/3.10/dev.txt
+++ b/requirements/3.10/dev.txt
@@ -13,9 +13,9 @@ click==8.1.7
     #   -c requirements/3.10/app.txt
     #   -c requirements/3.10/test.txt
     #   black
-distlib==0.3.7
+distlib==0.3.8
     # via virtualenv
-filelock==3.12.3
+filelock==3.13.1
     # via
     #   -c requirements/3.10/app.txt
     #   virtualenv
@@ -23,11 +23,11 @@ flake8==4.0.1
     # via -r requirements/dev.in
 flake8-blind-except==0.2.1
     # via -r requirements/dev.in
-identify==2.5.28
+identify==2.5.33
     # via pre-commit
 mccabe==0.6.1
     # via flake8
-mypy==1.5.1
+mypy==1.8.0
     # via -r requirements/dev.in
 mypy-extensions==1.0.0
     # via
@@ -35,32 +35,32 @@ mypy-extensions==1.0.0
     #   mypy
 nodeenv==1.8.0
     # via pre-commit
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.10/app.txt
     #   -c requirements/3.10/test.txt
     #   setuptools-scm
-pathspec==0.11.2
+pathspec==0.12.1
     # via black
-platformdirs==3.10.0
+platformdirs==4.2.0
     # via
     #   black
     #   virtualenv
-pre-commit==3.4.0
+pre-commit==3.6.0
     # via -r requirements/dev.in
 pycodestyle==2.8.0
     # via flake8
 pyflakes==2.4.0
     # via flake8
-pyinstrument==4.5.3
+pyinstrument==4.6.2
     # via -r requirements/dev.in
-pytest-runner==6.0.0
+pytest-runner==6.0.1
     # via -r requirements/dev.in
 pyyaml==6.0.1
     # via
     #   -c requirements/3.10/app.txt
     #   pre-commit
-setuptools-scm==7.1.0
+setuptools-scm==8.0.4
     # via -r requirements/dev.in
 tomli==2.0.1
     # via
@@ -68,13 +68,12 @@ tomli==2.0.1
     #   black
     #   mypy
     #   setuptools-scm
-typing-extensions==4.7.1
+typing-extensions==4.9.0
     # via
     #   -c requirements/3.10/app.txt
-    #   filelock
     #   mypy
     #   setuptools-scm
-virtualenv==20.24.5
+virtualenv==20.25.0
     # via pre-commit
 watchdog==3.0.0
     # via -r requirements/dev.in
diff --git a/requirements/3.10/docs.txt b/requirements/3.10/docs.txt
index 7ccdffa3c..9dd56b147 100644
--- a/requirements/3.10/docs.txt
+++ b/requirements/3.10/docs.txt
@@ -4,44 +4,44 @@
 #
 #    pip-compile --constraint=requirements/3.10/app.txt --output-file=requirements/3.10/docs.txt requirements/docs.in
 #
-alabaster==0.7.13
+alabaster==0.7.16
     # via sphinx
-babel==2.12.1
+babel==2.14.0
     # via
     #   -c requirements/3.10/app.txt
     #   sphinx
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.10/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.10/app.txt
     #   requests
-docutils==0.18.1
+docutils==0.20.1
     # via
     #   sphinx
     #   sphinx-rtd-theme
     #   sphinx-tabs
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.10/app.txt
     #   requests
 imagesize==1.4.1
     # via sphinx
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.10/app.txt
     #   sphinx
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.10/app.txt
     #   jinja2
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.10/app.txt
     #   sphinx
-pygments==2.16.1
+pygments==2.17.2
     # via
     #   sphinx
     #   sphinx-tabs
@@ -55,33 +55,28 @@ six==1.16.0
     #   sphinxcontrib-httpdomain
 snowballstemmer==2.2.0
     # via sphinx
-sphinx==7.2.5
+sphinx==7.2.6
     # via
     #   -r requirements/docs.in
     #   sphinx-copybutton
     #   sphinx-fontawesome
     #   sphinx-rtd-theme
     #   sphinx-tabs
-    #   sphinxcontrib-applehelp
-    #   sphinxcontrib-devhelp
-    #   sphinxcontrib-htmlhelp
     #   sphinxcontrib-httpdomain
     #   sphinxcontrib-jquery
-    #   sphinxcontrib-qthelp
-    #   sphinxcontrib-serializinghtml
 sphinx-copybutton==0.5.2
     # via -r requirements/docs.in
 sphinx-fontawesome==0.0.6
     # via -r requirements/docs.in
-sphinx-rtd-theme==1.3.0
+sphinx-rtd-theme==2.0.0
     # via -r requirements/docs.in
-sphinx-tabs==3.4.1
+sphinx-tabs==3.4.5
     # via -r requirements/docs.in
-sphinxcontrib-applehelp==1.0.7
+sphinxcontrib-applehelp==1.0.8
     # via sphinx
-sphinxcontrib-devhelp==1.0.5
+sphinxcontrib-devhelp==1.0.6
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.4
+sphinxcontrib-htmlhelp==2.0.5
     # via sphinx
 sphinxcontrib-httpdomain==1.8.1
     # via -r requirements/docs.in
@@ -89,11 +84,11 @@ sphinxcontrib-jquery==4.1
     # via sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.6
+sphinxcontrib-qthelp==1.0.7
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.9
+sphinxcontrib-serializinghtml==1.1.10
     # via sphinx
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.10/app.txt
     #   requests
diff --git a/requirements/3.11/app.txt b/requirements/3.11/app.txt
index 64f49cdca..2915601fb 100644
--- a/requirements/3.11/app.txt
+++ b/requirements/3.11/app.txt
@@ -4,31 +4,31 @@
 #
 #    pip-compile --output-file=requirements/3.11/app.txt requirements/app.in
 #
-alembic==1.12.0
+alembic==1.13.1
     # via flask-migrate
-altair==5.1.1
+altair==5.2.0
     # via -r requirements/app.in
-arrow==1.2.3
+arrow==1.3.0
     # via rq-dashboard
-attrs==23.1.0
+attrs==23.2.0
     # via
     #   jsonschema
     #   referencing
-babel==2.12.1
+babel==2.14.0
     # via py-moneyed
-bcrypt==4.0.1
+bcrypt==4.1.2
     # via -r requirements/app.in
-blinker==1.6.2
+blinker==1.7.0
     # via
     #   flask-mail
     #   flask-principal
     #   flask-security-too
     #   sentry-sdk
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   requests
     #   sentry-sdk
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via requests
 click==8.1.7
     # via
@@ -40,21 +40,21 @@ click-default-group==1.2.4
     # via -r requirements/app.in
 colour==0.1.5
     # via -r requirements/app.in
-contourpy==1.1.0
+contourpy==1.2.0
     # via matplotlib
 convertdate==2.4.0
     # via workalendar
-cycler==0.11.0
+cycler==0.12.1
     # via matplotlib
-dill==0.3.7
+dill==0.3.8
     # via openturns
-dnspython==2.4.2
+dnspython==2.5.0
     # via email-validator
-email-validator==2.0.0.post2
+email-validator==2.1.0.post1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-filelock==3.12.3
+filelock==3.13.1
     # via tldextract
 flask==2.2.5
     # via
@@ -79,42 +79,42 @@ flask-cors==4.0.0
     # via -r requirements/app.in
 flask-json==0.4.0
     # via -r requirements/app.in
-flask-login==0.6.2
+flask-login==0.6.3
     # via
     #   -r requirements/app.in
     #   flask-security-too
 flask-mail==0.9.1
     # via -r requirements/app.in
-flask-marshmallow==0.15.0
+flask-marshmallow==1.2.0
     # via -r requirements/app.in
-flask-migrate==4.0.4
+flask-migrate==4.0.5
     # via -r requirements/app.in
 flask-principal==0.4.0
     # via flask-security-too
 flask-security-too==5.1.2
     # via -r requirements/app.in
-flask-sqlalchemy==3.0.5
+flask-sqlalchemy==3.1.1
     # via
     #   -r requirements/app.in
     #   flask-migrate
 flask-sslify==0.1.5
     # via -r requirements/app.in
-flask-wtf==1.1.1
+flask-wtf==1.2.1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-fonttools==4.42.1
+fonttools==4.48.1
     # via matplotlib
-greenlet==2.0.2
+greenlet==3.0.3
     # via sqlalchemy
-humanize==4.8.0
+humanize==4.9.0
     # via -r requirements/app.in
-idna==3.4
+idna==3.6
     # via
     #   email-validator
     #   requests
     #   tldextract
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -r requirements/app.in
     #   timely-beliefs
@@ -122,7 +122,7 @@ inflect==6.0.2
     # via -r requirements/app.in
 inflection==0.5.1
     # via -r requirements/app.in
-iso8601==2.0.0
+iso8601==2.1.0
     # via -r requirements/app.in
 isodate==0.6.1
     # via
@@ -133,30 +133,31 @@ itsdangerous==2.1.2
     #   flask
     #   flask-security-too
     #   flask-wtf
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   altair
     #   flask
 joblib==1.3.2
     # via scikit-learn
-jsonschema==4.19.0
+jsonschema==4.21.1
     # via altair
-jsonschema-specifications==2023.7.1
+jsonschema-specifications==2023.12.1
     # via jsonschema
 kiwisolver==1.4.5
     # via matplotlib
-lunardate==0.2.0
+lunardate==0.2.2
     # via workalendar
-mako==1.2.4
+mako==1.3.2
     # via alembic
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
+    #   flask-security-too
     #   jinja2
     #   mako
     #   sentry-sdk
     #   werkzeug
     #   wtforms
-marshmallow==3.20.1
+marshmallow==3.20.2
     # via
     #   -r requirements/app.in
     #   flask-marshmallow
@@ -165,9 +166,9 @@ marshmallow==3.20.1
     #   webargs
 marshmallow-polyfield==5.11
     # via -r requirements/app.in
-marshmallow-sqlalchemy==0.29.0
+marshmallow-sqlalchemy==1.0.0
     # via -r requirements/app.in
-matplotlib==3.7.3
+matplotlib==3.8.2
     # via timetomodel
 numpy==1.24.4
     # via
@@ -185,19 +186,17 @@ numpy==1.24.4
     #   timely-beliefs
     #   timetomodel
     #   uniplot
-openturns==1.21
+openturns==1.22
     # via timely-beliefs
-packaging==23.1
+packaging==23.2
     # via
     #   altair
-    #   flask-marshmallow
     #   marshmallow
-    #   marshmallow-sqlalchemy
     #   matplotlib
     #   sktime
     #   statsmodels
     #   webargs
-pandas==2.0.3
+pandas==2.1.4
     # via
     #   -r requirements/app.in
     #   altair
@@ -207,13 +206,13 @@ pandas==2.0.3
     #   timetomodel
 passlib==1.7.4
     # via flask-security-too
-patsy==0.5.3
+patsy==0.5.6
     # via statsmodels
-pillow==10.0.1
+pillow==10.2.0
     # via
     #   -r requirements/app.in
     #   matplotlib
-pint==0.22
+pint==0.23
     # via -r requirements/app.in
 ply==3.11
     # via pyomo
@@ -221,15 +220,15 @@ properscoring==0.1
     # via timely-beliefs
 pscript==0.7.7
     # via -r requirements/app.in
-psutil==5.9.5
+psutil==5.9.8
     # via openturns
-psycopg2-binary==2.9.7
+psycopg2-binary==2.9.9
     # via
     #   -r requirements/app.in
     #   timely-beliefs
 py-moneyed==3.0
     # via -r requirements/app.in
-pydantic==1.10.12
+pydantic==1.10.14
     # via
     #   -r requirements/app.in
     #   inflect
@@ -237,7 +236,7 @@ pyluach==2.2.0
     # via workalendar
 pymeeus==0.5.12
     # via convertdate
-pyomo==6.6.2
+pyomo==6.7.0
     # via -r requirements/app.in
 pyparsing==3.1.1
     # via matplotlib
@@ -248,9 +247,9 @@ python-dateutil==2.8.2
     #   pandas
     #   timetomodel
     #   workalendar
-python-dotenv==1.0.0
+python-dotenv==1.0.1
     # via -r requirements/app.in
-pytz==2023.3.post1
+pytz==2024.1
     # via
     #   -r requirements/app.in
     #   pandas
@@ -266,7 +265,7 @@ redis==4.6.0
     #   rq-dashboard
 redis-sentinel-url==1.0.1
     # via rq-dashboard
-referencing==0.30.2
+referencing==0.33.0
     # via
     #   jsonschema
     #   jsonschema-specifications
@@ -274,9 +273,9 @@ requests==2.31.0
     # via
     #   requests-file
     #   tldextract
-requests-file==1.5.1
+requests-file==2.0.0
     # via tldextract
-rpds-py==0.10.2
+rpds-py==0.17.1
     # via
     #   jsonschema
     #   referencing
@@ -286,13 +285,13 @@ rq==1.15.1
     #   rq-dashboard
 rq-dashboard==0.6.7
     # via -r requirements/app.in
-scikit-base==0.5.1
+scikit-base==0.7.2
     # via sktime
-scikit-learn==1.3.0
+scikit-learn==1.4.0
     # via
     #   sktime
     #   timetomodel
-scipy==1.11.2
+scipy==1.12.0
     # via
     #   properscoring
     #   scikit-learn
@@ -300,17 +299,16 @@ scipy==1.11.2
     #   statsmodels
     #   timely-beliefs
     #   timetomodel
-sentry-sdk[flask]==1.30.0
+sentry-sdk[flask]==1.40.1
     # via -r requirements/app.in
 six==1.16.0
     # via
     #   isodate
     #   patsy
     #   python-dateutil
-    #   requests-file
-sktime==0.22.0
+sktime==0.26.0
     # via timely-beliefs
-sqlalchemy==1.4.49
+sqlalchemy==2.0.25
     # via
     #   -r requirements/app.in
     #   alembic
@@ -318,37 +316,40 @@ sqlalchemy==1.4.49
     #   marshmallow-sqlalchemy
     #   timely-beliefs
     #   timetomodel
-statsmodels==0.14.0
+statsmodels==0.14.1
     # via timetomodel
 tabulate==0.9.0
     # via -r requirements/app.in
 threadpoolctl==3.2.0
     # via scikit-learn
-timely-beliefs[forecast]==1.25.1
+timely-beliefs[forecast]==2.0.0
     # via -r requirements/app.in
 timetomodel==0.7.3
     # via -r requirements/app.in
-tldextract==3.5.0
+tldextract==5.1.1
     # via -r requirements/app.in
-toolz==0.12.0
+toolz==0.12.1
     # via altair
-typing-extensions==4.7.1
+types-python-dateutil==2.8.19.20240106
+    # via arrow
+typing-extensions==4.9.0
     # via
     #   alembic
     #   pint
     #   py-moneyed
     #   pydantic
-tzdata==2023.3
+    #   sqlalchemy
+tzdata==2023.4
     # via pandas
-uniplot==0.10.2
+uniplot==0.11.0
     # via -r requirements/app.in
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   requests
     #   sentry-sdk
-vl-convert-python==0.13.1
+vl-convert-python==1.2.3
     # via -r requirements/app.in
-webargs==8.3.0
+webargs==8.4.0
     # via -r requirements/app.in
 werkzeug==2.2.3
     # via
@@ -357,13 +358,13 @@ werkzeug==2.2.3
     #   flask-login
 workalendar==17.0.0
     # via -r requirements/app.in
-wtforms==3.0.1
+wtforms==3.1.2
     # via
     #   flask-security-too
     #   flask-wtf
 xlrd==2.0.1
     # via -r requirements/app.in
-zipp==3.16.2
+zipp==3.17.0
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
diff --git a/requirements/3.11/dev.txt b/requirements/3.11/dev.txt
index 42cc73acf..a11230867 100644
--- a/requirements/3.11/dev.txt
+++ b/requirements/3.11/dev.txt
@@ -13,9 +13,9 @@ click==8.1.7
     #   -c requirements/3.11/app.txt
     #   -c requirements/3.11/test.txt
     #   black
-distlib==0.3.7
+distlib==0.3.8
     # via virtualenv
-filelock==3.12.3
+filelock==3.13.1
     # via
     #   -c requirements/3.11/app.txt
     #   virtualenv
@@ -23,11 +23,11 @@ flake8==4.0.1
     # via -r requirements/dev.in
 flake8-blind-except==0.2.1
     # via -r requirements/dev.in
-identify==2.5.28
+identify==2.5.33
     # via pre-commit
 mccabe==0.6.1
     # via flake8
-mypy==1.5.1
+mypy==1.8.0
     # via -r requirements/dev.in
 mypy-extensions==1.0.0
     # via
@@ -35,39 +35,39 @@ mypy-extensions==1.0.0
     #   mypy
 nodeenv==1.8.0
     # via pre-commit
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.11/app.txt
     #   -c requirements/3.11/test.txt
     #   setuptools-scm
-pathspec==0.11.2
+pathspec==0.12.1
     # via black
-platformdirs==3.10.0
+platformdirs==4.2.0
     # via
     #   black
     #   virtualenv
-pre-commit==3.4.0
+pre-commit==3.6.0
     # via -r requirements/dev.in
 pycodestyle==2.8.0
     # via flake8
 pyflakes==2.4.0
     # via flake8
-pyinstrument==4.5.3
+pyinstrument==4.6.2
     # via -r requirements/dev.in
-pytest-runner==6.0.0
+pytest-runner==6.0.1
     # via -r requirements/dev.in
 pyyaml==6.0.1
     # via
     #   -c requirements/3.11/app.txt
     #   pre-commit
-setuptools-scm==7.1.0
+setuptools-scm==8.0.4
     # via -r requirements/dev.in
-typing-extensions==4.7.1
+typing-extensions==4.9.0
     # via
     #   -c requirements/3.11/app.txt
     #   mypy
     #   setuptools-scm
-virtualenv==20.24.5
+virtualenv==20.25.0
     # via pre-commit
 watchdog==3.0.0
     # via -r requirements/dev.in
diff --git a/requirements/3.11/docs.txt b/requirements/3.11/docs.txt
index 05e23084a..f583ea242 100644
--- a/requirements/3.11/docs.txt
+++ b/requirements/3.11/docs.txt
@@ -4,44 +4,44 @@
 #
 #    pip-compile --constraint=requirements/3.11/app.txt --output-file=requirements/3.11/docs.txt requirements/docs.in
 #
-alabaster==0.7.13
+alabaster==0.7.16
     # via sphinx
-babel==2.12.1
+babel==2.14.0
     # via
     #   -c requirements/3.11/app.txt
     #   sphinx
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.11/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.11/app.txt
     #   requests
-docutils==0.18.1
+docutils==0.20.1
     # via
     #   sphinx
     #   sphinx-rtd-theme
     #   sphinx-tabs
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.11/app.txt
     #   requests
 imagesize==1.4.1
     # via sphinx
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.11/app.txt
     #   sphinx
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.11/app.txt
     #   jinja2
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.11/app.txt
     #   sphinx
-pygments==2.16.1
+pygments==2.17.2
     # via
     #   sphinx
     #   sphinx-tabs
@@ -55,33 +55,28 @@ six==1.16.0
     #   sphinxcontrib-httpdomain
 snowballstemmer==2.2.0
     # via sphinx
-sphinx==7.2.5
+sphinx==7.2.6
     # via
     #   -r requirements/docs.in
     #   sphinx-copybutton
     #   sphinx-fontawesome
     #   sphinx-rtd-theme
     #   sphinx-tabs
-    #   sphinxcontrib-applehelp
-    #   sphinxcontrib-devhelp
-    #   sphinxcontrib-htmlhelp
     #   sphinxcontrib-httpdomain
     #   sphinxcontrib-jquery
-    #   sphinxcontrib-qthelp
-    #   sphinxcontrib-serializinghtml
 sphinx-copybutton==0.5.2
     # via -r requirements/docs.in
 sphinx-fontawesome==0.0.6
     # via -r requirements/docs.in
-sphinx-rtd-theme==1.3.0
+sphinx-rtd-theme==2.0.0
     # via -r requirements/docs.in
-sphinx-tabs==3.4.1
+sphinx-tabs==3.4.5
     # via -r requirements/docs.in
-sphinxcontrib-applehelp==1.0.7
+sphinxcontrib-applehelp==1.0.8
     # via sphinx
-sphinxcontrib-devhelp==1.0.5
+sphinxcontrib-devhelp==1.0.6
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.4
+sphinxcontrib-htmlhelp==2.0.5
     # via sphinx
 sphinxcontrib-httpdomain==1.8.1
     # via -r requirements/docs.in
@@ -89,11 +84,11 @@ sphinxcontrib-jquery==4.1
     # via sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.6
+sphinxcontrib-qthelp==1.0.7
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.9
+sphinxcontrib-serializinghtml==1.1.10
     # via sphinx
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.11/app.txt
     #   requests
diff --git a/requirements/3.8/app.txt b/requirements/3.8/app.txt
index 59b9b3cdb..069ef033a 100644
--- a/requirements/3.8/app.txt
+++ b/requirements/3.8/app.txt
@@ -4,35 +4,35 @@
 #
 #    pip-compile --output-file=requirements/3.8/app.txt requirements/app.in
 #
-alembic==1.12.0
+alembic==1.13.1
     # via flask-migrate
-altair==5.1.1
+altair==5.2.0
     # via -r requirements/app.in
-arrow==1.2.3
+arrow==1.3.0
     # via rq-dashboard
 async-timeout==4.0.3
     # via redis
-attrs==23.1.0
+attrs==23.2.0
     # via
     #   jsonschema
     #   referencing
-babel==2.12.1
+babel==2.14.0
     # via py-moneyed
 backports-zoneinfo==0.2.1
     # via workalendar
-bcrypt==4.0.1
+bcrypt==4.1.2
     # via -r requirements/app.in
-blinker==1.6.2
+blinker==1.7.0
     # via
     #   flask-mail
     #   flask-principal
     #   flask-security-too
     #   sentry-sdk
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   requests
     #   sentry-sdk
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via requests
 click==8.1.7
     # via
@@ -44,21 +44,21 @@ click-default-group==1.2.4
     # via -r requirements/app.in
 colour==0.1.5
     # via -r requirements/app.in
-contourpy==1.1.0
+contourpy==1.1.1
     # via matplotlib
 convertdate==2.4.0
     # via workalendar
-cycler==0.11.0
+cycler==0.12.1
     # via matplotlib
-dill==0.3.7
+dill==0.3.8
     # via openturns
-dnspython==2.4.2
+dnspython==2.5.0
     # via email-validator
-email-validator==2.0.0.post2
+email-validator==2.1.0.post1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-filelock==3.12.3
+filelock==3.13.1
     # via tldextract
 flask==2.2.5
     # via
@@ -83,50 +83,51 @@ flask-cors==4.0.0
     # via -r requirements/app.in
 flask-json==0.4.0
     # via -r requirements/app.in
-flask-login==0.6.2
+flask-login==0.6.3
     # via
     #   -r requirements/app.in
     #   flask-security-too
 flask-mail==0.9.1
     # via -r requirements/app.in
-flask-marshmallow==0.15.0
+flask-marshmallow==1.2.0
     # via -r requirements/app.in
-flask-migrate==4.0.4
+flask-migrate==4.0.5
     # via -r requirements/app.in
 flask-principal==0.4.0
     # via flask-security-too
 flask-security-too==5.1.2
     # via -r requirements/app.in
-flask-sqlalchemy==3.0.5
+flask-sqlalchemy==3.1.1
     # via
     #   -r requirements/app.in
     #   flask-migrate
 flask-sslify==0.1.5
     # via -r requirements/app.in
-flask-wtf==1.1.1
+flask-wtf==1.2.1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-fonttools==4.42.1
+fonttools==4.48.1
     # via matplotlib
-greenlet==2.0.2
+greenlet==3.0.3
     # via sqlalchemy
-humanize==4.8.0
+humanize==4.9.0
     # via -r requirements/app.in
-idna==3.4
+idna==3.6
     # via
     #   email-validator
     #   requests
     #   tldextract
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -r requirements/app.in
     #   alembic
     #   flask
     #   timely-beliefs
-importlib-resources==6.0.1
+importlib-resources==6.1.1
     # via
     #   alembic
+    #   flask-security-too
     #   jsonschema
     #   jsonschema-specifications
     #   matplotlib
@@ -134,7 +135,7 @@ inflect==6.0.2
     # via -r requirements/app.in
 inflection==0.5.1
     # via -r requirements/app.in
-iso8601==2.0.0
+iso8601==2.1.0
     # via -r requirements/app.in
 isodate==0.6.1
     # via
@@ -145,30 +146,31 @@ itsdangerous==2.1.2
     #   flask
     #   flask-security-too
     #   flask-wtf
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   altair
     #   flask
 joblib==1.3.2
     # via scikit-learn
-jsonschema==4.19.0
+jsonschema==4.21.1
     # via altair
-jsonschema-specifications==2023.7.1
+jsonschema-specifications==2023.12.1
     # via jsonschema
 kiwisolver==1.4.5
     # via matplotlib
-lunardate==0.2.0
+lunardate==0.2.2
     # via workalendar
-mako==1.2.4
+mako==1.3.2
     # via alembic
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
+    #   flask-security-too
     #   jinja2
     #   mako
     #   sentry-sdk
     #   werkzeug
     #   wtforms
-marshmallow==3.20.1
+marshmallow==3.20.2
     # via
     #   -r requirements/app.in
     #   flask-marshmallow
@@ -177,9 +179,9 @@ marshmallow==3.20.1
     #   webargs
 marshmallow-polyfield==5.11
     # via -r requirements/app.in
-marshmallow-sqlalchemy==0.29.0
+marshmallow-sqlalchemy==1.0.0
     # via -r requirements/app.in
-matplotlib==3.7.3
+matplotlib==3.7.4
     # via timetomodel
 numpy==1.24.4
     # via
@@ -197,14 +199,12 @@ numpy==1.24.4
     #   timely-beliefs
     #   timetomodel
     #   uniplot
-openturns==1.21
+openturns==1.22
     # via timely-beliefs
-packaging==23.1
+packaging==23.2
     # via
     #   altair
-    #   flask-marshmallow
     #   marshmallow
-    #   marshmallow-sqlalchemy
     #   matplotlib
     #   sktime
     #   statsmodels
@@ -219,9 +219,9 @@ pandas==2.0.3
     #   timetomodel
 passlib==1.7.4
     # via flask-security-too
-patsy==0.5.3
+patsy==0.5.6
     # via statsmodels
-pillow==10.0.1
+pillow==10.2.0
     # via
     #   -r requirements/app.in
     #   matplotlib
@@ -235,15 +235,15 @@ properscoring==0.1
     # via timely-beliefs
 pscript==0.7.7
     # via -r requirements/app.in
-psutil==5.9.5
+psutil==5.9.8
     # via openturns
-psycopg2-binary==2.9.7
+psycopg2-binary==2.9.9
     # via
     #   -r requirements/app.in
     #   timely-beliefs
 py-moneyed==3.0
     # via -r requirements/app.in
-pydantic==1.10.12
+pydantic==1.10.14
     # via
     #   -r requirements/app.in
     #   inflect
@@ -251,7 +251,7 @@ pyluach==2.2.0
     # via workalendar
 pymeeus==0.5.12
     # via convertdate
-pyomo==6.6.2
+pyomo==6.7.0
     # via -r requirements/app.in
 pyparsing==3.1.1
     # via matplotlib
@@ -262,9 +262,9 @@ python-dateutil==2.8.2
     #   pandas
     #   timetomodel
     #   workalendar
-python-dotenv==1.0.0
+python-dotenv==1.0.1
     # via -r requirements/app.in
-pytz==2023.3.post1
+pytz==2024.1
     # via
     #   -r requirements/app.in
     #   babel
@@ -281,7 +281,7 @@ redis==4.6.0
     #   rq-dashboard
 redis-sentinel-url==1.0.1
     # via rq-dashboard
-referencing==0.30.2
+referencing==0.33.0
     # via
     #   jsonschema
     #   jsonschema-specifications
@@ -289,9 +289,9 @@ requests==2.31.0
     # via
     #   requests-file
     #   tldextract
-requests-file==1.5.1
+requests-file==2.0.0
     # via tldextract
-rpds-py==0.10.2
+rpds-py==0.17.1
     # via
     #   jsonschema
     #   referencing
@@ -301,9 +301,9 @@ rq==1.15.1
     #   rq-dashboard
 rq-dashboard==0.6.7
     # via -r requirements/app.in
-scikit-base==0.5.1
+scikit-base==0.7.2
     # via sktime
-scikit-learn==1.3.0
+scikit-learn==1.3.2
     # via
     #   sktime
     #   timetomodel
@@ -315,17 +315,16 @@ scipy==1.10.1
     #   statsmodels
     #   timely-beliefs
     #   timetomodel
-sentry-sdk[flask]==1.30.0
+sentry-sdk[flask]==1.40.1
     # via -r requirements/app.in
 six==1.16.0
     # via
     #   isodate
     #   patsy
     #   python-dateutil
-    #   requests-file
-sktime==0.22.0
+sktime==0.26.0
     # via timely-beliefs
-sqlalchemy==1.4.49
+sqlalchemy==2.0.25
     # via
     #   -r requirements/app.in
     #   alembic
@@ -333,38 +332,40 @@ sqlalchemy==1.4.49
     #   marshmallow-sqlalchemy
     #   timely-beliefs
     #   timetomodel
-statsmodels==0.14.0
+statsmodels==0.14.1
     # via timetomodel
 tabulate==0.9.0
     # via -r requirements/app.in
 threadpoolctl==3.2.0
     # via scikit-learn
-timely-beliefs[forecast]==1.25.1
+timely-beliefs[forecast]==2.0.0
     # via -r requirements/app.in
 timetomodel==0.7.3
     # via -r requirements/app.in
-tldextract==3.5.0
+tldextract==5.1.1
     # via -r requirements/app.in
-toolz==0.12.0
+toolz==0.12.1
     # via altair
-typing-extensions==4.7.1
+types-python-dateutil==2.8.19.20240106
+    # via arrow
+typing-extensions==4.9.0
     # via
     #   alembic
     #   altair
-    #   filelock
     #   py-moneyed
     #   pydantic
-tzdata==2023.3
+    #   sqlalchemy
+tzdata==2023.4
     # via pandas
-uniplot==0.10.2
+uniplot==0.11.0
     # via -r requirements/app.in
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   requests
     #   sentry-sdk
-vl-convert-python==0.13.1
+vl-convert-python==1.2.3
     # via -r requirements/app.in
-webargs==8.3.0
+webargs==8.4.0
     # via -r requirements/app.in
 werkzeug==2.2.3
     # via
@@ -373,13 +374,13 @@ werkzeug==2.2.3
     #   flask-login
 workalendar==17.0.0
     # via -r requirements/app.in
-wtforms==3.0.1
+wtforms==3.1.2
     # via
     #   flask-security-too
     #   flask-wtf
 xlrd==2.0.1
     # via -r requirements/app.in
-zipp==3.16.2
+zipp==3.17.0
     # via
     #   importlib-metadata
     #   importlib-resources
diff --git a/requirements/3.8/dev.txt b/requirements/3.8/dev.txt
index 87145f421..467112958 100644
--- a/requirements/3.8/dev.txt
+++ b/requirements/3.8/dev.txt
@@ -13,9 +13,9 @@ click==8.1.7
     #   -c requirements/3.8/app.txt
     #   -c requirements/3.8/test.txt
     #   black
-distlib==0.3.7
+distlib==0.3.8
     # via virtualenv
-filelock==3.12.3
+filelock==3.13.1
     # via
     #   -c requirements/3.8/app.txt
     #   virtualenv
@@ -23,11 +23,11 @@ flake8==4.0.1
     # via -r requirements/dev.in
 flake8-blind-except==0.2.1
     # via -r requirements/dev.in
-identify==2.5.28
+identify==2.5.33
     # via pre-commit
 mccabe==0.6.1
     # via flake8
-mypy==1.5.1
+mypy==1.8.0
     # via -r requirements/dev.in
 mypy-extensions==1.0.0
     # via
@@ -35,32 +35,32 @@ mypy-extensions==1.0.0
     #   mypy
 nodeenv==1.8.0
     # via pre-commit
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.8/app.txt
     #   -c requirements/3.8/test.txt
     #   setuptools-scm
-pathspec==0.11.2
+pathspec==0.12.1
     # via black
-platformdirs==3.10.0
+platformdirs==4.2.0
     # via
     #   black
     #   virtualenv
-pre-commit==3.4.0
+pre-commit==3.5.0
     # via -r requirements/dev.in
 pycodestyle==2.8.0
     # via flake8
 pyflakes==2.4.0
     # via flake8
-pyinstrument==4.5.3
+pyinstrument==4.6.2
     # via -r requirements/dev.in
-pytest-runner==6.0.0
+pytest-runner==6.0.1
     # via -r requirements/dev.in
 pyyaml==6.0.1
     # via
     #   -c requirements/3.8/app.txt
     #   pre-commit
-setuptools-scm==7.1.0
+setuptools-scm==8.0.4
     # via -r requirements/dev.in
 tomli==2.0.1
     # via
@@ -68,14 +68,13 @@ tomli==2.0.1
     #   black
     #   mypy
     #   setuptools-scm
-typing-extensions==4.7.1
+typing-extensions==4.9.0
     # via
     #   -c requirements/3.8/app.txt
     #   black
-    #   filelock
     #   mypy
     #   setuptools-scm
-virtualenv==20.24.5
+virtualenv==20.25.0
     # via pre-commit
 watchdog==3.0.0
     # via -r requirements/dev.in
diff --git a/requirements/3.8/docs.txt b/requirements/3.8/docs.txt
index cb7e29d7a..369b0aac9 100644
--- a/requirements/3.8/docs.txt
+++ b/requirements/3.8/docs.txt
@@ -6,50 +6,50 @@
 #
 alabaster==0.7.13
     # via sphinx
-babel==2.12.1
+babel==2.14.0
     # via
     #   -c requirements/3.8/app.txt
     #   sphinx
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.8/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.8/app.txt
     #   requests
-docutils==0.18.1
+docutils==0.20.1
     # via
     #   sphinx
     #   sphinx-rtd-theme
     #   sphinx-tabs
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.8/app.txt
     #   requests
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -c requirements/3.8/app.txt
     #   sphinx
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.8/app.txt
     #   sphinx
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.8/app.txt
     #   jinja2
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.8/app.txt
     #   sphinx
-pygments==2.16.1
+pygments==2.17.2
     # via
     #   sphinx
     #   sphinx-tabs
-pytz==2023.3.post1
+pytz==2024.1
     # via
     #   -c requirements/3.8/app.txt
     #   babel
@@ -76,9 +76,9 @@ sphinx-copybutton==0.5.2
     # via -r requirements/docs.in
 sphinx-fontawesome==0.0.6
     # via -r requirements/docs.in
-sphinx-rtd-theme==1.3.0
+sphinx-rtd-theme==2.0.0
     # via -r requirements/docs.in
-sphinx-tabs==3.4.1
+sphinx-tabs==3.4.5
     # via -r requirements/docs.in
 sphinxcontrib-applehelp==1.0.4
     # via sphinx
@@ -96,11 +96,11 @@ sphinxcontrib-qthelp==1.0.3
     # via sphinx
 sphinxcontrib-serializinghtml==1.1.5
     # via sphinx
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.8/app.txt
     #   requests
-zipp==3.16.2
+zipp==3.17.0
     # via
     #   -c requirements/3.8/app.txt
     #   importlib-metadata
diff --git a/requirements/3.9/app.txt b/requirements/3.9/app.txt
index 5b15925b7..4b0cdd975 100644
--- a/requirements/3.9/app.txt
+++ b/requirements/3.9/app.txt
@@ -4,33 +4,33 @@
 #
 #    pip-compile --output-file=requirements/3.9/app.txt requirements/app.in
 #
-alembic==1.12.0
+alembic==1.13.1
     # via flask-migrate
-altair==5.1.1
+altair==5.2.0
     # via -r requirements/app.in
-arrow==1.2.3
+arrow==1.3.0
     # via rq-dashboard
 async-timeout==4.0.3
     # via redis
-attrs==23.1.0
+attrs==23.2.0
     # via
     #   jsonschema
     #   referencing
-babel==2.12.1
+babel==2.14.0
     # via py-moneyed
-bcrypt==4.0.1
+bcrypt==4.1.2
     # via -r requirements/app.in
-blinker==1.6.2
+blinker==1.7.0
     # via
     #   flask-mail
     #   flask-principal
     #   flask-security-too
     #   sentry-sdk
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   requests
     #   sentry-sdk
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via requests
 click==8.1.7
     # via
@@ -42,21 +42,21 @@ click-default-group==1.2.4
     # via -r requirements/app.in
 colour==0.1.5
     # via -r requirements/app.in
-contourpy==1.1.0
+contourpy==1.2.0
     # via matplotlib
 convertdate==2.4.0
     # via workalendar
-cycler==0.11.0
+cycler==0.12.1
     # via matplotlib
-dill==0.3.7
+dill==0.3.8
     # via openturns
-dnspython==2.4.2
+dnspython==2.5.0
     # via email-validator
-email-validator==2.0.0.post2
+email-validator==2.1.0.post1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-filelock==3.12.3
+filelock==3.13.1
     # via tldextract
 flask==2.2.5
     # via
@@ -81,53 +81,55 @@ flask-cors==4.0.0
     # via -r requirements/app.in
 flask-json==0.4.0
     # via -r requirements/app.in
-flask-login==0.6.2
+flask-login==0.6.3
     # via
     #   -r requirements/app.in
     #   flask-security-too
 flask-mail==0.9.1
     # via -r requirements/app.in
-flask-marshmallow==0.15.0
+flask-marshmallow==1.2.0
     # via -r requirements/app.in
-flask-migrate==4.0.4
+flask-migrate==4.0.5
     # via -r requirements/app.in
 flask-principal==0.4.0
     # via flask-security-too
 flask-security-too==5.1.2
     # via -r requirements/app.in
-flask-sqlalchemy==3.0.5
+flask-sqlalchemy==3.1.1
     # via
     #   -r requirements/app.in
     #   flask-migrate
 flask-sslify==0.1.5
     # via -r requirements/app.in
-flask-wtf==1.1.1
+flask-wtf==1.2.1
     # via
     #   -r requirements/app.in
     #   flask-security-too
-fonttools==4.42.1
+fonttools==4.48.1
     # via matplotlib
-greenlet==2.0.2
+greenlet==3.0.3
     # via sqlalchemy
-humanize==4.8.0
+humanize==4.9.0
     # via -r requirements/app.in
-idna==3.4
+idna==3.6
     # via
     #   email-validator
     #   requests
     #   tldextract
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -r requirements/app.in
     #   flask
     #   timely-beliefs
-importlib-resources==6.0.1
-    # via matplotlib
+importlib-resources==6.1.1
+    # via
+    #   flask-security-too
+    #   matplotlib
 inflect==6.0.2
     # via -r requirements/app.in
 inflection==0.5.1
     # via -r requirements/app.in
-iso8601==2.0.0
+iso8601==2.1.0
     # via -r requirements/app.in
 isodate==0.6.1
     # via
@@ -138,30 +140,31 @@ itsdangerous==2.1.2
     #   flask
     #   flask-security-too
     #   flask-wtf
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   altair
     #   flask
 joblib==1.3.2
     # via scikit-learn
-jsonschema==4.19.0
+jsonschema==4.21.1
     # via altair
-jsonschema-specifications==2023.7.1
+jsonschema-specifications==2023.12.1
     # via jsonschema
 kiwisolver==1.4.5
     # via matplotlib
-lunardate==0.2.0
+lunardate==0.2.2
     # via workalendar
-mako==1.2.4
+mako==1.3.2
     # via alembic
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
+    #   flask-security-too
     #   jinja2
     #   mako
     #   sentry-sdk
     #   werkzeug
     #   wtforms
-marshmallow==3.20.1
+marshmallow==3.20.2
     # via
     #   -r requirements/app.in
     #   flask-marshmallow
@@ -170,9 +173,9 @@ marshmallow==3.20.1
     #   webargs
 marshmallow-polyfield==5.11
     # via -r requirements/app.in
-marshmallow-sqlalchemy==0.29.0
+marshmallow-sqlalchemy==1.0.0
     # via -r requirements/app.in
-matplotlib==3.7.3
+matplotlib==3.8.2
     # via timetomodel
 numpy==1.24.4
     # via
@@ -190,19 +193,17 @@ numpy==1.24.4
     #   timely-beliefs
     #   timetomodel
     #   uniplot
-openturns==1.21
+openturns==1.22
     # via timely-beliefs
-packaging==23.1
+packaging==23.2
     # via
     #   altair
-    #   flask-marshmallow
     #   marshmallow
-    #   marshmallow-sqlalchemy
     #   matplotlib
     #   sktime
     #   statsmodels
     #   webargs
-pandas==2.0.3
+pandas==2.1.4
     # via
     #   -r requirements/app.in
     #   altair
@@ -212,13 +213,13 @@ pandas==2.0.3
     #   timetomodel
 passlib==1.7.4
     # via flask-security-too
-patsy==0.5.3
+patsy==0.5.6
     # via statsmodels
-pillow==10.0.1
+pillow==10.2.0
     # via
     #   -r requirements/app.in
     #   matplotlib
-pint==0.22
+pint==0.23
     # via -r requirements/app.in
 ply==3.11
     # via pyomo
@@ -226,15 +227,15 @@ properscoring==0.1
     # via timely-beliefs
 pscript==0.7.7
     # via -r requirements/app.in
-psutil==5.9.5
+psutil==5.9.8
     # via openturns
-psycopg2-binary==2.9.7
+psycopg2-binary==2.9.9
     # via
     #   -r requirements/app.in
     #   timely-beliefs
 py-moneyed==3.0
     # via -r requirements/app.in
-pydantic==1.10.12
+pydantic==1.10.14
     # via
     #   -r requirements/app.in
     #   inflect
@@ -242,7 +243,7 @@ pyluach==2.2.0
     # via workalendar
 pymeeus==0.5.12
     # via convertdate
-pyomo==6.6.2
+pyomo==6.7.0
     # via -r requirements/app.in
 pyparsing==3.1.1
     # via matplotlib
@@ -253,9 +254,9 @@ python-dateutil==2.8.2
     #   pandas
     #   timetomodel
     #   workalendar
-python-dotenv==1.0.0
+python-dotenv==1.0.1
     # via -r requirements/app.in
-pytz==2023.3.post1
+pytz==2024.1
     # via
     #   -r requirements/app.in
     #   pandas
@@ -271,7 +272,7 @@ redis==4.6.0
     #   rq-dashboard
 redis-sentinel-url==1.0.1
     # via rq-dashboard
-referencing==0.30.2
+referencing==0.33.0
     # via
     #   jsonschema
     #   jsonschema-specifications
@@ -279,9 +280,9 @@ requests==2.31.0
     # via
     #   requests-file
     #   tldextract
-requests-file==1.5.1
+requests-file==2.0.0
     # via tldextract
-rpds-py==0.10.2
+rpds-py==0.17.1
     # via
     #   jsonschema
     #   referencing
@@ -291,13 +292,13 @@ rq==1.15.1
     #   rq-dashboard
 rq-dashboard==0.6.7
     # via -r requirements/app.in
-scikit-base==0.5.1
+scikit-base==0.7.2
     # via sktime
-scikit-learn==1.3.0
+scikit-learn==1.4.0
     # via
     #   sktime
     #   timetomodel
-scipy==1.11.2
+scipy==1.12.0
     # via
     #   properscoring
     #   scikit-learn
@@ -305,17 +306,16 @@ scipy==1.11.2
     #   statsmodels
     #   timely-beliefs
     #   timetomodel
-sentry-sdk[flask]==1.30.0
+sentry-sdk[flask]==1.40.1
     # via -r requirements/app.in
 six==1.16.0
     # via
     #   isodate
     #   patsy
     #   python-dateutil
-    #   requests-file
-sktime==0.22.0
+sktime==0.26.0
     # via timely-beliefs
-sqlalchemy==1.4.49
+sqlalchemy==2.0.25
     # via
     #   -r requirements/app.in
     #   alembic
@@ -323,39 +323,41 @@ sqlalchemy==1.4.49
     #   marshmallow-sqlalchemy
     #   timely-beliefs
     #   timetomodel
-statsmodels==0.14.0
+statsmodels==0.14.1
     # via timetomodel
 tabulate==0.9.0
     # via -r requirements/app.in
 threadpoolctl==3.2.0
     # via scikit-learn
-timely-beliefs[forecast]==1.25.1
+timely-beliefs[forecast]==2.0.0
     # via -r requirements/app.in
 timetomodel==0.7.3
     # via -r requirements/app.in
-tldextract==3.5.0
+tldextract==5.1.1
     # via -r requirements/app.in
-toolz==0.12.0
+toolz==0.12.1
     # via altair
-typing-extensions==4.7.1
+types-python-dateutil==2.8.19.20240106
+    # via arrow
+typing-extensions==4.9.0
     # via
     #   alembic
     #   altair
-    #   filelock
     #   pint
     #   py-moneyed
     #   pydantic
-tzdata==2023.3
+    #   sqlalchemy
+tzdata==2023.4
     # via pandas
-uniplot==0.10.2
+uniplot==0.11.0
     # via -r requirements/app.in
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   requests
     #   sentry-sdk
-vl-convert-python==0.13.1
+vl-convert-python==1.2.3
     # via -r requirements/app.in
-webargs==8.3.0
+webargs==8.4.0
     # via -r requirements/app.in
 werkzeug==2.2.3
     # via
@@ -364,13 +366,13 @@ werkzeug==2.2.3
     #   flask-login
 workalendar==17.0.0
     # via -r requirements/app.in
-wtforms==3.0.1
+wtforms==3.1.2
     # via
     #   flask-security-too
     #   flask-wtf
 xlrd==2.0.1
     # via -r requirements/app.in
-zipp==3.16.2
+zipp==3.17.0
     # via
     #   importlib-metadata
     #   importlib-resources
diff --git a/requirements/3.9/dev.txt b/requirements/3.9/dev.txt
index 1bd2dba5d..9aeea581e 100644
--- a/requirements/3.9/dev.txt
+++ b/requirements/3.9/dev.txt
@@ -13,9 +13,9 @@ click==8.1.7
     #   -c requirements/3.9/app.txt
     #   -c requirements/3.9/test.txt
     #   black
-distlib==0.3.7
+distlib==0.3.8
     # via virtualenv
-filelock==3.12.3
+filelock==3.13.1
     # via
     #   -c requirements/3.9/app.txt
     #   virtualenv
@@ -23,11 +23,11 @@ flake8==4.0.1
     # via -r requirements/dev.in
 flake8-blind-except==0.2.1
     # via -r requirements/dev.in
-identify==2.5.28
+identify==2.5.33
     # via pre-commit
 mccabe==0.6.1
     # via flake8
-mypy==1.5.1
+mypy==1.8.0
     # via -r requirements/dev.in
 mypy-extensions==1.0.0
     # via
@@ -35,32 +35,32 @@ mypy-extensions==1.0.0
     #   mypy
 nodeenv==1.8.0
     # via pre-commit
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.9/app.txt
     #   -c requirements/3.9/test.txt
     #   setuptools-scm
-pathspec==0.11.2
+pathspec==0.12.1
     # via black
-platformdirs==3.10.0
+platformdirs==4.2.0
     # via
     #   black
     #   virtualenv
-pre-commit==3.4.0
+pre-commit==3.6.0
     # via -r requirements/dev.in
 pycodestyle==2.8.0
     # via flake8
 pyflakes==2.4.0
     # via flake8
-pyinstrument==4.5.3
+pyinstrument==4.6.2
     # via -r requirements/dev.in
-pytest-runner==6.0.0
+pytest-runner==6.0.1
     # via -r requirements/dev.in
 pyyaml==6.0.1
     # via
     #   -c requirements/3.9/app.txt
     #   pre-commit
-setuptools-scm==7.1.0
+setuptools-scm==8.0.4
     # via -r requirements/dev.in
 tomli==2.0.1
     # via
@@ -68,14 +68,13 @@ tomli==2.0.1
     #   black
     #   mypy
     #   setuptools-scm
-typing-extensions==4.7.1
+typing-extensions==4.9.0
     # via
     #   -c requirements/3.9/app.txt
     #   black
-    #   filelock
     #   mypy
     #   setuptools-scm
-virtualenv==20.24.5
+virtualenv==20.25.0
     # via pre-commit
 watchdog==3.0.0
     # via -r requirements/dev.in
diff --git a/requirements/3.9/docs.txt b/requirements/3.9/docs.txt
index 5984bcc91..700b53f67 100644
--- a/requirements/3.9/docs.txt
+++ b/requirements/3.9/docs.txt
@@ -4,48 +4,48 @@
 #
 #    pip-compile --constraint=requirements/3.9/app.txt --output-file=requirements/3.9/docs.txt requirements/docs.in
 #
-alabaster==0.7.13
+alabaster==0.7.16
     # via sphinx
-babel==2.12.1
+babel==2.14.0
     # via
     #   -c requirements/3.9/app.txt
     #   sphinx
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.9/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.9/app.txt
     #   requests
-docutils==0.18.1
+docutils==0.20.1
     # via
     #   sphinx
     #   sphinx-rtd-theme
     #   sphinx-tabs
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.9/app.txt
     #   requests
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -c requirements/3.9/app.txt
     #   sphinx
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.9/app.txt
     #   sphinx
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.9/app.txt
     #   jinja2
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.9/app.txt
     #   sphinx
-pygments==2.16.1
+pygments==2.17.2
     # via
     #   sphinx
     #   sphinx-tabs
@@ -59,33 +59,28 @@ six==1.16.0
     #   sphinxcontrib-httpdomain
 snowballstemmer==2.2.0
     # via sphinx
-sphinx==7.2.5
+sphinx==7.2.6
     # via
     #   -r requirements/docs.in
     #   sphinx-copybutton
     #   sphinx-fontawesome
     #   sphinx-rtd-theme
     #   sphinx-tabs
-    #   sphinxcontrib-applehelp
-    #   sphinxcontrib-devhelp
-    #   sphinxcontrib-htmlhelp
     #   sphinxcontrib-httpdomain
     #   sphinxcontrib-jquery
-    #   sphinxcontrib-qthelp
-    #   sphinxcontrib-serializinghtml
 sphinx-copybutton==0.5.2
     # via -r requirements/docs.in
 sphinx-fontawesome==0.0.6
     # via -r requirements/docs.in
-sphinx-rtd-theme==1.3.0
+sphinx-rtd-theme==2.0.0
     # via -r requirements/docs.in
-sphinx-tabs==3.4.1
+sphinx-tabs==3.4.5
     # via -r requirements/docs.in
-sphinxcontrib-applehelp==1.0.7
+sphinxcontrib-applehelp==1.0.8
     # via sphinx
-sphinxcontrib-devhelp==1.0.5
+sphinxcontrib-devhelp==1.0.6
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.4
+sphinxcontrib-htmlhelp==2.0.5
     # via sphinx
 sphinxcontrib-httpdomain==1.8.1
     # via -r requirements/docs.in
@@ -93,15 +88,15 @@ sphinxcontrib-jquery==4.1
     # via sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
-sphinxcontrib-qthelp==1.0.6
+sphinxcontrib-qthelp==1.0.7
     # via sphinx
-sphinxcontrib-serializinghtml==1.1.9
+sphinxcontrib-serializinghtml==1.1.10
     # via sphinx
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.9/app.txt
     #   requests
-zipp==3.16.2
+zipp==3.17.0
     # via
     #   -c requirements/3.9/app.txt
     #   importlib-metadata
diff --git a/requirements/Readme.md b/requirements/Readme.md
index eaec3c4d0..2e0096576 100644
--- a/requirements/Readme.md
+++ b/requirements/Readme.md
@@ -18,4 +18,6 @@ Here, we describe the requirements. We give the name of a requirement or even a
 
 These files are not to be edited by hand. They are created by `pip-compile` (or `make freeze-deps`).
 
-Each requirement is pinned to a specific version in these files. The great benefit is reproducability across environemnts (local dev as well as staging or production).
+Each requirement is pinned to a specific version in these files. The great benefit is reproducibility across environments (local dev as well as staging or production).
+
+To update the .txt files for all supported Python versions, see `ci/update-packages.sh`.
diff --git a/requirements/app.in b/requirements/app.in
index bb8e397ce..24bad1056 100644
--- a/requirements/app.in
+++ b/requirements/app.in
@@ -38,7 +38,7 @@ python-dotenv
 # a backport, not needed in Python3.8
 importlib_metadata
 # see GH#607 for issue on this pin
-sqlalchemy>=1.4.0, <2
+sqlalchemy>=2.0
 Flask-SSLify
 # use <0.4 if not compatible with Flask2.2 yet
 Flask_JSON


Test patch:
diff --git a/flexmeasures/api/dev/tests/conftest.py b/flexmeasures/api/dev/tests/conftest.py
index 540859eb3..7c5d24598 100644
--- a/flexmeasures/api/dev/tests/conftest.py
+++ b/flexmeasures/api/dev/tests/conftest.py
@@ -1,4 +1,5 @@
 import pytest
+from sqlalchemy import select
 
 from flexmeasures import User
 from flexmeasures.api.v3_0.tests.conftest import add_incineration_line
@@ -11,7 +12,9 @@ def setup_api_test_data(db, setup_roles_users, setup_generic_assets):
     Set up data for API dev tests.
     """
     print("Setting up data for API dev tests on %s" % db.engine)
-    add_incineration_line(db, User.query.get(setup_roles_users["Test Supplier User"]))
+    add_incineration_line(
+        db, db.session.get(User, setup_roles_users["Test Supplier User"])
+    )
 
 
 @pytest.fixture(scope="function")
@@ -22,8 +25,9 @@ def setup_api_fresh_test_data(
     Set up fresh data for API dev tests.
     """
     print("Setting up fresh data for API dev tests on %s" % fresh_db.engine)
-    for sensor in Sensor.query.all():
+    for sensor in fresh_db.session.scalars(select(Sensor)).all():
         fresh_db.delete(sensor)
     add_incineration_line(
-        fresh_db, User.query.get(setup_roles_users_fresh_db["Test Supplier User"])
+        fresh_db,
+        fresh_db.session.get(User, setup_roles_users_fresh_db["Test Supplier User"]),
     )
diff --git a/flexmeasures/api/tests/utils.py b/flexmeasures/api/tests/utils.py
index fb3b0bde9..61b71f8fe 100644
--- a/flexmeasures/api/tests/utils.py
+++ b/flexmeasures/api/tests/utils.py
@@ -3,6 +3,7 @@
 import json
 
 from flask import url_for, current_app, Response
+from sqlalchemy import select
 
 from flexmeasures.data import db
 from flexmeasures.data.services.users import find_user_by_email
@@ -45,9 +46,9 @@ class AccountContext(object):
     """
 
     def __init__(self, account_name: str):
-        self.the_account = Account.query.filter(
-            Account.name == account_name
-        ).one_or_none()
+        self.the_account = db.session.execute(
+            select(Account).filter(Account.name == account_name)
+        ).scalar_one_or_none()
 
     def __enter__(self):
         return self.the_account
diff --git a/flexmeasures/api/v3_0/tests/conftest.py b/flexmeasures/api/v3_0/tests/conftest.py
index 0ea0fb240..74a70383f 100644
--- a/flexmeasures/api/v3_0/tests/conftest.py
+++ b/flexmeasures/api/v3_0/tests/conftest.py
@@ -5,6 +5,7 @@
 import pandas as pd
 import pytest
 from flask_security import SQLAlchemySessionUserDatastore, hash_password
+from sqlalchemy import select, delete
 
 from flexmeasures import Sensor, Source, User, UserRole
 from flexmeasures.data.models.generic_assets import GenericAssetType, GenericAsset
@@ -20,7 +21,7 @@ def setup_api_test_data(
     """
     print("Setting up data for API v3.0 tests on %s" % db.engine)
     sensors = add_incineration_line(
-        db, User.query.get(setup_roles_users["Test Supplier User"])
+        db, db.session.get(User, setup_roles_users["Test Supplier User"])
     )
     return sensors
 
@@ -33,10 +34,11 @@ def setup_api_fresh_test_data(
     Set up fresh data for API dev tests.
     """
     print("Setting up fresh data for API 3.0 tests on %s" % fresh_db.engine)
-    for sensor in Sensor.query.all():
-        fresh_db.delete(sensor)
+    for sensor in fresh_db.session.scalars(select(Sensor)).all():
+        fresh_db.session.execute(delete(Sensor).filter_by(id=sensor.id))
     sensors = add_incineration_line(
-        fresh_db, User.query.get(setup_roles_users_fresh_db["Test Supplier User"])
+        fresh_db,
+        fresh_db.session.get(User, setup_roles_users_fresh_db["Test Supplier User"]),
     )
     return sensors
 
@@ -108,7 +110,8 @@ def add_asset_with_children(db, setup_roles_users):
         generic_asset_type=parent_type,
         account_id=test_supplier_user,
     )
-    db.session.flush()  # assign sensor ids
+    db.session.add(parent)
+    db.session.flush()  # assign parent asset id
 
     assets = [
         GenericAsset(
@@ -121,7 +124,7 @@ def add_asset_with_children(db, setup_roles_users):
     ]
 
     db.session.add_all(assets)
-    db.session.flush()  # assign sensor ids
+    db.session.flush()  # assign children asset ids
 
     assets.append(parent)
 
diff --git a/flexmeasures/api/v3_0/tests/test_assets_api.py b/flexmeasures/api/v3_0/tests/test_assets_api.py
index 53f72e736..13192545f 100644
--- a/flexmeasures/api/v3_0/tests/test_assets_api.py
+++ b/flexmeasures/api/v3_0/tests/test_assets_api.py
@@ -2,6 +2,7 @@
 
 from flask import url_for
 import pytest
+from sqlalchemy import select, func
 
 from flexmeasures.data.models.generic_assets import GenericAsset
 from flexmeasures.data.services.users import find_user_by_email
@@ -289,7 +290,9 @@ def test_posting_multiple_assets(client, setup_api_test_data, requesting_user):
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_post_an_asset_with_invalid_data(client, setup_api_test_data, requesting_user):
+def test_post_an_asset_with_invalid_data(
+    client, setup_api_test_data, requesting_user, db
+):
     """
     Add an asset with some fields having invalid data and one field missing.
     The right error messages should be in the response and the number of assets has not increased.
@@ -318,13 +321,17 @@ def test_post_an_asset_with_invalid_data(client, setup_api_test_data, requesting
     )
 
     assert (
-        GenericAsset.query.filter_by(account_id=requesting_user.account.id).count()
+        db.session.scalar(
+            select(func.count())
+            .select_from(GenericAsset)
+            .filter_by(account_id=requesting_user.account.id)
+        )
         == num_assets_before
     )
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_post_an_asset(client, setup_api_test_data, requesting_user):
+def test_post_an_asset(client, setup_api_test_data, requesting_user, db):
     """
     Post one extra asset, as an admin user.
     TODO: Soon we'll allow creating assets on an account-basis, i.e. for users
@@ -340,22 +347,24 @@ def test_post_an_asset(client, setup_api_test_data, requesting_user):
     assert post_assets_response.status_code == 201
     assert post_assets_response.json["latitude"] == 30.1
 
-    asset: GenericAsset = GenericAsset.query.filter_by(
-        name="Test battery 2"
-    ).one_or_none()
+    asset: GenericAsset = db.session.execute(
+        select(GenericAsset).filter_by(name="Test battery 2")
+    ).scalar_one_or_none()
     assert asset is not None
     assert asset.latitude == 30.1
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_delete_an_asset(client, setup_api_test_data, requesting_user):
+def test_delete_an_asset(client, setup_api_test_data, requesting_user, db):
     existing_asset_id = setup_api_test_data["some gas sensor"].generic_asset.id
 
     delete_asset_response = client.delete(
         url_for("AssetAPI:delete", id=existing_asset_id),
     )
     assert delete_asset_response.status_code == 204
-    deleted_asset = GenericAsset.query.filter_by(id=existing_asset_id).one_or_none()
+    deleted_asset = db.session.execute(
+        select(GenericAsset).filter_by(id=existing_asset_id)
+    ).scalar_one_or_none()
     assert deleted_asset is None
 
 
@@ -388,18 +397,15 @@ def test_consultant_can_read(
 
 @pytest.mark.parametrize("requesting_user", ["test_consultant@seita.nl"], indirect=True)
 def test_consultant_can_not_patch(
-    client,
-    setup_api_test_data,
-    setup_accounts,
-    requesting_user,
+    client, setup_api_test_data, setup_accounts, requesting_user, db
 ):
     """
     Try to edit an asset belonging to the ConsultancyClient account with the Consultant account.
     The Consultant account only has read access.
     """
-    consultancy_client_asset = GenericAsset.query.filter_by(
-        name="Test ConsultancyClient Asset"
-    ).one_or_none()
+    consultancy_client_asset = db.session.execute(
+        select(GenericAsset).filter_by(name="Test ConsultancyClient Asset")
+    ).scalar_one_or_none()
     print(consultancy_client_asset)
 
     asset_edit_response = client.patch(
@@ -448,7 +454,7 @@ def test_consultancy_user_without_consultant_role(
 )
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
 def test_post_an_asset_with_existing_name(
-    client, add_asset_with_children, parent_name, child_name, fails, requesting_user
+    client, add_asset_with_children, parent_name, child_name, fails, requesting_user, db
 ):
     """Catch DB error (Unique key violated) correctly.
 
@@ -462,7 +468,9 @@ def test_post_an_asset_with_existing_name(
     post_data = get_asset_post_data()
 
     def get_asset_with_name(asset_name):
-        return GenericAsset.query.filter(GenericAsset.name == asset_name).one_or_none()
+        return db.session.execute(
+            select(GenericAsset).filter_by(name=asset_name)
+        ).scalar_one_or_none()
 
     parent = get_asset_with_name(parent_name)
 
@@ -490,7 +498,9 @@ def get_asset_with_name(asset_name):
             assert asset_creation_response.json[key] == val
 
         # check that the asset exists
-        assert GenericAsset.query.get(asset_creation_response.json["id"]) is not None
+        assert (
+            db.session.get(GenericAsset, asset_creation_response.json["id"]) is not None
+        )
 
 
 @pytest.mark.parametrize(
@@ -499,21 +509,16 @@ def get_asset_with_name(asset_name):
     indirect=True,
 )
 def test_consultant_get_asset(
-    client,
-    setup_api_test_data,
-    setup_accounts,
-    requesting_user,
+    client, setup_api_test_data, setup_accounts, requesting_user, db
 ):
     """
     The Consultant Account reads an asset from the ConsultancyClient Account.
     """
-    asset_id = (
-        GenericAsset.query.filter(GenericAsset.name == "Test ConsultancyClient Asset")
-        .one_or_none()
-        .id
-    )
+    asset = db.session.execute(
+        select(GenericAsset).filter_by(name="Test ConsultancyClient Asset")
+    ).scalar_one_or_none()
 
-    get_asset_response = client.get(url_for("AssetAPI:get", id=asset_id))
+    get_asset_response = client.get(url_for("AssetAPI:get", id=asset.id))
     print("Server responded with:\n%s" % get_asset_response.json)
     assert get_asset_response.status_code == 200
     assert get_asset_response.json["name"] == "Test ConsultancyClient Asset"
diff --git a/flexmeasures/api/v3_0/tests/test_assets_api_fresh_db.py b/flexmeasures/api/v3_0/tests/test_assets_api_fresh_db.py
index 72d993017..8756fe19a 100644
--- a/flexmeasures/api/v3_0/tests/test_assets_api_fresh_db.py
+++ b/flexmeasures/api/v3_0/tests/test_assets_api_fresh_db.py
@@ -1,5 +1,6 @@
 from flask import url_for
 import pytest
+from sqlalchemy import select
 
 from flexmeasures.api.tests.utils import AccountContext
 from flexmeasures.data.models.generic_assets import GenericAsset
@@ -14,7 +15,7 @@
     ],
     indirect=True,
 )
-def test_post_an_asset_as_admin(client, setup_api_fresh_test_data, requesting_user):
+def test_post_an_asset_as_admin(client, setup_api_fresh_test_data, requesting_user, db):
     """
     Post one extra asset, as an admin user.
     """
@@ -33,15 +34,15 @@ def test_post_an_asset_as_admin(client, setup_api_fresh_test_data, requesting_us
     assert post_assets_response.status_code == 201
     assert post_assets_response.json["latitude"] == 30.1
 
-    asset: GenericAsset = GenericAsset.query.filter(
-        GenericAsset.name == post_data["name"]
-    ).one_or_none()
+    asset: GenericAsset = db.session.execute(
+        select(GenericAsset).filter_by(name=post_data["name"])
+    ).scalar_one_or_none()
     assert asset is not None
     assert asset.latitude == 30.1
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_edit_an_asset(client, setup_api_fresh_test_data, requesting_user):
+def test_edit_an_asset(client, setup_api_fresh_test_data, requesting_user, db):
     with AccountContext("Test Supplier Account") as supplier:
         existing_asset = supplier.generic_assets[0]
 
@@ -51,14 +52,16 @@ def test_edit_an_asset(client, setup_api_fresh_test_data, requesting_user):
         json=post_data,
     )
     assert edit_asset_response.status_code == 200
-    updated_asset = GenericAsset.query.filter_by(id=existing_asset.id).one_or_none()
+    updated_asset = db.session.execute(
+        select(GenericAsset).filter_by(id=existing_asset.id)
+    ).scalar_one_or_none()
     assert updated_asset.latitude == 10  # changed value
     assert updated_asset.longitude == existing_asset.longitude
     assert updated_asset.name == existing_asset.name
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_delete_an_asset(client, setup_api_fresh_test_data, requesting_user):
+def test_delete_an_asset(client, setup_api_fresh_test_data, requesting_user, db):
     with AccountContext("Test Prosumer Account") as prosumer:
         existing_asset_id = prosumer.generic_assets[0].id
 
@@ -66,5 +69,7 @@ def test_delete_an_asset(client, setup_api_fresh_test_data, requesting_user):
         url_for("AssetAPI:delete", id=existing_asset_id),
     )
     assert delete_asset_response.status_code == 204
-    deleted_asset = GenericAsset.query.filter_by(id=existing_asset_id).one_or_none()
+    deleted_asset = db.session.execute(
+        select(GenericAsset).filter_by(id=existing_asset_id)
+    ).scalar_one_or_none()
     assert deleted_asset is None
diff --git a/flexmeasures/api/v3_0/tests/test_sensor_data.py b/flexmeasures/api/v3_0/tests/test_sensor_data.py
index 11b59257c..577c43980 100644
--- a/flexmeasures/api/v3_0/tests/test_sensor_data.py
+++ b/flexmeasures/api/v3_0/tests/test_sensor_data.py
@@ -1,10 +1,10 @@
 from __future__ import annotations
 
 from datetime import timedelta
-
 from flask import url_for
 import pytest
 
+
 from flexmeasures import Sensor, Source, User
 from flexmeasures.api.v3_0.tests.utils import make_sensor_data_request_for_gas_sensor
 
@@ -46,11 +46,12 @@ def test_get_sensor_data(
     setup_api_test_data: dict[str, Sensor],
     setup_roles_users: dict[str, User],
     requesting_user,
+    db,
 ):
     """Check the /sensors/data endpoint for fetching 1 hour of data of a 10-minute resolution sensor."""
     sensor = setup_api_test_data["some gas sensor"]
-    source: Source = User.query.get(
-        setup_roles_users["Test Supplier User"]
+    source: Source = db.session.get(
+        User, setup_roles_users["Test Supplier User"]
     ).data_source[0]
     assert sensor.event_resolution == timedelta(minutes=10)
     message = {
@@ -82,11 +83,12 @@ def test_get_instantaneous_sensor_data(
     setup_api_test_data: dict[str, Sensor],
     setup_roles_users: dict[str, User],
     requesting_user,
+    db,
 ):
     """Check the /sensors/data endpoint for fetching 1 hour of data of an instantaneous sensor."""
     sensor = setup_api_test_data["some temperature sensor"]
-    source: Source = User.query.get(
-        setup_roles_users["Test Supplier User"]
+    source: Source = db.session.get(
+        User, setup_roles_users["Test Supplier User"]
     ).data_source[0]
     assert sensor.event_resolution == timedelta(minutes=0)
     message = {
diff --git a/flexmeasures/api/v3_0/tests/test_sensor_data_fresh_db.py b/flexmeasures/api/v3_0/tests/test_sensor_data_fresh_db.py
index 1fe8116ac..ec7a86cba 100644
--- a/flexmeasures/api/v3_0/tests/test_sensor_data_fresh_db.py
+++ b/flexmeasures/api/v3_0/tests/test_sensor_data_fresh_db.py
@@ -3,6 +3,7 @@
 import pytest
 from flask import url_for
 from timely_beliefs.tests.utils import equal_lists
+from sqlalchemy import select
 
 from flexmeasures import Sensor, Source
 from flexmeasures.api.v3_0.tests.utils import make_sensor_data_request_for_gas_sensor
@@ -56,16 +57,19 @@ def test_post_sensor_data(
     expected_value,
     expected_status,
     requesting_user,
+    db,
 ):
     post_data = make_sensor_data_request_for_gas_sensor(
         num_values=num_values, unit=unit, include_a_null=include_a_null
     )
-    sensor = Sensor.query.filter(Sensor.name == "some gas sensor").one_or_none()
+    sensor: Sensor = db.session.execute(
+        select(Sensor).filter_by(name="some gas sensor")
+    ).scalar_one_or_none()
     filters = (
         TimedBelief.sensor_id == sensor.id,
         TimedBelief.event_start >= post_data["start"],
     )
-    beliefs_before = TimedBelief.query.filter(*filters).all()
+    beliefs_before = db.session.scalars(select(TimedBelief).filter(*filters)).all()
     print(f"BELIEFS BEFORE: {beliefs_before}")
     assert len(beliefs_before) == 0
 
@@ -75,7 +79,7 @@ def test_post_sensor_data(
     )
     print(response.json)
     assert response.status_code == expected_status
-    beliefs = TimedBelief.query.filter(*filters).all()
+    beliefs = db.session.scalars(select(TimedBelief).filter(*filters)).all()
     print(f"BELIEFS AFTER: {beliefs}")
     assert len(beliefs) == expected_num_values
     # check that values are scaled to the sensor unit correctly
@@ -90,15 +94,16 @@ def test_auto_fix_missing_registration_of_user_as_data_source(
     setup_api_fresh_test_data,
     setup_user_without_data_source,
     requesting_user,
+    db,
 ):
     """Try to post sensor data as a user that has not been properly registered as a data source.
     The API call should succeed and the user should be automatically registered as a data source.
     """
 
     # Make sure the user is not yet registered as a data source
-    data_source = Source.query.filter_by(
-        user=setup_user_without_data_source
-    ).one_or_none()
+    data_source = db.session.execute(
+        select(Source).filter_by(user=setup_user_without_data_source)
+    ).scalar_one_or_none()
     assert data_source is None
 
     post_data = make_sensor_data_request_for_gas_sensor(
@@ -111,7 +116,7 @@ def test_auto_fix_missing_registration_of_user_as_data_source(
     assert response.status_code == 200
 
     # Make sure the user is now registered as a data source
-    data_source = Source.query.filter_by(
-        user=setup_user_without_data_source
-    ).one_or_none()
+    data_source = db.session.execute(
+        select(Source).filter_by(user=setup_user_without_data_source)
+    ).scalar_one_or_none()
     assert data_source is not None
diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py
index 186a25599..be7b67760 100644
--- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py
+++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py
@@ -3,14 +3,15 @@
 from isodate import parse_datetime
 
 from rq.job import Job
+from sqlalchemy import select
 
 from flexmeasures.api.common.responses import unknown_schedule, unrecognized_event
 from flexmeasures.api.tests.utils import check_deprecation
 from flexmeasures.api.v3_0.tests.utils import message_for_trigger_schedule
 from flexmeasures.data.models.data_sources import DataSource
-from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.tests.utils import work_on_rq
 from flexmeasures.data.services.scheduling import handle_scheduling_exception
+from flexmeasures.tests.utils import get_test_sensor
 
 
 @pytest.mark.parametrize(
@@ -109,6 +110,7 @@ def test_trigger_and_get_schedule_with_unknown_prices(
     keep_scheduling_queue_empty,
     message,
     requesting_user,
+    db,
 ):
     sensor = add_battery_assets["Test battery"].sensors[0]
 
@@ -139,9 +141,9 @@ def test_trigger_and_get_schedule_with_unknown_prices(
     )
 
     # check results are not in the database
-    scheduler_source = DataSource.query.filter_by(
-        name="Seita", type="scheduler"
-    ).one_or_none()
+    scheduler_source = db.session.execute(
+        select(DataSource).filter_by(name="Seita", type="scheduler")
+    ).scalar_one_or_none()
     assert (
         scheduler_source is None
     )  # Make sure the scheduler data source is still not there
@@ -168,6 +170,7 @@ def test_get_schedule_fallback(
     add_charging_station_assets,
     keep_scheduling_queue_empty,
     requesting_user,
+    db,
 ):
     """
     Test if the fallback job is created after a failing StorageScheduler call. This test
@@ -180,7 +183,7 @@ def test_get_schedule_fallback(
     charging_station_name = "Test charging station"
 
     start = "2015-01-02T00:00:00+01:00"
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
     charging_station = add_charging_station_assets[charging_station_name].sensors[0]
 
     assert charging_station.get_attribute("capacity_in_mw") == 2
@@ -314,6 +317,7 @@ def test_get_schedule_fallback_not_redirect(
     add_charging_station_assets,
     keep_scheduling_queue_empty,
     requesting_user,
+    db,
 ):
     """
     Test if the fallback scheduler is returned directly after a failing StorageScheduler call. This test
@@ -325,7 +329,7 @@ def test_get_schedule_fallback_not_redirect(
     charging_station_name = "Test charging station"
 
     start = "2015-01-02T00:00:00+01:00"
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
     charging_station = add_charging_station_assets[charging_station_name].sensors[0]
 
     assert charging_station.get_attribute("capacity_in_mw") == 2
diff --git a/flexmeasures/api/v3_0/tests/test_sensors_api.py b/flexmeasures/api/v3_0/tests/test_sensors_api.py
index 25088e35d..d6df14984 100644
--- a/flexmeasures/api/v3_0/tests/test_sensors_api.py
+++ b/flexmeasures/api/v3_0/tests/test_sensors_api.py
@@ -3,6 +3,7 @@
 import pytest
 
 from flask import url_for
+from sqlalchemy import select, func
 
 from flexmeasures.data.models.time_series import TimedBelief
 from flexmeasures import Sensor
@@ -17,9 +18,7 @@
     "requesting_user", ["test_supplier_user_4@seita.nl"], indirect=True
 )
 def test_fetch_one_sensor(
-    client,
-    setup_api_test_data: dict[str, Sensor],
-    requesting_user,
+    client, setup_api_test_data: dict[str, Sensor], requesting_user, db
 ):
     sensor_id = 1
     response = client.get(
@@ -31,9 +30,7 @@ def test_fetch_one_sensor(
     assert response.json["unit"] == "m³/h"
     assert response.json["timezone"] == "UTC"
     assert response.json["event_resolution"] == "PT10M"
-    asset = GenericAsset.query.filter_by(
-        id=response.json["generic_asset_id"]
-    ).one_or_none()
+    asset = db.session.get(GenericAsset, response.json["generic_asset_id"])
     assert asset.name == "incineration line"
 
 
@@ -69,7 +66,7 @@ def test_fetch_one_sensor_no_auth(
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_post_a_sensor(client, setup_api_test_data, requesting_user):
+def test_post_a_sensor(client, setup_api_test_data, requesting_user, db):
     post_data = get_sensor_post_data()
     response = client.post(
         url_for("SensorAPI:post"),
@@ -80,7 +77,9 @@ def test_post_a_sensor(client, setup_api_test_data, requesting_user):
     assert response.json["name"] == "power"
     assert response.json["event_resolution"] == "PT1H"
 
-    sensor: Sensor = Sensor.query.filter_by(name="power").one_or_none()
+    sensor: Sensor = db.session.execute(
+        select(Sensor).filter_by(name="power")
+    ).scalar_one_or_none()
     assert sensor is not None
     assert sensor.unit == "kWh"
     assert sensor.attributes["capacity_in_mw"] == 0.0074
@@ -108,9 +107,10 @@ def test_post_sensor_to_asset_from_unrelated_account(
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_patch_sensor(client, setup_api_test_data, requesting_user):
-    sensor = Sensor.query.filter(Sensor.name == "some gas sensor").one_or_none()
-
+def test_patch_sensor(client, setup_api_test_data, requesting_user, db):
+    sensor = db.session.execute(
+        select(Sensor).filter_by(name="some gas sensor")
+    ).scalar_one_or_none()
     response = client.patch(
         url_for("SensorAPI:patch", id=sensor.id),
         json={
@@ -119,9 +119,16 @@ def test_patch_sensor(client, setup_api_test_data, requesting_user):
         },
     )
     assert response.json["name"] == "Changed name"
-    new_sensor = Sensor.query.filter(Sensor.name == "Changed name").one_or_none()
+    new_sensor = db.session.execute(
+        select(Sensor).filter_by(name="Changed name")
+    ).scalar_one_or_none()
     assert new_sensor.name == "Changed name"
-    assert Sensor.query.filter(Sensor.name == "some gas sensor").one_or_none() is None
+    assert (
+        db.session.execute(
+            select(Sensor).filter_by(name="some gas sensor")
+        ).scalar_one_or_none()
+        is None
+    )
     assert new_sensor.attributes["test_attribute"] == "test_attribute_value"
 
 
@@ -135,12 +142,13 @@ def test_patch_sensor(client, setup_api_test_data, requesting_user):
 )
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
 def test_patch_sensor_for_excluded_attribute(
-    client, setup_api_test_data, attribute, value, requesting_user
+    client, setup_api_test_data, attribute, value, requesting_user, db
 ):
     """Test to change the generic_asset_id that should not be allowed.
     The generic_asset_id is excluded in the partial_sensor_schema"""
-    sensor = Sensor.query.filter(Sensor.name == "some temperature sensor").one_or_none()
-
+    sensor = db.session.execute(
+        select(Sensor).filter_by(name="some temperature sensor")
+    ).scalar_one_or_none()
     response = client.patch(
         url_for("SensorAPI:patch", id=sensor.id),
         json={
@@ -157,11 +165,12 @@ def test_patch_sensor_for_excluded_attribute(
 @pytest.mark.parametrize(
     "requesting_user", ["test_supplier_user_4@seita.nl"], indirect=True
 )
-def test_patch_sensor_non_admin(client, setup_api_test_data, requesting_user):
+def test_patch_sensor_non_admin(client, setup_api_test_data, requesting_user, db):
     """Try to change the name of a sensor with a non admin account"""
 
-    sensor = Sensor.query.filter(Sensor.name == "some temperature sensor").one_or_none()
-
+    sensor = db.session.execute(
+        select(Sensor).filter_by(name="some temperature sensor")
+    ).scalar_one_or_none()
     response = client.patch(
         url_for("SensorAPI:patch", id=sensor.id),
         json={
@@ -174,12 +183,12 @@ def test_patch_sensor_non_admin(client, setup_api_test_data, requesting_user):
 
 
 @pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
-def test_delete_a_sensor(client, setup_api_test_data, requesting_user):
+def test_delete_a_sensor(client, setup_api_test_data, requesting_user, db):
     existing_sensor_id = setup_api_test_data["some temperature sensor"].id
-    sensor_data = TimedBelief.query.filter(
-        TimedBelief.sensor_id == existing_sensor_id
+    sensor_data = db.session.scalars(
+        select(TimedBelief).filter(TimedBelief.sensor_id == existing_sensor_id)
     ).all()
-    sensor_count = len(Sensor.query.all())
+    sensor_count = db.session.scalar(select(func.count()).select_from(Sensor))
 
     assert isinstance(sensor_data[0].event_value, float)
 
@@ -187,10 +196,14 @@ def test_delete_a_sensor(client, setup_api_test_data, requesting_user):
         url_for("SensorAPI:delete", id=existing_sensor_id),
     )
     assert delete_sensor_response.status_code == 204
-    deleted_sensor = Sensor.query.filter_by(id=existing_sensor_id).one_or_none()
+    deleted_sensor = db.session.get(Sensor, existing_sensor_id)
     assert deleted_sensor is None
     assert (
-        TimedBelief.query.filter(TimedBelief.sensor_id == existing_sensor_id).all()
+        db.session.scalars(
+            select(TimedBelief).filter(TimedBelief.sensor_id == existing_sensor_id)
+        ).all()
         == []
     )
-    assert len(Sensor.query.all()) == sensor_count - 1
+    assert (
+        db.session.scalar(select(func.count()).select_from(Sensor)) == sensor_count - 1
+    )
diff --git a/flexmeasures/api/v3_0/tests/utils.py b/flexmeasures/api/v3_0/tests/utils.py
index 9e0ecd1ab..96c4095ab 100644
--- a/flexmeasures/api/v3_0/tests/utils.py
+++ b/flexmeasures/api/v3_0/tests/utils.py
@@ -1,4 +1,7 @@
+from sqlalchemy import select
+
 from flexmeasures import Sensor
+from flexmeasures.data import db
 
 
 def make_sensor_data_request_for_gas_sensor(
@@ -10,7 +13,9 @@ def make_sensor_data_request_for_gas_sensor(
     """Creates request to post sensor data for a gas sensor.
     This particular gas sensor measures units of m³/h with a 10-minute resolution.
     """
-    sensor = Sensor.query.filter(Sensor.name == "some gas sensor").one_or_none()
+    sensor = db.session.execute(
+        select(Sensor).filter_by(name="some gas sensor")
+    ).scalar_one_or_none()
     values = num_values * [-11.28]
     if include_a_null:
         values[0] = None
diff --git a/flexmeasures/cli/tests/conftest.py b/flexmeasures/cli/tests/conftest.py
index 1caabbd35..046eb32c0 100644
--- a/flexmeasures/cli/tests/conftest.py
+++ b/flexmeasures/cli/tests/conftest.py
@@ -50,7 +50,7 @@ def setup_dummy_data(fresh_db, app, setup_dummy_asset):
 
     db.session.add(pandas_report)
 
-    dummy_asset = GenericAsset.query.get(setup_dummy_asset)
+    dummy_asset = db.session.get(GenericAsset, setup_dummy_asset)
 
     sensor1 = Sensor(
         "sensor 1", generic_asset=dummy_asset, event_resolution=timedelta(hours=1)
diff --git a/flexmeasures/cli/tests/test_data_add.py b/flexmeasures/cli/tests/test_data_add.py
index d8f94d0bb..11083a0f8 100644
--- a/flexmeasures/cli/tests/test_data_add.py
+++ b/flexmeasures/cli/tests/test_data_add.py
@@ -4,6 +4,7 @@
 import os
 from datetime import datetime
 import pytz
+from sqlalchemy import select, func
 
 from flexmeasures import Asset
 from flexmeasures.cli.tests.utils import to_flags
@@ -17,6 +18,7 @@
 
 from flexmeasures.cli.tests.utils import get_click_commands
 from flexmeasures.utils.time_utils import server_now
+from flexmeasures.tests.utils import get_test_sensor
 
 
 @pytest.mark.skip_github
@@ -36,23 +38,23 @@ def test_add_annotation(app, db, setup_roles_users):
     assert "Successfully added annotation" in result.output
 
     # Check database for annotation entry
-    assert (
-        Annotation.query.filter(
-            Annotation.content == cli_input["content"],
-            Annotation.start == cli_input["at"],
+    assert db.session.execute(
+        select(Annotation)
+        .filter_by(
+            content=cli_input["content"],
+            start=cli_input["at"],
         )
         .join(AccountAnnotationRelationship)
-        .filter(
-            AccountAnnotationRelationship.account_id == cli_input["account"],
-            AccountAnnotationRelationship.annotation_id == Annotation.id,
+        .filter_by(
+            account_id=cli_input["account"],
+            annotation_id=Annotation.id,
         )
         .join(DataSource)
-        .filter(
-            DataSource.id == Annotation.source_id,
-            DataSource.user_id == cli_input["user"],
+        .filter_by(
+            id=Annotation.source_id,
+            user_id=cli_input["user"],
         )
-        .one_or_none()
-    )
+    ).scalar_one_or_none()
 
 
 @pytest.mark.skip_github
@@ -72,18 +74,21 @@ def test_add_holidays(app, db, setup_roles_users):
 
     # Check database for 11 annotation entries
     assert (
-        Annotation.query.join(AccountAnnotationRelationship)
-        .filter(
-            AccountAnnotationRelationship.account_id == cli_input["account"],
-            AccountAnnotationRelationship.annotation_id == Annotation.id,
+        db.session.scalar(
+            select(func.count())
+            .select_from(Annotation)
+            .join(AccountAnnotationRelationship)
+            .filter(
+                AccountAnnotationRelationship.account_id == cli_input["account"],
+                AccountAnnotationRelationship.annotation_id == Annotation.id,
+            )
+            .join(DataSource)
+            .filter(
+                DataSource.id == Annotation.source_id,
+                DataSource.name == "workalendar",
+                DataSource.model == cli_input["country"],
+            )
         )
-        .join(DataSource)
-        .filter(
-            DataSource.id == Annotation.source_id,
-            DataSource.name == "workalendar",
-            DataSource.model == cli_input["country"],
-        )
-        .count()
         == 11
     )
 
@@ -176,8 +181,8 @@ def test_add_reporter(app, fresh_db, setup_dummy_data):
 
         assert result.exit_code == 0  # run command without errors
 
-        report_sensor = Sensor.query.get(
-            report_sensor_id
+        report_sensor = fresh_db.session.get(
+            Sensor, report_sensor_id
         )  # get fresh report sensor instance
 
         assert "Reporter PandasReporter found" in result.output
@@ -230,8 +235,8 @@ def test_add_reporter(app, fresh_db, setup_dummy_data):
         assert result.exit_code == 0  # run command without errors
 
         # Check if the report is saved to the database
-        report_sensor = Sensor.query.get(
-            report_sensor_id
+        report_sensor = fresh_db.session.get(
+            Sensor, report_sensor_id
         )  # get fresh report sensor instance
 
         assert (
@@ -323,8 +328,8 @@ def test_add_multiple_output(app, fresh_db, setup_dummy_data):
 
         assert result.exit_code == 0  # run command without errors
 
-        report_sensor = Sensor.query.get(report_sensor_id)
-        report_sensor_2 = Sensor.query.get(report_sensor_2_id)
+        report_sensor = fresh_db.session.get(Sensor, report_sensor_id)
+        report_sensor_2 = fresh_db.session.get(Sensor, report_sensor_2_id)
 
         assert "Reporter PandasReporter found" in result.output
         assert f"Report computation done for sensor `{report_sensor}`." in result.output
@@ -345,7 +350,7 @@ def test_add_multiple_output(app, fresh_db, setup_dummy_data):
 @pytest.mark.skip_github
 @pytest.mark.parametrize("process_type", [("INFLEXIBLE"), ("SHIFTABLE"), ("BREAKABLE")])
 def test_add_process(
-    app, process_power_sensor, process_type, add_market_prices_fresh_db
+    app, process_power_sensor, process_type, add_market_prices_fresh_db, db
 ):
     """
     Schedule a 4h of consumption block at a constant power of 400kW in a day using
@@ -354,7 +359,7 @@ def test_add_process(
 
     from flexmeasures.cli.data_add import add_schedule_process
 
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
 
     process_power_sensor_id = process_power_sensor
 
@@ -379,8 +384,7 @@ def test_add_process(
 
     assert result.exit_code == 0  # run command without errors
 
-    process_power_sensor = Sensor.query.get(process_power_sensor_id)
-
+    process_power_sensor = db.session.get(Sensor, process_power_sensor_id)
     schedule = process_power_sensor.search_beliefs()
     # check if the schedule is not empty more detailed testing can be found
     # in data/models/planning/tests/test_processs.py.
@@ -408,7 +412,9 @@ def test_add_sensor(app, db, setup_dummy_asset, event_resolution, name, success)
     }
     runner = app.test_cli_runner()
     result = runner.invoke(add_sensor, to_flags(cli_input))
-    sensor: Sensor = Sensor.query.filter_by(name=name).one_or_none()
+    sensor: Sensor = db.session.execute(
+        select(Sensor).filter_by(name=name)
+    ).scalar_one_or_none()
     if success:
         assert result.exit_code == 0
         sensor.unit == "kWh"
@@ -442,7 +448,9 @@ def test_add_account(
     result = runner.invoke(new_account, to_flags(cli_input))
     if success:
         assert "successfully created." in result.output
-        account = Account.query.filter(Account.name == cli_input["name"]).one_or_none()
+        account = fresh_db.session.execute(
+            select(Account).filter_by(name=cli_input["name"])
+        ).scalar_one_or_none()
         assert account.consultancy_account_id == consultancy_account_id
 
     else:
@@ -459,6 +467,7 @@ def test_add_storage_scheduler(
     storage_schedule_sensors,
     storage_power_capacity,
     storage_efficiency,
+    db,
 ):
     """
     Test the 'flexmeasures add schedule for-storage' CLI command for adding storage schedules.
@@ -485,8 +494,12 @@ def test_add_storage_scheduler(
     runner = app.test_cli_runner()
     runner.invoke(add_toy_account)
 
-    toy_account = Account.query.filter_by(name="Toy Account").one_or_none()
-    battery = Asset.query.filter_by(name="toy-battery", owner=toy_account).one_or_none()
+    toy_account = db.session.execute(
+        select(Account).filter_by(name="Toy Account")
+    ).scalar_one_or_none()
+    battery = db.session.execute(
+        select(Asset).filter_by(name="toy-battery", owner=toy_account)
+    ).scalar_one_or_none()
     power_sensor = battery.sensors[0]
     prices = add_market_prices_fresh_db["epex_da"]
 
diff --git a/flexmeasures/cli/tests/test_data_edit.py b/flexmeasures/cli/tests/test_data_edit.py
index 591d68bef..aed4f4228 100644
--- a/flexmeasures/cli/tests/test_data_edit.py
+++ b/flexmeasures/cli/tests/test_data_edit.py
@@ -2,10 +2,12 @@
 
 import pandas as pd
 import timely_beliefs as tb
+from sqlalchemy import select
 
 from flexmeasures.cli.tests.utils import to_flags
-from flexmeasures.data.models.time_series import Sensor, TimedBelief
+from flexmeasures.data.models.time_series import TimedBelief
 from flexmeasures.cli.tests.utils import get_click_commands
+from flexmeasures.tests.utils import get_test_sensor
 
 
 @pytest.mark.skip
@@ -13,7 +15,7 @@ def test_add_one_sensor_attribute(app, db, setup_markets):
     from flexmeasures.cli.data_edit import edit_attribute
 
     # Load sensor from database and count attributes
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     n_attributes_before = len(sensor.attributes)
 
     cli_input = {
@@ -26,7 +28,7 @@ def test_add_one_sensor_attribute(app, db, setup_markets):
     assert result.exit_code == 0 and "Success" in result.output, result.exception
 
     # Reload sensor from database and count attributes
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     n_attributes_after = len(sensor.attributes)
 
     assert n_attributes_after == n_attributes_before + 1
@@ -47,7 +49,7 @@ def test_resample_sensor_data(
 
     from flexmeasures.cli.data_edit import resample_sensor_data
 
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     event_starts_after = pd.Timestamp(event_starts_after)
     event_ends_before = pd.Timestamp(event_ends_before)
     beliefs_before = sensor.search_beliefs(
@@ -60,14 +62,14 @@ def test_resample_sensor_data(
     assert sensor.id is not None
 
     # Check whether we have all desired beliefs
-    query = TimedBelief.query.filter(TimedBelief.sensor_id == sensor.id)
+    query = select(TimedBelief).filter(TimedBelief.sensor_id == sensor.id)
     if not pd.isnull(event_starts_after):
         query = query.filter(TimedBelief.event_start >= event_starts_after)
     if not pd.isnull(event_ends_before):
         query = query.filter(
             TimedBelief.event_start + sensor.event_resolution <= event_ends_before
         )
-    all_beliefs_for_given_sensor = query.all()
+    all_beliefs_for_given_sensor = db.session.scalars(query).all()
     pd.testing.assert_frame_equal(
         tb.BeliefsDataFrame(all_beliefs_for_given_sensor), beliefs_before
     )
@@ -85,7 +87,7 @@ def test_resample_sensor_data(
     assert "Successfully resampled" in result.output
 
     # Check that we now have twice as much data for this sensor
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     beliefs_after = sensor.search_beliefs(
         most_recent_beliefs_only=False,
         event_starts_after=event_starts_after,
diff --git a/flexmeasures/cli/tests/test_data_show.py b/flexmeasures/cli/tests/test_data_show.py
index 1c4b8a6fa..3788c9440 100644
--- a/flexmeasures/cli/tests/test_data_show.py
+++ b/flexmeasures/cli/tests/test_data_show.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.cli.tests.utils import get_click_commands
+from flexmeasures.tests.utils import get_test_sensor
 
 
 @pytest.mark.skip_github
@@ -93,7 +93,7 @@ def test_show_asset(app, fresh_db, setup_generic_assets_fresh_db):
 def test_plot_beliefs(app, fresh_db, setup_beliefs_fresh_db):
     from flexmeasures.cli.data_show import plot_beliefs
 
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = get_test_sensor(fresh_db)
 
     runner = app.test_cli_runner()
     result = runner.invoke(
@@ -130,7 +130,7 @@ def test_cli_help(app):
 def test_export_chart(app, fresh_db, setup_beliefs_fresh_db, _format):
     from flexmeasures.cli.data_show import chart
 
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = get_test_sensor(fresh_db)
     sensor_id = sensor.id
 
     runner = app.test_cli_runner()
diff --git a/flexmeasures/conftest.py b/flexmeasures/conftest.py
index 1beaccf6f..f97624f5b 100644
--- a/flexmeasures/conftest.py
+++ b/flexmeasures/conftest.py
@@ -4,7 +4,7 @@
 import pytest
 from random import random, seed
 from datetime import datetime, timedelta
-
+from sqlalchemy import select
 from isodate import parse_duration
 import pandas as pd
 import numpy as np
@@ -162,7 +162,9 @@ def create_test_accounts(db) -> dict[str, Account]:
         description="A client of a consultancy",
     )
     consultancy_account_id = (
-        Account.query.filter_by(name="Test Consultancy Account").one_or_none().id
+        db.session.execute(select(Account).filter_by(name="Test Consultancy Account"))
+        .scalar_one_or_none()
+        .id
     )
     consultancy_client_account = Account(
         name="Test ConsultancyClient Account",
@@ -401,7 +403,9 @@ def create_generic_asset_types(db) -> dict[str, GenericAssetType]:
     db.session.add(solar)
     wind = GenericAssetType(name="wind turbine")
     db.session.add(wind)
-    battery = GenericAssetType.query.filter_by(name="battery").one_or_none()
+    battery = db.session.execute(
+        select(GenericAssetType).filter_by(name="battery")
+    ).scalar_one_or_none()
     if (
         not battery
     ):  # legacy if-block, because create_test_battery_assets might have created it already - refactor!
@@ -532,7 +536,9 @@ def create_beliefs(db: SQLAlchemy, setup_markets, setup_sources) -> int:
     """
     :returns: the number of beliefs set up
     """
-    sensor = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    sensor = db.session.execute(
+        select(Sensor).filter(Sensor.name == "epex_da")
+    ).scalar_one_or_none()
     beliefs = [
         TimedBelief(
             sensor=sensor,
diff --git a/flexmeasures/data/models/planning/tests/test_process.py b/flexmeasures/data/models/planning/tests/test_process.py
index aba8b3308..d29944316 100644
--- a/flexmeasures/data/models/planning/tests/test_process.py
+++ b/flexmeasures/data/models/planning/tests/test_process.py
@@ -2,8 +2,8 @@
 import pytest
 import pytz
 
-from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.models.planning.process import ProcessScheduler
+from flexmeasures.tests.utils import get_test_sensor
 
 
 tz = pytz.timezone("Europe/Amsterdam")
@@ -16,14 +16,16 @@
     "process_type, optimal_start",
     [("INFLEXIBLE", datetime(2015, 1, 2, 0)), ("SHIFTABLE", datetime(2015, 1, 2, 8))],
 )
-def test_process_scheduler(add_battery_assets, process, process_type, optimal_start):
+def test_process_scheduler(
+    add_battery_assets, process, process_type, optimal_start, db
+):
     """
     Test scheduling a process of 4kW of power that last 4h using the ProcessScheduler
     without time restrictions.
     """
 
     # get the sensors from the database
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
 
     flex_model = {
         "duration": "PT4H",
@@ -60,14 +62,14 @@ def test_process_scheduler(add_battery_assets, process, process_type, optimal_st
     [("INFLEXIBLE", datetime(2015, 1, 2, 0)), ("SHIFTABLE", datetime(2015, 1, 2, 8))],
 )
 def test_duration_exceeds_planning_window(
-    add_battery_assets, process, process_type, optimal_start
+    add_battery_assets, process, process_type, optimal_start, db
 ):
     """
     Test scheduling a process that last longer than the planning window.
     """
 
     # get the sensors from the database
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
 
     flex_model = {
         "duration": "PT48H",
@@ -94,14 +96,14 @@ def test_duration_exceeds_planning_window(
     assert (schedule == 4).all()
 
 
-def test_process_scheduler_time_restrictions(add_battery_assets, process):
+def test_process_scheduler_time_restrictions(add_battery_assets, process, db):
     """
     Test ProcessScheduler with a time restrictions consisting of a block of 2h starting
     at 8am. The resulting schedules avoid the 8am-10am period and schedules for a valid period.
     """
 
     # get the sensors from the database
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
 
     # time parameters
 
@@ -143,7 +145,7 @@ def test_process_scheduler_time_restrictions(add_battery_assets, process):
     assert (schedule[time_restrictions] == 0).all()
 
 
-def test_breakable_scheduler_time_restrictions(add_battery_assets, process):
+def test_breakable_scheduler_time_restrictions(add_battery_assets, process, db):
     """
     Test BREAKABLE process_type of ProcessScheduler by introducing four 1-hour restrictions
     interspaced by 1 hour. The equivalent mask would be the following: [0,...,0,1,0,1,0,1,0,1,0, ...,0].
@@ -152,7 +154,7 @@ def test_breakable_scheduler_time_restrictions(add_battery_assets, process):
     """
 
     # get the sensors from the database
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
 
     # time parameters
 
@@ -202,7 +204,7 @@ def test_breakable_scheduler_time_restrictions(add_battery_assets, process):
     ],
 )
 def test_impossible_schedules(
-    add_battery_assets, process, process_type, time_restrictions
+    add_battery_assets, process, process_type, time_restrictions, db
 ):
     """
     Test schedules with time restrictions that make a 4h block not fit anytime during the
@@ -210,7 +212,7 @@ def test_impossible_schedules(
     """
 
     # get the sensors from the database
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
 
     flex_model = {
         "duration": "PT4H",
diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py
index 10302be28..795f782ad 100644
--- a/flexmeasures/data/models/planning/tests/test_solver.py
+++ b/flexmeasures/data/models/planning/tests/test_solver.py
@@ -6,6 +6,7 @@
 import numpy as np
 import pandas as pd
 from pandas.tseries.frequencies import to_offset
+from sqlalchemy import select
 
 from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.models.planning import Scheduler
@@ -24,6 +25,7 @@
     apply_stock_changes_and_losses,
     integrate_time_series,
 )
+from flexmeasures.tests.utils import get_test_sensor
 
 
 TOLERANCE = 0.00001
@@ -67,9 +69,10 @@ def test_battery_solver_day_1(
     add_inflexible_device_forecasts,
     use_inflexible_device,
     battery_name,
+    db,
 ):
     epex_da, battery = get_sensors_from_db(
-        add_battery_assets, battery_name=battery_name
+        db, add_battery_assets, battery_name=battery_name
     )
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 1))
@@ -108,7 +111,7 @@ def test_battery_solver_day_1(
     ],
 )
 def test_battery_solver_day_2(
-    add_battery_assets, roundtrip_efficiency: float, storage_efficiency: float
+    add_battery_assets, roundtrip_efficiency: float, storage_efficiency: float, db
 ):
     """Check battery scheduling results for day 2, which is set up with
     8 expensive, then 8 cheap, then again 8 expensive hours.
@@ -120,7 +123,7 @@ def test_battery_solver_day_2(
     and so we expect the scheduler to only:
     - completely discharge within the last 8 hours
     """
-    _epex_da, battery = get_sensors_from_db(add_battery_assets)
+    _epex_da, battery = get_sensors_from_db(db, add_battery_assets)
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 2))
     end = tz.localize(datetime(2015, 1, 3))
@@ -267,10 +270,7 @@ def run_test_charge_discharge_sign(
     return schedule.tz_convert(tz), soc_schedule.tz_convert(tz)
 
 
-def test_battery_solver_day_3(
-    add_battery_assets,
-    add_inflexible_device_forecasts,
-):
+def test_battery_solver_day_3(add_battery_assets, add_inflexible_device_forecasts, db):
     """Check battery scheduling results for day 3, which is set up with
     8 hours with negative prices, followed by 16 expensive hours.
 
@@ -288,10 +288,10 @@ def test_battery_solver_day_3(
     """
 
     roundtrip_efficiency = 0.9
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
-    epex_da_production = Sensor.query.filter(
-        Sensor.name == "epex_da_production"
-    ).one_or_none()
+    epex_da = get_test_sensor(db)
+    epex_da_production = db.session.execute(
+        select(Sensor).filter_by(name="epex_da_production")
+    ).scalar_one_or_none()
     battery = add_battery_assets["Test battery"].sensors[0]
 
     tz = pytz.timezone("Europe/Amsterdam")
@@ -355,7 +355,7 @@ def test_battery_solver_day_3(
     ],
 )
 def test_charging_station_solver_day_2(
-    target_soc, charging_station_name, setup_planning_test_data
+    target_soc, charging_station_name, setup_planning_test_data, db
 ):
     """Starting with a state of charge 1 kWh, within 2 hours we should be able to reach
     any state of charge in the range [1, 5] kWh for a unidirectional station,
@@ -364,7 +364,7 @@ def test_charging_station_solver_day_2(
     soc_at_start = 1
     duration_until_target = timedelta(hours=2)
 
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
     charging_station = setup_planning_test_data[charging_station_name].sensors[0]
     assert charging_station.get_attribute("capacity_in_mw") == 2
     assert charging_station.get_attribute("market_id") == epex_da.id
@@ -427,7 +427,7 @@ def test_charging_station_solver_day_2(
     ],
 )
 def test_fallback_to_unsolvable_problem(
-    target_soc, charging_station_name, setup_planning_test_data
+    target_soc, charging_station_name, setup_planning_test_data, db
 ):
     """Starting with a state of charge 10 kWh, within 2 hours we should be able to reach
     any state of charge in the range [10, 14] kWh for a unidirectional station,
@@ -443,7 +443,7 @@ def test_fallback_to_unsolvable_problem(
     duration_until_target = timedelta(hours=2)
     expected_gap = 1
 
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
     charging_station = setup_planning_test_data[charging_station_name].sensors[0]
     assert charging_station.get_attribute("capacity_in_mw") == 2
     assert charging_station.get_attribute("market_id") == epex_da.id
@@ -540,9 +540,7 @@ def test_building_solver_day_2(
     """
     battery = flexible_devices["battery power sensor"]
     building = battery.generic_asset
-    default_consumption_price_sensor = Sensor.query.filter(
-        Sensor.name == "epex_da"
-    ).one_or_none()
+    default_consumption_price_sensor = get_test_sensor(db)
     assert battery.get_attribute("market_id") == default_consumption_price_sensor.id
     if market_scenario == "dynamic contract":
         consumption_price_sensor = default_consumption_price_sensor
@@ -646,7 +644,7 @@ def test_building_solver_day_2(
     )
 
 
-def test_soc_bounds_timeseries(add_battery_assets):
+def test_soc_bounds_timeseries(db, add_battery_assets):
     """Check that the maxima and minima timeseries alter the result
     of the optimization.
 
@@ -656,7 +654,7 @@ def test_soc_bounds_timeseries(add_battery_assets):
     """
 
     # get the sensors from the database
-    epex_da, battery = get_sensors_from_db(add_battery_assets)
+    epex_da, battery = get_sensors_from_db(db, add_battery_assets)
 
     # time parameters
     tz = pytz.timezone("Europe/Amsterdam")
@@ -958,11 +956,11 @@ def test_validate_constraints(
     assert set(expected_constraint_type_violations) == constraint_type_violations_output
 
 
-def test_infeasible_problem_error(add_battery_assets):
+def test_infeasible_problem_error(db, add_battery_assets):
     """Try to create a schedule with infeasible constraints. soc-max is 4.5 and soc-target is 8.0"""
 
     # get the sensors from the database
-    _epex_da, battery = get_sensors_from_db(add_battery_assets)
+    _epex_da, battery = get_sensors_from_db(db, add_battery_assets)
 
     # time parameters
     tz = pytz.timezone("Europe/Amsterdam")
@@ -1007,22 +1005,22 @@ def compute_schedule(flex_model):
         compute_schedule(flex_model)
 
 
-def get_sensors_from_db(battery_assets, battery_name="Test battery"):
+def get_sensors_from_db(db, battery_assets, battery_name="Test battery"):
     # get the sensors from the database
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
     battery = battery_assets[battery_name].sensors[0]
     assert battery.get_attribute("market_id") == epex_da.id
 
     return epex_da, battery
 
 
-def test_numerical_errors(app_with_each_solver, setup_planning_test_data):
+def test_numerical_errors(app_with_each_solver, setup_planning_test_data, db):
     """Test that a soc-target = soc-max can exceed this value due to numerical errors in the operations
     to compute the device constraint DataFrame.
     In the case of HiGHS, the tiny difference creates an infeasible constraint.
     """
 
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
+    epex_da = get_test_sensor(db)
     charging_station = setup_planning_test_data[
         "Test charging station (bidirectional)"
     ].sensors[0]
@@ -1435,7 +1433,7 @@ def test_battery_power_capacity_as_sensor(
     expected_consumption,
 ):
     epex_da, battery = get_sensors_from_db(
-        add_battery_assets, battery_name=battery_name
+        db, add_battery_assets, battery_name=battery_name
     )
 
     tz = pytz.timezone("Europe/Amsterdam")
@@ -1478,9 +1476,9 @@ def test_battery_power_capacity_as_sensor(
 
 
 def get_efficiency_problem_device_constraints(
-    extra_flex_model, efficiency_sensors, add_battery_assets
+    extra_flex_model, efficiency_sensors, add_battery_assets, db
 ) -> pd.DataFrame:
-    _, battery = get_sensors_from_db(add_battery_assets)
+    _, battery = get_sensors_from_db(db, add_battery_assets)
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 1))
     end = tz.localize(datetime(2015, 1, 2))
@@ -1521,7 +1519,7 @@ def test_dis_charging_efficiency_as_sensor(
     }
 
     device_constraints = get_efficiency_problem_device_constraints(
-        extra_flex_model, efficiency_sensors, add_battery_assets
+        extra_flex_model, efficiency_sensors, add_battery_assets, db
     )
 
     assert all(
@@ -1542,7 +1540,7 @@ def test_dis_charging_efficiency_as_sensor(
     ["delta fails", "delta", "delta hourly", "delta 5min"],
 )
 def test_battery_stock_delta_sensor(
-    add_battery_assets, add_stock_delta, stock_delta_sensor
+    add_battery_assets, add_stock_delta, stock_delta_sensor, db
 ):
     """
     Test the SOC delta feature using sensors.
@@ -1560,7 +1558,7 @@ def test_battery_stock_delta_sensor(
     With these settings, the battery needs to charge at a power or greater than the usage forecast
     to keep the SOC within bounds ([0, 2 MWh]).
     """
-    _, battery = get_sensors_from_db(add_battery_assets)
+    _, battery = get_sensors_from_db(db, add_battery_assets)
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 1))
     end = tz.localize(datetime(2015, 1, 2))
@@ -1602,14 +1600,16 @@ def test_battery_stock_delta_sensor(
         ([], [], None),  # no gain defined -> no gain or loss happens
     ],
 )
-def test_battery_stock_delta_quantity(add_battery_assets, gain, usage, expected_delta):
+def test_battery_stock_delta_quantity(
+    add_battery_assets, gain, usage, expected_delta, db
+):
     """
     Test the stock gain field when a constant value is provided.
 
     We expect a constant gain/loss to happen in every time period equal to the energy
     value provided.
     """
-    _, battery = get_sensors_from_db(add_battery_assets)
+    _, battery = get_sensors_from_db(db, add_battery_assets)
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 1))
     end = tz.localize(datetime(2015, 1, 2))
@@ -1658,7 +1658,7 @@ def test_battery_stock_delta_quantity(add_battery_assets, gain, usage, expected_
     ],
 )
 def test_battery_efficiency_quantity(
-    add_battery_assets, efficiency, expected_efficiency
+    add_battery_assets, efficiency, expected_efficiency, db
 ):
     """
     Test to ensure correct handling of storage efficiency quantities in the StorageScheduler.
@@ -1667,7 +1667,7 @@ def test_battery_efficiency_quantity(
     case where the efficiency is not defined.
     """
 
-    _, battery = get_sensors_from_db(add_battery_assets)
+    _, battery = get_sensors_from_db(db, add_battery_assets)
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 1))
     end = tz.localize(datetime(2015, 1, 2))
@@ -1715,6 +1715,7 @@ def test_battery_storage_efficiency_sensor(
     add_storage_efficiency,
     efficiency_sensor_name,
     expected_efficiency,
+    db,
 ):
     """
     Test the handling of different storage efficiency sensors in the StorageScheduler.
@@ -1722,7 +1723,7 @@ def test_battery_storage_efficiency_sensor(
     It checks if the scheduler correctly handles regular values, values exceeding 100%, negative values,
     and values with different resolutions compared to the scheduling resolution.
     """
-    _, battery = get_sensors_from_db(add_battery_assets)
+    _, battery = get_sensors_from_db(db, add_battery_assets)
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 1))
     end = tz.localize(datetime(2015, 1, 2))
diff --git a/flexmeasures/data/models/reporting/tests/test_aggregator.py b/flexmeasures/data/models/reporting/tests/test_aggregator.py
index f010cc077..6d1a85554 100644
--- a/flexmeasures/data/models/reporting/tests/test_aggregator.py
+++ b/flexmeasures/data/models/reporting/tests/test_aggregator.py
@@ -21,7 +21,7 @@
         ("median", 0),
     ],
 )
-def test_aggregator(setup_dummy_data, aggregation_method, expected_value):
+def test_aggregator(setup_dummy_data, aggregation_method, expected_value, db):
     """
     This test computes the aggregation of two sensors containing 24 entries
     with value 1 and -1, respectively, for sensors 1 and 2.
@@ -40,8 +40,8 @@ def test_aggregator(setup_dummy_data, aggregation_method, expected_value):
 
     agg_reporter = AggregatorReporter(method=aggregation_method)
 
-    source_1 = DataSource.query.get(1)
-    source_2 = DataSource.query.get(2)
+    source_1 = db.session.get(DataSource, 1)
+    source_2 = db.session.get(DataSource, 2)
 
     result = agg_reporter.compute(
         input=[dict(sensor=s1, source=source_1), dict(sensor=s2, source=source_2)],
@@ -62,14 +62,14 @@ def test_aggregator(setup_dummy_data, aggregation_method, expected_value):
     [(1, 1, 0), (1, -1, 2), (2, 0, 2), (0, 2, -2)],
 )
 def test_aggregator_reporter_weights(
-    setup_dummy_data, weight_1, weight_2, expected_result
+    setup_dummy_data, weight_1, weight_2, expected_result, db
 ):
     s1, s2, s3, report_sensor, daily_report_sensor = setup_dummy_data
 
     reporter_config = dict(method="sum", weights={"s1": weight_1, "sensor_2": weight_2})
 
-    source_1 = DataSource.query.get(1)
-    source_2 = DataSource.query.get(2)
+    source_1 = db.session.get(DataSource, 1)
+    source_2 = db.session.get(DataSource, 2)
 
     agg_reporter = AggregatorReporter(config=reporter_config)
 
@@ -90,7 +90,7 @@ def test_aggregator_reporter_weights(
     assert (result == expected_result).all().event_value
 
 
-def test_dst_transition(setup_dummy_data):
+def test_dst_transition(setup_dummy_data, db):
     s1, s2, s3, report_sensor, daily_report_sensor = setup_dummy_data
 
     agg_reporter = AggregatorReporter()
@@ -99,7 +99,7 @@ def test_dst_transition(setup_dummy_data):
 
     # transition from winter (CET) to summer (CEST)
     result = agg_reporter.compute(
-        input=[dict(sensor=s3, source=DataSource.query.get(1))],
+        input=[dict(sensor=s3, source=db.session.get(DataSource, 1))],
         output=[dict(sensor=report_sensor)],
         start=tz.localize(datetime(2023, 3, 26)),
         end=tz.localize(datetime(2023, 3, 27)),
@@ -110,7 +110,7 @@ def test_dst_transition(setup_dummy_data):
 
     # transition from summer (CEST) to winter (CET)
     result = agg_reporter.compute(
-        input=[dict(sensor=s3, source=DataSource.query.get(1))],
+        input=[dict(sensor=s3, source=db.session.get(DataSource, 1))],
         output=[dict(sensor=report_sensor)],
         start=tz.localize(datetime(2023, 10, 29)),
         end=tz.localize(datetime(2023, 10, 30)),
@@ -120,7 +120,7 @@ def test_dst_transition(setup_dummy_data):
     assert len(result) == 25
 
 
-def test_resampling(setup_dummy_data):
+def test_resampling(setup_dummy_data, db):
     s1, s2, s3, report_sensor, daily_report_sensor = setup_dummy_data
 
     agg_reporter = AggregatorReporter()
@@ -131,8 +131,8 @@ def test_resampling(setup_dummy_data):
     result = agg_reporter.compute(
         start=tz.localize(datetime(2023, 3, 27)),
         end=tz.localize(datetime(2023, 3, 28)),
-        input=[dict(sensor=s3, source=DataSource.query.get(1))],
-        output=[dict(sensor=daily_report_sensor, source=DataSource.query.get(1))],
+        input=[dict(sensor=s3, source=db.session.get(DataSource, 1))],
+        output=[dict(sensor=daily_report_sensor, source=db.session.get(DataSource, 1))],
         belief_time=tz.localize(datetime(2023, 12, 1)),
         resolution=pd.Timedelta("1D"),
     )[0]["data"]
@@ -145,8 +145,8 @@ def test_resampling(setup_dummy_data):
     result = agg_reporter.compute(
         start=tz.localize(datetime(2023, 10, 29)),
         end=tz.localize(datetime(2023, 10, 30)),
-        input=[dict(sensor=s3, source=DataSource.query.get(1))],
-        output=[dict(sensor=daily_report_sensor, source=DataSource.query.get(1))],
+        input=[dict(sensor=s3, source=db.session.get(DataSource, 1))],
+        output=[dict(sensor=daily_report_sensor, source=db.session.get(DataSource, 1))],
         belief_time=tz.localize(datetime(2023, 12, 1)),
         resolution=pd.Timedelta("1D"),
     )[0]["data"]
@@ -156,7 +156,7 @@ def test_resampling(setup_dummy_data):
     )
 
 
-def test_source_transition(setup_dummy_data):
+def test_source_transition(setup_dummy_data, db):
     """The first 13 hours of the time window "belong" to Source 1 and are filled with 1.0.
     From 12:00 to 24:00, there are events belonging to Source 2 with value -1.
 
@@ -171,8 +171,8 @@ def test_source_transition(setup_dummy_data):
 
     tz = timezone("UTC")
 
-    ds1 = DataSource.query.get(1)
-    ds2 = DataSource.query.get(2)
+    ds1 = db.session.get(DataSource, 1)
+    ds2 = db.session.get(DataSource, 2)
 
     # considering DataSource 1 and 2
     result = agg_reporter.compute(
diff --git a/flexmeasures/data/models/reporting/tests/test_profit_reporter.py b/flexmeasures/data/models/reporting/tests/test_profit_reporter.py
index c8f579f80..39ed97ab3 100644
--- a/flexmeasures/data/models/reporting/tests/test_profit_reporter.py
+++ b/flexmeasures/data/models/reporting/tests/test_profit_reporter.py
@@ -1,10 +1,13 @@
 import pytest
 
 from datetime import timedelta
-from flexmeasures.data.models.reporting.profit import ProfitOrLossReporter
-from flexmeasures.data.models.time_series import Sensor
 from datetime import datetime
 from pytz import timezone
+from sqlalchemy import select
+
+from flexmeasures.data.models.reporting.profit import ProfitOrLossReporter
+from flexmeasures.data.models.time_series import Sensor
+from flexmeasures.tests.utils import get_test_sensor
 
 
 @pytest.mark.parametrize(
@@ -23,10 +26,10 @@ def test_profit_reporter(app, db, profit_report, use_power_sensor, loss_is_posit
     if use_power_sensor:
         output_sensor = power_sensor
 
-    epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
-    epex_da_production = Sensor.query.filter(
-        Sensor.name == "epex_da_production"
-    ).one_or_none()
+    epex_da = get_test_sensor(db)
+    epex_da_production = db.session.execute(
+        select(Sensor).filter(Sensor.name == "epex_da_production")
+    ).scalar_one_or_none()
 
     profit_reporter = ProfitOrLossReporter(
         consumption_price_sensor=epex_da,
diff --git a/flexmeasures/data/schemas/tests/test_sensor.py b/flexmeasures/data/schemas/tests/test_sensor.py
index 4b2c8e9c8..3891b7b17 100644
--- a/flexmeasures/data/schemas/tests/test_sensor.py
+++ b/flexmeasures/data/schemas/tests/test_sensor.py
@@ -78,7 +78,7 @@ def test_quantity_or_sensor_conversion(
     ],
 )
 def test_quantity_or_sensor_field(
-    setup_dummy_sensors, sensor_id, input_param, dst_unit, fails
+    setup_dummy_sensors, sensor_id, input_param, dst_unit, fails, db
 ):
 
     field = QuantityOrSensor(to_unit=dst_unit)
@@ -89,7 +89,7 @@ def test_quantity_or_sensor_field(
             assert val.units == ur.Unit(dst_unit)
         else:
             val = field.convert(input_param, None, None)
-            assert val == Sensor.query.get(sensor_id)
+            assert val == db.session.get(Sensor, sensor_id)
 
         assert not fails
     except Exception:
diff --git a/flexmeasures/data/tests/conftest.py b/flexmeasures/data/tests/conftest.py
index 8f2e37ad5..b85995b80 100644
--- a/flexmeasures/data/tests/conftest.py
+++ b/flexmeasures/data/tests/conftest.py
@@ -9,6 +9,7 @@
 from flask_sqlalchemy import SQLAlchemy
 from statsmodels.api import OLS
 import timely_beliefs as tb
+from sqlalchemy import select
 from flexmeasures.data.models.reporting import Reporter
 
 from flexmeasures.data.schemas.reporting import ReporterParametersSchema
@@ -60,9 +61,9 @@ def setup_fresh_test_data(
 
 def add_test_weather_sensor_and_forecasts(db: SQLAlchemy, setup_generic_asset_types):
     """one day of test data (one complete sine curve) for two sensors"""
-    data_source = DataSource.query.filter_by(
-        name="Seita", type="demo script"
-    ).one_or_none()
+    data_source = db.session.execute(
+        select(DataSource).filter_by(name="Seita", type="demo script")
+    ).scalar_one_or_none()
     weather_station = GenericAsset(
         name="Test weather station farther away",
         generic_asset_type=setup_generic_asset_types["weather_station"],
@@ -115,9 +116,9 @@ def test_specs(**args):
 @pytest.fixture(scope="module")
 def add_nearby_weather_sensors(db, add_weather_sensors) -> dict[str, Sensor]:
     temp_sensor_location = add_weather_sensors["temperature"].generic_asset.location
-    weather_station_type = GenericAssetType.query.filter(
-        GenericAssetType.name == "weather station"
-    ).one_or_none()
+    weather_station_type = db.session.execute(
+        select(GenericAssetType).filter_by(name="weather station")
+    ).scalar_one_or_none()
     farther_weather_station = GenericAsset(
         name="Test weather station farther away",
         generic_asset_type=weather_station_type,
diff --git a/flexmeasures/data/tests/test_annotations.py b/flexmeasures/data/tests/test_annotations.py
index dd8c86ba0..678b89c42 100644
--- a/flexmeasures/data/tests/test_annotations.py
+++ b/flexmeasures/data/tests/test_annotations.py
@@ -1,4 +1,5 @@
 import pandas as pd
+from sqlalchemy import select, func
 
 from flexmeasures.data.models.annotations import Annotation, get_or_create_annotation
 from flexmeasures.data.models.data_sources import DataSource
@@ -6,8 +7,10 @@
 
 def test_get_or_create_annotation(db, setup_sources):
     """Save an annotation, then get_or_create a new annotation with the same contents."""
-    num_annotations_before = Annotation.query.count()
-    source = DataSource.query.first()
+    num_annotations_before = db.session.scalar(
+        select(func.count()).select_from(Annotation)
+    )
+    source = db.session.scalars(select(DataSource).limit(1)).first()
     first_annotation = Annotation(
         content="Dutch new year",
         start=pd.Timestamp("2020-01-01 00:00+01"),
@@ -16,16 +19,20 @@ def test_get_or_create_annotation(db, setup_sources):
         type="holiday",
     )
     assert first_annotation == get_or_create_annotation(first_annotation)
-    num_annotations_intermediate = Annotation.query.count()
+    num_annotations_intermediate = db.session.scalar(
+        select(func.count()).select_from(Annotation)
+    )
     assert num_annotations_intermediate == num_annotations_before + 1
     assert (
-        Annotation.query.filter(
-            Annotation.content == first_annotation.content,
-            Annotation.start == first_annotation.start,
-            Annotation.end == first_annotation.end,
-            Annotation.source == first_annotation.source,
-            Annotation.type == first_annotation.type,
-        ).one_or_none()
+        db.session.execute(
+            select(Annotation).filter_by(
+                content=first_annotation.content,
+                start=first_annotation.start,
+                end=first_annotation.end,
+                source=first_annotation.source,
+                type=first_annotation.type,
+            )
+        ).scalar_one_or_none()
     ) == first_annotation
     assert first_annotation.id is not None
     second_annotation = Annotation(
@@ -36,7 +43,7 @@ def test_get_or_create_annotation(db, setup_sources):
         type="holiday",
     )
     assert first_annotation == get_or_create_annotation(second_annotation)
-    num_annotations_after = Annotation.query.count()
+    num_annotations_after = db.session.scalar(select(func.count(Annotation.id)))
     assert num_annotations_after == num_annotations_intermediate
     assert second_annotation.id is None
 
diff --git a/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py b/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py
index 841e233b8..19cf6a758 100644
--- a/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py
+++ b/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py
@@ -1,7 +1,7 @@
 from datetime import timedelta, datetime
 
 import pytest
-from sqlalchemy.orm import Query
+from sqlalchemy import select, Select
 
 from flexmeasures.data.models.time_series import Sensor, TimedBelief
 from flexmeasures.data.services.forecasting import (
@@ -19,7 +19,7 @@
 
 
 def test_forecasting_three_hours_of_wind(
-    app, run_as_cli, setup_fresh_test_data, clean_redis
+    app, run_as_cli, setup_fresh_test_data, clean_redis, fresh_db
 ):
     # asset has only 1 power sensor
     wind_device_2: Sensor = setup_fresh_test_data["wind-asset-2"].sensors[0]
@@ -37,21 +37,21 @@ def test_forecasting_three_hours_of_wind(
 
     work_on_rq(app.queues["forecasting"], exc_handler=handle_forecasting_exception)
 
-    forecasts = (
-        TimedBelief.query.filter(TimedBelief.sensor_id == wind_device_2.id)
+    forecasts = fresh_db.session.scalars(
+        select(TimedBelief)
+        .filter(TimedBelief.sensor_id == wind_device_2.id)
         .filter(TimedBelief.belief_horizon == horizon)
         .filter(
             (TimedBelief.event_start >= as_server_time(datetime(2015, 1, 1, 11)))
             & (TimedBelief.event_start < as_server_time(datetime(2015, 1, 1, 14)))
         )
-        .all()
-    )
+    ).all()
     assert len(forecasts) == 12
     check_aggregate(12, horizon, wind_device_2.id)
 
 
 def test_forecasting_two_hours_of_solar(
-    app, run_as_cli, setup_fresh_test_data, clean_redis
+    app, run_as_cli, setup_fresh_test_data, clean_redis, fresh_db
 ):
     # asset has only 1 power sensor
     solar_device_1: Sensor = setup_fresh_test_data["solar-asset-1"].sensors[0]
@@ -68,15 +68,15 @@ def test_forecasting_two_hours_of_solar(
     print("Job: %s" % job[0].id)
 
     work_on_rq(app.queues["forecasting"], exc_handler=handle_forecasting_exception)
-    forecasts = (
-        TimedBelief.query.filter(TimedBelief.sensor_id == solar_device_1.id)
+    forecasts = fresh_db.session.scalars(
+        select(TimedBelief)
+        .filter(TimedBelief.sensor_id == solar_device_1.id)
         .filter(TimedBelief.belief_horizon == horizon)
         .filter(
             (TimedBelief.event_start >= as_server_time(datetime(2015, 1, 1, 13)))
             & (TimedBelief.event_start < as_server_time(datetime(2015, 1, 1, 15)))
         )
-        .all()
-    )
+    ).all()
     assert len(forecasts) == 8
     check_aggregate(8, horizon, solar_device_1.id)
 
@@ -95,6 +95,7 @@ def test_failed_model_with_too_much_training_then_succeed_with_fallback(
     setup_fresh_test_data,
     model_to_start_with,
     model_version,
+    fresh_db,
 ):
     """
     Here we fail once - because we start with a model that needs too much training.
@@ -139,10 +140,11 @@ def test_failed_model_with_too_much_training_then_succeed_with_fallback(
     )
 
     # this query is useful to check data:
-    def make_query(the_horizon_hours: int) -> Query:
+    def make_query(the_horizon_hours: int) -> Select:
         the_horizon = timedelta(hours=the_horizon_hours)
         return (
-            TimedBelief.query.filter(TimedBelief.sensor_id == solar_device_1.id)
+            select(TimedBelief)
+            .filter(TimedBelief.sensor_id == solar_device_1.id)
             .filter(TimedBelief.belief_horizon == the_horizon)
             .filter(
                 (
@@ -161,13 +163,15 @@ def make_query(the_horizon_hours: int) -> Query:
         )
 
     # The successful (linear or naive) OLS leads to these.
-    forecasts = make_query(the_horizon_hours=horizon_hours).all()
+    forecasts = fresh_db.session.scalars(
+        make_query(the_horizon_hours=horizon_hours)
+    ).all()
 
     assert len(forecasts) == 8
     check_aggregate(8, horizon, solar_device_1.id)
 
     if model_to_start_with == "linear-OLS":
-        existing_data = make_query(the_horizon_hours=0).all()
+        existing_data = fresh_db.session.scalars(make_query(the_horizon_hours=0)).all()
 
         for ed, fd in zip(existing_data, forecasts):
             assert ed.event_value == fd.event_value
diff --git a/flexmeasures/data/tests/test_queries.py b/flexmeasures/data/tests/test_queries.py
index 42c70dc6f..56c01dc9b 100644
--- a/flexmeasures/data/tests/test_queries.py
+++ b/flexmeasures/data/tests/test_queries.py
@@ -5,6 +5,7 @@
 import pytest
 import pytz
 import timely_beliefs as tb
+from sqlalchemy import select
 
 from flexmeasures.data.models.data_sources import DataSource
 from flexmeasures.data.models.planning.utils import initialize_index
@@ -13,6 +14,7 @@
     multiply_dataframe_with_deterministic_beliefs,
     simplify_index,
 )
+from flexmeasures.tests.utils import get_test_sensor
 
 
 @pytest.mark.parametrize(
@@ -42,7 +44,9 @@
 def test_collect_power(db, app, query_start, query_end, num_values, setup_test_data):
     # asset has only 1 power sensor
     wind_device_1: Sensor = setup_test_data["wind-asset-1"].sensors[0]
-    data = TimedBelief.query.filter(TimedBelief.sensor_id == wind_device_1.id).all()
+    data = db.session.scalars(
+        select(TimedBelief).filter(TimedBelief.sensor_id == wind_device_1.id)
+    ).all()
     print(data)
     bdf: tb.BeliefsDataFrame = TimedBelief.search(
         wind_device_1,
@@ -224,10 +228,12 @@ def test_simplify_index(setup_test_data, check_empty_frame):
     assert df.event_resolution == timedelta(minutes=15)
 
 
-def test_query_beliefs(setup_beliefs):
+def test_query_beliefs(setup_beliefs, db):
     """Check various ways of querying for beliefs."""
-    sensor = Sensor.query.filter_by(name="epex_da").one_or_none()
-    source = DataSource.query.filter_by(name="ENTSO-E").one_or_none()
+    sensor = get_test_sensor(db)
+    source = db.session.execute(
+        select(DataSource).filter_by(name="ENTSO-E")
+    ).scalar_one_or_none()
     bdfs = [
         TimedBelief.search(sensor, source=source, most_recent_beliefs_only=False),
         TimedBelief.search(sensor.id, source=source, most_recent_beliefs_only=False),
@@ -244,13 +250,15 @@ def test_query_beliefs(setup_beliefs):
         assert len(bdf) == setup_beliefs
 
 
-def test_persist_beliefs(setup_beliefs, setup_test_data):
+def test_persist_beliefs(setup_beliefs, setup_test_data, db):
     """Check whether persisting beliefs works.
 
     We load the already set up beliefs, and form new beliefs an hour later.
     """
-    sensor = Sensor.query.filter_by(name="epex_da").one_or_none()
-    source = DataSource.query.filter_by(name="ENTSO-E").one_or_none()
+    sensor = get_test_sensor(db)
+    source = db.session.execute(
+        select(DataSource).filter_by(name="ENTSO-E")
+    ).scalar_one_or_none()
     bdf: tb.BeliefsDataFrame = TimedBelief.search(
         sensor, source=source, most_recent_beliefs_only=False
     )
diff --git a/flexmeasures/data/tests/test_scheduling_jobs.py b/flexmeasures/data/tests/test_scheduling_jobs.py
index f21a366c3..46b0305f6 100644
--- a/flexmeasures/data/tests/test_scheduling_jobs.py
+++ b/flexmeasures/data/tests/test_scheduling_jobs.py
@@ -5,6 +5,7 @@
 import pytz
 import pytest
 from rq.job import Job
+from sqlalchemy import select
 
 from flexmeasures.data.models.planning import Scheduler
 from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException
@@ -31,7 +32,9 @@ def test_scheduling_a_battery(db, app, add_battery_assets, setup_test_data):
     resolution = timedelta(minutes=15)
 
     assert (
-        DataSource.query.filter_by(name="FlexMeasures", type="scheduler").one_or_none()
+        db.session.execute(
+            select(DataSource).filter_by(name="FlexMeasures", type="scheduler")
+        ).scalar_one_or_none()
         is None
     )  # Make sure the scheduler data source isn't there
 
@@ -51,18 +54,18 @@ def test_scheduling_a_battery(db, app, add_battery_assets, setup_test_data):
 
     work_on_rq(app.queues["scheduling"], exc_handler=exception_reporter)
 
-    scheduler_source = DataSource.query.filter_by(
-        name="Seita", type="scheduler"
-    ).one_or_none()
+    scheduler_source = db.session.execute(
+        select(DataSource).filter_by(name="Seita", type="scheduler")
+    ).scalar_one_or_none()
     assert (
         scheduler_source is not None
     )  # Make sure the scheduler data source is now there
 
-    power_values = (
-        TimedBelief.query.filter(TimedBelief.sensor_id == battery.id)
+    power_values = db.session.scalars(
+        select(TimedBelief)
+        .filter(TimedBelief.sensor_id == battery.id)
         .filter(TimedBelief.source_id == scheduler_source.id)
-        .all()
-    )
+    ).all()
     print([v.event_value for v in power_values])
     assert len(power_values) == 96
     assert (
@@ -132,19 +135,21 @@ def test_assigning_custom_scheduler(db, app, add_battery_assets, is_path: bool):
     finished_job = Job.fetch(job.id, connection=redis_connection)
     assert finished_job.meta["data_source_info"]["model"] == scheduler_specs["class"]
 
-    scheduler_source = DataSource.query.filter_by(
-        type="scheduler",
-        **finished_job.meta["data_source_info"],
-    ).one_or_none()
+    scheduler_source = db.session.execute(
+        select(DataSource).filter_by(
+            type="scheduler",
+            **finished_job.meta["data_source_info"],
+        )
+    ).scalar_one_or_none()
     assert (
         scheduler_source is not None
     )  # Make sure the scheduler data source is now there
 
-    power_values = (
-        TimedBelief.query.filter(TimedBelief.sensor_id == battery.id)
+    power_values = db.session.scalars(
+        select(TimedBelief)
+        .filter(TimedBelief.sensor_id == battery.id)
         .filter(TimedBelief.source_id == scheduler_source.id)
-        .all()
-    )
+    ).all()
     assert len(power_values) == 96
     # test for negative value as we schedule consumption
     assert all(
diff --git a/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py b/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py
index 80b4162e8..096fa7ebd 100644
--- a/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py
+++ b/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py
@@ -2,6 +2,7 @@
 import pytz
 
 import pandas as pd
+from sqlalchemy import select
 
 from flexmeasures.data.models.data_sources import DataSource
 from flexmeasures.data.models.time_series import TimedBelief
@@ -31,7 +32,10 @@ def test_scheduling_a_charging_station(
     soc_targets = [dict(datetime=target_datetime.isoformat(), value=target_soc)]
 
     assert (
-        DataSource.query.filter_by(name="Seita", type="scheduler").one_or_none() is None
+        db.session.execute(
+            select(DataSource).filter_by(name="Seita", type="scheduler")
+        ).scalar_one_or_none()
+        is None
     )  # Make sure the scheduler data source isn't there
 
     job = create_scheduling_job(
@@ -52,18 +56,18 @@ def test_scheduling_a_charging_station(
 
     work_on_rq(app.queues["scheduling"], exc_handler=exception_reporter)
 
-    scheduler_source = DataSource.query.filter_by(
-        name="Seita", type="scheduler"
-    ).one_or_none()
+    scheduler_source = db.session.execute(
+        select(DataSource).filter_by(name="Seita", type="scheduler")
+    ).scalar_one_or_none()
     assert (
         scheduler_source is not None
     )  # Make sure the scheduler data source is now there
 
-    power_values = (
-        TimedBelief.query.filter(TimedBelief.sensor_id == charging_station.id)
+    power_values = db.session.scalars(
+        select(TimedBelief)
+        .filter(TimedBelief.sensor_id == charging_station.id)
         .filter(TimedBelief.source_id == scheduler_source.id)
-        .all()
-    )
+    ).all()
     consumption_schedule = pd.Series(
         [-v.event_value for v in power_values],
         index=pd.DatetimeIndex([v.event_start for v in power_values]),
diff --git a/flexmeasures/data/tests/test_scheduling_repeated_jobs.py b/flexmeasures/data/tests/test_scheduling_repeated_jobs.py
index cdae54bb1..7f681273d 100644
--- a/flexmeasures/data/tests/test_scheduling_repeated_jobs.py
+++ b/flexmeasures/data/tests/test_scheduling_repeated_jobs.py
@@ -7,6 +7,7 @@
 import pytz
 import pytest
 from rq.job import Job, JobStatus
+from sqlalchemy import select
 
 from flexmeasures.data.models.data_sources import DataSource
 from flexmeasures.data.models.generic_assets import GenericAsset
@@ -135,13 +136,13 @@ def test_hashing(db, app, add_charging_station_assets, setup_test_data):
 
     # Here, we need to obtain the object through a db query, otherwise we run into session issues with deepcopy later on
     # charging_station = add_charging_station_assets["Test charging station"].sensors[0]
-    charging_station = (
-        Sensor.query.filter(Sensor.name == "power")
+    charging_station = db.session.execute(
+        select(Sensor)
+        .filter(Sensor.name == "power")
         .join(GenericAsset)
         .filter(GenericAsset.id == Sensor.generic_asset_id)
         .filter(GenericAsset.name == "Test charging stations")
-        .one_or_none()
-    )
+    ).scalar_one_or_none()
     tz = pytz.timezone("Europe/Amsterdam")
     start = tz.localize(datetime(2015, 1, 2))
     end = tz.localize(datetime(2015, 1, 3))
@@ -197,9 +198,9 @@ def test_scheduling_multiple_triggers(
     soc_start = 2.5
 
     assert (
-        DataSource.query.filter_by(name="FlexMeasures", type="scheduling script")
-        .where()
-        .one_or_none()
+        db.session.execute(
+            select(DataSource).filter_by(name="FlexMeasures", type="scheduling script")
+        ).scalar_one_or_none()
         is None
     )  # Make sure the scheduler data source isn't there
 
diff --git a/flexmeasures/data/tests/test_time_series_services.py b/flexmeasures/data/tests/test_time_series_services.py
index d14c0cbe9..2e117dbba 100644
--- a/flexmeasures/data/tests/test_time_series_services.py
+++ b/flexmeasures/data/tests/test_time_series_services.py
@@ -3,17 +3,17 @@
 
 from flexmeasures.data.utils import save_to_db
 from flexmeasures.data.models.data_sources import DataSource
-from flexmeasures.data.models.time_series import Sensor
+from flexmeasures.tests.utils import get_test_sensor
 
 
-def test_drop_unchanged_beliefs(setup_beliefs):
+def test_drop_unchanged_beliefs(setup_beliefs, db):
     """Trying to save beliefs that are already in the database shouldn't raise an error.
 
     Even after updating the belief time, we expect to persist only the older belief time.
     """
 
     # Set a reference for the number of beliefs stored and their belief times
-    sensor = Sensor.query.filter_by(name="epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     bdf = sensor.search_beliefs(most_recent_beliefs_only=False)
     num_beliefs_before = len(bdf)
     belief_times_before = bdf.belief_times
@@ -37,11 +37,11 @@ def test_drop_unchanged_beliefs(setup_beliefs):
     assert list(bdf.belief_times) == list(belief_times_before)
 
 
-def test_do_not_drop_beliefs_copied_by_another_source(setup_beliefs):
+def test_do_not_drop_beliefs_copied_by_another_source(setup_beliefs, db):
     """Trying to copy beliefs from one source to another should double the number of beliefs."""
 
     # Set a reference for the number of beliefs stored
-    sensor = Sensor.query.filter_by(name="epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     bdf = sensor.search_beliefs(most_recent_beliefs_only=False)
     num_beliefs_before = len(bdf)
 
@@ -58,7 +58,7 @@ def test_do_not_drop_beliefs_copied_by_another_source(setup_beliefs):
     assert num_beliefs_after == 2 * num_beliefs_before
 
 
-def test_do_not_drop_changed_probabilistic_belief(setup_beliefs):
+def test_do_not_drop_changed_probabilistic_belief(setup_beliefs, db):
     """Trying to save a changed probabilistic belief should result in saving the whole belief.
 
     For example, given a belief that defines both cp=0.2 and cp=0.5,
@@ -67,7 +67,7 @@ def test_do_not_drop_changed_probabilistic_belief(setup_beliefs):
     """
 
     # Set a reference for the number of beliefs stored
-    sensor = Sensor.query.filter_by(name="epex_da").one_or_none()
+    sensor = get_test_sensor(db)
     bdf = sensor.search_beliefs(source="ENTSO-E", most_recent_beliefs_only=False)
     num_beliefs_before = len(bdf)
 
diff --git a/flexmeasures/data/tests/test_timerange_services.py b/flexmeasures/data/tests/test_timerange_services.py
new file mode 100644
index 000000000..0f426af9d
--- /dev/null
+++ b/flexmeasures/data/tests/test_timerange_services.py
@@ -0,0 +1,15 @@
+from flexmeasures.data.services.timerange import get_timerange
+from flexmeasures.tests.utils import get_test_sensor
+
+
+def test_get_sensor_timerange(setup_beliefs, db):
+    """Test getting the timerange of a sensor."""
+
+    # Set a reference for the number of beliefs stored and their belief times
+    sensor = get_test_sensor(db)
+    bdf = sensor.search_beliefs()
+
+    expected_timerange = bdf.event_starts[0], bdf.event_ends[-1]
+    timerange = get_timerange(sensor_ids=[sensor.id])
+
+    assert timerange == expected_timerange
diff --git a/flexmeasures/data/tests/test_user_services.py b/flexmeasures/data/tests/test_user_services.py
index 932fb4556..6af764197 100644
--- a/flexmeasures/data/tests/test_user_services.py
+++ b/flexmeasures/data/tests/test_user_services.py
@@ -1,4 +1,5 @@
 import pytest
+from sqlalchemy import select, func
 
 from flexmeasures.data.models.user import User, Role
 from flexmeasures.data.services.users import (
@@ -16,20 +17,30 @@ def test_create_user(
     fresh_db, setup_accounts_fresh_db, setup_roles_users_fresh_db, app
 ):
     """Create a user"""
-    num_users = User.query.count()
+    num_users = fresh_db.session.scalar(select(func.count()).select_from(User))
     user = create_user(
         email="new_user@seita.nl",
         password="testtest",
         account_name=setup_accounts_fresh_db["Prosumer"].name,
         user_roles=["SomeRole"],
     )
-    assert User.query.count() == num_users + 1
+    assert (
+        fresh_db.session.scalar(select(func.count()).select_from(User)) == num_users + 1
+    )
     assert user.email == "new_user@seita.nl"
     assert user.username == "new_user"
     assert user.account.name == "Test Prosumer Account"
-    assert user.roles == [Role.query.filter_by(name="SomeRole").one_or_none()]
-    assert DataSource.query.filter_by(user_id=user.id).one_or_none()
-    assert DataSource.query.filter_by(name=user.username).one_or_none()
+    assert user.roles == [
+        fresh_db.session.execute(
+            select(Role).filter_by(name="SomeRole")
+        ).scalar_one_or_none()
+    ]
+    assert fresh_db.session.execute(
+        select(DataSource).filter_by(user_id=user.id)
+    ).scalar_one_or_none()
+    assert fresh_db.session.execute(
+        select(DataSource).filter_by(name=user.username)
+    ).scalar_one_or_none()
 
 
 def test_create_invalid_user(
@@ -82,13 +93,11 @@ def test_create_invalid_user(
 def test_delete_user(fresh_db, setup_roles_users_fresh_db, setup_assets_fresh_db, app):
     """Check that deleting a user does not lead to deleting their organisation's (asset/sensor/beliefs) data."""
     prosumer: User = find_user_by_email("test_prosumer_user@seita.nl")
-    num_users_before = User.query.count()
+    num_users_before = fresh_db.session.scalar(select(func.count(User.id)))
 
     # Find assets belonging to the user's organisation
-    asset_query = GenericAsset.query.filter(
-        GenericAsset.account_id == prosumer.account_id
-    )
-    assets_before = asset_query.all()
+    asset_query = select(GenericAsset).filter_by(account_id=prosumer.account_id)
+    assets_before = fresh_db.session.scalars(asset_query).all()
     assert (
         len(assets_before) > 0
     ), "Test assets should have been set up, otherwise we'd not be testing whether they're kept."
@@ -99,10 +108,10 @@ def test_delete_user(fresh_db, setup_roles_users_fresh_db, setup_assets_fresh_db
         sensors_before.extend(asset.sensors)
 
     # Count all the organisation's beliefs
-    beliefs_query = TimedBelief.query.filter(
+    beliefs_query = select(func.count()).filter(
         TimedBelief.sensor_id.in_([sensor.id for sensor in sensors_before])
     )
-    num_beliefs_before = beliefs_query.count()
+    num_beliefs_before = fresh_db.session.scalar(beliefs_query)
     assert (
         num_beliefs_before > 0
     ), "Some beliefs should have been set up, otherwise we'd not be testing whether they're kept."
@@ -110,11 +119,11 @@ def test_delete_user(fresh_db, setup_roles_users_fresh_db, setup_assets_fresh_db
     # Delete the user
     delete_user(prosumer)
     assert find_user_by_email("test_prosumer_user@seita.nl") is None
-    assert User.query.count() == num_users_before - 1
+    assert fresh_db.session.scalar(select(func.count(User.id))) == num_users_before - 1
 
     # Check whether the organisation's assets, sensors and beliefs were kept
-    assets_after = asset_query.all()
+    assets_after = fresh_db.session.scalars(asset_query).all()
     assert assets_after == assets_before
 
-    num_beliefs_after = beliefs_query.count()
+    num_beliefs_after = fresh_db.session.scalar(beliefs_query)
     assert num_beliefs_after == num_beliefs_before
diff --git a/flexmeasures/tests/utils.py b/flexmeasures/tests/utils.py
new file mode 100644
index 000000000..5b303d46e
--- /dev/null
+++ b/flexmeasures/tests/utils.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from sqlalchemy import select
+
+from flexmeasures.data.models.time_series import Sensor
+
+
+def get_test_sensor(db) -> Sensor | None:
+    sensor = db.session.execute(
+        select(Sensor).filter_by(name="epex_da")
+    ).scalar_one_or_none()
+    return sensor
diff --git a/requirements/3.10/test.txt b/requirements/3.10/test.txt
index 28b42d24d..0ada396d4 100644
--- a/requirements/3.10/test.txt
+++ b/requirements/3.10/test.txt
@@ -8,11 +8,11 @@ async-timeout==4.0.3
     # via
     #   -c requirements/3.10/app.txt
     #   redis
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.10/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.10/app.txt
     #   requests
@@ -20,11 +20,11 @@ click==8.1.7
     # via
     #   -c requirements/3.10/app.txt
     #   flask
-coverage[toml]==7.3.1
+coverage[toml]==7.4.1
     # via pytest-cov
-exceptiongroup==1.1.3
+exceptiongroup==1.2.0
     # via pytest
-fakeredis==2.18.1
+fakeredis==2.21.0
     # via -r requirements/test.in
 flask==2.2.5
     # via
@@ -32,7 +32,7 @@ flask==2.2.5
     #   pytest-flask
 highspy==1.5.3
     # via -r requirements/test.in
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.10/app.txt
     #   requests
@@ -42,25 +42,25 @@ itsdangerous==2.1.2
     # via
     #   -c requirements/3.10/app.txt
     #   flask
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.10/app.txt
     #   flask
 lupa==2.0
     # via -r requirements/test.in
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.10/app.txt
     #   jinja2
     #   werkzeug
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.10/app.txt
     #   pytest
     #   pytest-sugar
-pluggy==1.3.0
+pluggy==1.4.0
     # via pytest
-pytest==7.4.2
+pytest==8.0.0
     # via
     #   -r requirements/test.in
     #   pytest-cov
@@ -68,9 +68,9 @@ pytest==7.4.2
     #   pytest-sugar
 pytest-cov==4.1.0
     # via -r requirements/test.in
-pytest-flask==1.2.0
+pytest-flask==1.3.0
     # via -r requirements/test.in
-pytest-sugar==0.9.7
+pytest-sugar==1.0.0
     # via -r requirements/test.in
 redis==4.6.0
     # via
@@ -89,13 +89,13 @@ six==1.16.0
     #   requests-mock
 sortedcontainers==2.4.0
     # via fakeredis
-termcolor==2.3.0
+termcolor==2.4.0
     # via pytest-sugar
 tomli==2.0.1
     # via
     #   coverage
     #   pytest
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.10/app.txt
     #   requests
diff --git a/requirements/3.11/test.txt b/requirements/3.11/test.txt
index 8f41c53e1..40da2552b 100644
--- a/requirements/3.11/test.txt
+++ b/requirements/3.11/test.txt
@@ -4,11 +4,11 @@
 #
 #    pip-compile --constraint=requirements/3.11/app.txt --output-file=requirements/3.11/test.txt requirements/test.in
 #
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.11/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.11/app.txt
     #   requests
@@ -16,9 +16,9 @@ click==8.1.7
     # via
     #   -c requirements/3.11/app.txt
     #   flask
-coverage[toml]==7.3.1
+coverage[toml]==7.4.1
     # via pytest-cov
-fakeredis==2.18.1
+fakeredis==2.21.0
     # via -r requirements/test.in
 flask==2.2.5
     # via
@@ -26,7 +26,7 @@ flask==2.2.5
     #   pytest-flask
 highspy==1.5.3
     # via -r requirements/test.in
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.11/app.txt
     #   requests
@@ -36,25 +36,25 @@ itsdangerous==2.1.2
     # via
     #   -c requirements/3.11/app.txt
     #   flask
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.11/app.txt
     #   flask
 lupa==2.0
     # via -r requirements/test.in
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.11/app.txt
     #   jinja2
     #   werkzeug
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.11/app.txt
     #   pytest
     #   pytest-sugar
-pluggy==1.3.0
+pluggy==1.4.0
     # via pytest
-pytest==7.4.2
+pytest==8.0.0
     # via
     #   -r requirements/test.in
     #   pytest-cov
@@ -62,9 +62,9 @@ pytest==7.4.2
     #   pytest-sugar
 pytest-cov==4.1.0
     # via -r requirements/test.in
-pytest-flask==1.2.0
+pytest-flask==1.3.0
     # via -r requirements/test.in
-pytest-sugar==0.9.7
+pytest-sugar==1.0.0
     # via -r requirements/test.in
 redis==4.6.0
     # via
@@ -83,9 +83,9 @@ six==1.16.0
     #   requests-mock
 sortedcontainers==2.4.0
     # via fakeredis
-termcolor==2.3.0
+termcolor==2.4.0
     # via pytest-sugar
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.11/app.txt
     #   requests
diff --git a/requirements/3.8/test.txt b/requirements/3.8/test.txt
index 28b1c0488..26dcca76f 100644
--- a/requirements/3.8/test.txt
+++ b/requirements/3.8/test.txt
@@ -8,11 +8,11 @@ async-timeout==4.0.3
     # via
     #   -c requirements/3.8/app.txt
     #   redis
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.8/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.8/app.txt
     #   requests
@@ -20,11 +20,11 @@ click==8.1.7
     # via
     #   -c requirements/3.8/app.txt
     #   flask
-coverage[toml]==7.3.1
+coverage[toml]==7.4.1
     # via pytest-cov
-exceptiongroup==1.1.3
+exceptiongroup==1.2.0
     # via pytest
-fakeredis==2.18.1
+fakeredis==2.21.0
     # via -r requirements/test.in
 flask==2.2.5
     # via
@@ -32,11 +32,11 @@ flask==2.2.5
     #   pytest-flask
 highspy==1.5.3
     # via -r requirements/test.in
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.8/app.txt
     #   requests
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -c requirements/3.8/app.txt
     #   flask
@@ -46,25 +46,25 @@ itsdangerous==2.1.2
     # via
     #   -c requirements/3.8/app.txt
     #   flask
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.8/app.txt
     #   flask
 lupa==2.0
     # via -r requirements/test.in
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.8/app.txt
     #   jinja2
     #   werkzeug
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.8/app.txt
     #   pytest
     #   pytest-sugar
-pluggy==1.3.0
+pluggy==1.4.0
     # via pytest
-pytest==7.4.2
+pytest==8.0.0
     # via
     #   -r requirements/test.in
     #   pytest-cov
@@ -72,9 +72,9 @@ pytest==7.4.2
     #   pytest-sugar
 pytest-cov==4.1.0
     # via -r requirements/test.in
-pytest-flask==1.2.0
+pytest-flask==1.3.0
     # via -r requirements/test.in
-pytest-sugar==0.9.7
+pytest-sugar==1.0.0
     # via -r requirements/test.in
 redis==4.6.0
     # via
@@ -93,13 +93,13 @@ six==1.16.0
     #   requests-mock
 sortedcontainers==2.4.0
     # via fakeredis
-termcolor==2.3.0
+termcolor==2.4.0
     # via pytest-sugar
 tomli==2.0.1
     # via
     #   coverage
     #   pytest
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.8/app.txt
     #   requests
@@ -108,7 +108,7 @@ werkzeug==2.2.3
     #   -c requirements/3.8/app.txt
     #   flask
     #   pytest-flask
-zipp==3.16.2
+zipp==3.17.0
     # via
     #   -c requirements/3.8/app.txt
     #   importlib-metadata
diff --git a/requirements/3.9/test.txt b/requirements/3.9/test.txt
index 8b273d9fb..27dd26d6b 100644
--- a/requirements/3.9/test.txt
+++ b/requirements/3.9/test.txt
@@ -8,11 +8,11 @@ async-timeout==4.0.3
     # via
     #   -c requirements/3.9/app.txt
     #   redis
-certifi==2023.7.22
+certifi==2024.2.2
     # via
     #   -c requirements/3.9/app.txt
     #   requests
-charset-normalizer==3.2.0
+charset-normalizer==3.3.2
     # via
     #   -c requirements/3.9/app.txt
     #   requests
@@ -20,11 +20,11 @@ click==8.1.7
     # via
     #   -c requirements/3.9/app.txt
     #   flask
-coverage[toml]==7.3.1
+coverage[toml]==7.4.1
     # via pytest-cov
-exceptiongroup==1.1.3
+exceptiongroup==1.2.0
     # via pytest
-fakeredis==2.18.1
+fakeredis==2.21.0
     # via -r requirements/test.in
 flask==2.2.5
     # via
@@ -32,11 +32,11 @@ flask==2.2.5
     #   pytest-flask
 highspy==1.5.3
     # via -r requirements/test.in
-idna==3.4
+idna==3.6
     # via
     #   -c requirements/3.9/app.txt
     #   requests
-importlib-metadata==6.8.0
+importlib-metadata==7.0.1
     # via
     #   -c requirements/3.9/app.txt
     #   flask
@@ -46,25 +46,25 @@ itsdangerous==2.1.2
     # via
     #   -c requirements/3.9/app.txt
     #   flask
-jinja2==3.1.2
+jinja2==3.1.3
     # via
     #   -c requirements/3.9/app.txt
     #   flask
 lupa==2.0
     # via -r requirements/test.in
-markupsafe==2.1.3
+markupsafe==2.1.5
     # via
     #   -c requirements/3.9/app.txt
     #   jinja2
     #   werkzeug
-packaging==23.1
+packaging==23.2
     # via
     #   -c requirements/3.9/app.txt
     #   pytest
     #   pytest-sugar
-pluggy==1.3.0
+pluggy==1.4.0
     # via pytest
-pytest==7.4.2
+pytest==8.0.0
     # via
     #   -r requirements/test.in
     #   pytest-cov
@@ -72,9 +72,9 @@ pytest==7.4.2
     #   pytest-sugar
 pytest-cov==4.1.0
     # via -r requirements/test.in
-pytest-flask==1.2.0
+pytest-flask==1.3.0
     # via -r requirements/test.in
-pytest-sugar==0.9.7
+pytest-sugar==1.0.0
     # via -r requirements/test.in
 redis==4.6.0
     # via
@@ -93,13 +93,13 @@ six==1.16.0
     #   requests-mock
 sortedcontainers==2.4.0
     # via fakeredis
-termcolor==2.3.0
+termcolor==2.4.0
     # via pytest-sugar
 tomli==2.0.1
     # via
     #   coverage
     #   pytest
-urllib3==2.0.4
+urllib3==2.2.0
     # via
     #   -c requirements/3.9/app.txt
     #   requests
@@ -108,7 +108,7 @@ werkzeug==2.2.3
     #   -c requirements/3.9/app.txt
     #   flask
     #   pytest-flask
-zipp==3.16.2
+zipp==3.17.0
     # via
     #   -c requirements/3.9/app.txt
     #   importlib-metadata