diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 080e6e6b21..637fd5d26f 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -12,7 +12,7 @@ v3.0-32 | July XX, 2026 - Extended ``GET /api/v3_0/jobs/`` with a ``result`` field containing ``unresolved`` and ``resolved`` arrays, each keyed by asset ID. For scheduling jobs, this surfaces soft state-of-charge constraint analysis: ``soc-minima`` and ``soc-maxima`` violations (with a ``violation`` magnitude) or satisfied constraints (with a ``margin`` headroom). Both arrays are empty when no SoC constraints were defined. - **Field canonicalization** for background job tracking: * The ``job`` field is now the canonical way to identify background jobs returned by `/sensors//schedules/trigger`, `/assets//schedules/trigger`, and `/sensors//forecasts/trigger` endpoints. If applicable, the triggered response now also returns a ``results-url`` pointing to the sensor-specific results endpoint, alongside the generic ``job-url``. - * Legacy ``schedule`` field (in scheduling endpoints) and ``forecast`` field (in forecasting endpoints) remain in responses, unchanged, for backward compatibility. New clients should prefer ``job``; see :ref:`api_background_jobs` for the full response format. These fields are not (yet) formally deprecated — see the "Planned API v4" discussion linked from :ref:`api_deprecation` for where and when their removal is being tracked. + * Legacy ``schedule`` field (in scheduling endpoints) and ``forecast`` field (in forecasting endpoints) remain in responses, unchanged, for backward compatibility. New clients should prefer ``job``; see :ref:`api_background_jobs` for the full response format and the "Planned API v4" discussion linked from :ref:`api_deprecation` for where and when their removal is being tracked. - ``GET /api/v3_0/jobs/`` now returns ``202 Accepted`` while a job is queued or running, ``422 Unprocessable Entity`` for failed jobs, and ``200 OK`` for finished jobs. See :ref:`api_background_jobs` for the response format and polling flow. - ``GET /api/v3_0/jobs/`` now also returns kebab-case metadata fields such as ``func-name`` and ``enqueued-at``, alongside the existing snake_case fields (``func_name``, ``enqueued_at``, etc.), which remain unchanged for backward compatibility. New clients should prefer the kebab-case fields. - Added a ``group`` field to the storage flex-model, accepted by the `/assets/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_ (POST) endpoint, referencing a power sensor representing a group of devices (e.g. a shared inverter or feeder). The group's ``power-capacity`` is enforced as a hard constraint on the group's aggregate power, while its ``consumption-capacity``/``production-capacity`` are enforced as soft constraints with default breach prices; the group's scheduled aggregate power is saved to the group sensor. diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index 760d902ee8..809599e7b1 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -295,9 +295,9 @@ Response field naming (``job``, ``schedule``, ``forecast``) """"""""""""""""""""""""""""""""""""""""""""""""""""""""""" Background-job trigger endpoints (e.g. ``POST /sensors//schedules/trigger``, ``POST /assets//schedules/trigger``, ``POST /sensors//forecasts/trigger``) and ``GET /api/v3_0/jobs/`` return a canonical ``job`` field (and, for job status, kebab-case metadata fields such as ``func-name``) alongside older field names (``schedule``, ``forecast``, ``func_name``, etc.) kept purely for backward compatibility. -These older field names are **not currently marked as deprecated** via the ``Deprecation``/``Sunset``/``Link`` headers described above: within API version ``v3_0`` they are additive and will keep working unchanged. +Within API version ``v3_0``, these older field names are additive aliases and will keep working unchanged. -New clients should prefer the canonical fields. Any eventual removal of the older field names is planned to happen only as part of a new, whole API version (following the same versioned deprecation/sunset flow described in :ref:`api_deprecation_hosts`), not as a field-by-field change within ``v3_0``. See the `API v4 planning discussion `_ for the current state of that plan. +New clients should prefer the canonical fields. Any eventual removal of the older field names is planned to happen only as part of a new, whole API version, following the same versioned deprecation/sunset flow described in :ref:`api_deprecation_hosts`. See the `API v4 planning discussion `_ for the current state of that plan. .. _api_deprecation_hosts: @@ -311,6 +311,7 @@ FlexMeasures versions go through the following stages for deprecating major feat - :ref:`api_deprecation_stage_3`: status 410 (Gone) Let's go over these stages in more detail. +Hosts can customize the dates and information links used in deprecation and sunset headers with ``FLEXMEASURES_DEPRECATION_AND_SUNSET`` (see :ref:`Sunset Configuration`). .. _api_deprecation_stage_1: diff --git a/documentation/changelog.rst b/documentation/changelog.rst index b3af30cec6..37e5777706 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -8,7 +8,7 @@ FlexMeasures Changelog v1.0.0 | July XX, 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`` remain available for backward compatibility; clients should migrate to ``job`` (see the Infrastructure / Support section below for migration details). .. warning:: Upgrading to this version requires running ``flexmeasures db upgrade`` (you can create a backup first with ``flexmeasures db-ops dump``). @@ -50,7 +50,8 @@ Infrastructure / Support * Upgraded dependencies [see `PR #1485 `_, `PR #2215 `_, `PR #2243 `_ and `PR #2348 `_] * Speed up post-processing of sensor data searches: latest-version filtering, deterministic-belief selection per event and chart-data serialization are now vectorized (up to three orders of magnitude faster on large search results) [see `PR #2328 `_] * Prepare the ``device_scheduler`` to deal with commitments per device group [see `PR #1934 `_] -* 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 `_]. +* 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, and new clients should prefer ``job`` [see `PR #2224 `_]. +* Add ``FLEXMEASURES_DEPRECATION_AND_SUNSET`` so hosts can configure deprecation and sunset dates and information links per deprecated API version [see `PR #2362 `_]. * Speed up scheduling on longer horizons using a recursive stock model, making the solve time linear with the horizon instead of quadratic (about 10x faster solves at the 2-day default horizon, 23x at 3 days) [see `PR #2282 `_] * 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 `_] * Documentation section on the modelling choice for recording measurements, forecasts and schedules under one or multiple sensors [see `PR #2217 `_] diff --git a/documentation/configuration.rst b/documentation/configuration.rst index bfa87ccc6e..ac75fc0995 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -40,43 +40,6 @@ This is used to turn on certain extra behaviours, see :ref:`modes-dev` for detai Default: ``""`` -.. _overwrite-config: - -FLEXMEASURES_ALLOW_DATA_OVERWRITE -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Whether to allow overwriting existing data when saving data to the database. - -Default: ``False`` - - -.. _solver-config: - -FLEXMEASURES_LP_SOLVER -^^^^^^^^^^^^^^^^^^^^^^ - -The command to run the scheduling solver. This is the executable command which FlexMeasures calls via the `pyomo library `_. Potential values might be ``cbc``, ``cplex``, ``glpk`` or ``appsi_highs``. Consult `their documentation `_ to learn more. -We have tested FlexMeasures with `HiGHS `_ and `Cbc `_. -Note that you need to install the solver, read more at :ref:`installing-a-solver`. - -Default: ``"appsi_highs"`` - - -FLEXMEASURES_LP_SOLVER_OPTIONS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Solver options passed to the scheduling solver, overriding the defaults FlexMeasures sets itself. Use this to tune the solver without patching code, for example to trade optimality for speed:: - - FLEXMEASURES_LP_SOLVER_OPTIONS = {"mip_rel_gap": "1e-4"} - -When the solver is HiGHS, FlexMeasures validates these against the installed HiGHS build and raises on an unknown option name, an invalid value, or a feature the build lacks. This matters because Pyomo's ``appsi_highs`` interface otherwise applies solver options without checking whether HiGHS accepted them, so a typo would be silently ignored. - -.. note:: HiGHS initializes its thread scheduler once per process. Setting ``threads`` or ``parallel`` therefore only affects the first solve in a worker process; later solves fail with ``global scheduler has already been initialized`` and return no schedule. FlexMeasures logs a warning if you set either. - -Default: ``{}`` - - - FLEXMEASURES_HOSTS_AND_AUTH_START ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -105,44 +68,30 @@ Default: ``[]`` .. note:: This setting is also recognized as environment variable (since v0.14, which is also the version required to pass this setting as a string). +UI +-- +.. _mapbox_access_token: -FLEXMEASURES_PROFILE_REQUESTS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If True, the processing time of requests are profiled. - -The overall time used by requests are logged to the console. In addition, if `pyinstrument` is installed, then a profiling report is made (of time being spent in different function calls) for all Flask API endpoints. - -The profiling results are stored in the ``profile_reports`` folder in the instance directory. - -Note: Profile reports for API endpoints are overwritten on repetition of the same request. - -Interesting for developers. - -Default: ``False`` +MAPBOX_ACCESS_TOKEN +^^^^^^^^^^^^^^^^^^^ +Token for accessing the MapBox API (for displaying maps on the dashboard and asset pages). You can learn how to obtain one `here `_ -FLEXMEASURES_PROFILER_CONFIG -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Default: ``None`` -Keyword arguments passed to the profiler, such as the sampling interval (in seconds) for profiling the processing time of requests. +.. note:: This setting is also recognized as environment variable. -Interesting for developers. -Default: +.. _bounding_box_config: -.. code-block:: python +FLEXMEASURES_DEFAULT_BOUNDING_BOX +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - dict( - async_mode="disabled", - interval=0.01, # 10 ms sampling interval, enables coarse timer - use_timing_thread=True, - ) +The default bounding box of maps if the user has no geolocated assets yet. +Default: ``(54, 2), (50.732, 7.808)`` (`The Netherlands after the oceans drop 50 meters `_) -UI --- FLEXMEASURES_PLATFORM_NAME ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -356,13 +305,6 @@ Example: ``{"forecasting": "PT2M", "scheduling": "PT5M", "ingestion": "PT30S"}`` Default: ``{}`` -FLEXMEASURES_PLANNING_TTL -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Time to live for schedule UUIDs of successful scheduling jobs. Set a negative timedelta to persist forever. - -Default: ``timedelta(days=7)`` - FLEXMEASURES_JOB_CACHE_TTL ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -380,6 +322,20 @@ Set a negative value to persist forever. Default: ``3600`` + +Data +---- + +.. _overwrite-config: + +FLEXMEASURES_ALLOW_DATA_OVERWRITE +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Whether to allow overwriting existing data when saving data to the database. + +Default: ``False`` + + FLEXMEASURES_MAX_SENSOR_DATA_INGESTION_BYTES ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -391,21 +347,48 @@ Default: ``3 * 1024 * 1024`` .. _datasource_config: FLEXMEASURES_DEFAULT_DATASOURCE -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The default DataSource of the resulting data from `DataGeneration` classes. Default: ``"FlexMeasures"`` -.. _bounding_box_config: +Scheduling +---------- -FLEXMEASURES_DEFAULT_BOUNDING_BOX -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _solver-config: -The default bounding box of maps if the user has no geolocated assets yet. +FLEXMEASURES_LP_SOLVER +^^^^^^^^^^^^^^^^^^^^^^ -Default: ``(54, 2), (50.732, 7.808)`` (`The Netherlands after the oceans drop 50 meters `_) +The command to run the scheduling solver. This is the executable command which FlexMeasures calls via the `pyomo library `_. Potential values might be ``cbc``, ``cplex``, ``glpk`` or ``appsi_highs``. Consult `their documentation `_ to learn more. +We have tested FlexMeasures with `HiGHS `_ and `Cbc `_. +Note that you need to install the solver, read more at :ref:`installing-a-solver`. + +Default: ``"appsi_highs"`` + + +FLEXMEASURES_LP_SOLVER_OPTIONS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Solver options passed to the scheduling solver, overriding the defaults FlexMeasures sets itself. Use this to tune the solver without patching code, for example to trade optimality for speed:: + + FLEXMEASURES_LP_SOLVER_OPTIONS = {"mip_rel_gap": "1e-4"} + +When the solver is HiGHS, FlexMeasures validates these against the installed HiGHS build and raises on an unknown option name, an invalid value, or a feature the build lacks. This matters because Pyomo's ``appsi_highs`` interface otherwise applies solver options without checking whether HiGHS accepted them, so a typo would be silently ignored. + +.. note:: HiGHS initializes its thread scheduler once per process. Setting ``threads`` or ``parallel`` therefore only affects the first solve in a worker process; later solves fail with ``global scheduler has already been initialized`` and return no schedule. FlexMeasures logs a warning if you set either. + +Default: ``{}`` + + +FLEXMEASURES_PLANNING_TTL +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Time to live for schedule UUIDs of successful scheduling jobs. Set a negative timedelta to persist forever. + +Default: ``timedelta(days=7)`` .. _planning_horizon_config: @@ -430,28 +413,19 @@ Set to ``None`` to forgo this limitation altoghether. Default: ``2520`` (e.g. 7 days for a 4-minute resolution sensor, 105 days for a 1-hour resolution sensor) -Access Tokens ---------------- - -.. _mapbox_access_token: - -MAPBOX_ACCESS_TOKEN -^^^^^^^^^^^^^^^^^^^ - -Token for accessing the MapBox API (for displaying maps on the dashboard and asset pages). You can learn how to obtain one `here `_ - -Default: ``None`` +.. _fallback-redirect-config: -.. note:: This setting is also recognized as environment variable. +FLEXMEASURES_FALLBACK_REDIRECT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. _sentry_access_token: +Control how the API handles a failed scheduling job when a fallback schedule has been computed. -SENTRY_SDN -^^^^^^^^^^^^ +If ``True``, the API returns ``HTTP status 303 (See Other)`` with a ``Location`` header pointing to the fallback schedule endpoint. +Clients must follow this redirect themselves to obtain the fallback schedule (see :ref:`api_see_other`). -Deprecated misspelling of ``SENTRY_DSN`` (see below). Only the environment variable is still accepted as a fallback for backward compatibility; config files should use ``SENTRY_DSN``. +If ``False``, the API transparently follows the fallback job and returns the fallback schedule directly in the response. -Default: ``None`` +Default: ``False`` SQLAlchemy @@ -815,10 +789,6 @@ E-mail addresses to send monitoring alerts to from the CLI tasks ``flexmeasures Default: ``[]`` -.. deprecated:: 0.33 - - ``FLEXMEASURES_MONITORING_MAIL_RECIPIENTS`` is deprecated. Use ``FLEXMEASURES_DEFAULT_MONITORING_MAIL_RECIPIENTS`` instead. - .. _redis-config: @@ -891,46 +861,57 @@ Default: ``True`` .. _sunset-config: -Sunset ------- - -FLEXMEASURES_API_SUNSET_ACTIVE -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +API Deprecation and Sunset +-------------------------- -Allow control over the effect of sunsetting API versions. -Specifically, if True, the endpoints of sunset API versions will return ``HTTP status 410 (Gone)`` status codes. -If False, these endpoints will either return ``HTTP status 410 (Gone) status codes``, or work like before (including Deprecation and Sunset headers in their response), depending on whether the installed FlexMeasures version still contains the endpoint implementations. +FLEXMEASURES_DEPRECATION_AND_SUNSET +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Default: ``False`` +Allow hosts to override the built-in deprecation and sunset metadata for API +versions. -FLEXMEASURES_API_SUNSET_DATE -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Use one entry per API version. The supported keys are ``deprecation-date``, ``deprecation-link``, ``sunset-date`` and ``sunset-link``. +Dates may be strings of a format like ``"2026-08-01"``. -Allow to override the default sunset date for your clients. +.. code-block:: python -Default: ``None`` (defaults are set internally for each sunset API version, e.g. ``"2023-05-01"`` for v2.0) + FLEXMEASURES_DEPRECATION_AND_SUNSET = { + "api-v2_0": { + "deprecation-date": "2026-08-01", + "deprecation-link": "https://example.com/api/v2-deprecation", + "sunset-date": "2026-11-01", + "sunset-link": "https://example.com/api/v2-sunset", + }, + } -FLEXMEASURES_API_SUNSET_LINK -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The currently known API-version keys are: -Allow to override the default sunset link for your clients. +* ``api-v1`` +* ``api-v1_1`` +* ``api-v1_2`` +* ``api-v1_3`` +* ``api-v2_0`` -Default: ``None`` (defaults are set internally for each sunset API version, e.g. ``"https://flexmeasures.readthedocs.io/en/v0.13.0/api/v2_0.html"`` for v2.0) +Default: ``{}`` (built-in metadata is used) -.. _fallback-redirect-config: +The built-in API-version deprecation metadata is defined in +``flexmeasures/api/sunset/__init__.py`` as ``SUNSET_INFO``. Hosts can use those +entries as the starting point for their own ``FLEXMEASURES_DEPRECATION_AND_SUNSET`` +overrides. -FLEXMEASURES_FALLBACK_REDIRECT -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. note:: Deprecated fields are documented as legacy aliases and keep working silently until they can be removed in a future API version. -Control how the API handles a failed scheduling job when a fallback schedule has been computed. -If ``True``, the API returns ``HTTP status 303 (See Other)`` with a ``Location`` header pointing to the fallback schedule endpoint. -Clients must follow this redirect themselves to obtain the fallback schedule (see :ref:`api_see_other`). +FLEXMEASURES_API_SUNSET_ACTIVE +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If ``False``, the API transparently follows the fallback job and returns the fallback schedule directly in the response. +Allow control over the effect of sunsetting API versions. +Specifically, if True, the endpoints of sunset API versions will return ``HTTP status 410 (Gone)`` status codes. +If False, these endpoints will either return ``HTTP status 410 (Gone) status codes``, or work like before (including Deprecation and Sunset headers in their response), depending on whether the installed FlexMeasures version still contains the endpoint implementations. Default: ``False`` + .. _reporters-config: Reporters @@ -948,3 +929,92 @@ Extend this list if you want to permit additional pseudo-methods in reporter pip .. note:: Only add trusted pseudo-methods here. Since these methods bypass Python signature validation, loosening this list unnecessarily can reduce safety guarantees in your data processing pipeline. Default: ``["get_attribute"]`` + + +Development +----------- + +FLEXMEASURES_PROFILE_REQUESTS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If True, the processing time of requests are profiled. + +The overall time used by requests are logged to the console. In addition, if `pyinstrument` is installed, then a profiling report is made (of time being spent in different function calls) for all Flask API endpoints. + +The profiling results are stored in the ``profile_reports`` folder in the instance directory. + +Note: Profile reports for API endpoints are overwritten on repetition of the same request. + +Interesting for developers. + +Default: ``False`` + + +FLEXMEASURES_PROFILER_CONFIG +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Keyword arguments passed to the profiler, such as the sampling interval (in seconds) for profiling the processing time of requests. + +Interesting for developers. + +Default: + +.. code-block:: python + + dict( + async_mode="disabled", + interval=0.01, # 10 ms sampling interval, enables coarse timer + use_timing_thread=True, + ) + + +Old settings +------------ + +These settings are still accepted as fallbacks for backward compatibility, but +new host configuration should use the replacement settings mentioned below. + +.. _sentry_access_token: + +SENTRY_SDN +^^^^^^^^^^ + +Deprecated misspelling of ``SENTRY_DSN``. Only the environment variable is still +accepted as a fallback; config files should use ``SENTRY_DSN``. + +Default: ``None`` + +FLEXMEASURES_MONITORING_MAIL_RECIPIENTS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 0.33 + + Use ``FLEXMEASURES_DEFAULT_MONITORING_MAIL_RECIPIENTS`` instead. + +Default: ``[]`` + +FLEXMEASURES_API_SUNSET_DATE +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 1.0.0 + + Use ``FLEXMEASURES_DEPRECATION_AND_SUNSET`` instead. + +Overrides the default sunset date across all sunset API versions if +``FLEXMEASURES_DEPRECATION_AND_SUNSET`` does not define a ``sunset-date`` for the +API version. + +Default: ``None`` + +FLEXMEASURES_API_SUNSET_LINK +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 1.0.0 + + Use ``FLEXMEASURES_DEPRECATION_AND_SUNSET`` instead. + +Overrides the default sunset link across all sunset API versions if +``FLEXMEASURES_DEPRECATION_AND_SUNSET`` does not define a ``sunset-link`` for the +API version. + +Default: ``None`` diff --git a/flexmeasures/api/common/utils/deprecation_utils.py b/flexmeasures/api/common/utils/deprecation_utils.py index 104169ec04..43b367277a 100644 --- a/flexmeasures/api/common/utils/deprecation_utils.py +++ b/flexmeasures/api/common/utils/deprecation_utils.py @@ -36,8 +36,12 @@ def return_410_unless_host_rolls_back_sunrise(): # so we let the request pass to the endpoint implementation pass else: - # Override with custom info link, if set by host - link = override_from_config(sunset_link, "FLEXMEASURES_API_SUNSET_LINK") + link = _api_version_deprecation_setting( + api_version_being_sunset, + "sunset-link", + sunset_link, + "FLEXMEASURES_API_SUNSET_LINK", + ) abort( 410, @@ -127,6 +131,7 @@ def _after_request_handler(response: Response) -> Response: def deprecate_blueprint( blueprint: Blueprint, + api_version_being_sunset: str, deprecation_date: pd.Timestamp | str | None = None, deprecation_link: str | None = None, sunset_date: pd.Timestamp | str | None = None, @@ -142,6 +147,7 @@ def deprecate_blueprint( >>> deprecated_bp = Blueprint('API version 1', 'v1_bp') >>> deprecate_blueprint( ... deprecated_bp, + ... api_version_being_sunset="1.0", ... deprecation_date="2022-12-14", ... deprecation_link="https://flexmeasures.readthedocs.io/some-deprecation-notice", ... sunset_date="2023-02-01", @@ -150,6 +156,7 @@ def deprecate_blueprint( >>> app.register_blueprint(deprecated_bp, url_prefix='/v1') :param blueprint: The blueprint to be deprecated + :param api_version_being_sunset: The API version used to look up host-specific deprecation metadata :param deprecation_date: date indicating when the API endpoint was deprecated, used for the "Deprecation" header if no date is given, defaults to "true" see https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-deprecation-header#section-2-1 @@ -162,26 +169,40 @@ def deprecate_blueprint( - Deprecation header: https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-deprecation-header - Sunset header: https://www.rfc-editor.org/rfc/rfc8594 """ - deprecation = _format_deprecation(deprecation_date) - sunset = _format_sunset(sunset_date) def _after_request_handler(response: Response) -> Response: current_app.logger.warning( f"Deprecated endpoint {request.endpoint} called by {current_user}" ) - # Override sunset date if host used corresponding config setting - _sunset = override_from_config(sunset, "FLEXMEASURES_API_SUNSET_DATE") - - # Override sunset link if host used corresponding config setting - _sunset_link = override_from_config(sunset_link, "FLEXMEASURES_API_SUNSET_LINK") - return _add_headers( response, - deprecation, - deprecation_link, - _sunset, - _sunset_link, + _format_deprecation( + _api_version_deprecation_setting( + api_version_being_sunset, + "deprecation-date", + deprecation_date, + ) + ), + _api_version_deprecation_setting( + api_version_being_sunset, + "deprecation-link", + deprecation_link, + ), + _format_sunset( + _api_version_deprecation_setting( + api_version_being_sunset, + "sunset-date", + sunset_date, + "FLEXMEASURES_API_SUNSET_DATE", + ) + ), + _api_version_deprecation_setting( + api_version_being_sunset, + "sunset-link", + sunset_link, + "FLEXMEASURES_API_SUNSET_LINK", + ), ) blueprint.after_request(_after_request_handler) @@ -226,6 +247,24 @@ def _format_sunset(sunset_date): return sunset +def _api_version_deprecation_setting( + api_version: str, + setting_name: str, + default: Any, + legacy_config_setting_name: str | None = None, +) -> Any: + """Return host-configured API-version deprecation metadata, if available.""" + config = current_app.config.get("FLEXMEASURES_DEPRECATION_AND_SUNSET", {}) or {} + api_version_key = f"api-v{api_version.replace('.', '_')}" + api_version_config = config.get(api_version_key, config.get(api_version, {})) + + if setting_name in api_version_config: + return api_version_config[setting_name] + if legacy_config_setting_name is not None: + return override_from_config(default, legacy_config_setting_name) + return default + + def override_from_config(setting: Any, config_setting_name: str) -> Any: """Override setting by config setting, unless the latter is None or is missing.""" config_setting = current_app.config.get(config_setting_name) diff --git a/flexmeasures/api/sunset/tests/test_sunset.py b/flexmeasures/api/sunset/tests/test_sunset.py index 9d87264f65..c916b5dce6 100644 --- a/flexmeasures/api/sunset/tests/test_sunset.py +++ b/flexmeasures/api/sunset/tests/test_sunset.py @@ -20,3 +20,32 @@ def test_sunset(client, blueprint, api_version_being_sunset): f"API version {api_version_being_sunset} has been sunset" in gone.json["message"] ) + + +def test_sunset_uses_api_version_deprecation_config(app, client): + original_config = app.config["FLEXMEASURES_DEPRECATION_AND_SUNSET"] + app.config["FLEXMEASURES_DEPRECATION_AND_SUNSET"] = { + "api-v2_0": { + "deprecation-date": "2026-08-01", + "deprecation-link": "https://example.com/api/v2-deprecation", + "sunset-date": "2026-11-01", + "sunset-link": "https://example.com/api/v2-sunset", + }, + } + try: + gone = client.get(url_for("flexmeasures_api_v2_0.implementation_gone")) + finally: + app.config["FLEXMEASURES_DEPRECATION_AND_SUNSET"] = original_config + + assert gone.status_code == 410 + assert "https://example.com/api/v2-sunset" in gone.json["message"] + assert gone.headers["Deprecation"] == "Fri, 31 Jul 2026 23:59:59 GMT" + assert gone.headers["Sunset"] == "Sat, 31 Oct 2026 23:59:59 GMT" + links = gone.headers.getlist("Link") + assert ( + '; rel="deprecation"; type="text/html"' + in links + ) + assert ( + '; rel="sunset"; type="text/html"' in links + ) diff --git a/flexmeasures/api/v3_0/tests/test_jobs_api.py b/flexmeasures/api/v3_0/tests/test_jobs_api.py index 13d07d8b5e..abffbeb542 100644 --- a/flexmeasures/api/v3_0/tests/test_jobs_api.py +++ b/flexmeasures/api/v3_0/tests/test_jobs_api.py @@ -21,7 +21,7 @@ from flexmeasures.utils.job_utils import work_on_rq -JOB_STATUS_DEPRECATED_FIELDS = { +JOB_STATUS_LEGACY_FIELD_ALIASES = { "func_name": "func-name", "enqueued_at": "enqueued-at", "started_at": "started-at", @@ -31,9 +31,8 @@ def assert_legacy_job_status_fields(data: dict): - for legacy_key, canonical_key in JOB_STATUS_DEPRECATED_FIELDS.items(): + for legacy_key, canonical_key in JOB_STATUS_LEGACY_FIELD_ALIASES.items(): assert data[legacy_key] == data[canonical_key] - assert "deprecated-fields" not in data @pytest.mark.parametrize( diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 1efeebb07f..e71ea09cae 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -6832,4 +6832,4 @@ } } } -} \ No newline at end of file +} diff --git a/flexmeasures/utils/config_defaults.py b/flexmeasures/utils/config_defaults.py index 114b26d2b8..44e171859e 100644 --- a/flexmeasures/utils/config_defaults.py +++ b/flexmeasures/utils/config_defaults.py @@ -205,6 +205,7 @@ class Config(object): FLEXMEASURES_API_SUNSET_LINK: str | None = ( None # e.g. https://flexmeasures.readthedocs.io/en/latest/api/introduction.html#deprecation-and-sunset ) + FLEXMEASURES_DEPRECATION_AND_SUNSET: dict = dict() # if True, all requests are forced to be via HTTPS. FLEXMEASURES_FORCE_HTTPS: bool = False