From bf18c4dad4e603fe48b28a92bb98d64f714301a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Fri, 5 Jun 2026 14:54:18 +0200 Subject: [PATCH 1/8] use the 202 status consistently in triggering endpoints (forecasting, scheduling) and provide follow-up URLs in the response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/api/change_log.rst | 9 +- documentation/api/introduction.rst | 117 +++++++++++++++++ documentation/changelog.rst | 5 + documentation/tut/toy-example-expanded.rst | 4 + .../tut/toy-example-from-scratch.rst | 14 +++ flexmeasures/api/common/responses.py | 46 +++++-- flexmeasures/api/v3_0/assets.py | 31 +++-- flexmeasures/api/v3_0/sensors.py | 106 +++++++++++++--- .../tests/test_asset_schedules_fresh_db.py | 2 +- .../tests/test_forecasting_api_fresh_db.py | 5 +- flexmeasures/api/v3_0/tests/test_jobs_api.py | 10 +- .../api/v3_0/tests/test_sensor_schedules.py | 12 +- .../tests/test_sensor_schedules_fresh_db.py | 19 +-- flexmeasures/ui/static/openapi-specs.json | 119 +++++++++++++++--- 14 files changed, 425 insertions(+), 74 deletions(-) diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 8728d753cb..9400eba56a 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -5,9 +5,16 @@ API change log .. note:: The FlexMeasures API follows its own versioning scheme. This is also reflected in the URL (e.g. `/api/v3_0`), allowing developers to upgrade at their own pace. -v3.0-31 | 2026-06-01 +v3.0-32 | 2026-06-05 """""""""""""""""""" +- **Field canonicalization** for background job tracking: + + * The ``job_id`` 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 ``job_results_url`` pointing to the sensor-specific results endpoint, alongside the generic ``job_monitor_url``. + * Legacy `schedule` field (in scheduling endpoints) and `forecast` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. All responses include a ``_deprecated_fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and removal date. + +v3.0-31 | 2026-06-01 +"""""""""""""""""""" - Added a unified job status endpoint ``GET /api/v3_0/jobs/`` that returns the current execution status and a human-readable result message for any background job (scheduling, forecasting, etc.) identified by its UUID. - Switched from ``force_new_job_creation`` to ``force-new-job-creation`` (maintaining backwards compatibility) and added the field to `/assets/(id)/schedules/trigger` (POST) endpoint, too. - Support both snake_case and kebab-case fields in `/sensors//data` (GET), while only documenting the kebab-case ones. diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index caa047d716..c59fe65055 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -155,6 +155,91 @@ Here is a client-side code example in Python for handling 303 redirects (this me print(f"Failed to fetch fallback schedule: {response.status_code} {response.text}") return response +.. _api_background_jobs: + +Background job monitoring +-------------------------- + +Several API endpoints queue background jobs for asynchronous processing (scheduling, forecasting, data ingestion) and return a ``202 Accepted`` response. +These responses include a ``job_id`` field (the canonical identifier) that clients can use to monitor job progress and retrieve results. +They also include both ``job_monitor_url`` for generic status monitoring and (if applicable) ``job_results_url`` for the sensor-specific results endpoint. + +**Example 202 Accepted response from a scheduling endpoint:** + +.. code-block:: json + + { + "status": "ACCEPTED", + "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "message": "Request has been accepted for processing." + } + +**Monitoring job status:** + +Clients should use the ``job_id`` to query the unified job status endpoint: + +.. code-block:: bash + + GET /api/v3_0/jobs/ + +This returns the current execution status and a human-readable result message. For example: + +.. code-block:: python + + import requests + import time + + def wait_for_job(job_id, job_monitor_url, timeout=300, poll_interval=5): + """Wait for a background job to complete and return the result. + + Parameters + ---------- + job_id : str + The UUID of the background job. + job_monitor_url : str + The URL to query for job status (e.g., "/api/v3_0/jobs/"). + timeout : int + Maximum seconds to wait for job completion. + poll_interval : int + Seconds between status checks. + """ + start_time = time.time() + while time.time() - start_time < timeout: + response = requests.get(job_monitor_url) + if not response.ok: + raise RuntimeError(f"Failed to query job status: {response.status_code} {response.text}") + + job_data = response.json() + status = job_data.get("status") + + if status in ("PENDING", "RUNNING"): + print(f"Job {job_id} is still {status.lower()}...") + time.sleep(poll_interval) + elif status == "SUCCEEDED": + return job_data.get("result") + else: # Failed, error, etc. + raise RuntimeError(f"Job failed with status {status}: {job_data.get('message')}") + + raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds") + +.. note:: + + For **schedules**, after the job completes successfully, use the job ID (same value as the legacy ``schedule`` field) to retrieve the actual schedule or follow the returned ``job_results_url``: + + .. code-block:: bash + + GET /api/v3_0/sensors//schedules/ + + For **forecasts**, after the job completes successfully, use the job ID to retrieve the forecast or follow the returned ``job_results_url``: + + .. code-block:: bash + + GET /api/v3_0/sensors//forecasts/ + + Both of these endpoints will also return `202 Accepted` if the job is still being computed, so clients can continue to poll them directly if they prefer. + .. _api_deprecation: Deprecation and sunset @@ -166,6 +251,38 @@ For more information on our multi-stage deprecation approach and available optio .. _api_deprecation_clients: +Deprecated response fields +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to deprecating entire endpoints, we sometimes deprecate individual fields in API responses while maintaining backward compatibility by including both the legacy and canonical fields. +When this happens, responses include a ``_deprecated_fields`` object containing machine-readable information about deprecated fields. + +For example, when scheduling endpoints switched from ``schedule`` to ``job_id`` as the canonical field identifier for background jobs: + +.. code-block:: json + + { + "status": "ACCEPTED", + "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "_deprecated_fields": { + "schedule": { + "use": "job_id", + "deprecated_since": "1.0.0", + "removal_date": "2.0.0", + "note": "The 'schedule' response field is deprecated; use 'job_id' instead" + } + } + } + +Clients should: + +- Use the ``_deprecated_fields`` object to identify which fields are deprecated in their version. +- Migrate to use the canonical field names (indicated by the ``"use"`` field). +- Plan upgrades before the ``"removal_date"`` to avoid breakage when the deprecated fields are removed in a future API version. + Clients ^^^^^^^ diff --git a/documentation/changelog.rst b/documentation/changelog.rst index c2d79a3685..c8fea69e67 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -7,6 +7,9 @@ FlexMeasures Changelog v1.0.0 | July XX, 2026 ============================ +.. warning:: As of this release we standardize asynchronous job responses to use the `job_id` field and return HTTP `202 Accepted` when a background job is queued. Legacy response fields such as ``schedule`` and ``forecast`` will be deprecated; clients should migrate to `job_id` (see the Infrastructure / Support section below for migration details). + + New features ------------- * Floor off-clock API datetimes to a non-instantaneous sensor's resolution by default when ingesting sensor data, uploading sensor data, and handling scheduler flex-model timed events; configurable with the ``floor_datetimes_to_resolution`` sensor attribute [see `PR #2146 `_] @@ -15,6 +18,8 @@ New features Infrastructure / Support ---------------------- +* Standardize job-trigger API responses to return `202 Accepted` and a canonical `job_id` field; legacy response fields such as ``schedule`` and ``forecast`` are preserved for backward-compatibility but marked deprecated with a migration guide in the API docs (see issue #2171). + * Upgraded dependencies [see `PR #1485 `_ and `PR #2215 `_] * Prepare the ``device_scheduler`` to deal with commitments per device group [see `PR #1934 `_] * Documentation section on the modelling choice for recording measurements, forecasts and schedules under one or multiple sensors [see `PR #2217 `_] diff --git a/documentation/tut/toy-example-expanded.rst b/documentation/tut/toy-example-expanded.rst index 3922ad9a3e..9ccd602ae5 100644 --- a/documentation/tut/toy-example-expanded.rst +++ b/documentation/tut/toy-example-expanded.rst @@ -119,6 +119,10 @@ This will have an effect on the available headroom for the battery, given the `` } } + .. note:: + The API returns a **202 Accepted** response containing a ``job_id`` field. + Use this UUID to monitor the job status via :ref:`api_background_jobs`. + .. tab:: FlexMeasures Client Using the `FlexMeasures Client `_: diff --git a/documentation/tut/toy-example-from-scratch.rst b/documentation/tut/toy-example-from-scratch.rst index e4a9d40d04..94f1a9b0fd 100644 --- a/documentation/tut/toy-example-from-scratch.rst +++ b/documentation/tut/toy-example-from-scratch.rst @@ -90,6 +90,20 @@ There is more information being used by the scheduler, such as the battery's cap } .. note:: You can try this right in Swagger UI, too! You should find it at `http://localhost:5000/api/v3_0/docs `_ after starting FlexMeasures locally. + + The API returns a **202 Accepted** response with a ``job_id`` field to track the scheduling job: + + .. code-block:: json + + { + "status": "ACCEPTED", + "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_results_url": "/api/v3_0/sensors/2/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" + } + + **Important:** Use the ``job_id`` field to reference the scheduling job. You can also follow the returned ``job_results_url`` to fetch the schedule results once they are ready. For more details, see :ref:`api_background_jobs`. .. tab:: FlexMeasures Client diff --git a/flexmeasures/api/common/responses.py b/flexmeasures/api/common/responses.py index 5b161ffa27..3a9294a61a 100644 --- a/flexmeasures/api/common/responses.py +++ b/flexmeasures/api/common/responses.py @@ -6,6 +6,7 @@ from flask import url_for from flexmeasures.auth.error_handling import FORBIDDEN_MSG, FORBIDDEN_STATUS_CODE +from flexmeasures.utils.time_utils import server_now p = inflect.engine() @@ -383,16 +384,45 @@ def request_processed(message: str) -> ResponseTuple: def request_accepted_for_processing( job_id: str, message: str = "Request has been accepted for processing.", + legacy_key: str | None = None, + deprecated_since: str | None = None, + removal_date: str | None = None, + job_results_url: str | None = None, ) -> ResponseTuple: - return ( - dict( - status="ACCEPTED", - message=message, - job_monitor_url=url_for("JobAPI:get_job_status", uuid=job_id), - job_id=job_id, - ), - 202, + """ + Standard 202 response when a background job is accepted. + + Optional backwards-compatibility: if `legacy_key` is provided the response + will include the job id under that legacy key (e.g. `schedule` or + `forecast`). The response will also include a `_deprecated_fields` object + with guidance for migrating to `job_id`. + + Optional `job_results_url` may be supplied to provide a direct link to the + sensor-specific job results endpoint, e.g. `/api/v3_0/sensors//schedules/`. + """ + resp: dict[str, object] = dict( + status="ACCEPTED", + message=message, + job_monitor_url=url_for("JobAPI:get_job_status", uuid=job_id), + job_id=job_id, ) + if job_results_url: + resp["job_results_url"] = job_results_url + + if legacy_key: + # keep legacy key for backwards compatibility + resp[legacy_key] = job_id + # include machine-readable deprecation info so clients can detect it + resp["_deprecated_fields"] = { + legacy_key: dict( + use="job_id", + deprecated_since=deprecated_since or server_now().date().isoformat(), + removal_date=removal_date, + note=(f"The '{legacy_key}' response field is deprecated; use 'job_id'"), + ) + } + + return resp, 202 def request_too_large(message: str) -> ResponseTuple: diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index 409abce35a..cabfe21f7a 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -66,7 +66,7 @@ ) from flexmeasures.api.common.responses import ( unprocessable_entity, - request_processed, + request_accepted_for_processing, ) from flexmeasures.api.common.schemas.users import AccountIdField from flexmeasures.api.common.schemas.assets import default_response_fields @@ -1472,8 +1472,8 @@ def trigger_schedule( site-consumption-capacity: {sensor: 32} responses: - 200: - description: PROCESSED + 202: + description: ACCEPTED (Scheduling job queued for processing) content: application/json: schema: @@ -1481,14 +1481,25 @@ def trigger_schedule( examples: successful_response: description: | - This message indicates that the scheduling request has been processed without any error. + This message indicates that the scheduling request has been accepted for processing (202 Accepted). A scheduling job has been created with some Universally Unique Identifier (UUID), which will be picked up by a worker. - The given UUID may be used to obtain the resulting schedule for each flexible device: see [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_). + The given UUID is returned in the canonical `job_id` field. + For backward-compatibility, the legacy `schedule` field is also included but is deprecated; + use `job_id` instead. The `_deprecated_fields` object provides migration guidance. + The given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_). value: - status: PROCESSED + status: ACCEPTED + job_id: "364bfd06-c1fa-430b-8d25-8f5a547651fb" schedule: "364bfd06-c1fa-430b-8d25-8f5a547651fb" - message: "Request has been processed." + job_monitor_url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" + message: "Request has been accepted for processing." + _deprecated_fields: + schedule: + use: "job_id" + deprecated_since: "1.0.0" + removal_date: "2.0.0" + note: "The 'schedule' response field is deprecated; use 'job_id'" 400: description: INVALID_DATA 401: @@ -1529,9 +1540,9 @@ def trigger_schedule( except ValueError as err: return unprocessable_entity(str(err)) - response = dict(schedule=job.id) - d, s = request_processed() - return dict(**response, **d), s + # Keep legacy `schedule` key for backward compatibility; prefer `job_id`. + d, s = request_accepted_for_processing(job.id, legacy_key="schedule") + return d, s @route("//kpis", methods=["GET"]) @use_kwargs( diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 2cf9212b0c..ee307f589b 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -19,6 +19,7 @@ from sqlalchemy import delete, select, or_ from flexmeasures.api.common.responses import ( + request_accepted_for_processing, request_processed, unrecognized_event, unknown_forecast, @@ -962,24 +963,59 @@ def trigger_schedule( site-peak-consumption-price: "260 EUR/MW" site-peak-production-price: "260 EUR/MW" responses: - 200: - description: PROCESSED + 202: + description: ACCEPTED (Scheduling job queued for processing) content: application/json: schema: type: object + properties: + job_id: + type: string + description: UUID of the queued scheduling job (canonical field; use this). + status: + type: string + enum: ["ACCEPTED"] + message: + type: string + job_monitor_url: + type: string + format: uri + description: URL to query the generic job status API. + job_results_url: + type: string + format: uri + description: URL to fetch the schedule results for this job once it is complete. + schedule: + type: string + description: (DEPRECATED) UUID of the queued scheduling job. Use `job_id` instead. + _deprecated_fields: + type: object + description: Machine-readable mapping of deprecated fields with migration guidance. examples: schedule_created: summary: Schedule response description: | - This message indicates that the scheduling request has been processed without any error. + This message indicates that the scheduling request has been accepted for processing (202 Accepted). A scheduling job has been created with some Universally Unique Identifier (UUID), which will be picked up by a worker. + The given UUID is returned in the canonical `job_id` field. + For backward-compatibility, the legacy `schedule` field is also included but is deprecated; + use `job_id` instead. The `_deprecated_fields` object provides migration guidance. The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. value: - status: "PROCESSED" + status: "ACCEPTED" + job_id: "364bfd06-c1fa-430b-8d25-8f5a547651fb" schedule: "364bfd06-c1fa-430b-8d25-8f5a547651fb" - message: "Request has been processed." + job_monitor_url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" + job_results_url: "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" + message: "Request has been accepted for processing." + _deprecated_fields: + schedule: + use: "job_id" + deprecated_since: "1.0.0" + removal_date: "2.0.0" + note: "The 'schedule' response field is deprecated; use 'job_id'" 400: description: INVALID_DATA 401: @@ -1027,9 +1063,15 @@ def trigger_schedule( db.session.commit() - response = dict(schedule=job.id) - d, s = request_processed() - return dict(**response, **d), s + # Keep legacy `schedule` key for backward compatibility; prefer `job_id`. + d, s = request_accepted_for_processing( + job.id, + legacy_key="schedule", + job_results_url=url_for( + "SensorAPI:get_schedule", id=sensor.id, uuid=job.id + ), + ) + return dict(**d), s # mark endpoint as deprecated trigger_schedule.after_request = lambda response: add_deprecation_header(response) @@ -1871,25 +1913,48 @@ def trigger_forecast(self, id: int, **params): start: "2026-01-15T00:00:00+01:00" duration: "P2D" responses: - 200: - description: PROCESSED + 202: + description: ACCEPTED (Forecasting job queued for processing) content: application/json: schema: type: object properties: - forecast: + job_id: type: string - description: UUID of the queued forecasting job. + description: UUID of the queued forecasting job (canonical field; use this). status: type: string - enum: ["PROCESSED"] + enum: ["ACCEPTED"] message: type: string + job_monitor_url: + type: string + format: uri + description: URL to query the generic job status API. + job_results_url: + type: string + format: uri + description: URL to fetch the forecast results for this job once it is complete. + forecast: + type: string + description: (DEPRECATED) UUID of the queued forecasting job. Use `job_id` instead. + _deprecated_fields: + type: object + description: Machine-readable mapping of deprecated fields with migration guidance. example: - forecast: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" - status: "PROCESSED" + status: "ACCEPTED" + job_id: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" message: "Forecasting job has been queued." + job_monitor_url: "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" + job_results_url: "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" + forecast: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" + _deprecated_fields: + forecast: + use: "job_id" + deprecated_since: "1.0.0" + removal_date: "2.0.0" + note: "The 'forecast' response field is deprecated; use 'job_id' instead" 401: description: UNAUTHORIZED 403: @@ -1930,8 +1995,15 @@ def trigger_forecast(self, id: int, **params): current_app.logger.exception("Forecast job failed to enqueue.") return unprocessable_entity(str(e)) - d, s = request_processed() - return dict(forecast=pipeline_returns["job_id"], **d), s + # Keep legacy `forecast` key for backward compatibility; prefer `job_id`. + d, s = request_accepted_for_processing( + pipeline_returns["job_id"], + legacy_key="forecast", + job_results_url=url_for( + "SensorAPI:get_forecast", id=id, uuid=pipeline_returns["job_id"] + ), + ) + return dict(**d), s @route("//forecasts/", methods=["GET"]) @use_kwargs( diff --git a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py index aac1b80ffa..b864b97759 100644 --- a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py @@ -95,7 +95,7 @@ def test_asset_trigger_and_get_schedule( json=message, ) print("Server responded with:\n%s" % trigger_schedule_response.json) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue diff --git a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py index 6b5a643e15..86175c8d2f 100644 --- a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py @@ -48,7 +48,10 @@ def test_trigger_and_fetch_forecasts( trigger_res = client.post( trigger_url, json=payload, headers={"Authorization": token} ) - assert trigger_res.status_code == 200, trigger_res.json + assert trigger_res.status_code == 202, trigger_res.json + assert trigger_res.json["job_results_url"] == url_for( + "SensorAPI:get_forecast", id=sensor_0.id, uuid=trigger_res.json["forecast"] + ) trigger_json = trigger_res.get_json() wrap_up_job = app.queues["forecasting"].fetch_job(trigger_json["forecast"]) diff --git a/flexmeasures/api/v3_0/tests/test_jobs_api.py b/flexmeasures/api/v3_0/tests/test_jobs_api.py index 28454db63f..335160177f 100644 --- a/flexmeasures/api/v3_0/tests/test_jobs_api.py +++ b/flexmeasures/api/v3_0/tests/test_jobs_api.py @@ -110,7 +110,7 @@ def test_get_job_status_queued( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # immediately query the generic job endpoint – job is still queued @@ -156,7 +156,7 @@ def test_get_job_status_started( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # simulate the job being picked up by a worker @@ -205,7 +205,7 @@ def test_get_job_status_finished( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # run the scheduling job @@ -266,7 +266,7 @@ def fail_with_assertion(self): url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message_for_trigger_schedule(), ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] work_on_rq( @@ -302,7 +302,7 @@ def test_get_job_status_failed_infeasible_schedule_includes_exc_info( url_for("SensorAPI:trigger_schedule", id=charging_station.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] work_on_rq( diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index 6928c452ab..1d302ac2bd 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -143,7 +143,7 @@ def test_trigger_schedule_preserves_flex_model_datetimes_in_job_kwargs( json=message, ) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 assert len(app.queues["scheduling"]) == 1 job = app.queues["scheduling"].jobs[0] @@ -184,7 +184,7 @@ def test_trigger_schedule_preserves_flex_model_datetimes_when_flooring_disabled( ) sensor.attributes = original_attributes - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 assert len(app.queues["scheduling"]) == 1 job = app.queues["scheduling"].jobs[0] @@ -298,7 +298,7 @@ def test_trigger_and_get_schedule_with_unknown_prices( ) print("Server responded with:\n%s" % trigger_schedule_response.json) check_deprecation(trigger_schedule_response, deprecation=None, sunset=None) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue @@ -410,7 +410,7 @@ def test_get_schedule_fallback( ) # check that the call is successful - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue @@ -567,7 +567,7 @@ def test_get_schedule_fallback_not_redirect( ) # check that the call is successful - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue @@ -655,7 +655,7 @@ def test_get_schedule_with_unit( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_schedule_response.status_code == 200, trigger_schedule_response.json + assert trigger_schedule_response.status_code == 202, trigger_schedule_response.json job_id = trigger_schedule_response.json["schedule"] # process the scheduling queue diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py index 4ee841a502..d8eefa689e 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py @@ -73,7 +73,12 @@ def test_trigger_and_get_schedule( json=message, ) print("Server responded with:\n%s" % trigger_schedule_response.json) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 + assert trigger_schedule_response.json["job_results_url"] == url_for( + "SensorAPI:get_schedule", + id=sensor.id, + uuid=trigger_schedule_response.json["schedule"], + ) job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue @@ -263,7 +268,7 @@ def test_trigger_schedule_uses_state_of_charge_sensor_for_soc_at_start( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 job_id = trigger_schedule_response.json["schedule"] work_on_rq(app.queues["scheduling"], exc_handler=handle_scheduling_exception) @@ -481,7 +486,7 @@ def test_price_sensor_priority( json=message, ) print("Server responded with:\n%s" % trigger_schedule_response.json) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 # Patch TimedBelief.search method with patch.object( @@ -572,7 +577,7 @@ def test_inflexible_device_sensors_priority( json=message, ) print("Server responded with:\n%s" % trigger_schedule_response.json) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 with patch( "flexmeasures.data.models.planning.storage.get_power_values", @@ -653,7 +658,7 @@ def test_multiple_contracts( json=message, ) print("Server responded with:\n%s" % trigger_schedule_response.json) - assert trigger_schedule_response.status_code == 200 + assert trigger_schedule_response.status_code == 202 job_id = trigger_schedule_response.json["schedule"] # process the scheduling queue @@ -860,7 +865,7 @@ def test_get_schedule_sign_convention_json_flex_model( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] work_on_rq(app.queues["scheduling"], exc_handler=handle_scheduling_exception) @@ -967,7 +972,7 @@ def test_get_schedule_sign_convention_db_flex_model( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] work_on_rq(app.queues["scheduling"], exc_handler=handle_scheduling_exception) diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 3f122566b7..1aecdf0dd2 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -1352,32 +1352,61 @@ } }, "responses": { - "200": { - "description": "PROCESSED", + "202": { + "description": "ACCEPTED (Forecasting job queued for processing)", "content": { "application/json": { "schema": { "type": "object", "properties": { - "forecast": { + "job_id": { "type": "string", - "description": "UUID of the queued forecasting job." + "description": "UUID of the queued forecasting job (canonical field; use this)." }, "status": { "type": "string", "enum": [ - "PROCESSED" + "ACCEPTED" ] }, "message": { "type": "string" + }, + "job_monitor_url": { + "type": "string", + "format": "uri", + "description": "URL to query the generic job status API." + }, + "job_results_url": { + "type": "string", + "format": "uri", + "description": "URL to fetch the forecast results for this job once it is complete." + }, + "forecast": { + "type": "string", + "description": "(DEPRECATED) UUID of the queued forecasting job. Use `job_id` instead." + }, + "_deprecated_fields": { + "type": "object", + "description": "Machine-readable mapping of deprecated fields with migration guidance." } } }, "example": { + "status": "ACCEPTED", + "job_id": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", + "message": "Forecasting job has been queued.", + "job_monitor_url": "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", + "job_results_url": "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", "forecast": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", - "status": "PROCESSED", - "message": "Forecasting job has been queued." + "_deprecated_fields": { + "forecast": { + "use": "job_id", + "deprecated_since": "1.0.0", + "removal_date": "2.0.0", + "note": "The 'forecast' response field is deprecated; use 'job_id' instead" + } + } } } } @@ -1502,21 +1531,65 @@ } }, "responses": { - "200": { - "description": "PROCESSED", + "202": { + "description": "ACCEPTED (Scheduling job queued for processing)", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "job_id": { + "type": "string", + "description": "UUID of the queued scheduling job (canonical field; use this)." + }, + "status": { + "type": "string", + "enum": [ + "ACCEPTED" + ] + }, + "message": { + "type": "string" + }, + "job_monitor_url": { + "type": "string", + "format": "uri", + "description": "URL to query the generic job status API." + }, + "job_results_url": { + "type": "string", + "format": "uri", + "description": "URL to fetch the schedule results for this job once it is complete." + }, + "schedule": { + "type": "string", + "description": "(DEPRECATED) UUID of the queued scheduling job. Use `job_id` instead." + }, + "_deprecated_fields": { + "type": "object", + "description": "Machine-readable mapping of deprecated fields with migration guidance." + } + } }, "examples": { "schedule_created": { "summary": "Schedule response", - "description": "This message indicates that the scheduling request has been processed without any error.\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID may be used to obtain the resulting schedule: see /sensors//schedules/.\n", + "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job_id` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job_id` instead. The `_deprecated_fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule: see /sensors//schedules/.\n", "value": { - "status": "PROCESSED", + "status": "ACCEPTED", + "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "message": "Request has been processed." + "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "message": "Request has been accepted for processing.", + "_deprecated_fields": { + "schedule": { + "use": "job_id", + "deprecated_since": "1.0.0", + "removal_date": "2.0.0", + "note": "The 'schedule' response field is deprecated; use 'job_id'" + } + } } } } @@ -3985,8 +4058,8 @@ } }, "responses": { - "200": { - "description": "PROCESSED", + "202": { + "description": "ACCEPTED (Scheduling job queued for processing)", "content": { "application/json": { "schema": { @@ -3994,11 +4067,21 @@ }, "examples": { "successful_response": { - "description": "This message indicates that the scheduling request has been processed without any error.\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID may be used to obtain the resulting schedule for each flexible device: see [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).\n", + "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job_id` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job_id` instead. The `_deprecated_fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).\n", "value": { - "status": "PROCESSED", + "status": "ACCEPTED", + "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "message": "Request has been processed." + "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "message": "Request has been accepted for processing.", + "_deprecated_fields": { + "schedule": { + "use": "job_id", + "deprecated_since": "1.0.0", + "removal_date": "2.0.0", + "note": "The 'schedule' response field is deprecated; use 'job_id'" + } + } } } } From 70913c8d76c5440068d5a03eb195261e706a9a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Sat, 6 Jun 2026 01:18:21 +0200 Subject: [PATCH 2/8] let get_schedule endpoint use status 202 as well, if jobs are not finished (just like get_forecast) - for now only use this (over the old 400 code) if API_SUNSET is being used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- flexmeasures/api/v3_0/sensors.py | 37 +++++++++++++++++ .../api/v3_0/tests/test_sensor_schedules.py | 41 +++++++++++++++++++ flexmeasures/ui/static/openapi-specs.json | 34 +++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index ee307f589b..1b008ef36d 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -1222,6 +1222,27 @@ def get_schedule( # noqa: C901 start: "2015-06-02T10:00:00+00:00" duration: "PT45M" unit: "MW" + 202: + description: Scheduling job status while the job is still running + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: ["QUEUED", "STARTED", "DEFERRED"] + description: Processing status of the scheduling job. + + message: + type: string + description: Human-readable message about current job processing. + examples: + started: + summary: Scheduling job is still running + value: + status: "STARTED" + message: "The scheduling job is currently running." 400: description: INVALID_TIMEZONE, INVALID_DOMAIN, UNKNOWN_SCHEDULE, UNRECOGNIZED_CONNECTION_GROUP 401: @@ -1278,7 +1299,23 @@ def get_schedule( # noqa: C901 _external=True, ), ) + 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)) schedule_start = job.kwargs["start"] diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index 1d302ac2bd..d4c28e4cd1 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -337,6 +337,47 @@ def test_trigger_and_get_schedule_with_unknown_prices( assert "prices unknown" in get_schedule_response.json["message"].lower() +@pytest.mark.parametrize( + "requesting_user", ["test_prosumer_user@seita.nl"], indirect=True +) +def test_get_schedule_unfinished_job_returns_202_when_sunset_active( + app, + add_battery_assets, + keep_scheduling_queue_empty, + requesting_user, +): + sensor = add_battery_assets["Test battery"].sensors[0] + original_sunset_active = app.config.get("FLEXMEASURES_API_SUNSET_ACTIVE") + app.config["FLEXMEASURES_API_SUNSET_ACTIVE"] = True + + with app.test_client() as client: + trigger_schedule_response = client.post( + url_for("SensorAPI:trigger_schedule", id=sensor.id), + json=message_for_trigger_schedule(), + ) + assert trigger_schedule_response.status_code == 202 + job_id = trigger_schedule_response.json["schedule"] + + get_schedule_response = client.get( + url_for("SensorAPI:get_schedule", id=sensor.id, uuid=job_id), + ) + + assert get_schedule_response.status_code == 202 + assert get_schedule_response.json["status"] in {"QUEUED", "STARTED", "DEFERRED"} + assert "message" in get_schedule_response.json + + app.config["FLEXMEASURES_API_SUNSET_ACTIVE"] = False + with app.test_client() as client: + get_schedule_response_old = client.get( + url_for("SensorAPI:get_schedule", id=sensor.id, uuid=job_id), + ) + + app.config["FLEXMEASURES_API_SUNSET_ACTIVE"] = original_sunset_active + + assert get_schedule_response_old.status_code == 400 + assert get_schedule_response_old.json["status"] == unknown_schedule()[0]["status"] + + @pytest.mark.parametrize( "requesting_user", ["test_prosumer_user@seita.nl"], indirect=True ) diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 1aecdf0dd2..b3889676ff 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -821,6 +821,40 @@ } } }, + "202": { + "description": "Scheduling job status while the job is still running", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "QUEUED", + "STARTED", + "DEFERRED" + ], + "description": "Processing status of the scheduling job." + }, + "message": { + "type": "string", + "description": "Human-readable message about current job processing." + } + } + }, + "examples": { + "started": { + "summary": "Scheduling job is still running", + "value": { + "status": "STARTED", + "message": "The scheduling job is currently running." + } + } + } + } + } + }, "400": { "description": "INVALID_TIMEZONE, INVALID_DOMAIN, UNKNOWN_SCHEDULE, UNRECOGNIZED_CONNECTION_GROUP" }, From 4de328dd8b5616d7be8e649a9a9685ccca7509de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Sun, 12 Jul 2026 01:52:34 +0200 Subject: [PATCH 3/8] simplify deprecation, was mostly unused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/api/change_log.rst | 8 ++++---- documentation/api/introduction.rst | 3 +-- flexmeasures/api/common/responses.py | 2 -- flexmeasures/api/v3_0/assets.py | 5 +++-- flexmeasures/api/v3_0/sensors.py | 4 ++-- flexmeasures/ui/static/openapi-specs.json | 5 +---- 6 files changed, 11 insertions(+), 16 deletions(-) diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 576b0a64d8..15b3cf0fb7 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -9,14 +9,14 @@ 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_id`` 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 ``job_results_url`` pointing to the sensor-specific results endpoint, alongside the generic ``job_monitor_url``. + * Legacy `schedule` field (in scheduling endpoints) and `forecast` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. All responses include a ``_deprecated_fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and version in which it was deprecated. + v3.0-31 | 2026-06-01 """""""""""""""""""" - Support filtering assets and sensors by ID prefix in the ``filter`` query parameter of ``GET /assets``, ``GET /assets//sensors`` and ``GET /sensors``. -- **Field canonicalization** for background job tracking: - * The ``job_id`` 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 ``job_results_url`` pointing to the sensor-specific results endpoint, alongside the generic ``job_monitor_url``. - * Legacy `schedule` field (in scheduling endpoints) and `forecast` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. All responses include a ``_deprecated_fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and removal date. - v3.0-31 | 2026-06-01 """""""""""""""""""" diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index c59fe65055..d8d8fbb3d0 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -271,7 +271,6 @@ For example, when scheduling endpoints switched from ``schedule`` to ``job_id`` "schedule": { "use": "job_id", "deprecated_since": "1.0.0", - "removal_date": "2.0.0", "note": "The 'schedule' response field is deprecated; use 'job_id' instead" } } @@ -281,7 +280,7 @@ Clients should: - Use the ``_deprecated_fields`` object to identify which fields are deprecated in their version. - Migrate to use the canonical field names (indicated by the ``"use"`` field). -- Plan upgrades before the ``"removal_date"`` to avoid breakage when the deprecated fields are removed in a future API version. +- Plan upgrades based on the deprecation guidance to avoid breakage when deprecated fields are eventually removed in a future API version. Clients ^^^^^^^ diff --git a/flexmeasures/api/common/responses.py b/flexmeasures/api/common/responses.py index 3a9294a61a..2cb0d87023 100644 --- a/flexmeasures/api/common/responses.py +++ b/flexmeasures/api/common/responses.py @@ -386,7 +386,6 @@ def request_accepted_for_processing( message: str = "Request has been accepted for processing.", legacy_key: str | None = None, deprecated_since: str | None = None, - removal_date: str | None = None, job_results_url: str | None = None, ) -> ResponseTuple: """ @@ -417,7 +416,6 @@ def request_accepted_for_processing( legacy_key: dict( use="job_id", deprecated_since=deprecated_since or server_now().date().isoformat(), - removal_date=removal_date, note=(f"The '{legacy_key}' response field is deprecated; use 'job_id'"), ) } diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index 43c9cc1fc4..c28145c6dc 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -1513,7 +1513,6 @@ def trigger_schedule( schedule: use: "job_id" deprecated_since: "1.0.0" - removal_date: "2.0.0" note: "The 'schedule' response field is deprecated; use 'job_id'" 400: description: INVALID_DATA @@ -1556,7 +1555,9 @@ def trigger_schedule( return unprocessable_entity(str(err)) # Keep legacy `schedule` key for backward compatibility; prefer `job_id`. - d, s = request_accepted_for_processing(job.id, legacy_key="schedule") + d, s = request_accepted_for_processing( + job.id, legacy_key="schedule", deprecated_since="1.0.0" + ) return d, s @route("//kpis", methods=["GET"]) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 153baba6cc..0ec96b2198 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -1021,7 +1021,6 @@ def trigger_schedule( schedule: use: "job_id" deprecated_since: "1.0.0" - removal_date: "2.0.0" note: "The 'schedule' response field is deprecated; use 'job_id'" 400: description: INVALID_DATA @@ -1074,6 +1073,7 @@ def trigger_schedule( d, s = request_accepted_for_processing( job.id, legacy_key="schedule", + deprecated_since="1.0.0", job_results_url=url_for( "SensorAPI:get_schedule", id=sensor.id, uuid=job.id ), @@ -2010,7 +2010,6 @@ def trigger_forecast(self, id: int, **params): forecast: use: "job_id" deprecated_since: "1.0.0" - removal_date: "2.0.0" note: "The 'forecast' response field is deprecated; use 'job_id' instead" 401: description: UNAUTHORIZED @@ -2056,6 +2055,7 @@ def trigger_forecast(self, id: int, **params): d, s = request_accepted_for_processing( pipeline_returns["job_id"], legacy_key="forecast", + deprecated_since="1.0.0", job_results_url=url_for( "SensorAPI:get_forecast", id=id, uuid=pipeline_returns["job_id"] ), diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 13b1ccda29..ce57385698 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -7,7 +7,7 @@ }, "termsOfService": null, "title": "FlexMeasures", - "version": "1.0.0" + "version": "0.33.2" }, "externalDocs": { "description": "FlexMeasures runs on the open source FlexMeasures technology. Read the docs here.", @@ -1438,7 +1438,6 @@ "forecast": { "use": "job_id", "deprecated_since": "1.0.0", - "removal_date": "2.0.0", "note": "The 'forecast' response field is deprecated; use 'job_id' instead" } } @@ -1621,7 +1620,6 @@ "schedule": { "use": "job_id", "deprecated_since": "1.0.0", - "removal_date": "2.0.0", "note": "The 'schedule' response field is deprecated; use 'job_id'" } } @@ -4123,7 +4121,6 @@ "schedule": { "use": "job_id", "deprecated_since": "1.0.0", - "removal_date": "2.0.0", "note": "The 'schedule' response field is deprecated; use 'job_id'" } } From f0364e6e584ae7040ec7eb4b0deed90276323786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Sun, 12 Jul 2026 21:55:35 +0200 Subject: [PATCH 4/8] chsnge 200 to 202 in some more tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- flexmeasures/api/v3_0/jobs.py | 6 +++--- .../api/v3_0/tests/test_asset_schedules_fresh_db.py | 10 +++++----- flexmeasures/api/v3_0/tests/test_jobs_api.py | 2 +- flexmeasures/ui/static/openapi-specs.json | 7 ++++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/flexmeasures/api/v3_0/jobs.py b/flexmeasures/api/v3_0/jobs.py index 384732d710..74dccc884f 100644 --- a/flexmeasures/api/v3_0/jobs.py +++ b/flexmeasures/api/v3_0/jobs.py @@ -141,9 +141,9 @@ def get_job_status(self, job_id: str, **kwargs): constraint analysis (empty arrays when the flex model defines no ``soc-minima``/``soc-maxima``, or when a scheduler other than ``StorageScheduler`` was used). - The ``num-beliefs`` field holds the total number of - beliefs (scheduled values) saved to the database. - nullable: true + The ``num-beliefs`` field holds the total number of + beliefs (scheduled values) saved to the database. + nullable: true func_name: type: string description: Fully-qualified name of the function executed by this job. diff --git a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py index 420665ad18..1afa692d9e 100644 --- a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py @@ -379,7 +379,7 @@ def test_asset_trigger_and_get_aggregate_schedule( url_for("AssetAPI:trigger_schedule", id=charging_hub.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # Process the scheduling queue @@ -588,9 +588,9 @@ def test_asset_trigger_with_multi_commodity_flex_context( url_for("AssetAPI:trigger_schedule", id=charging_hub.id), json=message, ) - if trigger_response.status_code != 200: + if trigger_response.status_code != 202: print(f"Error response: {trigger_response.json}") - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # Process the scheduling queue @@ -737,9 +737,9 @@ def test_asset_trigger_with_flex_context_commodity_not_used( url_for("AssetAPI:trigger_schedule", id=charging_hub.id), json=message, ) - if trigger_response.status_code != 200: + if trigger_response.status_code != 202: print(f"Error response: {trigger_response.json}") - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # Process the scheduling queue diff --git a/flexmeasures/api/v3_0/tests/test_jobs_api.py b/flexmeasures/api/v3_0/tests/test_jobs_api.py index df9fb950ff..247c001c22 100644 --- a/flexmeasures/api/v3_0/tests/test_jobs_api.py +++ b/flexmeasures/api/v3_0/tests/test_jobs_api.py @@ -279,7 +279,7 @@ def test_get_job_status_finished_with_unresolved_soc_minima( url_for("SensorAPI:trigger_schedule", id=sensor.id), json=message, ) - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # run the scheduling job diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 563bbf9d59..10a16ff818 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -4384,7 +4384,7 @@ "/api/v3_0/jobs/{uuid}": { "get": { "summary": "Get the status of a background job", - "description": "Look up a background job by its UUID and see whether it is\nqueued, running, finished, or failed.\n\nThe response includes a status message plus job metadata such\nas the queue name, function name, timestamps, and the job\nresult when available.\n\nFailed jobs also include traceback information when the worker\nstored it with the job result.\n\nFor a finished scheduling job, result is an object. For a\nStorageScheduler job it holds soft state-of-charge constraint\nanalysis: unresolved lists constraints the scheduler could not\nsatisfy, and resolved lists constraints that were satisfied\nwith some margin. Each device entry's soc-minima/soc-maxima\nvalue is a list, holding one entry per violated slot (for\nunresolved) or per met slot with its margin (for resolved),\nordered chronologically. Both arrays are empty when the flex model\ndefines no soc-minima/soc-maxima, or when a scheduler other\nthan StorageScheduler was used. This is the only place\nconstraint analysis is available \u2014 the sensor schedule endpoint\n(GET /api/v3_0/sensors//schedules/) returns power\nvalues only.\n", + "description": "Look up a background job by its UUID and see whether it is\nqueued, running, finished, or failed.\n\nThe response includes a status message plus job metadata such\nas the queue name, function name, timestamps, and the job\nresult when available.\n\nFailed jobs also include traceback information when the worker\nstored it with the job result.\n\nFor a finished scheduling job, result is an object. For a\nStorageScheduler job it holds soft state-of-charge constraint\nanalysis: unresolved lists constraints the scheduler could not\nsatisfy, and resolved lists constraints that were satisfied\nwith some margin. Each device entry's soc-minima/soc-maxima\nvalue is a list, holding one entry per violated slot (for\nunresolved) or per met slot with its margin (for resolved),\nordered chronologically. Both arrays are empty when the flex model\ndefines no soc-minima/soc-maxima, or when a scheduler other\nthan StorageScheduler was used. The num-beliefs field holds\nthe total number of beliefs (scheduled values) saved to the database.\nThis is the only place constraint analysis is available \u2014 the sensor\nschedule endpoint (GET /api/v3_0/sensors//schedules/)\nreturns power values only.\n", "security": [ { "ApiKeyAuth": [] @@ -4429,7 +4429,7 @@ "description": "Human-readable description of the job status." }, "result": { - "description": "Return value of the job function, or null when not yet available. For a finished scheduling job, this is an object; a StorageScheduler job populates it with unresolved/resolved soft state-of-charge constraint analysis (empty arrays when the flex model defines no soc-minima/soc-maxima, or when a scheduler other than StorageScheduler was used).\n", + "description": "Return value of the job function, or null when not yet available. For a finished scheduling job, this is an object; a StorageScheduler job populates it with unresolved/resolved soft state-of-charge constraint analysis (empty arrays when the flex model defines no soc-minima/soc-maxima, or when a scheduler other than StorageScheduler was used). The num-beliefs field holds the total number of beliefs (scheduled values) saved to the database.\n", "nullable": true }, "func_name": { @@ -4501,7 +4501,8 @@ ] } ], - "resolved": [] + "resolved": [], + "num-beliefs": 96 }, "func_name": "flexmeasures.data.services.scheduling.create_schedule", "origin": "scheduling", From fe17212a5325ba2be96b7adef8e87ddf75f31882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Wed, 15 Jul 2026 00:43:38 +0200 Subject: [PATCH 5/8] job status emdpoint also gets 202 status (and 422 for failures); use camel-case - even in existing job status endpoitn fields (using the new field-deprecation logic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/api/change_log.rst | 6 +- documentation/api/introduction.rst | 68 +++---- documentation/changelog.rst | 4 +- documentation/tut/toy-example-expanded.rst | 2 +- .../tut/toy-example-from-scratch.rst | 10 +- flexmeasures/api/common/responses.py | 22 +-- flexmeasures/api/v3_0/assets.py | 18 +- flexmeasures/api/v3_0/jobs.py | 137 +++++++++++--- flexmeasures/api/v3_0/sensors.py | 56 +++--- .../tests/test_forecasting_api_fresh_db.py | 2 +- flexmeasures/api/v3_0/tests/test_jobs_api.py | 70 +++++--- .../api/v3_0/tests/test_sensor_data.py | 6 +- .../tests/test_sensor_schedules_fresh_db.py | 2 +- .../api/v3_0/tests/test_sensors_api.py | 6 +- flexmeasures/ui/static/openapi-specs.json | 169 ++++++++++++------ 15 files changed, 382 insertions(+), 196 deletions(-) diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 15b3cf0fb7..1b248207bc 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -10,8 +10,10 @@ 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_id`` 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 ``job_results_url`` pointing to the sensor-specific results endpoint, alongside the generic ``job_monitor_url``. - * Legacy `schedule` field (in scheduling endpoints) and `forecast` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. All responses include a ``_deprecated_fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and version in which it was deprecated. + * 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) are deprecated but remain in responses for backward compatibility. All responses include a ``deprecated-fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and version in which it was deprecated. +- ``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. +- ``GET /api/v3_0/jobs/`` now returns kebab-case metadata fields such as ``func-name`` and ``enqueued-at``. Legacy snake_case metadata fields such as ``func_name`` and ``enqueued_at`` remain available and are listed under ``deprecated-fields``. v3.0-31 | 2026-06-01 diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index d8d8fbb3d0..33635aeb09 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -161,8 +161,8 @@ Background job monitoring -------------------------- Several API endpoints queue background jobs for asynchronous processing (scheduling, forecasting, data ingestion) and return a ``202 Accepted`` response. -These responses include a ``job_id`` field (the canonical identifier) that clients can use to monitor job progress and retrieve results. -They also include both ``job_monitor_url`` for generic status monitoring and (if applicable) ``job_results_url`` for the sensor-specific results endpoint. +These responses include a ``job`` field (the canonical identifier) that clients can use to monitor job progress and retrieve results. +They also include both ``job-url`` for generic status monitoring and (if applicable) ``results-url`` for the sensor-specific results endpoint. **Example 202 Accepted response from a scheduling endpoint:** @@ -170,19 +170,19 @@ They also include both ``job_monitor_url`` for generic status monitoring and (if { "status": "ACCEPTED", - "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "results-url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", "message": "Request has been accepted for processing." } **Monitoring job status:** -Clients should use the ``job_id`` to query the unified job status endpoint: +Clients should use the ``job.id`` to query the unified job status endpoint: .. code-block:: bash - GET /api/v3_0/jobs/ + GET /api/v3_0/jobs/ This returns the current execution status and a human-readable result message. For example: @@ -191,14 +191,14 @@ This returns the current execution status and a human-readable result message. F import requests import time - def wait_for_job(job_id, job_monitor_url, timeout=300, poll_interval=5): + def wait_for_job(job_id, job_url, timeout=300, poll_interval=5): """Wait for a background job to complete and return the result. - + Parameters ---------- job_id : str - The UUID of the background job. - job_monitor_url : str + The UUID of the background job, we use it for logging here.. + job_url : str The URL to query for job status (e.g., "/api/v3_0/jobs/"). timeout : int Maximum seconds to wait for job completion. @@ -207,36 +207,38 @@ This returns the current execution status and a human-readable result message. F """ start_time = time.time() while time.time() - start_time < timeout: - response = requests.get(job_monitor_url) - if not response.ok: - raise RuntimeError(f"Failed to query job status: {response.status_code} {response.text}") - + response = requests.get(job_url) + if response.status_code not in (200, 202, 422): + raise RuntimeError( + f"Failed to query job status: {response.status_code} {response.text}" + ) + job_data = response.json() status = job_data.get("status") - - if status in ("PENDING", "RUNNING"): + + if response.status_code == 202: print(f"Job {job_id} is still {status.lower()}...") time.sleep(poll_interval) - elif status == "SUCCEEDED": + elif status == "FINISHED": return job_data.get("result") else: # Failed, error, etc. raise RuntimeError(f"Job failed with status {status}: {job_data.get('message')}") - + raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds") .. note:: - For **schedules**, after the job completes successfully, use the job ID (same value as the legacy ``schedule`` field) to retrieve the actual schedule or follow the returned ``job_results_url``: + For **schedules**, after the job completes successfully, use the job ID (same value as the legacy ``schedule`` field) to retrieve the actual schedule or follow the returned ``results-url``: .. code-block:: bash - GET /api/v3_0/sensors//schedules/ + GET /api/v3_0/sensors//schedules/ - For **forecasts**, after the job completes successfully, use the job ID to retrieve the forecast or follow the returned ``job_results_url``: + For **forecasts**, after the job completes successfully, use the job ID to retrieve the forecast or follow the returned ``results-url``: .. code-block:: bash - GET /api/v3_0/sensors//forecasts/ + GET /api/v3_0/sensors//forecasts/ Both of these endpoints will also return `202 Accepted` if the job is still being computed, so clients can continue to poll them directly if they prefer. @@ -255,30 +257,30 @@ Deprecated response fields ^^^^^^^^^^^^^^^^^^^^^^^^^^^ In addition to deprecating entire endpoints, we sometimes deprecate individual fields in API responses while maintaining backward compatibility by including both the legacy and canonical fields. -When this happens, responses include a ``_deprecated_fields`` object containing machine-readable information about deprecated fields. +When this happens, responses include a ``deprecated-fields`` object containing machine-readable information about deprecated fields. -For example, when scheduling endpoints switched from ``schedule`` to ``job_id`` as the canonical field identifier for background jobs: +For example, when scheduling endpoints switched from ``schedule`` to ``job`` as the canonical field identifier for background jobs: .. code-block:: json { "status": "ACCEPTED", - "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "_deprecated_fields": { + "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "results-url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "deprecated-fields": { "schedule": { - "use": "job_id", - "deprecated_since": "1.0.0", - "note": "The 'schedule' response field is deprecated; use 'job_id' instead" + "use": "job", + "deprecated-since": "1.0.0", + "note": "The 'schedule' response field is deprecated; use 'job' instead" } } } Clients should: -- Use the ``_deprecated_fields`` object to identify which fields are deprecated in their version. +- Use the ``deprecated-fields`` object to identify which fields are deprecated in their version. - Migrate to use the canonical field names (indicated by the ``"use"`` field). - Plan upgrades based on the deprecation guidance to avoid breakage when deprecated fields are eventually removed in a future API version. diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 6ced56f04d..2289d5fdfd 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_id` field and return HTTP `202 Accepted` when a background job is queued. Legacy response fields such as ``schedule`` and ``forecast`` will be deprecated; clients should migrate to `job_id` (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. 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:: Upgrading to this version requires running ``flexmeasures db upgrade`` (you can create a backup first with ``flexmeasures db-ops dump``). @@ -32,7 +32,7 @@ Infrastructure / Support ---------------------- * Upgraded dependencies [see `PR #1485 `_, `PR #2215 `_ and `PR #2243 `_] * 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_id` field; legacy response fields such as ``schedule`` and ``forecast`` are preserved for backward-compatibility but marked deprecated with a migration guide in the API docs (see issue #2171). +* 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 a migration guide in the API docs [see `PR #2224 `_]. * 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 `_] * Document source filters better, and make use of the source-types filter in the PV curtailment tutorial [see `PR #2261 `_] diff --git a/documentation/tut/toy-example-expanded.rst b/documentation/tut/toy-example-expanded.rst index 6eacc5683c..9fe8c37ae2 100644 --- a/documentation/tut/toy-example-expanded.rst +++ b/documentation/tut/toy-example-expanded.rst @@ -120,7 +120,7 @@ This will have an effect on the available headroom for the battery, given the `` } .. note:: - The API returns a **202 Accepted** response containing a ``job_id`` field. + The API returns a **202 Accepted** response containing a ``job`` field. Use this UUID to monitor the job status via :ref:`api_background_jobs`. .. tab:: FlexMeasures Client diff --git a/documentation/tut/toy-example-from-scratch.rst b/documentation/tut/toy-example-from-scratch.rst index 0ca7491e6c..2f77140060 100644 --- a/documentation/tut/toy-example-from-scratch.rst +++ b/documentation/tut/toy-example-from-scratch.rst @@ -91,19 +91,19 @@ There is more information being used by the scheduler, such as the battery's cap .. note:: You can try this right in Swagger UI, too! You should find it at `http://localhost:5000/api/v3_0/docs `_ after starting FlexMeasures locally. - The API returns a **202 Accepted** response with a ``job_id`` field to track the scheduling job: + The API returns a **202 Accepted** response with a ``job`` field to track the scheduling job: .. code-block:: json { "status": "ACCEPTED", - "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_results_url": "/api/v3_0/sensors/2/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" + "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "results-url": "/api/v3_0/sensors/2/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" } - **Important:** Use the ``job_id`` field to reference the scheduling job. You can also follow the returned ``job_results_url`` to fetch the schedule results once they are ready. For more details, see :ref:`api_background_jobs`. + **Important:** Use the ``job`` field to reference the scheduling job. You can also follow the returned ``results-url`` to fetch the schedule results once they are ready. For more details, see :ref:`api_background_jobs`. .. tab:: FlexMeasures Client diff --git a/flexmeasures/api/common/responses.py b/flexmeasures/api/common/responses.py index 2cb0d87023..ac6deb8d12 100644 --- a/flexmeasures/api/common/responses.py +++ b/flexmeasures/api/common/responses.py @@ -393,8 +393,8 @@ def request_accepted_for_processing( Optional backwards-compatibility: if `legacy_key` is provided the response will include the job id under that legacy key (e.g. `schedule` or - `forecast`). The response will also include a `_deprecated_fields` object - with guidance for migrating to `job_id`. + `forecast`). The response will also include a `deprecated-fields` object + with guidance for migrating to `job`. Optional `job_results_url` may be supplied to provide a direct link to the sensor-specific job results endpoint, e.g. `/api/v3_0/sensors//schedules/`. @@ -402,22 +402,22 @@ def request_accepted_for_processing( resp: dict[str, object] = dict( status="ACCEPTED", message=message, - job_monitor_url=url_for("JobAPI:get_job_status", uuid=job_id), - job_id=job_id, + job=job_id, ) + resp["job-url"] = url_for("JobAPI:get_job_status", uuid=job_id) if job_results_url: - resp["job_results_url"] = job_results_url + resp["results-url"] = job_results_url if legacy_key: # keep legacy key for backwards compatibility resp[legacy_key] = job_id # include machine-readable deprecation info so clients can detect it - resp["_deprecated_fields"] = { - legacy_key: dict( - use="job_id", - deprecated_since=deprecated_since or server_now().date().isoformat(), - note=(f"The '{legacy_key}' response field is deprecated; use 'job_id'"), - ) + resp["deprecated-fields"] = { + legacy_key: { + "use": "job", + "deprecated-since": deprecated_since or server_now().date().isoformat(), + "note": f"The '{legacy_key}' response field is deprecated; use 'job'", + } } return resp, 202 diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index c28145c6dc..f1cb7044de 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -1499,21 +1499,21 @@ def trigger_schedule( This message indicates that the scheduling request has been accepted for processing (202 Accepted). A scheduling job has been created with some Universally Unique Identifier (UUID), which will be picked up by a worker. - The given UUID is returned in the canonical `job_id` field. + The given UUID is returned in the canonical `job` field. For backward-compatibility, the legacy `schedule` field is also included but is deprecated; - use `job_id` instead. The `_deprecated_fields` object provides migration guidance. + use `job` instead. The `deprecated-fields` object provides migration guidance. The given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_). value: status: ACCEPTED - job_id: "364bfd06-c1fa-430b-8d25-8f5a547651fb" + job: "364bfd06-c1fa-430b-8d25-8f5a547651fb" schedule: "364bfd06-c1fa-430b-8d25-8f5a547651fb" - job_monitor_url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" + job-url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" message: "Request has been accepted for processing." - _deprecated_fields: + deprecated-fields: schedule: - use: "job_id" - deprecated_since: "1.0.0" - note: "The 'schedule' response field is deprecated; use 'job_id'" + use: "job" + deprecated-since: "1.0.0" + note: "The 'schedule' response field is deprecated; use 'job'" 400: description: INVALID_DATA 401: @@ -1554,7 +1554,7 @@ def trigger_schedule( except ValueError as err: return unprocessable_entity(str(err)) - # Keep legacy `schedule` key for backward compatibility; prefer `job_id`. + # Keep legacy `schedule` key for backward compatibility; prefer `job`. d, s = request_accepted_for_processing( job.id, legacy_key="schedule", deprecated_since="1.0.0" ) diff --git a/flexmeasures/api/v3_0/jobs.py b/flexmeasures/api/v3_0/jobs.py index 74dccc884f..315ee59537 100644 --- a/flexmeasures/api/v3_0/jobs.py +++ b/flexmeasures/api/v3_0/jobs.py @@ -55,6 +55,29 @@ def _job_queue_unavailable_response(): ) +JOB_STATUS_DEPRECATED_FIELDS = { + "func_name": "func-name", + "enqueued_at": "enqueued-at", + "started_at": "started-at", + "ended_at": "ended-at", + "exc_info": "exc-info", +} + + +def _job_status_deprecated_fields() -> dict[str, dict[str, str]]: + return { + legacy_key: { + "use": canonical_key, + "deprecated-since": "1.0.0", + "note": ( + f"The '{legacy_key}' response field is deprecated; " + f"use '{canonical_key}' instead." + ), + } + for legacy_key, canonical_key in JOB_STATUS_DEPRECATED_FIELDS.items() + } + + class JobAPI(FlaskView): """ Endpoint for querying the status of background jobs by UUID. @@ -111,7 +134,7 @@ def get_job_status(self, job_id: str, **kwargs): type: string responses: 200: - description: Job status retrieved successfully. + description: Finished job status retrieved successfully. content: application/json: schema: @@ -144,31 +167,61 @@ def get_job_status(self, job_id: str, **kwargs): The ``num-beliefs`` field holds the total number of beliefs (scheduled values) saved to the database. nullable: true - func_name: + func-name: type: string description: Fully-qualified name of the function executed by this job. origin: type: string description: Name of the queue the job was placed on. - enqueued_at: + enqueued-at: type: string format: date-time nullable: true description: ISO-8601 timestamp of when the job was enqueued. - started_at: + started-at: type: string format: date-time nullable: true description: ISO-8601 timestamp of when the job started executing. - ended_at: + ended-at: type: string format: date-time nullable: true description: ISO-8601 timestamp of when the job finished executing. - exc_info: + exc-info: type: string nullable: true description: Traceback information for failed jobs, or null otherwise. + func_name: + type: string + deprecated: true + description: (DEPRECATED) Fully-qualified name of the function executed by this job. Use `func-name` instead. + enqueued_at: + type: string + format: date-time + nullable: true + deprecated: true + description: (DEPRECATED) ISO-8601 timestamp of when the job was enqueued. Use `enqueued-at` instead. + started_at: + type: string + format: date-time + nullable: true + deprecated: true + description: (DEPRECATED) ISO-8601 timestamp of when the job started executing. Use `started-at` instead. + ended_at: + type: string + format: date-time + nullable: true + deprecated: true + description: (DEPRECATED) ISO-8601 timestamp of when the job finished executing. Use `ended-at` instead. + exc_info: + type: string + nullable: true + deprecated: true + description: (DEPRECATED) Traceback information for failed jobs, or null otherwise. Use `exc-info` instead. + deprecated-fields: + type: object + description: Machine-readable mapping of deprecated fields with migration guidance. examples: queued: summary: Queued job @@ -176,12 +229,33 @@ def get_job_status(self, job_id: str, **kwargs): status: QUEUED message: "Scheduling job waiting to be processed." result: null - func_name: "flexmeasures.data.services.scheduling.create_schedule" + func-name: "flexmeasures.data.services.scheduling.create_schedule" origin: scheduling + enqueued-at: "2026-04-28T10:00:00+00:00" + started-at: null + ended-at: null + exc-info: null + func_name: "flexmeasures.data.services.scheduling.create_schedule" enqueued_at: "2026-04-28T10:00:00+00:00" started_at: null ended_at: null exc_info: null + deprecated-fields: + func_name: + use: "func-name" + deprecated-since: "1.0.0" + enqueued_at: + use: "enqueued-at" + deprecated-since: "1.0.0" + started_at: + use: "started-at" + deprecated-since: "1.0.0" + ended_at: + use: "ended-at" + deprecated-since: "1.0.0" + exc_info: + use: "exc-info" + deprecated-since: "1.0.0" finished: summary: Finished job value: @@ -197,24 +271,28 @@ def get_job_status(self, job_id: str, **kwargs): violation: "180.0 kWh" resolved: [] num-beliefs: 96 - func_name: "flexmeasures.data.services.scheduling.create_schedule" + func-name: "flexmeasures.data.services.scheduling.create_schedule" origin: scheduling - enqueued_at: "2026-04-28T10:00:00+00:00" - started_at: "2026-04-28T10:00:01+00:00" - ended_at: "2026-04-28T10:00:05+00:00" - exc_info: null + enqueued-at: "2026-04-28T10:00:00+00:00" + started-at: "2026-04-28T10:00:01+00:00" + ended-at: "2026-04-28T10:00:05+00:00" + exc-info: null failed: summary: Failed job value: status: FAILED message: "Scheduling job failed with ValueError: ..." result: null - func_name: "flexmeasures.data.services.scheduling.create_schedule" + func-name: "flexmeasures.data.services.scheduling.create_schedule" origin: scheduling - enqueued_at: "2026-04-28T10:00:00+00:00" - started_at: "2026-04-28T10:00:01+00:00" - ended_at: "2026-04-28T10:00:02+00:00" - exc_info: "Traceback (most recent call last): ..." + enqueued-at: "2026-04-28T10:00:00+00:00" + started-at: "2026-04-28T10:00:01+00:00" + ended-at: "2026-04-28T10:00:02+00:00" + exc-info: "Traceback (most recent call last): ..." + 202: + description: Job is still queued, scheduled, deferred, or running. + 422: + description: Job has failed. 404: description: NOT_FOUND 401: @@ -264,12 +342,25 @@ def get_job_status(self, job_id: str, **kwargs): status=status_name, message=job_status_description(job), result=result, - func_name=job.func_name, origin=job.origin, - enqueued_at=_isoformat_or_none(job.enqueued_at), - started_at=_isoformat_or_none(job.started_at), - ended_at=_isoformat_or_none(job.ended_at), - exc_info=failed_job_exc_info(job), ) + response["func-name"] = job.func_name + response["enqueued-at"] = _isoformat_or_none(job.enqueued_at) + response["started-at"] = _isoformat_or_none(job.started_at) + response["ended-at"] = _isoformat_or_none(job.ended_at) + response["exc-info"] = failed_job_exc_info(job) + response["func_name"] = response["func-name"] + response["enqueued_at"] = response["enqueued-at"] + response["started_at"] = response["started-at"] + response["ended_at"] = response["ended-at"] + response["exc_info"] = response["exc-info"] + response["deprecated-fields"] = _job_status_deprecated_fields() + + if status_name == JobStatus.FAILED.name: + status_code = 422 + elif status_name == JobStatus.FINISHED.name: + status_code = 200 + else: + status_code = 202 - return response, 200 + return response, status_code diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 0ec96b2198..50782d37f4 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -977,7 +977,7 @@ def trigger_schedule( schema: type: object properties: - job_id: + job: type: string description: UUID of the queued scheduling job (canonical field; use this). status: @@ -985,18 +985,18 @@ def trigger_schedule( enum: ["ACCEPTED"] message: type: string - job_monitor_url: + job-url: type: string format: uri description: URL to query the generic job status API. - job_results_url: + results-url: type: string format: uri description: URL to fetch the schedule results for this job once it is complete. schedule: type: string - description: (DEPRECATED) UUID of the queued scheduling job. Use `job_id` instead. - _deprecated_fields: + description: (DEPRECATED) UUID of the queued scheduling job. Use `job` instead. + deprecated-fields: type: object description: Machine-readable mapping of deprecated fields with migration guidance. examples: @@ -1006,22 +1006,22 @@ def trigger_schedule( This message indicates that the scheduling request has been accepted for processing (202 Accepted). A scheduling job has been created with some Universally Unique Identifier (UUID), which will be picked up by a worker. - The given UUID is returned in the canonical `job_id` field. + The given UUID is returned in the canonical `job` field. For backward-compatibility, the legacy `schedule` field is also included but is deprecated; - use `job_id` instead. The `_deprecated_fields` object provides migration guidance. + use `job` instead. The `deprecated-fields` object provides migration guidance. The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. value: status: "ACCEPTED" - job_id: "364bfd06-c1fa-430b-8d25-8f5a547651fb" + job: "364bfd06-c1fa-430b-8d25-8f5a547651fb" schedule: "364bfd06-c1fa-430b-8d25-8f5a547651fb" - job_monitor_url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" - job_results_url: "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" + job-url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" + results-url: "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" message: "Request has been accepted for processing." - _deprecated_fields: + deprecated-fields: schedule: - use: "job_id" - deprecated_since: "1.0.0" - note: "The 'schedule' response field is deprecated; use 'job_id'" + use: "job" + deprecated-since: "1.0.0" + note: "The 'schedule' response field is deprecated; use 'job'" 400: description: INVALID_DATA 401: @@ -1069,7 +1069,7 @@ def trigger_schedule( db.session.commit() - # Keep legacy `schedule` key for backward compatibility; prefer `job_id`. + # Keep legacy `schedule` key for backward compatibility; prefer `job`. d, s = request_accepted_for_processing( job.id, legacy_key="schedule", @@ -1977,7 +1977,7 @@ def trigger_forecast(self, id: int, **params): schema: type: object properties: - job_id: + job: type: string description: UUID of the queued forecasting job (canonical field; use this). status: @@ -1985,32 +1985,32 @@ def trigger_forecast(self, id: int, **params): enum: ["ACCEPTED"] message: type: string - job_monitor_url: + job-url: type: string format: uri description: URL to query the generic job status API. - job_results_url: + results-url: type: string format: uri description: URL to fetch the forecast results for this job once it is complete. forecast: type: string - description: (DEPRECATED) UUID of the queued forecasting job. Use `job_id` instead. - _deprecated_fields: + description: (DEPRECATED) UUID of the queued forecasting job. Use `job` instead. + deprecated-fields: type: object description: Machine-readable mapping of deprecated fields with migration guidance. example: status: "ACCEPTED" - job_id: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" + job: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" message: "Forecasting job has been queued." - job_monitor_url: "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" - job_results_url: "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" + job-url: "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" + results-url: "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" forecast: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" - _deprecated_fields: + deprecated-fields: forecast: - use: "job_id" - deprecated_since: "1.0.0" - note: "The 'forecast' response field is deprecated; use 'job_id' instead" + use: "job" + deprecated-since: "1.0.0" + note: "The 'forecast' response field is deprecated; use 'job' instead" 401: description: UNAUTHORIZED 403: @@ -2051,7 +2051,7 @@ def trigger_forecast(self, id: int, **params): current_app.logger.exception("Forecast job failed to enqueue.") return unprocessable_entity(str(e)) - # Keep legacy `forecast` key for backward compatibility; prefer `job_id`. + # Keep legacy `forecast` key for backward compatibility; prefer `job`. d, s = request_accepted_for_processing( pipeline_returns["job_id"], legacy_key="forecast", diff --git a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py index 86175c8d2f..8877dc8343 100644 --- a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py @@ -49,7 +49,7 @@ def test_trigger_and_fetch_forecasts( trigger_url, json=payload, headers={"Authorization": token} ) assert trigger_res.status_code == 202, trigger_res.json - assert trigger_res.json["job_results_url"] == url_for( + assert trigger_res.json["results-url"] == url_for( "SensorAPI:get_forecast", id=sensor_0.id, uuid=trigger_res.json["forecast"] ) diff --git a/flexmeasures/api/v3_0/tests/test_jobs_api.py b/flexmeasures/api/v3_0/tests/test_jobs_api.py index 247c001c22..c87c6d2465 100644 --- a/flexmeasures/api/v3_0/tests/test_jobs_api.py +++ b/flexmeasures/api/v3_0/tests/test_jobs_api.py @@ -21,6 +21,23 @@ from flexmeasures.utils.job_utils import work_on_rq +JOB_STATUS_DEPRECATED_FIELDS = { + "func_name": "func-name", + "enqueued_at": "enqueued-at", + "started_at": "started-at", + "ended_at": "ended-at", + "exc_info": "exc-info", +} + + +def assert_legacy_job_status_fields(data: dict): + assert data["deprecated-fields"].keys() >= JOB_STATUS_DEPRECATED_FIELDS.keys() + for legacy_key, canonical_key in JOB_STATUS_DEPRECATED_FIELDS.items(): + assert data[legacy_key] == data[canonical_key] + assert data["deprecated-fields"][legacy_key]["use"] == canonical_key + assert data["deprecated-fields"][legacy_key]["deprecated-since"] == "1.0.0" + + @pytest.mark.parametrize( "requesting_user", ["test_prosumer_user@seita.nl"], indirect=True ) @@ -43,7 +60,7 @@ def test_get_job_status_unknown_uuid( @pytest.mark.parametrize( "requesting_user, expected_status_code", [ - ("test_prosumer_user@seita.nl", 200), + ("test_prosumer_user@seita.nl", 202), ("test_dummy_user_3@seita.nl", 403), ], indirect=["requesting_user"], @@ -84,7 +101,7 @@ def test_get_job_status_requires_read_access( ) assert response.status_code == expected_status_code - if expected_status_code == 200: + if expected_status_code == 202: assert response.json["status"] == "QUEUED" else: assert response.json["status"] == "INVALID_SENDER" @@ -120,21 +137,22 @@ def test_get_job_status_queued( ) print("Server responded with:\n%s" % response.json) - assert response.status_code == 200 + assert response.status_code == 202 data = response.json assert data["status"] == "QUEUED" assert "waiting" in data["message"].lower() # metadata fields present - assert "func_name" in data + assert "func-name" in data assert "origin" in data assert data["origin"] == "scheduling" - # enqueued_at is set when a job is queued; started_at and ended_at are not yet - assert data["enqueued_at"] is not None - assert data["started_at"] is None - assert data["ended_at"] is None + # enqueued-at is set when a job is queued; started-at and ended-at are not yet + assert data["enqueued-at"] is not None + assert data["started-at"] is None + assert data["ended-at"] is None # result is not yet available assert data["result"] is None - assert data["exc_info"] is None + assert data["exc-info"] is None + assert_legacy_job_status_fields(data) @pytest.mark.parametrize( @@ -169,19 +187,20 @@ def test_get_job_status_started( ) print("Server responded with:\n%s" % response.json) - assert response.status_code == 200 + assert response.status_code == 202 data = response.json assert data["status"] == "STARTED" assert "in progress" in data["message"].lower() # metadata fields present - assert "func_name" in data + assert "func-name" in data assert data["origin"] == "scheduling" - # enqueued_at is set; ended_at is not yet available - assert data["enqueued_at"] is not None - assert data["ended_at"] is None + # enqueued-at is set; ended-at is not yet available + assert data["enqueued-at"] is not None + assert data["ended-at"] is None # result is not yet available assert data["result"] is None - assert data["exc_info"] is None + assert data["exc-info"] is None + assert_legacy_job_status_fields(data) @pytest.mark.parametrize( @@ -225,12 +244,12 @@ def test_get_job_status_finished( assert data["status"] == "FINISHED" assert "finished" in data["message"].lower() # metadata fields present - assert "func_name" in data + assert "func-name" in data assert data["origin"] == "scheduling" # timing fields - assert data["enqueued_at"] is not None - assert data["started_at"] is not None - assert data["ended_at"] is not None + assert data["enqueued-at"] is not None + assert data["started-at"] is not None + assert data["ended-at"] is not None # every finished scheduling job now returns an object (not the boolean # True it used to return unconditionally); this is a StorageScheduler # job, so `result` is the soft SoC constraint analysis dict, and since @@ -241,7 +260,8 @@ def test_get_job_status_finished( assert result["unresolved"] == [] assert result["resolved"] == [] assert result["num-beliefs"] == 96 - assert data["exc_info"] is None + assert data["exc-info"] is None + assert_legacy_job_status_fields(data) @pytest.mark.parametrize( @@ -361,11 +381,12 @@ def fail_with_assertion(self): response = client.get(url_for("JobAPI:get_job_status", uuid=job_id)) - assert response.status_code == 200 + assert response.status_code == 422 data = response.json assert data["status"] == "FAILED" assert "assert 1 == 2" in data["message"] - assert "AssertionError: assert 1 == 2" in data["exc_info"] + assert "AssertionError: assert 1 == 2" in data["exc-info"] + assert_legacy_job_status_fields(data) @pytest.mark.parametrize( @@ -397,13 +418,14 @@ def test_get_job_status_failed_infeasible_schedule_includes_exc_info( response = client.get(url_for("JobAPI:get_job_status", uuid=job_id)) - assert response.status_code == 200 + assert response.status_code == 422 data = response.json assert data["status"] == "FAILED" assert "infeasible problem" in data["message"].lower() assert ( - "ValueError: The input data yields an infeasible problem." in data["exc_info"] + "ValueError: The input data yields an infeasible problem." in data["exc-info"] ) + assert_legacy_job_status_fields(data) def test_get_job_status_unauthenticated( diff --git a/flexmeasures/api/v3_0/tests/test_sensor_data.py b/flexmeasures/api/v3_0/tests/test_sensor_data.py index 8ab8063089..25bcc36670 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_data.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_data.py @@ -319,10 +319,10 @@ def test_post_sensor_data_returns_accepted_job( assert response.status_code == 202 assert response.json["status"] == "ACCEPTED" - assert response.json["job_monitor_url"] == url_for( - "JobAPI:get_job_status", uuid=response.json["job_id"] + assert response.json["job-url"] == url_for( + "JobAPI:get_job_status", uuid=response.json["job"] ) - job = current_app.queues["ingestion"].fetch_job(response.json["job_id"]) + job = current_app.queues["ingestion"].fetch_job(response.json["job"]) assert job.kwargs["sensor_id"] == sensor.id assert job.kwargs["sensor_data"] == post_data assert "data" not in job.kwargs diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py index d8eefa689e..b1180598a5 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py @@ -74,7 +74,7 @@ def test_trigger_and_get_schedule( ) print("Server responded with:\n%s" % trigger_schedule_response.json) assert trigger_schedule_response.status_code == 202 - assert trigger_schedule_response.json["job_results_url"] == url_for( + assert trigger_schedule_response.json["results-url"] == url_for( "SensorAPI:get_schedule", id=sensor.id, uuid=trigger_schedule_response.json["schedule"], diff --git a/flexmeasures/api/v3_0/tests/test_sensors_api.py b/flexmeasures/api/v3_0/tests/test_sensors_api.py index d8d033ca16..aaccb5453e 100644 --- a/flexmeasures/api/v3_0/tests/test_sensors_api.py +++ b/flexmeasures/api/v3_0/tests/test_sensors_api.py @@ -366,10 +366,10 @@ def test_upload_csv_file_returns_accepted_job( assert response.status_code == 202 assert response.json["status"] == "ACCEPTED" - assert response.json["job_monitor_url"] == url_for( - "JobAPI:get_job_status", uuid=response.json["job_id"] + assert response.json["job-url"] == url_for( + "JobAPI:get_job_status", uuid=response.json["job"] ) - job = current_app.queues["ingestion"].fetch_job(response.json["job_id"]) + job = current_app.queues["ingestion"].fetch_job(response.json["job"]) assert job.kwargs["sensor_id"] == sensor.id assert job.kwargs["uploaded_files"][0]["filename"] == "test.csv" assert job.kwargs["uploaded_files"][0]["content"] == csv_content.encode("utf-8") diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 10a16ff818..be1d7809ae 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -1394,7 +1394,7 @@ "schema": { "type": "object", "properties": { - "job_id": { + "job": { "type": "string", "description": "UUID of the queued forecasting job (canonical field; use this)." }, @@ -1407,21 +1407,21 @@ "message": { "type": "string" }, - "job_monitor_url": { + "job-url": { "type": "string", "format": "uri", "description": "URL to query the generic job status API." }, - "job_results_url": { + "results-url": { "type": "string", "format": "uri", "description": "URL to fetch the forecast results for this job once it is complete." }, "forecast": { "type": "string", - "description": "(DEPRECATED) UUID of the queued forecasting job. Use `job_id` instead." + "description": "(DEPRECATED) UUID of the queued forecasting job. Use `job` instead." }, - "_deprecated_fields": { + "deprecated-fields": { "type": "object", "description": "Machine-readable mapping of deprecated fields with migration guidance." } @@ -1429,16 +1429,16 @@ }, "example": { "status": "ACCEPTED", - "job_id": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", + "job": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", "message": "Forecasting job has been queued.", - "job_monitor_url": "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", - "job_results_url": "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", + "job-url": "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", + "results-url": "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", "forecast": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", - "_deprecated_fields": { + "deprecated-fields": { "forecast": { - "use": "job_id", - "deprecated_since": "1.0.0", - "note": "The 'forecast' response field is deprecated; use 'job_id' instead" + "use": "job", + "deprecated-since": "1.0.0", + "note": "The 'forecast' response field is deprecated; use 'job' instead" } } } @@ -1572,7 +1572,7 @@ "schema": { "type": "object", "properties": { - "job_id": { + "job": { "type": "string", "description": "UUID of the queued scheduling job (canonical field; use this)." }, @@ -1585,21 +1585,21 @@ "message": { "type": "string" }, - "job_monitor_url": { + "job-url": { "type": "string", "format": "uri", "description": "URL to query the generic job status API." }, - "job_results_url": { + "results-url": { "type": "string", "format": "uri", "description": "URL to fetch the schedule results for this job once it is complete." }, "schedule": { "type": "string", - "description": "(DEPRECATED) UUID of the queued scheduling job. Use `job_id` instead." + "description": "(DEPRECATED) UUID of the queued scheduling job. Use `job` instead." }, - "_deprecated_fields": { + "deprecated-fields": { "type": "object", "description": "Machine-readable mapping of deprecated fields with migration guidance." } @@ -1608,19 +1608,19 @@ "examples": { "schedule_created": { "summary": "Schedule response", - "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job_id` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job_id` instead. The `_deprecated_fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule: see /sensors//schedules/.\n", + "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job` instead. The `deprecated-fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule: see /sensors//schedules/.\n", "value": { "status": "ACCEPTED", - "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_results_url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "results-url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", "message": "Request has been accepted for processing.", - "_deprecated_fields": { + "deprecated-fields": { "schedule": { - "use": "job_id", - "deprecated_since": "1.0.0", - "note": "The 'schedule' response field is deprecated; use 'job_id'" + "use": "job", + "deprecated-since": "1.0.0", + "note": "The 'schedule' response field is deprecated; use 'job'" } } } @@ -4155,18 +4155,18 @@ }, "examples": { "successful_response": { - "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job_id` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job_id` instead. The `_deprecated_fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).\n", + "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job` instead. The `deprecated-fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).\n", "value": { "status": "ACCEPTED", - "job_id": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "job_monitor_url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", + "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", "message": "Request has been accepted for processing.", - "_deprecated_fields": { + "deprecated-fields": { "schedule": { - "use": "job_id", - "deprecated_since": "1.0.0", - "note": "The 'schedule' response field is deprecated; use 'job_id'" + "use": "job", + "deprecated-since": "1.0.0", + "note": "The 'schedule' response field is deprecated; use 'job'" } } } @@ -4404,7 +4404,7 @@ ], "responses": { "200": { - "description": "Job status retrieved successfully.", + "description": "Finished job status retrieved successfully.", "content": { "application/json": { "schema": { @@ -4432,7 +4432,7 @@ "description": "Return value of the job function, or null when not yet available. For a finished scheduling job, this is an object; a StorageScheduler job populates it with unresolved/resolved soft state-of-charge constraint analysis (empty arrays when the flex model defines no soc-minima/soc-maxima, or when a scheduler other than StorageScheduler was used). The num-beliefs field holds the total number of beliefs (scheduled values) saved to the database.\n", "nullable": true }, - "func_name": { + "func-name": { "type": "string", "description": "Fully-qualified name of the function executed by this job." }, @@ -4440,28 +4440,64 @@ "type": "string", "description": "Name of the queue the job was placed on." }, - "enqueued_at": { + "enqueued-at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO-8601 timestamp of when the job was enqueued." }, - "started_at": { + "started-at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO-8601 timestamp of when the job started executing." }, - "ended_at": { + "ended-at": { "type": "string", "format": "date-time", "nullable": true, "description": "ISO-8601 timestamp of when the job finished executing." }, - "exc_info": { + "exc-info": { "type": "string", "nullable": true, "description": "Traceback information for failed jobs, or null otherwise." + }, + "func_name": { + "type": "string", + "deprecated": true, + "description": "(DEPRECATED) Fully-qualified name of the function executed by this job. Use `func-name` instead." + }, + "enqueued_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "deprecated": true, + "description": "(DEPRECATED) ISO-8601 timestamp of when the job was enqueued. Use `enqueued-at` instead." + }, + "started_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "deprecated": true, + "description": "(DEPRECATED) ISO-8601 timestamp of when the job started executing. Use `started-at` instead." + }, + "ended_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "deprecated": true, + "description": "(DEPRECATED) ISO-8601 timestamp of when the job finished executing. Use `ended-at` instead." + }, + "exc_info": { + "type": "string", + "nullable": true, + "deprecated": true, + "description": "(DEPRECATED) Traceback information for failed jobs, or null otherwise. Use `exc-info` instead." + }, + "deprecated-fields": { + "type": "object", + "description": "Machine-readable mapping of deprecated fields with migration guidance." } } }, @@ -4472,12 +4508,39 @@ "status": "QUEUED", "message": "Scheduling job waiting to be processed.", "result": null, - "func_name": "flexmeasures.data.services.scheduling.create_schedule", + "func-name": "flexmeasures.data.services.scheduling.create_schedule", "origin": "scheduling", + "enqueued-at": "2026-04-28T10:00:00+00:00", + "started-at": null, + "ended-at": null, + "exc-info": null, + "func_name": "flexmeasures.data.services.scheduling.create_schedule", "enqueued_at": "2026-04-28T10:00:00+00:00", "started_at": null, "ended_at": null, - "exc_info": null + "exc_info": null, + "deprecated-fields": { + "func_name": { + "use": "func-name", + "deprecated-since": "1.0.0" + }, + "enqueued_at": { + "use": "enqueued-at", + "deprecated-since": "1.0.0" + }, + "started_at": { + "use": "started-at", + "deprecated-since": "1.0.0" + }, + "ended_at": { + "use": "ended-at", + "deprecated-since": "1.0.0" + }, + "exc_info": { + "use": "exc-info", + "deprecated-since": "1.0.0" + } + } } }, "finished": { @@ -4504,12 +4567,12 @@ "resolved": [], "num-beliefs": 96 }, - "func_name": "flexmeasures.data.services.scheduling.create_schedule", + "func-name": "flexmeasures.data.services.scheduling.create_schedule", "origin": "scheduling", - "enqueued_at": "2026-04-28T10:00:00+00:00", - "started_at": "2026-04-28T10:00:01+00:00", - "ended_at": "2026-04-28T10:00:05+00:00", - "exc_info": null + "enqueued-at": "2026-04-28T10:00:00+00:00", + "started-at": "2026-04-28T10:00:01+00:00", + "ended-at": "2026-04-28T10:00:05+00:00", + "exc-info": null } }, "failed": { @@ -4518,18 +4581,24 @@ "status": "FAILED", "message": "Scheduling job failed with ValueError: ...", "result": null, - "func_name": "flexmeasures.data.services.scheduling.create_schedule", + "func-name": "flexmeasures.data.services.scheduling.create_schedule", "origin": "scheduling", - "enqueued_at": "2026-04-28T10:00:00+00:00", - "started_at": "2026-04-28T10:00:01+00:00", - "ended_at": "2026-04-28T10:00:02+00:00", - "exc_info": "Traceback (most recent call last): ..." + "enqueued-at": "2026-04-28T10:00:00+00:00", + "started-at": "2026-04-28T10:00:01+00:00", + "ended-at": "2026-04-28T10:00:02+00:00", + "exc-info": "Traceback (most recent call last): ..." } } } } } }, + "202": { + "description": "Job is still queued, scheduled, deferred, or running." + }, + "422": { + "description": "Job has failed." + }, "404": { "description": "NOT_FOUND" }, From e75848994cf38979a15e5241a6777736920c880f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Fri, 17 Jul 2026 18:45:27 +0200 Subject: [PATCH 6/8] Put info about deprecated fields into the header; also react to some other review comments dealing with docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/api/change_log.rst | 6 +- documentation/api/introduction.rst | 49 +++-- documentation/changelog.rst | 4 +- flexmeasures/api/common/responses.py | 32 ++-- flexmeasures/api/v3_0/assets.py | 23 +-- flexmeasures/api/v3_0/jobs.py | 79 ++++---- flexmeasures/api/v3_0/sensors.py | 50 +++--- .../tests/test_forecasting_api_fresh_db.py | 4 + flexmeasures/api/v3_0/tests/test_jobs_api.py | 16 +- .../api/v3_0/tests/test_sensor_schedules.py | 4 +- .../tests/test_sensor_schedules_fresh_db.py | 4 + flexmeasures/ui/static/openapi-specs.json | 169 +++++++++++------- 12 files changed, 260 insertions(+), 180 deletions(-) diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 1b248207bc..f22e8a9813 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -11,9 +11,9 @@ 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) are deprecated but remain in responses for backward compatibility. All responses include a ``deprecated-fields`` object with machine-readable migration guidance for each deprecated field, including the recommended replacement and version in which it was deprecated. -- ``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. -- ``GET /api/v3_0/jobs/`` now returns kebab-case metadata fields such as ``func-name`` and ``enqueued-at``. Legacy snake_case metadata fields such as ``func_name`` and ``enqueued_at`` remain available and are listed under ``deprecated-fields``. + * Legacy ``schedule`` field (in scheduling endpoints) and ``forecast`` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. Responses containing these fields include ``Deprecation`` and ``Link`` headers pointing to migration guidance. +- ``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 returns kebab-case metadata fields such as ``func-name`` and ``enqueued-at``. Legacy snake_case metadata fields such as ``func_name`` and ``enqueued_at`` remain available and are signalled with ``Deprecation`` and ``Link`` headers. v3.0-31 | 2026-06-01 diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index 33635aeb09..94301f59ec 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -257,39 +257,52 @@ Deprecated response fields ^^^^^^^^^^^^^^^^^^^^^^^^^^^ In addition to deprecating entire endpoints, we sometimes deprecate individual fields in API responses while maintaining backward compatibility by including both the legacy and canonical fields. -When this happens, responses include a ``deprecated-fields`` object containing machine-readable information about deprecated fields. +When this happens, responses include a ``Deprecation`` response header and a ``Link`` header pointing to migration guidance. -For example, when scheduling endpoints switched from ``schedule`` to ``job`` as the canonical field identifier for background jobs: +For example, when scheduling endpoints switched from ``schedule`` to ``job`` as the canonical field identifier for background jobs, a response that still contains the legacy ``schedule`` field also carries deprecation headers: -.. code-block:: json +.. code-block:: http + + HTTP/1.1 202 Accepted + Deprecation: true + Link: ; rel="deprecation"; type="text/html" + Content-Type: application/json { "status": "ACCEPTED", "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "results-url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "deprecated-fields": { - "schedule": { - "use": "job", - "deprecated-since": "1.0.0", - "note": "The 'schedule' response field is deprecated; use 'job' instead" - } - } + "results-url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" } -Clients should: +In this example, clients should treat ``job`` as the canonical field and ``schedule`` as a backward-compatible alias. +The ``Deprecation`` header tells clients that the response includes deprecated content, and the ``Link`` header points to migration guidance. + +Professional API users should monitor API responses for the ``"Deprecation"`` and ``"Sunset"`` response headers [see `draft-ietf-httpapi-deprecation-header-02 `_ and `RFC 8594 `_, respectively], so system administrators can be warned when using API endpoints that are flagged for deprecation and/or are likely to become unresponsive in the future. + +For deprecated response fields, clients should: -- Use the ``deprecated-fields`` object to identify which fields are deprecated in their version. -- Migrate to use the canonical field names (indicated by the ``"use"`` field). +- Monitor the ``Deprecation`` response header to detect responses that contain deprecated fields. +- Follow the ``Link`` response header to find migration guidance. +- Migrate to use the canonical field names documented in the API schema. - Plan upgrades based on the deprecation guidance to avoid breakage when deprecated fields are eventually removed in a future API version. -Clients -^^^^^^^ +Client code should therefore inspect both headers and body fields, for example: -Professional API users should monitor API responses for the ``"Deprecation"`` and ``"Sunset"`` response headers [see `draft-ietf-httpapi-deprecation-header-02 `_ and `RFC 8594 `_, respectively], so system administrators can be warned when using API endpoints that are flagged for deprecation and/or are likely to become unresponsive in the future. +.. code-block:: python + + response = requests.post(trigger_url, json=payload, headers=headers) + response.raise_for_status() + + if response.headers.get("Deprecation") == "true": + print(f"Deprecated response fields detected; see {response.headers.get('Link')}") + + data = response.json() + job_id = data["job"] -The deprecation header field shows an `IMF-fixdate `_ indicating when the API endpoint was deprecated. +For endpoint deprecations, the deprecation header field shows an `IMF-fixdate `_ indicating when the API endpoint was deprecated. +For deprecated response fields, the header value can be ``true`` and the ``Link`` header points to migration guidance. The sunset header field shows an `IMF-fixdate `_ indicating when the API endpoint is likely to become unresponsive. More information about a deprecation, sunset, and possibly recommended replacements, can be found under the ``"Link"`` response header. Relevant relations are: diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 2289d5fdfd..e7e55d1af9 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. 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). .. warning:: Upgrading to this version requires running ``flexmeasures db upgrade`` (you can create a backup first with ``flexmeasures db-ops dump``). @@ -32,7 +32,7 @@ Infrastructure / Support ---------------------- * Upgraded dependencies [see `PR #1485 `_, `PR #2215 `_ and `PR #2243 `_] * 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 a migration guide in the API docs [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 but marked deprecated with migration guidance in :ref:`api_background_jobs` [see `PR #2224 `_]. * 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 `_] * Document source filters better, and make use of the source-types filter in the PV curtailment tutorial [see `PR #2261 `_] diff --git a/flexmeasures/api/common/responses.py b/flexmeasures/api/common/responses.py index ac6deb8d12..5e6e15f236 100644 --- a/flexmeasures/api/common/responses.py +++ b/flexmeasures/api/common/responses.py @@ -6,7 +6,6 @@ from flask import url_for from flexmeasures.auth.error_handling import FORBIDDEN_MSG, FORBIDDEN_STATUS_CODE -from flexmeasures.utils.time_utils import server_now p = inflect.engine() @@ -15,16 +14,32 @@ ResponseTuple = tuple[dict, int] | tuple[dict, int, dict] +DEPRECATED_RESPONSE_FIELDS_LINK = ( + "https://flexmeasures.readthedocs.io/en/latest/api/" + "introduction.html#background-job-monitoring" +) + + +def deprecated_response_fields_headers() -> dict[str, str]: + """Headers used when a response contains deprecated fields.""" + return { + "Deprecation": "true", + "Link": f'<{DEPRECATED_RESPONSE_FIELDS_LINK}>; rel="deprecation"; type="text/html"', + } + + def is_response_tuple(value) -> bool: """Check if an object qualifies as a ResponseTuple""" if not isinstance(value, tuple): return False - if not len(value) == 2: + if len(value) not in (2, 3): return False if not isinstance(value[0], dict): return False if not isinstance(value[1], int): return False + if len(value) == 3 and not isinstance(value[2], dict): + return False return True @@ -385,7 +400,6 @@ def request_accepted_for_processing( job_id: str, message: str = "Request has been accepted for processing.", legacy_key: str | None = None, - deprecated_since: str | None = None, job_results_url: str | None = None, ) -> ResponseTuple: """ @@ -393,8 +407,7 @@ def request_accepted_for_processing( Optional backwards-compatibility: if `legacy_key` is provided the response will include the job id under that legacy key (e.g. `schedule` or - `forecast`). The response will also include a `deprecated-fields` object - with guidance for migrating to `job`. + `forecast`) and response headers marking the field deprecation. Optional `job_results_url` may be supplied to provide a direct link to the sensor-specific job results endpoint, e.g. `/api/v3_0/sensors//schedules/`. @@ -411,14 +424,7 @@ def request_accepted_for_processing( if legacy_key: # keep legacy key for backwards compatibility resp[legacy_key] = job_id - # include machine-readable deprecation info so clients can detect it - resp["deprecated-fields"] = { - legacy_key: { - "use": "job", - "deprecated-since": deprecated_since or server_now().date().isoformat(), - "note": f"The '{legacy_key}' response field is deprecated; use 'job'", - } - } + return resp, 202, deprecated_response_fields_headers() return resp, 202 diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index f1cb7044de..fcbcacaa6f 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -1489,6 +1489,17 @@ def trigger_schedule( responses: 202: description: ACCEPTED (Scheduling job queued for processing) + headers: + Deprecation: + description: Indicates that the response contains deprecated fields. + schema: + type: string + example: "true" + Link: + description: Link to migration guidance for deprecated response fields. + schema: + type: string + example: '; rel="deprecation"; type="text/html"' content: application/json: schema: @@ -1501,7 +1512,7 @@ def trigger_schedule( which will be picked up by a worker. The given UUID is returned in the canonical `job` field. For backward-compatibility, the legacy `schedule` field is also included but is deprecated; - use `job` instead. The `deprecated-fields` object provides migration guidance. + use `job` instead. The given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_). value: status: ACCEPTED @@ -1509,11 +1520,6 @@ def trigger_schedule( schedule: "364bfd06-c1fa-430b-8d25-8f5a547651fb" job-url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" message: "Request has been accepted for processing." - deprecated-fields: - schedule: - use: "job" - deprecated-since: "1.0.0" - note: "The 'schedule' response field is deprecated; use 'job'" 400: description: INVALID_DATA 401: @@ -1555,10 +1561,7 @@ def trigger_schedule( return unprocessable_entity(str(err)) # Keep legacy `schedule` key for backward compatibility; prefer `job`. - d, s = request_accepted_for_processing( - job.id, legacy_key="schedule", deprecated_since="1.0.0" - ) - return d, s + return request_accepted_for_processing(job.id, legacy_key="schedule") @route("//kpis", methods=["GET"]) @use_kwargs( diff --git a/flexmeasures/api/v3_0/jobs.py b/flexmeasures/api/v3_0/jobs.py index 315ee59537..04dc92c654 100644 --- a/flexmeasures/api/v3_0/jobs.py +++ b/flexmeasures/api/v3_0/jobs.py @@ -13,6 +13,7 @@ from flexmeasures.data.services.utils import failed_job_exc_info, job_status_description from flexmeasures.auth.policy import check_access +from flexmeasures.api.common.responses import deprecated_response_fields_headers from flexmeasures.data import db from flexmeasures.data.models.time_series import Sensor from flexmeasures.data.services.utils import get_asset_or_sensor_from_ref @@ -55,29 +56,6 @@ def _job_queue_unavailable_response(): ) -JOB_STATUS_DEPRECATED_FIELDS = { - "func_name": "func-name", - "enqueued_at": "enqueued-at", - "started_at": "started-at", - "ended_at": "ended-at", - "exc_info": "exc-info", -} - - -def _job_status_deprecated_fields() -> dict[str, dict[str, str]]: - return { - legacy_key: { - "use": canonical_key, - "deprecated-since": "1.0.0", - "note": ( - f"The '{legacy_key}' response field is deprecated; " - f"use '{canonical_key}' instead." - ), - } - for legacy_key, canonical_key in JOB_STATUS_DEPRECATED_FIELDS.items() - } - - class JobAPI(FlaskView): """ Endpoint for querying the status of background jobs by UUID. @@ -135,6 +113,17 @@ def get_job_status(self, job_id: str, **kwargs): responses: 200: description: Finished job status retrieved successfully. + headers: + Deprecation: + description: Indicates that the response contains deprecated fields. + schema: + type: string + example: "true" + Link: + description: Link to migration guidance for deprecated response fields. + schema: + type: string + example: '; rel="deprecation"; type="text/html"' content: application/json: schema: @@ -219,9 +208,6 @@ def get_job_status(self, job_id: str, **kwargs): nullable: true deprecated: true description: (DEPRECATED) Traceback information for failed jobs, or null otherwise. Use `exc-info` instead. - deprecated-fields: - type: object - description: Machine-readable mapping of deprecated fields with migration guidance. examples: queued: summary: Queued job @@ -240,22 +226,6 @@ def get_job_status(self, job_id: str, **kwargs): started_at: null ended_at: null exc_info: null - deprecated-fields: - func_name: - use: "func-name" - deprecated-since: "1.0.0" - enqueued_at: - use: "enqueued-at" - deprecated-since: "1.0.0" - started_at: - use: "started-at" - deprecated-since: "1.0.0" - ended_at: - use: "ended-at" - deprecated-since: "1.0.0" - exc_info: - use: "exc-info" - deprecated-since: "1.0.0" finished: summary: Finished job value: @@ -291,8 +261,30 @@ def get_job_status(self, job_id: str, **kwargs): exc-info: "Traceback (most recent call last): ..." 202: description: Job is still queued, scheduled, deferred, or running. + headers: + Deprecation: + description: Indicates that the response contains deprecated fields. + schema: + type: string + example: "true" + Link: + description: Link to migration guidance for deprecated response fields. + schema: + type: string + example: '; rel="deprecation"; type="text/html"' 422: description: Job has failed. + headers: + Deprecation: + description: Indicates that the response contains deprecated fields. + schema: + type: string + example: "true" + Link: + description: Link to migration guidance for deprecated response fields. + schema: + type: string + example: '; rel="deprecation"; type="text/html"' 404: description: NOT_FOUND 401: @@ -354,7 +346,6 @@ def get_job_status(self, job_id: str, **kwargs): response["started_at"] = response["started-at"] response["ended_at"] = response["ended-at"] response["exc_info"] = response["exc-info"] - response["deprecated-fields"] = _job_status_deprecated_fields() if status_name == JobStatus.FAILED.name: status_code = 422 @@ -363,4 +354,4 @@ def get_job_status(self, job_id: str, **kwargs): else: status_code = 202 - return response, status_code + return response, status_code, deprecated_response_fields_headers() diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 50782d37f4..2a859f3ca3 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -972,6 +972,17 @@ def trigger_schedule( responses: 202: description: ACCEPTED (Scheduling job queued for processing) + headers: + Deprecation: + description: Indicates that the response contains deprecated fields. + schema: + type: string + example: "true" + Link: + description: Link to migration guidance for deprecated response fields. + schema: + type: string + example: '; rel="deprecation"; type="text/html"' content: application/json: schema: @@ -995,10 +1006,8 @@ def trigger_schedule( description: URL to fetch the schedule results for this job once it is complete. schedule: type: string + deprecated: true description: (DEPRECATED) UUID of the queued scheduling job. Use `job` instead. - deprecated-fields: - type: object - description: Machine-readable mapping of deprecated fields with migration guidance. examples: schedule_created: summary: Schedule response @@ -1008,7 +1017,7 @@ def trigger_schedule( which will be picked up by a worker. The given UUID is returned in the canonical `job` field. For backward-compatibility, the legacy `schedule` field is also included but is deprecated; - use `job` instead. The `deprecated-fields` object provides migration guidance. + use `job` instead. The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. value: status: "ACCEPTED" @@ -1017,11 +1026,6 @@ def trigger_schedule( job-url: "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb" results-url: "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb" message: "Request has been accepted for processing." - deprecated-fields: - schedule: - use: "job" - deprecated-since: "1.0.0" - note: "The 'schedule' response field is deprecated; use 'job'" 400: description: INVALID_DATA 401: @@ -1070,15 +1074,13 @@ def trigger_schedule( db.session.commit() # Keep legacy `schedule` key for backward compatibility; prefer `job`. - d, s = request_accepted_for_processing( + return request_accepted_for_processing( job.id, legacy_key="schedule", - deprecated_since="1.0.0", job_results_url=url_for( "SensorAPI:get_schedule", id=sensor.id, uuid=job.id ), ) - return dict(**d), s # mark endpoint as deprecated trigger_schedule.after_request = lambda response: add_deprecation_header(response) @@ -1972,6 +1974,17 @@ def trigger_forecast(self, id: int, **params): responses: 202: description: ACCEPTED (Forecasting job queued for processing) + headers: + Deprecation: + description: Indicates that the response contains deprecated fields. + schema: + type: string + example: "true" + Link: + description: Link to migration guidance for deprecated response fields. + schema: + type: string + example: '; rel="deprecation"; type="text/html"' content: application/json: schema: @@ -1995,10 +2008,8 @@ def trigger_forecast(self, id: int, **params): description: URL to fetch the forecast results for this job once it is complete. forecast: type: string + deprecated: true description: (DEPRECATED) UUID of the queued forecasting job. Use `job` instead. - deprecated-fields: - type: object - description: Machine-readable mapping of deprecated fields with migration guidance. example: status: "ACCEPTED" job: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" @@ -2006,11 +2017,6 @@ def trigger_forecast(self, id: int, **params): job-url: "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" results-url: "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" forecast: "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" - deprecated-fields: - forecast: - use: "job" - deprecated-since: "1.0.0" - note: "The 'forecast' response field is deprecated; use 'job' instead" 401: description: UNAUTHORIZED 403: @@ -2052,15 +2058,13 @@ def trigger_forecast(self, id: int, **params): return unprocessable_entity(str(e)) # Keep legacy `forecast` key for backward compatibility; prefer `job`. - d, s = request_accepted_for_processing( + return request_accepted_for_processing( pipeline_returns["job_id"], legacy_key="forecast", - deprecated_since="1.0.0", job_results_url=url_for( "SensorAPI:get_forecast", id=id, uuid=pipeline_returns["job_id"] ), ) - return dict(**d), s @route("//forecasts/", methods=["GET"]) @use_kwargs( diff --git a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py index 8877dc8343..dfe09368b1 100644 --- a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py @@ -49,6 +49,10 @@ def test_trigger_and_fetch_forecasts( trigger_url, json=payload, headers={"Authorization": token} ) assert trigger_res.status_code == 202, trigger_res.json + assert trigger_res.headers["Deprecation"] == "true" + assert 'rel="deprecation"' in trigger_res.headers["Link"] + assert "background-job-monitoring" in trigger_res.headers["Link"] + assert "deprecated-fields" not in trigger_res.json assert trigger_res.json["results-url"] == url_for( "SensorAPI:get_forecast", id=sensor_0.id, uuid=trigger_res.json["forecast"] ) diff --git a/flexmeasures/api/v3_0/tests/test_jobs_api.py b/flexmeasures/api/v3_0/tests/test_jobs_api.py index c87c6d2465..89d5348b80 100644 --- a/flexmeasures/api/v3_0/tests/test_jobs_api.py +++ b/flexmeasures/api/v3_0/tests/test_jobs_api.py @@ -31,11 +31,15 @@ def assert_legacy_job_status_fields(data: dict): - assert data["deprecated-fields"].keys() >= JOB_STATUS_DEPRECATED_FIELDS.keys() for legacy_key, canonical_key in JOB_STATUS_DEPRECATED_FIELDS.items(): assert data[legacy_key] == data[canonical_key] - assert data["deprecated-fields"][legacy_key]["use"] == canonical_key - assert data["deprecated-fields"][legacy_key]["deprecated-since"] == "1.0.0" + assert "deprecated-fields" not in data + + +def assert_deprecated_response_fields_headers(response): + assert response.headers["Deprecation"] == "true" + assert 'rel="deprecation"' in response.headers["Link"] + assert "background-job-monitoring" in response.headers["Link"] @pytest.mark.parametrize( @@ -103,6 +107,7 @@ def test_get_job_status_requires_read_access( assert response.status_code == expected_status_code if expected_status_code == 202: assert response.json["status"] == "QUEUED" + assert_deprecated_response_fields_headers(response) else: assert response.json["status"] == "INVALID_SENDER" @@ -153,6 +158,8 @@ def test_get_job_status_queued( assert data["result"] is None assert data["exc-info"] is None assert_legacy_job_status_fields(data) + assert_deprecated_response_fields_headers(response) + assert_deprecated_response_fields_headers(response) @pytest.mark.parametrize( @@ -201,6 +208,7 @@ def test_get_job_status_started( assert data["result"] is None assert data["exc-info"] is None assert_legacy_job_status_fields(data) + assert_deprecated_response_fields_headers(response) @pytest.mark.parametrize( @@ -387,6 +395,7 @@ def fail_with_assertion(self): assert "assert 1 == 2" in data["message"] assert "AssertionError: assert 1 == 2" in data["exc-info"] assert_legacy_job_status_fields(data) + assert_deprecated_response_fields_headers(response) @pytest.mark.parametrize( @@ -426,6 +435,7 @@ def test_get_job_status_failed_infeasible_schedule_includes_exc_info( "ValueError: The input data yields an infeasible problem." in data["exc-info"] ) assert_legacy_job_status_fields(data) + assert_deprecated_response_fields_headers(response) def test_get_job_status_unauthenticated( diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index d4c28e4cd1..a5247d1ef6 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -297,8 +297,10 @@ def test_trigger_and_get_schedule_with_unknown_prices( json=message, ) print("Server responded with:\n%s" % trigger_schedule_response.json) - check_deprecation(trigger_schedule_response, deprecation=None, sunset=None) assert trigger_schedule_response.status_code == 202 + assert trigger_schedule_response.headers["Deprecation"] == "true" + assert 'rel="deprecation"' in trigger_schedule_response.headers["Link"] + assert "background-job-monitoring" in trigger_schedule_response.headers["Link"] job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py index b1180598a5..901e7dba1d 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py @@ -74,6 +74,10 @@ def test_trigger_and_get_schedule( ) print("Server responded with:\n%s" % trigger_schedule_response.json) assert trigger_schedule_response.status_code == 202 + assert trigger_schedule_response.headers["Deprecation"] == "true" + assert 'rel="deprecation"' in trigger_schedule_response.headers["Link"] + assert "background-job-monitoring" in trigger_schedule_response.headers["Link"] + assert "deprecated-fields" not in trigger_schedule_response.json assert trigger_schedule_response.json["results-url"] == url_for( "SensorAPI:get_schedule", id=sensor.id, diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index be1d7809ae..61e8cbe51a 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -1389,6 +1389,22 @@ "responses": { "202": { "description": "ACCEPTED (Forecasting job queued for processing)", + "headers": { + "Deprecation": { + "description": "Indicates that the response contains deprecated fields.", + "schema": { + "type": "string", + "example": "true" + } + }, + "Link": { + "description": "Link to migration guidance for deprecated response fields.", + "schema": { + "type": "string", + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + } + }, "content": { "application/json": { "schema": { @@ -1419,11 +1435,8 @@ }, "forecast": { "type": "string", + "deprecated": true, "description": "(DEPRECATED) UUID of the queued forecasting job. Use `job` instead." - }, - "deprecated-fields": { - "type": "object", - "description": "Machine-readable mapping of deprecated fields with migration guidance." } } }, @@ -1433,14 +1446,7 @@ "message": "Forecasting job has been queued.", "job-url": "/api/v3_0/jobs/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", "results-url": "/api/v3_0/sensors/3/forecasts/b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", - "forecast": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b", - "deprecated-fields": { - "forecast": { - "use": "job", - "deprecated-since": "1.0.0", - "note": "The 'forecast' response field is deprecated; use 'job' instead" - } - } + "forecast": "b3d26a8a-7a43-4a9f-93e1-fc2a869ea97b" } } } @@ -1567,6 +1573,22 @@ "responses": { "202": { "description": "ACCEPTED (Scheduling job queued for processing)", + "headers": { + "Deprecation": { + "description": "Indicates that the response contains deprecated fields.", + "schema": { + "type": "string", + "example": "true" + } + }, + "Link": { + "description": "Link to migration guidance for deprecated response fields.", + "schema": { + "type": "string", + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + } + }, "content": { "application/json": { "schema": { @@ -1597,32 +1619,22 @@ }, "schedule": { "type": "string", + "deprecated": true, "description": "(DEPRECATED) UUID of the queued scheduling job. Use `job` instead." - }, - "deprecated-fields": { - "type": "object", - "description": "Machine-readable mapping of deprecated fields with migration guidance." } } }, "examples": { "schedule_created": { "summary": "Schedule response", - "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job` instead. The `deprecated-fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule: see /sensors//schedules/.\n", + "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job` instead.\nThe given UUID may be used to obtain the resulting schedule: see /sensors//schedules/.\n", "value": { "status": "ACCEPTED", "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", "results-url": "/api/v3_0/sensors/3/schedules/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "message": "Request has been accepted for processing.", - "deprecated-fields": { - "schedule": { - "use": "job", - "deprecated-since": "1.0.0", - "note": "The 'schedule' response field is deprecated; use 'job'" - } - } + "message": "Request has been accepted for processing." } } } @@ -4148,6 +4160,22 @@ "responses": { "202": { "description": "ACCEPTED (Scheduling job queued for processing)", + "headers": { + "Deprecation": { + "description": "Indicates that the response contains deprecated fields.", + "schema": { + "type": "string", + "example": "true" + } + }, + "Link": { + "description": "Link to migration guidance for deprecated response fields.", + "schema": { + "type": "string", + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + } + }, "content": { "application/json": { "schema": { @@ -4155,20 +4183,13 @@ }, "examples": { "successful_response": { - "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job` instead. The `deprecated-fields` object provides migration guidance.\nThe given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).\n", + "description": "This message indicates that the scheduling request has been accepted for processing (202 Accepted).\nA scheduling job has been created with some Universally Unique Identifier (UUID),\nwhich will be picked up by a worker.\nThe given UUID is returned in the canonical `job` field.\nFor backward-compatibility, the legacy `schedule` field is also included but is deprecated;\nuse `job` instead.\nThe given UUID may be used to obtain the resulting schedule for each flexible device via [/sensors/schedules/](#/Sensors/get_api_v3_0_sensors__id__schedules__uuid_).\n", "value": { "status": "ACCEPTED", "job": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", "job-url": "/api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb", - "message": "Request has been accepted for processing.", - "deprecated-fields": { - "schedule": { - "use": "job", - "deprecated-since": "1.0.0", - "note": "The 'schedule' response field is deprecated; use 'job'" - } - } + "message": "Request has been accepted for processing." } } } @@ -4405,6 +4426,22 @@ "responses": { "200": { "description": "Finished job status retrieved successfully.", + "headers": { + "Deprecation": { + "description": "Indicates that the response contains deprecated fields.", + "schema": { + "type": "string", + "example": "true" + } + }, + "Link": { + "description": "Link to migration guidance for deprecated response fields.", + "schema": { + "type": "string", + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + } + }, "content": { "application/json": { "schema": { @@ -4494,10 +4531,6 @@ "nullable": true, "deprecated": true, "description": "(DEPRECATED) Traceback information for failed jobs, or null otherwise. Use `exc-info` instead." - }, - "deprecated-fields": { - "type": "object", - "description": "Machine-readable mapping of deprecated fields with migration guidance." } } }, @@ -4518,29 +4551,7 @@ "enqueued_at": "2026-04-28T10:00:00+00:00", "started_at": null, "ended_at": null, - "exc_info": null, - "deprecated-fields": { - "func_name": { - "use": "func-name", - "deprecated-since": "1.0.0" - }, - "enqueued_at": { - "use": "enqueued-at", - "deprecated-since": "1.0.0" - }, - "started_at": { - "use": "started-at", - "deprecated-since": "1.0.0" - }, - "ended_at": { - "use": "ended-at", - "deprecated-since": "1.0.0" - }, - "exc_info": { - "use": "exc-info", - "deprecated-since": "1.0.0" - } - } + "exc_info": null } }, "finished": { @@ -4594,10 +4605,42 @@ } }, "202": { - "description": "Job is still queued, scheduled, deferred, or running." + "description": "Job is still queued, scheduled, deferred, or running.", + "headers": { + "Deprecation": { + "description": "Indicates that the response contains deprecated fields.", + "schema": { + "type": "string", + "example": "true" + } + }, + "Link": { + "description": "Link to migration guidance for deprecated response fields.", + "schema": { + "type": "string", + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + } + } }, "422": { - "description": "Job has failed." + "description": "Job has failed.", + "headers": { + "Deprecation": { + "description": "Indicates that the response contains deprecated fields.", + "schema": { + "type": "string", + "example": "true" + } + }, + "Link": { + "description": "Link to migration guidance for deprecated response fields.", + "schema": { + "type": "string", + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + } + } }, "404": { "description": "NOT_FOUND" From 5a00aee1a8dde2fbda2eea0310cbbe049b867735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Mon, 20 Jul 2026 18:34:52 +0200 Subject: [PATCH 7/8] clarify how to signal deprecated fields in API endpoints, introducing the FlexMeasures-Deprecated-Response-Fields header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- documentation/api/change_log.rst | 4 +- documentation/api/introduction.rst | 22 +++--- flexmeasures/api/common/responses.py | 28 +++++--- flexmeasures/api/v3_0/assets.py | 22 +++++- flexmeasures/api/v3_0/deprecations.py | 13 ++++ flexmeasures/api/v3_0/jobs.py | 50 ++++++++++++-- flexmeasures/api/v3_0/sensors.py | 31 +++++++-- .../tests/test_asset_schedules_fresh_db.py | 15 ++++ .../tests/test_forecasting_api_fresh_db.py | 7 +- flexmeasures/api/v3_0/tests/test_jobs_api.py | 9 ++- .../api/v3_0/tests/test_sensor_schedules.py | 21 +++++- .../tests/test_sensor_schedules_fresh_db.py | 16 ++++- flexmeasures/ui/static/openapi-specs.json | 68 +++++++++++++++---- 13 files changed, 252 insertions(+), 54 deletions(-) create mode 100644 flexmeasures/api/v3_0/deprecations.py diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index f22e8a9813..d3f454a6dc 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -11,9 +11,9 @@ 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) are deprecated but remain in responses for backward compatibility. Responses containing these fields include ``Deprecation`` and ``Link`` headers pointing to migration guidance. + * Legacy ``schedule`` field (in scheduling endpoints) and ``forecast`` field (in forecasting endpoints) are deprecated but remain in responses for backward compatibility. Responses containing these fields include a dated ``Deprecation`` header, a ``Link`` header pointing to migration guidance, and a ``FlexMeasures-Deprecated-Response-Fields`` header naming the deprecated fields. - ``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 returns kebab-case metadata fields such as ``func-name`` and ``enqueued-at``. Legacy snake_case metadata fields such as ``func_name`` and ``enqueued_at`` remain available and are signalled with ``Deprecation`` and ``Link`` headers. +- ``GET /api/v3_0/jobs/`` now returns kebab-case metadata fields such as ``func-name`` and ``enqueued-at``. Legacy snake_case metadata fields such as ``func_name`` and ``enqueued_at`` remain available and are signalled with a dated ``Deprecation`` header, a ``Link`` header, and a ``FlexMeasures-Deprecated-Response-Fields`` header. v3.0-31 | 2026-06-01 diff --git a/documentation/api/introduction.rst b/documentation/api/introduction.rst index 94301f59ec..657666227e 100644 --- a/documentation/api/introduction.rst +++ b/documentation/api/introduction.rst @@ -257,15 +257,16 @@ Deprecated response fields ^^^^^^^^^^^^^^^^^^^^^^^^^^^ In addition to deprecating entire endpoints, we sometimes deprecate individual fields in API responses while maintaining backward compatibility by including both the legacy and canonical fields. -When this happens, responses include a ``Deprecation`` response header and a ``Link`` header pointing to migration guidance. +When this happens, responses include a ``Deprecation`` response header with the deprecation date, a ``Link`` header pointing to migration guidance, and a ``FlexMeasures-Deprecated-Response-Fields`` header identifying the deprecated fields in that response. For example, when scheduling endpoints switched from ``schedule`` to ``job`` as the canonical field identifier for background jobs, a response that still contains the legacy ``schedule`` field also carries deprecation headers: .. code-block:: http HTTP/1.1 202 Accepted - Deprecation: true - Link: ; rel="deprecation"; type="text/html" + Deprecation: Wed, 01 Jul 2026 00:00:00 GMT + Link: ; rel="deprecation"; type="text/html" + FlexMeasures-Deprecated-Response-Fields: schedule Content-Type: application/json { @@ -277,13 +278,16 @@ For example, when scheduling endpoints switched from ``schedule`` to ``job`` as } In this example, clients should treat ``job`` as the canonical field and ``schedule`` as a backward-compatible alias. -The ``Deprecation`` header tells clients that the response includes deprecated content, and the ``Link`` header points to migration guidance. +The ``FlexMeasures-Deprecated-Response-Fields`` header tells clients which response fields are deprecated, the ``Deprecation`` header tells clients when they were deprecated, and the ``Link`` header points to migration guidance for this endpoint. Professional API users should monitor API responses for the ``"Deprecation"`` and ``"Sunset"`` response headers [see `draft-ietf-httpapi-deprecation-header-02 `_ and `RFC 8594 `_, respectively], so system administrators can be warned when using API endpoints that are flagged for deprecation and/or are likely to become unresponsive in the future. +The ``Deprecation`` header may describe either the endpoint itself or specific response fields. +Clients can tell the difference by checking for ``FlexMeasures-Deprecated-Response-Fields``: if it is present, only the named response fields are deprecated; if it is absent, the deprecation applies to the endpoint or API version as a whole. For deprecated response fields, clients should: -- Monitor the ``Deprecation`` response header to detect responses that contain deprecated fields. +- Monitor the ``Deprecation`` response header to detect deprecated API behavior. +- Use the ``FlexMeasures-Deprecated-Response-Fields`` header to identify which response fields are deprecated. - Follow the ``Link`` response header to find migration guidance. - Migrate to use the canonical field names documented in the API schema. - Plan upgrades based on the deprecation guidance to avoid breakage when deprecated fields are eventually removed in a future API version. @@ -295,14 +299,16 @@ Client code should therefore inspect both headers and body fields, for example: response = requests.post(trigger_url, json=payload, headers=headers) response.raise_for_status() - if response.headers.get("Deprecation") == "true": - print(f"Deprecated response fields detected; see {response.headers.get('Link')}") + deprecated_fields = response.headers.get("FlexMeasures-Deprecated-Response-Fields") + if deprecated_fields: + print(f"Deprecated response fields detected: {deprecated_fields}") + print(f"See {response.headers.get('Link')}") data = response.json() job_id = data["job"] For endpoint deprecations, the deprecation header field shows an `IMF-fixdate `_ indicating when the API endpoint was deprecated. -For deprecated response fields, the header value can be ``true`` and the ``Link`` header points to migration guidance. +For deprecated response fields, the deprecation header field also shows an IMF-fixdate, while the presence of the ``FlexMeasures-Deprecated-Response-Fields`` header narrows the deprecation to the named response fields. The sunset header field shows an `IMF-fixdate `_ indicating when the API endpoint is likely to become unresponsive. More information about a deprecation, sunset, and possibly recommended replacements, can be found under the ``"Link"`` response header. Relevant relations are: diff --git a/flexmeasures/api/common/responses.py b/flexmeasures/api/common/responses.py index 5e6e15f236..b7adbeaab7 100644 --- a/flexmeasures/api/common/responses.py +++ b/flexmeasures/api/common/responses.py @@ -14,17 +14,19 @@ ResponseTuple = tuple[dict, int] | tuple[dict, int, dict] -DEPRECATED_RESPONSE_FIELDS_LINK = ( - "https://flexmeasures.readthedocs.io/en/latest/api/" - "introduction.html#background-job-monitoring" -) +DEPRECATED_RESPONSE_FIELDS_HEADER = "FlexMeasures-Deprecated-Response-Fields" -def deprecated_response_fields_headers() -> dict[str, str]: +def deprecated_response_fields_headers( + fields: Sequence[str], + deprecation_link: str, + deprecation_date: str, +) -> dict[str, str]: """Headers used when a response contains deprecated fields.""" return { - "Deprecation": "true", - "Link": f'<{DEPRECATED_RESPONSE_FIELDS_LINK}>; rel="deprecation"; type="text/html"', + "Deprecation": deprecation_date, + "Link": f'<{deprecation_link}>; rel="deprecation"; type="text/html"', + DEPRECATED_RESPONSE_FIELDS_HEADER: ", ".join(fields), } @@ -401,6 +403,8 @@ def request_accepted_for_processing( message: str = "Request has been accepted for processing.", legacy_key: str | None = None, job_results_url: str | None = None, + deprecation_link: str | None = None, + deprecation_date: str | None = None, ) -> ResponseTuple: """ Standard 202 response when a background job is accepted. @@ -424,7 +428,15 @@ def request_accepted_for_processing( if legacy_key: # keep legacy key for backwards compatibility resp[legacy_key] = job_id - return resp, 202, deprecated_response_fields_headers() + assert deprecation_link is not None + assert deprecation_date is not None + return ( + resp, + 202, + deprecated_response_fields_headers( + [legacy_key], deprecation_link, deprecation_date + ), + ) return resp, 202 diff --git a/flexmeasures/api/v3_0/assets.py b/flexmeasures/api/v3_0/assets.py index fcbcacaa6f..223c136c86 100644 --- a/flexmeasures/api/v3_0/assets.py +++ b/flexmeasures/api/v3_0/assets.py @@ -69,6 +69,7 @@ unprocessable_entity, request_accepted_for_processing, ) +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.common.schemas.users import AccountIdField from flexmeasures.api.common.schemas.assets import default_response_fields from flexmeasures.ui.utils.view_utils import clear_session, set_session_variables @@ -81,6 +82,11 @@ from flexmeasures.data.models.time_series import Sensor from flexmeasures.data.utils import get_downsample_function_and_value +API_V3_0_DOCS_URL = "https://flexmeasures.readthedocs.io/latest/api/v3_0.html" +ASSET_SCHEDULE_TRIGGER_DOCS_URL = ( + f"{API_V3_0_DOCS_URL}#post--api-v3_0-assets-id-schedules-trigger" +) + asset_type_schema = AssetTypeSchema() asset_schema = AssetSchema() annotation_schema = AnnotationSchema() @@ -1494,12 +1500,17 @@ def trigger_schedule( description: Indicates that the response contains deprecated fields. schema: type: string - example: "true" + example: "Wed, 01 Jul 2026 00:00:00 GMT" Link: description: Link to migration guidance for deprecated response fields. schema: type: string - example: '; rel="deprecation"; type="text/html"' + example: '; rel="deprecation"; type="text/html"' + FlexMeasures-Deprecated-Response-Fields: + description: Comma-separated response fields that are deprecated. + schema: + type: string + example: "schedule" content: application/json: schema: @@ -1561,7 +1572,12 @@ def trigger_schedule( return unprocessable_entity(str(err)) # Keep legacy `schedule` key for backward compatibility; prefer `job`. - return request_accepted_for_processing(job.id, legacy_key="schedule") + return request_accepted_for_processing( + job.id, + legacy_key="schedule", + deprecation_link=ASSET_SCHEDULE_TRIGGER_DOCS_URL, + deprecation_date=JOB_RESPONSE_FIELDS_DEPRECATION_DATE, + ) @route("//kpis", methods=["GET"]) @use_kwargs( diff --git a/flexmeasures/api/v3_0/deprecations.py b/flexmeasures/api/v3_0/deprecations.py new file mode 100644 index 0000000000..ea7b4f506e --- /dev/null +++ b/flexmeasures/api/v3_0/deprecations.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +from flexmeasures.utils.time_utils import to_http_time + + +# TODO: In another iteration, this will be saved in a more generic deprecations structure, +# With the ability for hosts to set the dates or other specifics +# to their own cadence. +JOB_RESPONSE_FIELDS_DEPRECATION_DATE = to_http_time( + datetime(2026, 8, 1, tzinfo=timezone.utc) +) diff --git a/flexmeasures/api/v3_0/jobs.py b/flexmeasures/api/v3_0/jobs.py index 04dc92c654..6d1bcc001d 100644 --- a/flexmeasures/api/v3_0/jobs.py +++ b/flexmeasures/api/v3_0/jobs.py @@ -14,10 +14,23 @@ from flexmeasures.data.services.utils import failed_job_exc_info, job_status_description from flexmeasures.auth.policy import check_access from flexmeasures.api.common.responses import deprecated_response_fields_headers +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.data import db from flexmeasures.data.models.time_series import Sensor from flexmeasures.data.services.utils import get_asset_or_sensor_from_ref +JOBS_API_DOCS_URL = ( + "https://flexmeasures.readthedocs.io/latest/api/v3_0.html" + "#get--api-v3_0-jobs-uuid" +) +JOB_STATUS_DEPRECATED_RESPONSE_FIELDS = ( + "func_name", + "enqueued_at", + "started_at", + "ended_at", + "exc_info", +) + def _isoformat_or_none(dt: datetime | None) -> str | None: """Return an ISO-8601 string for *dt*, or ``None`` when *dt* is absent.""" @@ -118,12 +131,17 @@ def get_job_status(self, job_id: str, **kwargs): description: Indicates that the response contains deprecated fields. schema: type: string - example: "true" + example: "Wed, 01 Jul 2026 00:00:00 GMT" Link: description: Link to migration guidance for deprecated response fields. schema: type: string - example: '; rel="deprecation"; type="text/html"' + example: '; rel="deprecation"; type="text/html"' + FlexMeasures-Deprecated-Response-Fields: + description: Comma-separated response fields that are deprecated. + schema: + type: string + example: "func_name, enqueued_at, started_at, ended_at, exc_info" content: application/json: schema: @@ -266,12 +284,17 @@ def get_job_status(self, job_id: str, **kwargs): description: Indicates that the response contains deprecated fields. schema: type: string - example: "true" + example: "Wed, 01 Jul 2026 00:00:00 GMT" Link: description: Link to migration guidance for deprecated response fields. schema: type: string - example: '; rel="deprecation"; type="text/html"' + example: '; rel="deprecation"; type="text/html"' + FlexMeasures-Deprecated-Response-Fields: + description: Comma-separated response fields that are deprecated. + schema: + type: string + example: "func_name, enqueued_at, started_at, ended_at, exc_info" 422: description: Job has failed. headers: @@ -279,12 +302,17 @@ def get_job_status(self, job_id: str, **kwargs): description: Indicates that the response contains deprecated fields. schema: type: string - example: "true" + example: "Wed, 01 Jul 2026 00:00:00 GMT" Link: description: Link to migration guidance for deprecated response fields. schema: type: string - example: '; rel="deprecation"; type="text/html"' + example: '; rel="deprecation"; type="text/html"' + FlexMeasures-Deprecated-Response-Fields: + description: Comma-separated response fields that are deprecated. + schema: + type: string + example: "func_name, enqueued_at, started_at, ended_at, exc_info" 404: description: NOT_FOUND 401: @@ -354,4 +382,12 @@ def get_job_status(self, job_id: str, **kwargs): else: status_code = 202 - return response, status_code, deprecated_response_fields_headers() + return ( + response, + status_code, + deprecated_response_fields_headers( + JOB_STATUS_DEPRECATED_RESPONSE_FIELDS, + JOBS_API_DOCS_URL, + JOB_RESPONSE_FIELDS_DEPRECATION_DATE, + ), + ) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 2a859f3ca3..d21288dbf8 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -27,6 +27,7 @@ unprocessable_entity, fallback_schedule_redirect, ) +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.common.schemas.utils import make_openapi_compatible from flexmeasures.api.common.schemas.sensor_data import ( # noqa F401 SensorDataDescriptionSchema, @@ -86,6 +87,14 @@ ForecastingTriggerSchema, ) +API_V3_0_DOCS_URL = "https://flexmeasures.readthedocs.io/latest/api/v3_0.html" +SENSOR_SCHEDULE_TRIGGER_DOCS_URL = ( + f"{API_V3_0_DOCS_URL}#post--api-v3_0-sensors-id-schedules-trigger" +) +SENSOR_FORECAST_TRIGGER_DOCS_URL = ( + f"{API_V3_0_DOCS_URL}#post--api-v3_0-sensors-id-forecasts-trigger" +) + # Instantiate schemes outside of endpoint logic to minimize response time sensors_schema = SensorSchema(many=True) sensor_schema = SensorSchema() @@ -977,12 +986,17 @@ def trigger_schedule( description: Indicates that the response contains deprecated fields. schema: type: string - example: "true" + example: "Wed, 01 Jul 2026 00:00:00 GMT" Link: description: Link to migration guidance for deprecated response fields. schema: type: string - example: '; rel="deprecation"; type="text/html"' + example: '; rel="deprecation"; type="text/html"' + FlexMeasures-Deprecated-Response-Fields: + description: Comma-separated response fields that are deprecated. + schema: + type: string + example: "schedule" content: application/json: schema: @@ -1080,6 +1094,8 @@ def trigger_schedule( job_results_url=url_for( "SensorAPI:get_schedule", id=sensor.id, uuid=job.id ), + deprecation_link=SENSOR_SCHEDULE_TRIGGER_DOCS_URL, + deprecation_date=JOB_RESPONSE_FIELDS_DEPRECATION_DATE, ) # mark endpoint as deprecated @@ -1979,12 +1995,17 @@ def trigger_forecast(self, id: int, **params): description: Indicates that the response contains deprecated fields. schema: type: string - example: "true" + example: "Wed, 01 Jul 2026 00:00:00 GMT" Link: description: Link to migration guidance for deprecated response fields. schema: type: string - example: '; rel="deprecation"; type="text/html"' + example: '; rel="deprecation"; type="text/html"' + FlexMeasures-Deprecated-Response-Fields: + description: Comma-separated response fields that are deprecated. + schema: + type: string + example: "forecast" content: application/json: schema: @@ -2064,6 +2085,8 @@ def trigger_forecast(self, id: int, **params): job_results_url=url_for( "SensorAPI:get_forecast", id=id, uuid=pipeline_returns["job_id"] ), + deprecation_link=SENSOR_FORECAST_TRIGGER_DOCS_URL, + deprecation_date=JOB_RESPONSE_FIELDS_DEPRECATION_DATE, ) @route("//forecasts/", methods=["GET"]) diff --git a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py index 1afa692d9e..3bdd0a75fb 100644 --- a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py @@ -9,6 +9,8 @@ from rq.job import Job from flexmeasures import Sensor +from flexmeasures.api.common.responses import DEPRECATED_RESPONSE_FIELDS_HEADER +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.v3_0.tests.utils import message_for_trigger_schedule from flexmeasures.data.models.planning.tests.utils import check_constraints from flexmeasures.data.models.generic_assets import GenericAsset @@ -98,6 +100,19 @@ def test_asset_trigger_and_get_schedule( ) print("Server responded with:\n%s" % trigger_schedule_response.json) assert trigger_schedule_response.status_code == 202 + assert ( + trigger_schedule_response.headers["Deprecation"] + == JOB_RESPONSE_FIELDS_DEPRECATION_DATE + ) + assert 'rel="deprecation"' in trigger_schedule_response.headers["Link"] + assert ( + "post--api-v3_0-assets-id-schedules-trigger" + in trigger_schedule_response.headers["Link"] + ) + assert ( + trigger_schedule_response.headers[DEPRECATED_RESPONSE_FIELDS_HEADER] + == "schedule" + ) job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue diff --git a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py index dfe09368b1..b45d832d19 100644 --- a/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_forecasting_api_fresh_db.py @@ -5,6 +5,8 @@ from rq.job import Job +from flexmeasures.api.common.responses import DEPRECATED_RESPONSE_FIELDS_HEADER +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.tests.utils import get_auth_token from flexmeasures.data.models.forecasting.pipelines import TrainPredictPipeline from flexmeasures.data.services.forecasting import handle_forecasting_exception @@ -49,9 +51,10 @@ def test_trigger_and_fetch_forecasts( trigger_url, json=payload, headers={"Authorization": token} ) assert trigger_res.status_code == 202, trigger_res.json - assert trigger_res.headers["Deprecation"] == "true" + assert trigger_res.headers["Deprecation"] == JOB_RESPONSE_FIELDS_DEPRECATION_DATE assert 'rel="deprecation"' in trigger_res.headers["Link"] - assert "background-job-monitoring" in trigger_res.headers["Link"] + assert "post--api-v3_0-sensors-id-forecasts-trigger" in trigger_res.headers["Link"] + assert trigger_res.headers[DEPRECATED_RESPONSE_FIELDS_HEADER] == "forecast" assert "deprecated-fields" not in trigger_res.json assert trigger_res.json["results-url"] == url_for( "SensorAPI:get_forecast", id=sensor_0.id, uuid=trigger_res.json["forecast"] diff --git a/flexmeasures/api/v3_0/tests/test_jobs_api.py b/flexmeasures/api/v3_0/tests/test_jobs_api.py index 89d5348b80..408ab30dcd 100644 --- a/flexmeasures/api/v3_0/tests/test_jobs_api.py +++ b/flexmeasures/api/v3_0/tests/test_jobs_api.py @@ -10,6 +10,8 @@ from redis.exceptions import ConnectionError as RedisConnectionError from rq.job import Job, JobStatus +from flexmeasures.api.common.responses import DEPRECATED_RESPONSE_FIELDS_HEADER +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.v3_0.tests.utils import message_for_trigger_schedule from flexmeasures.data.services.scheduling import ( create_scheduling_job, @@ -37,9 +39,12 @@ def assert_legacy_job_status_fields(data: dict): def assert_deprecated_response_fields_headers(response): - assert response.headers["Deprecation"] == "true" + assert response.headers["Deprecation"] == JOB_RESPONSE_FIELDS_DEPRECATION_DATE assert 'rel="deprecation"' in response.headers["Link"] - assert "background-job-monitoring" in response.headers["Link"] + assert "get--api-v3_0-jobs-uuid" in response.headers["Link"] + assert response.headers[DEPRECATED_RESPONSE_FIELDS_HEADER] == ", ".join( + JOB_STATUS_DEPRECATED_FIELDS.keys() + ) @pytest.mark.parametrize( diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index a5247d1ef6..b238d9f0f8 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -8,8 +8,13 @@ from rq.job import Job from sqlalchemy import select -from flexmeasures.api.common.responses import unknown_schedule, unrecognized_event +from flexmeasures.api.common.responses import ( + DEPRECATED_RESPONSE_FIELDS_HEADER, + unknown_schedule, + unrecognized_event, +) from flexmeasures.api.tests.utils import check_deprecation +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.v3_0.tests.utils import ( get_sensor_by_name, message_for_trigger_schedule, @@ -298,9 +303,19 @@ def test_trigger_and_get_schedule_with_unknown_prices( ) print("Server responded with:\n%s" % trigger_schedule_response.json) assert trigger_schedule_response.status_code == 202 - assert trigger_schedule_response.headers["Deprecation"] == "true" + assert ( + trigger_schedule_response.headers["Deprecation"] + == JOB_RESPONSE_FIELDS_DEPRECATION_DATE + ) assert 'rel="deprecation"' in trigger_schedule_response.headers["Link"] - assert "background-job-monitoring" in trigger_schedule_response.headers["Link"] + assert ( + "post--api-v3_0-sensors-id-schedules-trigger" + in trigger_schedule_response.headers["Link"] + ) + assert ( + trigger_schedule_response.headers[DEPRECATED_RESPONSE_FIELDS_HEADER] + == "schedule" + ) job_id = trigger_schedule_response.json["schedule"] # look for scheduling jobs in queue diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py index 901e7dba1d..54b0d437ea 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py @@ -7,6 +7,8 @@ from rq.job import Job from unittest.mock import patch +from flexmeasures.api.common.responses import DEPRECATED_RESPONSE_FIELDS_HEADER +from flexmeasures.api.v3_0.deprecations import JOB_RESPONSE_FIELDS_DEPRECATION_DATE from flexmeasures.api.v3_0.tests.utils import message_for_trigger_schedule from flexmeasures.data.models.generic_assets import GenericAsset from flexmeasures.data.models.planning.utils import get_power_values @@ -74,9 +76,19 @@ def test_trigger_and_get_schedule( ) print("Server responded with:\n%s" % trigger_schedule_response.json) assert trigger_schedule_response.status_code == 202 - assert trigger_schedule_response.headers["Deprecation"] == "true" + assert ( + trigger_schedule_response.headers["Deprecation"] + == JOB_RESPONSE_FIELDS_DEPRECATION_DATE + ) assert 'rel="deprecation"' in trigger_schedule_response.headers["Link"] - assert "background-job-monitoring" in trigger_schedule_response.headers["Link"] + assert ( + "post--api-v3_0-sensors-id-schedules-trigger" + in trigger_schedule_response.headers["Link"] + ) + assert ( + trigger_schedule_response.headers[DEPRECATED_RESPONSE_FIELDS_HEADER] + == "schedule" + ) assert "deprecated-fields" not in trigger_schedule_response.json assert trigger_schedule_response.json["results-url"] == url_for( "SensorAPI:get_schedule", diff --git a/flexmeasures/ui/static/openapi-specs.json b/flexmeasures/ui/static/openapi-specs.json index 9226b26739..7808559c7e 100644 --- a/flexmeasures/ui/static/openapi-specs.json +++ b/flexmeasures/ui/static/openapi-specs.json @@ -7,7 +7,7 @@ }, "termsOfService": null, "title": "FlexMeasures", - "version": "1.0.0" + "version": "0.33.2" }, "externalDocs": { "description": "FlexMeasures runs on the open source FlexMeasures technology. Read the docs here.", @@ -1394,14 +1394,21 @@ "description": "Indicates that the response contains deprecated fields.", "schema": { "type": "string", - "example": "true" + "example": "Wed, 01 Jul 2026 00:00:00 GMT" } }, "Link": { "description": "Link to migration guidance for deprecated response fields.", "schema": { "type": "string", - "example": "; rel=\"deprecation\"; type=\"text/html\"" + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + }, + "FlexMeasures-Deprecated-Response-Fields": { + "description": "Comma-separated response fields that are deprecated.", + "schema": { + "type": "string", + "example": "forecast" } } }, @@ -1578,14 +1585,21 @@ "description": "Indicates that the response contains deprecated fields.", "schema": { "type": "string", - "example": "true" + "example": "Wed, 01 Jul 2026 00:00:00 GMT" } }, "Link": { "description": "Link to migration guidance for deprecated response fields.", "schema": { "type": "string", - "example": "; rel=\"deprecation\"; type=\"text/html\"" + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + }, + "FlexMeasures-Deprecated-Response-Fields": { + "description": "Comma-separated response fields that are deprecated.", + "schema": { + "type": "string", + "example": "schedule" } } }, @@ -4165,14 +4179,21 @@ "description": "Indicates that the response contains deprecated fields.", "schema": { "type": "string", - "example": "true" + "example": "Wed, 01 Jul 2026 00:00:00 GMT" } }, "Link": { "description": "Link to migration guidance for deprecated response fields.", "schema": { "type": "string", - "example": "; rel=\"deprecation\"; type=\"text/html\"" + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + }, + "FlexMeasures-Deprecated-Response-Fields": { + "description": "Comma-separated response fields that are deprecated.", + "schema": { + "type": "string", + "example": "schedule" } } }, @@ -4431,14 +4452,21 @@ "description": "Indicates that the response contains deprecated fields.", "schema": { "type": "string", - "example": "true" + "example": "Wed, 01 Jul 2026 00:00:00 GMT" } }, "Link": { "description": "Link to migration guidance for deprecated response fields.", "schema": { "type": "string", - "example": "; rel=\"deprecation\"; type=\"text/html\"" + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + }, + "FlexMeasures-Deprecated-Response-Fields": { + "description": "Comma-separated response fields that are deprecated.", + "schema": { + "type": "string", + "example": "func_name, enqueued_at, started_at, ended_at, exc_info" } } }, @@ -4611,14 +4639,21 @@ "description": "Indicates that the response contains deprecated fields.", "schema": { "type": "string", - "example": "true" + "example": "Wed, 01 Jul 2026 00:00:00 GMT" } }, "Link": { "description": "Link to migration guidance for deprecated response fields.", "schema": { "type": "string", - "example": "; rel=\"deprecation\"; type=\"text/html\"" + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + }, + "FlexMeasures-Deprecated-Response-Fields": { + "description": "Comma-separated response fields that are deprecated.", + "schema": { + "type": "string", + "example": "func_name, enqueued_at, started_at, ended_at, exc_info" } } } @@ -4630,14 +4665,21 @@ "description": "Indicates that the response contains deprecated fields.", "schema": { "type": "string", - "example": "true" + "example": "Wed, 01 Jul 2026 00:00:00 GMT" } }, "Link": { "description": "Link to migration guidance for deprecated response fields.", "schema": { "type": "string", - "example": "; rel=\"deprecation\"; type=\"text/html\"" + "example": "; rel=\"deprecation\"; type=\"text/html\"" + } + }, + "FlexMeasures-Deprecated-Response-Fields": { + "description": "Comma-separated response fields that are deprecated.", + "schema": { + "type": "string", + "example": "func_name, enqueued_at, started_at, ended_at, exc_info" } } } From 72695497f94624cd87ed9af02aac152a4fa4b94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Mon, 20 Jul 2026 18:59:45 +0200 Subject: [PATCH 8/8] some new tests still needed to assert 202 instead of 200 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicolas Höning --- .../api/v3_0/tests/test_asset_schedules_fresh_db.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py index 289eece0f5..be810ce3b5 100644 --- a/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py +++ b/flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py @@ -902,9 +902,9 @@ def make_flex_model(sensor: Sensor, soc_sensor: Sensor, group_sensor: Sensor): url_for("AssetAPI:trigger_schedule", id=charging_hub.id), json=message, ) - if trigger_response.status_code != 200: + if trigger_response.status_code != 202: print(f"Error response: {trigger_response.json}") - assert trigger_response.status_code == 200 + assert trigger_response.status_code == 202 job_id = trigger_response.json["schedule"] # Process the scheduling queue @@ -970,7 +970,7 @@ def get_beliefs_as_series(sensor: Sensor) -> pd.Series: url_for("AssetAPI:trigger_schedule", id=charging_hub.id), json=control_message, ) - assert control_response.status_code == 200 + assert control_response.status_code == 202 control_job_id = control_response.json["schedule"] work_on_rq(app.queues["scheduling"], exc_handler=handle_scheduling_exception)