Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Bugfixes
v1.0.0 | August 14, 2026
============================

.. warning:: As of this release we standardize asynchronous job responses to use the ``job`` field and return HTTP ``202 Accepted`` while a background job is queued or running. See :ref:`api_background_jobs` for the response format and polling flow. Legacy response fields such as ``schedule`` and ``forecast`` will be deprecated; clients should migrate to ``job`` (see the Infrastructure / Support section below for migration details).
.. warning:: As of this release we standardize asynchronous job responses to use the ``job`` field and return HTTP ``202 Accepted`` while a background job is queued or running. See :ref:`api_background_jobs` for the response format and polling flow. Legacy response fields such as ``schedule`` and ``forecast`` will be deprecated; clients should migrate to ``job`` (see the Infrastructure / Support section below for migration details). To receive legacy status codes (e.g. for older clients), hosts can use :ref:`legacy-schedule-client-config`.

.. warning:: Upgrading to this version requires running ``flexmeasures db upgrade`` (you can create a backup first with ``flexmeasures db-ops dump``).

Expand Down Expand Up @@ -87,7 +87,7 @@ Infrastructure / Support
-------------------------

* Support storing encrypted connection secrets on organisations and assets, including utility functions, encryption key configuration, CLI commands to set and delete secrets, and UI tables that show stored secret names and optional expiration times without exposing their values [see `PR #2236 <https://www.github.com/FlexMeasures/flexmeasures/pull/2236>`_]
* Standardize job-trigger API responses to return ``202 Accepted`` and a canonical ``job`` field; legacy response fields such as ``schedule`` and ``forecast`` are preserved for backward-compatibility but marked deprecated with migration guidance in :ref:`api_background_jobs` [see `PR #2224 <https://github.com/FlexMeasures/flexmeasures/pull/2224>`_].
* Standardize job-trigger API responses to return ``202 Accepted`` and a canonical ``job`` field, and likewise return ``202 Accepted`` when polling a schedule whose job has not finished yet; legacy response fields such as ``schedule`` and ``forecast`` are preserved for backward-compatibility but marked deprecated with migration guidance in :ref:`api_background_jobs`. Hosts still serving clients that expect the previous status codes can opt individual assets back in, see :ref:`legacy-schedule-client-config` [see `PR #2224 <https://github.com/FlexMeasures/flexmeasures/pull/2224>`_ and `PR #2429 <https://github.com/FlexMeasures/flexmeasures/pull/2429>`_].
* Warn on startup when ``TRUSTED_HOSTS`` is unset, as that lets clients poison the URLs FlexMeasures generates, such as password reset links; the setting can now also be given as a comma-separated environment variable, and the ``development`` environment trusts loopback hosts by default (so reaching a development server by its LAN address or through a tunnel now means listing that host) [see `PR #2389 <https://www.github.com/FlexMeasures/flexmeasures/pull/2389>`_]
* Upgraded dependencies [see `PR #1485 <https://www.github.com/FlexMeasures/flexmeasures/pull/1485>`_, `PR #2215 <https://www.github.com/FlexMeasures/flexmeasures/pull/2215>`_, `PR #2243 <https://www.github.com/FlexMeasures/flexmeasures/pull/2243>`_, `PR #2348 <https://www.github.com/FlexMeasures/flexmeasures/pull/2348>`_ and `PR #2388 <https://www.github.com/FlexMeasures/flexmeasures/pull/2388>`_]
* Add a ``FLEXMEASURES_SENTRY_DAILY_RATE_LIMIT`` setting for spreading a host's Sentry error allowance across the month with a fail-open daily Redis counter, and send the startup error about the database schema not being at the Alembic head revision to Sentry at most once per UTC calendar day per pair of current and expected revisions (it is still logged in full on every start) [see `PR #2366 <https://www.github.com/FlexMeasures/flexmeasures/pull/2366>`_]
Expand Down
24 changes: 24 additions & 0 deletions documentation/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,30 @@ If ``False``, the API transparently follows the fallback job and returns the fal

Default: ``False``


.. _legacy-schedule-client-config:

FLEXMEASURES_LEGACY_SCHEDULEACCEPTED_STATUS_MAX_INCOMPATIBLE_CLIENT_VERSION
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Backwards-compatibility switch for scheduling-related endpoints in API v3.

Mapping of version-valued asset attribute names to the maximum incompatible version for clients that still expect schedule trigger requests to return ``HTTP status 200 (OK)`` and unfinished schedule requests to return ``HTTP status 400`` with a message about the scheduling job "waiting to be processed".
For each configured attribute, FlexMeasures checks the scheduled asset itself, its parent asset, and its grandparent asset.
If any of these attributes contains its configured maximum version or a lower version, the client receives the legacy schedule responses.
When empty, all clients receive the standard ``202 Accepted`` response for accepted trigger requests and unfinished schedule requests.

For example:

.. code-block:: python

FLEXMEASURES_LEGACY_SCHEDULEACCEPTED_STATUS_MAX_INCOMPATIBLE_CLIENT_VERSION = {
"v2g-liberty-version": "0.9.1",
}

Default: ``{}``


.. _reporters-config:

Reporters
Expand Down
3 changes: 2 additions & 1 deletion flexmeasures/api/common/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ def request_accepted_for_processing(
message: str = "Request has been accepted for processing.",
legacy_key: str | None = None,
job_results_url: str | None = None,
status_code: int = 202,
) -> ResponseTuple:
"""
Standard 202 response when a background job is accepted.
Expand Down Expand Up @@ -414,7 +415,7 @@ def request_accepted_for_processing(
# keep legacy key for backwards compatibility; not (yet) deprecated, see docstring
resp[legacy_key] = job_id

return resp, 202
return resp, status_code


def request_too_large(message: str) -> ResponseTuple:
Expand Down
53 changes: 53 additions & 0 deletions flexmeasures/api/common/utils/api_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

from collections.abc import Mapping
from copy import deepcopy
import json
import re
from packaging.version import InvalidVersion, Version
from timely_beliefs.beliefs.classes import BeliefsDataFrame
from timely_beliefs.sensors.func_store import knowledge_horizons
from typing import Sequence
Expand Down Expand Up @@ -62,6 +64,57 @@ def upsample_values(
return value_groups


def use_legacy_schedule_accepted_status(asset: GenericAsset) -> bool:
version_limits = current_app.config.get(
"FLEXMEASURES_LEGACY_SCHEDULEACCEPTED_STATUS_MAX_INCOMPATIBLE_CLIENT_VERSION",
{},
)
if not isinstance(version_limits, Mapping):
current_app.logger.warning(
"Invalid FLEXMEASURES_LEGACY_SCHEDULEACCEPTED_STATUS_MAX_INCOMPATIBLE_"
"CLIENT_VERSION %r: expected a mapping of asset attribute names to "
"maximum incompatible client versions. Ignoring compatibility setting.",
version_limits,
)
return False

for version_attribute, max_version in version_limits.items():
client_version, attribute_asset = _get_asset_attribute_from_nearby_hierarchy(
asset, version_attribute
)
if client_version is None or attribute_asset is None:
continue
try:
if Version(str(client_version)) <= Version(str(max_version)):
return True
except InvalidVersion:
current_app.logger.warning(
"Ignoring invalid schedule client version %r or maximum incompatible "
"version %r for attribute %r on asset %s.",
client_version,
max_version,
version_attribute,
attribute_asset.id,
)
return False


def _get_asset_attribute_from_nearby_hierarchy(
asset: GenericAsset, attribute: str, max_parent_depth: int = 2
) -> tuple[object | None, GenericAsset | None]:
current_asset = asset
for _ in range(max_parent_depth + 1):
# A null or empty value means the attribute is not set here, so keep looking up the hierarchy.
# Stopping on mere key presence would let such a value on a device shadow a version set on its site.
value = (current_asset.attributes or {}).get(attribute)
if value:
return value, current_asset
if current_asset.parent_asset is None:
break
current_asset = current_asset.parent_asset
return None, None


def unique_ever_seen(iterable: Sequence, selector: Sequence):
"""
Return unique iterable elements with corresponding lists of selector elements, preserving order.
Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
from flexmeasures.api.common.utils.api_utils import (
get_accessible_accounts,
copy_asset,
use_legacy_schedule_accepted_status,
)
from flexmeasures.api.common.responses import (
unprocessable_entity,
Expand Down Expand Up @@ -1794,6 +1795,7 @@ def trigger_schedule(
return request_accepted_for_processing(
job.id,
legacy_key="schedule",
status_code=200 if use_legacy_schedule_accepted_status(asset) else 202,
)

@route("/<id>/kpis", methods=["GET"])
Expand Down
39 changes: 23 additions & 16 deletions flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
from flexmeasures.api.common.schemas.sensors import SensorId # noqa F401
from flexmeasures.api.common.schemas.users import AccountIdField
from flexmeasures.api.common.rate_limiting import limit_triggers
from flexmeasures.api.common.utils.api_utils import process_sensor_data_ingestion
from flexmeasures.api.common.utils.api_utils import (
process_sensor_data_ingestion,
use_legacy_schedule_accepted_status,
)
from flexmeasures.data.services.utils import job_status_description
from flexmeasures.api.common.utils.deprecation_utils import (
_add_headers as add_deprecation_header,
Expand Down Expand Up @@ -1090,6 +1093,11 @@ def trigger_schedule(
job_results_url=url_for(
"SensorAPI:get_schedule", id=sensor.id, uuid=job.id
),
status_code=(
200
if use_legacy_schedule_accepted_status(sensor.generic_asset)
else 202
),
)

# mark endpoint as deprecated
Expand Down Expand Up @@ -1326,21 +1334,20 @@ def get_schedule( # noqa: C901
elif job.is_failed:
return unknown_schedule(job_status_description(job, scheduler_info_msg))
else:
if current_app.config.get("FLEXMEASURES_API_SUNSET_ACTIVE"):
job_status = job.get_status()
job_status_name = (
job_status.upper()
if isinstance(job_status, str)
else job_status.name
)
return (
dict(
status=job_status_name,
message=job_status_description(job, scheduler_info_msg),
),
202,
)
return unknown_schedule(job_status_description(job, scheduler_info_msg))
job_status = job.get_status()
job_status_name = (
job_status.upper() if isinstance(job_status, str) else job_status.name
)
response = dict(
status=job_status_name,
message=job_status_description(job, scheduler_info_msg),
)
if use_legacy_schedule_accepted_status(sensor.generic_asset):
return response, 400
return (
response,
202,
)
schedule_start = job.kwargs["start"]

data_source = get_data_source_for_job(job)
Expand Down
Loading
Loading