Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
146 commits
Select commit Hold shift + click to select a range
f97c2f0
feat: add Automation data model
Flix6x Jul 11, 2026
f410437
feat: CLI commands to manage and run automations
Flix6x Jul 11, 2026
6f6591c
feat: record on forecasting jobs how they were created
Flix6x Jul 11, 2026
fb74bfc
feat: API endpoints to list an asset's automations
Flix6x Jul 11, 2026
ab62b9f
feat: UI page listing an asset's automations
Flix6x Jul 11, 2026
db8f288
test: cover automations CLI, API and UI
Flix6x Jul 11, 2026
9cb4f63
docs: document automations
Flix6x Jul 11, 2026
e0575c7
docs: changelog entry for automations
Flix6x Jul 11, 2026
b1f56bf
fix: render cron descriptions in 24-hour format regardless of locale
Flix6x Jul 11, 2026
de6fb71
fix: address code review findings for automations
Flix6x Jul 11, 2026
bf2cc60
fix: address code review findings for automations (remaining files)
Flix6x Jul 11, 2026
0b345b2
feat: record on scheduling jobs how they were created
Flix6x Jul 11, 2026
4c0c5db
feat: schedules as automations
Flix6x Jul 11, 2026
ac4c74a
docs: changelog entry for schedule automations
Flix6x Jul 11, 2026
f6e1e10
feat: CRUD for automations via API and UI
Flix6x Jul 11, 2026
073abd9
docs: changelog entry for automations CRUD
Flix6x Jul 11, 2026
6184940
feat: reports can run as background jobs
Flix6x Jul 11, 2026
79e1609
feat: reports as automations
Flix6x Jul 11, 2026
5eac468
docs: changelog entry for reports as jobs and automations
Flix6x Jul 11, 2026
791fcf4
feat: anchor default report windows to the automation's actual last run
Flix6x Jul 11, 2026
0dca8fc
docs: add an Automations concept page
Flix6x Jul 11, 2026
93ee584
feat: API endpoint to trigger reports as jobs
Flix6x Jul 11, 2026
176d800
fix: address stack code review findings
Flix6x Jul 11, 2026
d5210c8
Merge branch 'feat/2288-report-automations' into feat/2288-report-tri…
Flix6x Jul 11, 2026
df77725
fix: check sensor access when triggering reports via the API
Flix6x Jul 11, 2026
7493c13
test: assert on the cron validation failure without pinning click's m…
Flix6x Jul 12, 2026
7bc3056
merge: synchronize automations with current main
BelhsanHmida Jul 31, 2026
15c9937
data/schemas: restrict automations to five-field cron
BelhsanHmida Jul 31, 2026
3090cf5
data/schemas/tests: cover automation cron field count
BelhsanHmida Jul 31, 2026
8cc3920
cli/jobs: retain automation guard after queueing failure
BelhsanHmida Jul 31, 2026
29c46b3
cli/tests: cover partial automation queue failure
BelhsanHmida Jul 31, 2026
8666c31
cli: normalize YAML forecasting option files
BelhsanHmida Jul 31, 2026
3433020
cli/tests: cover automation YAML option files
BelhsanHmida Jul 31, 2026
430dcef
data/services: redact inaccessible automation provenance
BelhsanHmida Jul 31, 2026
6c4bbc6
api/v3_0/tests: cover automation provenance authorization
BelhsanHmida Jul 31, 2026
337ad3b
ui/assets: distinguish automation load failures
BelhsanHmida Jul 31, 2026
119fc9d
ui/tests: cover automation load error state
BelhsanHmida Jul 31, 2026
04daae4
utils/docs: preserve standalone asterisks in RST conversion
BelhsanHmida Jul 31, 2026
22addda
utils/tests: cover RST cron wildcard conversion
BelhsanHmida Jul 31, 2026
edf77a5
docs/forecasting: clarify automation execution contract
BelhsanHmida Jul 31, 2026
a6cee99
changelog: record automation API and runner contract
BelhsanHmida Jul 31, 2026
194f8cc
api/docs: show job creation provenance
BelhsanHmida Jul 31, 2026
a3d7ebe
test: keep forecast CLI stub compatible with job provenance
BelhsanHmida Jul 31, 2026
de76798
Merge main into forecast automations
BelhsanHmida Aug 4, 2026
646f9fd
fix: require valid automation generators
BelhsanHmida Aug 4, 2026
4664afe
test: cover automation generator retention
BelhsanHmida Aug 4, 2026
b801d74
fix: constrain forecast automation outputs
BelhsanHmida Aug 5, 2026
006f85c
test: cover forecast automation output scope
BelhsanHmida Aug 5, 2026
dba4edc
docs: explain forecast automation ownership rules
BelhsanHmida Aug 5, 2026
efdda5a
fix: merge automation and main migration heads
BelhsanHmida Aug 5, 2026
5e86232
merge: synchronize schedule automations with reviewed parent
BelhsanHmida Aug 5, 2026
bc1fc32
data/models: allow schedule automations without generators
BelhsanHmida Aug 5, 2026
2f4afe2
cli/tests: cover schedule automation validation
BelhsanHmida Aug 5, 2026
d3c3082
data/tests: cover schedule automation dispatch
BelhsanHmida Aug 5, 2026
9505e2b
api/v3_0/tests: cover schedule job provenance
BelhsanHmida Aug 5, 2026
5de1f20
ui/tests: cover automation type tabs
BelhsanHmida Aug 5, 2026
6bf6143
scheduling: harden automation dispatch
BelhsanHmida Aug 5, 2026
03f1b88
cli: reject forecast options for schedule automations
BelhsanHmida Aug 5, 2026
75b2be6
ui/assets: resize automation tables on tab changes
BelhsanHmida Aug 5, 2026
71a7196
docs: clarify schedule automation inputs
BelhsanHmida Aug 5, 2026
d39f60f
cli/tests: cover malformed automation YAML
BelhsanHmida Aug 5, 2026
5b04664
cli: report malformed automation YAML
BelhsanHmida Aug 5, 2026
1ec9324
data/tests: cover stored schedule flex configuration
BelhsanHmida Aug 5, 2026
90025d3
scheduling: load stored flex config for minimal triggers
BelhsanHmida Aug 5, 2026
2f0b09c
docs/scheduling: describe trigger propagation
BelhsanHmida Aug 5, 2026
c783e8f
data/models: let data generators report their input and output sensors
Flix6x Aug 5, 2026
8b2cc73
data/models/forecasting: only announce a pipeline run when actually r…
Flix6x Aug 5, 2026
b37256f
cli: default the automation recurrence to daily, and reject options t…
Flix6x Aug 5, 2026
aa64afb
api/v3_0: report an automation's input and output sensors
Flix6x Aug 5, 2026
b438e8f
api/v3_0: add an endpoint for one data source
Flix6x Aug 5, 2026
17f432b
api/v3_0: regenerate the OpenAPI specs
Flix6x Aug 5, 2026
1ea8f78
ui: link an automation's details to its sensors, and make the listing…
Flix6x Aug 5, 2026
2b87d5e
ui: show a sensor's data source record and the automations feeding it
Flix6x Aug 5, 2026
1869247
tests: cover the automation and data source review follow-ups
Flix6x Aug 5, 2026
03626b4
docs: describe the automation and data source follow-ups
Flix6x Aug 5, 2026
0f8fe58
Merge the current automations branch into the review follow-ups
Flix6x Aug 5, 2026
1dccf9f
api/v3_0: regenerate the OpenAPI specs after merging
Flix6x Aug 5, 2026
9f1d075
cli: only reject configuration options that were actually given with …
Flix6x Aug 5, 2026
b397907
data/services: only consider automations that could feed a sensor
Flix6x Aug 5, 2026
318cd91
tests: follow the merged automation behaviour
Flix6x Aug 5, 2026
2237245
data/services: only let a user automate sensors they can access thems…
Flix6x Aug 5, 2026
4765fd8
api/v3_0: regenerate the OpenAPI specs
Flix6x Aug 5, 2026
5d92ea0
api/v3_0/tests: cover automating an inaccessible sensor
Flix6x Aug 5, 2026
d4bf80e
docs: describe which sensors an automation may involve
Flix6x Aug 5, 2026
d8701c6
data/services: check every sensor a schedule would be recorded on
Flix6x Aug 5, 2026
05e00ac
api/v3_0/tests: cover a schedule aggregated onto an inaccessible sensor
Flix6x Aug 5, 2026
1eb3a50
cli: keep mypy happy about click 8 attributes
BelhsanHmida Aug 7, 2026
9653e10
cli: keep the automation help focused on the automation
BelhsanHmida Aug 7, 2026
87e07f6
data/models: count a source-filtered regressor as an input sensor
BelhsanHmida Aug 7, 2026
8860bd0
data/services: do not report no sensors when an automation's sensors …
BelhsanHmida Aug 10, 2026
2b961cc
Feat automation timezones catchup (#2396)
BelhsanHmida Aug 10, 2026
f75bb6d
Merge branch 'main' into feat/2288-automations-for-forecasts
BelhsanHmida Aug 10, 2026
1e53696
Merge the forecast automation base into the schedule automations
BelhsanHmida Aug 10, 2026
db0a57b
Merge the schedule automations into the automations CRUD
BelhsanHmida Aug 10, 2026
1f68beb
data/migrations: rejoin the two automation migration branches
BelhsanHmida Aug 11, 2026
f625330
Merge branch 'work/2293-sync' into work/2294-sync
BelhsanHmida Aug 11, 2026
64ad898
data/services: let the forecaster say which sensors an automation inv…
BelhsanHmida Aug 11, 2026
7a3c38a
api/v3_0: let an automation's timezone be set and changed through the…
BelhsanHmida Aug 11, 2026
5612387
data/services: set up the forecaster's data source only once the auto…
BelhsanHmida Aug 11, 2026
a98b4fc
Merge origin/main into automation fixes
BelhsanHmida Aug 11, 2026
8eac59f
fix(data/schemas): reject cron expressions without dates
BelhsanHmida Aug 11, 2026
249642f
fix(data/services): isolate invalid recurrences and stale claims
BelhsanHmida Aug 11, 2026
bf23428
fix(api/v3_0): protect automation sensor details
BelhsanHmida Aug 11, 2026
af5957b
test(cli): cover impossible recurrence input
BelhsanHmida Aug 11, 2026
c18f7de
test(data/services): cover resilient automation claims
BelhsanHmida Aug 11, 2026
23ea45a
test(api/v3_0): cover private automation dependencies
BelhsanHmida Aug 11, 2026
a7671de
Merge reviewed forecast automation fixes
BelhsanHmida Aug 11, 2026
5af2170
fix(data/services): resolve schedule automation sensors
BelhsanHmida Aug 11, 2026
32c4970
test(data/services): cover schedule sensor resolution
BelhsanHmida Aug 11, 2026
a0a1944
test(api/v3_0): expose schedule dependency details
BelhsanHmida Aug 11, 2026
87f670b
Merge reviewed schedule automation fixes
BelhsanHmida Aug 11, 2026
1c7b3d3
fix(data/services): hide inaccessible sensor names
BelhsanHmida Aug 11, 2026
b7979b5
test(api/v3_0): isolate automation endpoint tests
BelhsanHmida Aug 11, 2026
10b9155
feat(ui/views): provide automation timezones
BelhsanHmida Aug 11, 2026
a39c4ed
feat(ui): edit automation recurrence timezones
BelhsanHmida Aug 11, 2026
69fd5bf
test(ui): cover automation timezone controls
BelhsanHmida Aug 11, 2026
aa469bc
docs(changelog): mention automation timezones
BelhsanHmida Aug 11, 2026
9b5af64
fix(data/services): preserve schedule validation errors
BelhsanHmida Aug 11, 2026
881c9bd
feat(automations): integrate reports with the CRUD stack
BelhsanHmida Aug 12, 2026
292a0fa
fix(automations): authorize reporter sensor dependencies
BelhsanHmida Aug 12, 2026
8b76815
fix(reporting): require report inputs and outputs
BelhsanHmida Aug 12, 2026
82cb3bb
fix(automations): constrain report output scope
BelhsanHmida Aug 12, 2026
3e6c2ec
fix(automations): require generators for reports
BelhsanHmida Aug 12, 2026
601f951
fix(automations): anchor reports to claimed occurrences
BelhsanHmida Aug 12, 2026
f0c3fce
fix(automations): advance report coverage monotonically
BelhsanHmida Aug 12, 2026
2350b68
test(ui): cover report automation listings
BelhsanHmida Aug 12, 2026
77c6c2d
docs(automations): document report occurrence semantics
BelhsanHmida Aug 12, 2026
95b6187
test(api): close rejected report transactions
BelhsanHmida Aug 12, 2026
ed64921
Merge report automation base into trigger API
BelhsanHmida Aug 12, 2026
1b6b8cd
test(api/v3_0): secure report trigger dependencies
BelhsanHmida Aug 12, 2026
db27c1b
docs(api): define report trigger authorization
BelhsanHmida Aug 12, 2026
c5971d6
feat(reporting): run one-off reports as jobs
BelhsanHmida Aug 17, 2026
ab5bc11
feat(api/v3_0): trigger one-off report jobs safely
BelhsanHmida Aug 17, 2026
28c9c90
test(reporting): cover secured report job entry points
BelhsanHmida Aug 17, 2026
07bc596
docs(reporting): describe one-off report jobs
BelhsanHmida Aug 17, 2026
aadbdd0
docs(changelog): announce one-off report jobs
BelhsanHmida Aug 17, 2026
47b7946
fix(reporting): reject incomplete reporter dataflows
BelhsanHmida Aug 17, 2026
c3faaa0
test(reporting): cover incomplete queued reports
BelhsanHmida Aug 17, 2026
3e59d3c
Merge the standalone report trigger work with its former branch history
BelhsanHmida Aug 17, 2026
f122fdc
style(tests): format error message for clarity in data source validation
BelhsanHmida Aug 17, 2026
982355f
fix(reporting): honor configured job timeouts
BelhsanHmida Aug 18, 2026
623f1d2
fix(data/services): identify failed jobs by queue
BelhsanHmida Aug 18, 2026
89316d1
fix(deployment): process reporting jobs by default
BelhsanHmida Aug 18, 2026
7271aae
docs(api): define accepted report job responses
BelhsanHmida Aug 18, 2026
99f3002
docs(host): complete reporting worker setup
BelhsanHmida Aug 18, 2026
20bd04c
test(reporting): cover timeout and status contracts
BelhsanHmida Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ services:
if [ -f /usr/var/flexmeasures-instance/requirements.txt ]; then
pip install --no-cache-dir -r /usr/var/flexmeasures-instance/requirements.txt
fi
flexmeasures jobs run-worker --name flexmeasures-worker --queue forecasting\|scheduling\|ingestion
flexmeasures jobs run-worker --name flexmeasures-worker --queue forecasting\|scheduling\|ingestion\|reporting
test-db:
image: postgres
expose:
Expand Down
1 change: 1 addition & 0 deletions documentation/api/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ API change log

v3.0-32 | August 11, 2026
""""""""""""""""""""""""""
- Added ``POST /api/v3_0/assets/<id>/reports/trigger`` to queue a one-off report as a background job. It returns ``202 Accepted`` with the canonical ``job`` and ``job-url`` fields, and shares the trigger rate limit with forecast and schedule endpoints.
- API endpoints are now rate-limited. A request which exceeds a limit is answered with a ``429 (Too Many Requests)`` status code and a ``Retry-After`` header stating how many seconds to wait. Responses also carry ``X-RateLimit-*`` headers, describing the limit that applied, how much of it is left, and when it resets. A stricter limit applies to ``POST /assets/<id>/schedules/trigger``, ``POST /sensors/<id>/schedules/trigger`` and ``POST /sensors/<id>/forecasts/trigger`` than to other endpoints; the health endpoints are exempt. Per-account overrides are set by assigning the account a plan (a ``Plan`` database row), rather than through an account attribute.
- Introduced the ``inflexible-consumption`` and ``inflexible-production`` flex-context fields, which make explicit how the sign of each inflexible device's power data should be read: positive values denote consumption resp. production. Each entry is a sensor reference (``{"sensor": <id>}``), optionally with source filters (``source-types``, ``exclude-source-types``, ``sources``, ``source-account``). Deprecated the ``inflexible-device-sensors`` field (a list of bare sensor IDs, whose sign convention is read from each sensor's ``consumption_is_positive`` attribute); it remains supported, but cannot be combined with the new fields in one flex-context.
- Added a ``role`` query parameter to ``GET /api/v3_0/accounts`` for filtering accessible organisations by account role.
Expand Down
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ v1.1.0 | September XX, 2026
New features
-------------

* Run one-off reports as background jobs from the CLI or the asset API, with sensor-level authorization and a dedicated reporting worker queue [see `PR #2298 <https://github.com/FlexMeasures/flexmeasures/pull/2298>`_ and `issue #2426 <https://github.com/FlexMeasures/flexmeasures/issues/2426>`_]

Infrastructure / Support
-------------------------

Expand Down
1 change: 1 addition & 0 deletions documentation/cli/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ FlexMeasures CLI Changelog
since v1.0.0 | August 11, 2026
=================================

* Add ``flexmeasures add report --as-job`` and the ``reporting`` worker queue for asynchronous one-off reports.
* ``flexmeasures add user --roles`` now parses comma-separated role lists and accepts repeated ``--roles`` options.
* ``flexmeasures db upgrade`` now runs ``VACUUM ANALYZE`` after upgrading (refreshing the query planner's statistics); opt out with ``--no-vacuum``.
* Add ``flexmeasures add plan``, ``flexmeasures show plans`` and ``flexmeasures edit plan``, to manage the rate limits and quotas which apply to the accounts on a plan.
Expand Down
11 changes: 5 additions & 6 deletions documentation/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ Default: ``timedelta(days=1)``
FLEXMEASURES_DEFAULT_JOB_TIMEOUT
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Default timeout for jobs (e.g. forecasting, scheduling and ingestion), expressed as a fixed ISO 8601 duration.
Default timeout for jobs (e.g. forecasting, scheduling, ingestion and reporting), expressed as a fixed ISO 8601 duration.
Jobs that exceed this timeout are moved to RQ's failed queue.

Default: ``timedelta(seconds=180)`` (``"PT180S"``)
Expand All @@ -356,9 +356,9 @@ FLEXMEASURES_JOB_TIMEOUT

Timeouts per queue, expressed as fixed ISO 8601 durations.
Queue-specific values override ``FLEXMEASURES_DEFAULT_JOB_TIMEOUT``.
Supported queue names are ``forecasting``, ``scheduling`` and ``ingestion``.
Supported queue names are ``forecasting``, ``scheduling``, ``ingestion`` and ``reporting``.

Example: ``{"forecasting": "PT2M", "scheduling": "PT5M", "ingestion": "PT30S"}``
Example: ``{"forecasting": "PT2M", "scheduling": "PT5M", "ingestion": "PT30S", "reporting": "PT10M"}``

Default: ``{}``

Expand Down Expand Up @@ -925,9 +925,8 @@ Default: ``"500 per minute"``
FLEXMEASURES_API_TRIGGER_RATE_LIMIT
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

How often a client may trigger a schedule or a forecast. This is the expensive work, so this limit is stricter
than the default one. The trigger endpoints share this budget, so triggering a forecast and triggering a schedule
draw on the same one.
How often a client may trigger a schedule, forecast or report. This is the expensive work, so this limit is stricter
than the default one. The trigger endpoints share this budget, so all three kinds of computation draw on the same one.

Default: ``"10 per 5 minutes"``

Expand Down
8 changes: 7 additions & 1 deletion documentation/features/reporting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ We added an infrastructure that allows us to define computation pipelines and CL
- ``flexmeasures show reporters``
- ``flexmeasures add report``

Reports can be queued for asynchronous processing with ``flexmeasures add report --as-job``.
Run ``flexmeasures jobs run-worker --queue reporting`` to process these jobs. A one-off report
can also be queued through ``POST /api/v3_0/assets/<id>/reports/trigger``. The caller needs read
access to every input and configuration sensor and permission to record data on each output;
outputs are limited to the asset in the URL and its descendants.

The reporter classes we are designing are using pandas under the hood and can be sub-classed, allowing us to build new reporters from stable simpler ones, and even pipelines. Remember: re-use is developer power!

We believe this infrastructure will become very powerful and enable FlexMeasures hosts and plugin developers to implement exciting new features.
Expand Down Expand Up @@ -122,4 +128,4 @@ The input sensor stores the power/energy flow, and the output sensor will store
Here, the ``ProfitOrLossReporter`` used as source (with Id 6) is the one we configured above.
With the offsets, we control the timing ― we indicate that we want the new report to encompass the day of tomorrow (see Pandas offset strings).

The report sensor will now store all costs which we know will be made tomorrow by the schedule.
The report sensor will now store all costs which we know will be made tomorrow by the schedule.
2 changes: 1 addition & 1 deletion documentation/host/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ Then, start workers in a console (or some other method to keep a long-running pr

.. code-block:: bash

$ flexmeasures jobs run-worker --queue "scheduling|forecasting|ingestion"
$ flexmeasures jobs run-worker --queue "scheduling|forecasting|ingestion|reporting"


You can go to `http://localhost:5000/tasks/` and see the state of job queues and find individual jobs (and investigate why they failed, for instance).
Expand Down
4 changes: 3 additions & 1 deletion documentation/host/queues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Here is how to run one worker for each kind of job (in separate terminals):

.. code-block:: bash

$ flexmeasures jobs run-worker --name our-only-worker --queue forecasting|scheduling|ingestion
$ flexmeasures jobs run-worker --name our-only-worker --queue "forecasting|scheduling|ingestion|reporting"

Running multiple workers in parallel might be a great idea.

Expand All @@ -33,6 +33,7 @@ Running multiple workers in parallel might be a great idea.
$ flexmeasures jobs run-worker --name forecaster --queue forecasting
$ flexmeasures jobs run-worker --name scheduler --queue scheduling
$ flexmeasures jobs run-worker --name ingester --queue ingestion
$ flexmeasures jobs run-worker --name reporter --queue reporting

You can also clear the job queues:

Expand All @@ -41,6 +42,7 @@ You can also clear the job queues:
$ flexmeasures jobs clear-queue --queue forecasting
$ flexmeasures jobs clear-queue --queue scheduling
$ flexmeasures jobs clear-queue --queue ingestion
$ flexmeasures jobs clear-queue --queue reporting


When the main FlexMeasures process runs (e.g. by ``flexmeasures run``\ ), the queues of forecasting and scheduling jobs can be visited at ``http://localhost:5000/tasks/forecasting`` and ``http://localhost:5000/tasks/schedules``\ , respectively (by admins).
Expand Down
28 changes: 25 additions & 3 deletions flexmeasures/api/common/schemas/tests/test_sensor_data_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import pytest
import pytz
from rq.job import JobStatus

from marshmallow import ValidationError
import pandas as pd
Expand Down Expand Up @@ -465,7 +466,7 @@ def test_asset_sensors_metadata(


def test_build_asset_jobs_data(db, app, add_battery_assets, clean_redis):
"""Check that we get both types of jobs for a battery asset."""
"""Check that we get scheduling, forecasting and reporting jobs."""
battery_asset = add_battery_assets["Test battery"]
battery = battery_asset.sensors[0]
tz = pytz.timezone("Europe/Amsterdam")
Expand Down Expand Up @@ -495,26 +496,47 @@ def test_build_asset_jobs_data(db, app, add_battery_assets, clean_redis):
},
)
forecasting_job = app.queues["forecasting"].fetch_job(pipeline_returns["job_id"])
reporting_job = app.queues["reporting"].enqueue(sum, [1, 2])
reporting_job.meta["exception"] = "report failed"
reporting_job.save_meta()
reporting_job.set_status(JobStatus.FAILED)
app.job_cache.add(
battery.id,
reporting_job.id,
queue="reporting",
asset_or_sensor_type="sensor",
)

jobs_data = build_asset_jobs_data(battery_asset)
forecasting_jobs_data = [j for j in jobs_data if j["queue"] == "forecasting"]
scheduling_jobs_data = [j for j in jobs_data if j["queue"] == "scheduling"]
reporting_jobs_data = [j for j in jobs_data if j["queue"] == "reporting"]
assert len(forecasting_jobs_data) == 1
assert scheduling_jobs_data
assert len(reporting_jobs_data) == 1
assert (
reporting_jobs_data[0]["err"] == "Reporting job failed with str: report failed"
)
scheduling_job_ids = set()
for job_data in jobs_data:
metadata = json.loads(job_data["metadata"])
if job_data["queue"] == "forecasting":
assert metadata["job_id"] == forecasting_job.id
assert job_data["entity"] == f"sensor: {battery.name} (Id: {battery.id})"
else:
assert job_data["status"] == "queued"
elif job_data["queue"] == "scheduling":
scheduling_job_ids.add(metadata["job_id"])
assert job_data["status"] == "queued"
assert job_data["status"] == "queued"
else:
assert metadata["job_id"] == reporting_job.id
assert job_data["status"] == JobStatus.FAILED

assert scheduling_job.id in scheduling_job_ids

# Clean up queues
app.queues["scheduling"].empty()
app.queues["forecasting"].empty()
app.queues["reporting"].empty()
assert app.queues["scheduling"].count == 0
assert app.queues["forecasting"].count == 0
assert app.queues["reporting"].count == 0
20 changes: 2 additions & 18 deletions flexmeasures/api/common/utils/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
add_beliefs_to_db_and_enqueue_forecasting_jobs,
)
from flexmeasures.data.models.generic_assets import GenericAsset
from flexmeasures.data.queries.generic_assets import asset_is_in_subtree
from flexmeasures.data.models.time_series import Sensor
from flexmeasures.data.utils import (
SAVE_TO_DB_SUCCESS,
Expand Down Expand Up @@ -570,23 +571,6 @@ def _determine_copy_name(
return f"{source_name} (Copy {max_index + 1})"


def _asset_is_in_subtree(root_asset_id: int, candidate_asset_id: int) -> bool:
"""Return True if candidate_asset_id is root or a descendant of root_asset_id."""
current_asset_id = candidate_asset_id
visited: set[int] = set()

while current_asset_id is not None and current_asset_id not in visited:
if current_asset_id == root_asset_id:
return True
visited.add(current_asset_id)
current_asset = db.session.get(GenericAsset, current_asset_id)
if current_asset is None:
return False
current_asset_id = current_asset.parent_asset_id

return False


def copy_asset(
asset: GenericAsset,
account=None,
Expand Down Expand Up @@ -634,7 +618,7 @@ def copy_asset(
target_account_id = int(account.id)
target_parent_asset_id = int(parent_asset.id)

if target_parent_asset_id is not None and _asset_is_in_subtree(
if target_parent_asset_id is not None and asset_is_in_subtree(
root_asset_id=asset.id,
candidate_asset_id=target_parent_asset_id,
):
Expand Down
2 changes: 2 additions & 0 deletions flexmeasures/api/v3_0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
)
from flexmeasures.data.schemas.annotations import AnnotationSchema
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema as AssetSchema
from flexmeasures.data.schemas.reporting import ReportTriggerSchema
from flexmeasures.data.schemas.sensors import QuantitySchema, TimeSeriesSchema
from flexmeasures.data.schemas.account import (
AccountSchema,
Expand Down Expand Up @@ -222,6 +223,7 @@ def create_openapi_specs(app: Flask):
("AssetAPIQuerySchema", AssetAPIQuerySchema),
("AssetSchema", AssetSchema),
("AnnotationSchema", AnnotationSchema),
("ReportTriggerSchema", ReportTriggerSchema),
("CopyAssetSchema", CopyAssetSchema),
("DefaultAssetViewJSONSchema", DefaultAssetViewJSONSchema),
("AccountSchema", AccountSchema(partial=True)),
Expand Down
131 changes: 131 additions & 0 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,21 @@
from flexmeasures.data.models.user import Account
from flexmeasures.data.models.audit_log import AssetAuditLog
from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
from flexmeasures.data.models.reporting import Reporter
from flexmeasures.data.queries.generic_assets import (
asset_is_in_subtree,
filter_assets_under_root,
query_assets_by_search_terms,
)
from flexmeasures.data.queries.utils import id_prefix_filter
from flexmeasures.data.schemas import AwareDateTimeField
from flexmeasures.data.schemas.annotations import AnnotationSchema
from flexmeasures.data.schemas.reporting import ReportTriggerSchema
from flexmeasures.data.services.data_generators import (
check_sensor_access,
resolve_data_generator_sensors,
)
from flexmeasures.data.services.data_sources import get_data_generator
from flexmeasures.data.services.annotations import prepare_annotations_for_chart
from flexmeasures.data.schemas.generic_assets import (
GenericAssetSchema as AssetSchema,
Expand Down Expand Up @@ -1571,6 +1579,129 @@ def update_keep_legends_below_graphs(self, **kwargs):
"message": "Default legend position updated successfully.",
}, 200

@route("/<id>/reports/trigger", methods=["POST"])
@limit_triggers()
@use_kwargs({"asset": AssetIdField(data_key="id")}, location="path")
@permission_required_for_context("create-children", ctx_arg_name="asset")
@as_json
def trigger_report(self, id: int, asset: GenericAsset):
"""
.. :quickref: Assets; Trigger a one-off reporting job for this asset.
---
post:
summary: Trigger a one-off reporting job for this asset.
description: |
Queue a one-off report for a worker processing the `reporting` queue.
The caller must be able to read every input/configuration sensor and
record data on every output sensor. Each output must belong to the
asset in the URL or one of its descendants.
security:
- ApiKeyAuth: []
parameters:
- in: path
name: id
required: true
$ref: '#/components/parameters/AssetIdPath'
requestBody:
content:
application/json:
schema: ReportTriggerSchema
responses:
202:
description: ACCEPTED
content:
application/json:
schema:
type: object
required:
- status
- message
- job
- job-url
properties:
status:
type: string
enum:
- ACCEPTED
message:
type: string
job:
type: string
description: UUID of the queued reporting job.
job-url:
type: string
format: uri
description: URL to query the generic job status API.
example:
status: ACCEPTED
message: Request has been accepted for processing.
job: 364bfd06-c1fa-430b-8d25-8f5a547651fb
job-url: /api/v3_0/jobs/364bfd06-c1fa-430b-8d25-8f5a547651fb
400:
description: INVALID_REQUEST, REQUIRED_INFO_MISSING, UNEXPECTED_PARAMS
401:
description: UNAUTHORIZED
403:
description: INVALID_SENDER
422:
description: UNPROCESSABLE_ENTITY
tags:
- Assets
"""
body = request.get_json(silent=True)
if not body:
return unprocessable_entity("No JSON data provided.")
try:
report_data = ReportTriggerSchema().load(body)
except ValidationError as exc:
return unprocessable_entity(exc.messages)

try:
reporter = get_data_generator(
source=None,
model=report_data["reporter"],
config=report_data["config"],
save_config=True,
data_generator_type=Reporter,
)
except ValidationError as exc:
db.session.rollback()
return unprocessable_entity({"config": exc.messages})
if reporter is None:
db.session.rollback()
return unprocessable_entity(
f"Reporter class `{report_data['reporter']}` not available."
)

parameters = report_data["parameters"]
try:
deserialized_parameters = reporter._parameters_schema.load(parameters)
report_sensors = resolve_data_generator_sensors(
reporter, deserialized_parameters
)
check_sensor_access(
report_sensors["input_sensors"], report_sensors["output_sensors"]
)
for output_sensor in report_sensors["output_sensors"]:
if not asset_is_in_subtree(asset.id, output_sensor.generic_asset_id):
raise ValueError(
f"Report output sensor {output_sensor.id} must belong to asset"
f" {asset.id} or one of its descendants."
)
reporter.set_job_trigger("API")
returns = reporter.compute(as_job=True, parameters=parameters)
except ValidationError as exc:
db.session.rollback()
return unprocessable_entity({"parameters": exc.messages})
except ValueError as exc:
db.session.rollback()
return unprocessable_entity(str(exc))
except Forbidden:
db.session.rollback()
raise

return request_accepted_for_processing(returns["job_id"])

@route("/<id>/schedules/trigger", methods=["POST"])
@limit_triggers()
@use_args(AssetTriggerSchemaV3(), location="args_and_json", as_kwargs=True)
Expand Down
Loading
Loading