diff --git a/.cache/state/jobs.json b/.cache/state/jobs.json new file mode 100644 index 0000000..93c96df --- /dev/null +++ b/.cache/state/jobs.json @@ -0,0 +1 @@ +{"c4e10ad3-03a8-4ccd-b64e-4b36da4ced28": {"jobId": "c4e10ad3-03a8-4ccd-b64e-4b36da4ced28", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:58:59.188643Z", "updated": "2026-02-23T21:58:59.188643Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 7.0}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 14.0}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 21.0}}}]}}, "7fd59008-4d1c-471c-b3b6-959e14b69edd": {"jobId": "7fd59008-4d1c-471c-b3b6-959e14b69edd", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:58:59.202771Z", "updated": "2026-02-23T21:58:59.202771Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "e3e1dbbc-c230-44e9-8c09-067057cf68d9": {"jobId": "e3e1dbbc-c230-44e9-8c09-067057cf68d9", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.590659Z", "updated": "2026-02-23T21:59:01.590659Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "b6a5a683-f18a-4af5-aeca-4a539e028e67": {"jobId": "b6a5a683-f18a-4af5-aeca-4a539e028e67", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.602127Z", "updated": "2026-02-23T21:59:01.602127Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "c50ec201-9057-44f3-b102-37203efb8682": {"jobId": "c50ec201-9057-44f3-b102-37203efb8682", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.607210Z", "updated": "2026-02-23T21:59:01.607210Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 7.0}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 14.0}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 21.0}}}]}}, "98e09e8a-fc6c-4f0b-b227-c7bfe30d8675": {"jobId": "98e09e8a-fc6c-4f0b-b227-c7bfe30d8675", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.620520Z", "updated": "2026-02-23T21:59:01.620520Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 7.0}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 14.0}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 21.0}}}]}}, "fd1d2a89-519d-4ead-afb4-a76ef18cdf6d": {"jobId": "fd1d2a89-519d-4ead-afb4-a76ef18cdf6d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.632737Z", "updated": "2026-02-23T21:59:01.632737Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 7.0}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 14.0}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 21.0}}}]}}, "9ed82292-1c23-4a6f-8077-561c8d431e5a": {"jobId": "9ed82292-1c23-4a6f-8077-561c8d431e5a", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T21:59:01.656924Z", "updated": "2026-02-23T21:59:01.657957Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "865232a5-c544-4120-a2b7-47c8f2a27806": {"jobId": "865232a5-c544-4120-a2b7-47c8f2a27806", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.665259Z", "updated": "2026-02-23T21:59:01.666993Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "8337900a-0fc6-4f53-a13e-89540fb03d2e": {"jobId": "8337900a-0fc6-4f53-a13e-89540fb03d2e", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.677649Z", "updated": "2026-02-23T21:59:01.677649Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 7.0}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 14.0}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 21.0}}}]}}, "2f057f66-9e2f-417a-a416-18a788cc2c4a": {"jobId": "2f057f66-9e2f-417a-a416-18a788cc2c4a", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:01.688591Z", "updated": "2026-02-23T21:59:01.688591Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 7.0}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 14.0}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "values": {"precip": 21.0}}}]}}, "75de8c95-32fd-4e9a-91a6-756110b67124": {"jobId": "75de8c95-32fd-4e9a-91a6-756110b67124", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T21:59:02.163691Z", "updated": "2026-02-23T21:59:02.163691Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "bdebbf54-ca89-48f2-b2a6-3492d2b7676c": {"jobId": "bdebbf54-ca89-48f2-b2a6-3492d2b7676c", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:02:58.655213Z", "updated": "2026-02-23T22:02:58.655213Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "78c36058-d757-4dad-92fa-f8703a052d41": {"jobId": "78c36058-d757-4dad-92fa-f8703a052d41", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:02:58.669440Z", "updated": "2026-02-23T22:02:58.669440Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "af6c1ed5-0a33-47ae-aef6-9e17653cf0a4": {"jobId": "af6c1ed5-0a33-47ae-aef6-9e17653cf0a4", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:00.948460Z", "updated": "2026-02-23T22:03:00.948460Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "c6398375-2a54-47dc-a910-7d6942cc290d": {"jobId": "c6398375-2a54-47dc-a910-7d6942cc290d", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:00.963801Z", "updated": "2026-02-23T22:03:00.963801Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "33118ebd-6849-4b47-b61d-a59b3b9747a3": {"jobId": "33118ebd-6849-4b47-b61d-a59b3b9747a3", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.108286Z", "updated": "2026-02-23T22:03:01.108286Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "1a111b92-83f4-44a6-9bcd-c9d7c05b7032": {"jobId": "1a111b92-83f4-44a6-9bcd-c9d7c05b7032", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.242058Z", "updated": "2026-02-23T22:03:01.242058Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "8ae1afa0-88d9-493a-89e7-d118c6651e43": {"jobId": "8ae1afa0-88d9-493a-89e7-d118c6651e43", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.347704Z", "updated": "2026-02-23T22:03:01.347704Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "fef7a2ef-eda6-42b0-a95d-da21341ab899": {"jobId": "fef7a2ef-eda6-42b0-a95d-da21341ab899", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:03:01.375491Z", "updated": "2026-02-23T22:03:01.377348Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "785832c4-e2a6-43d1-a64a-53d3148f18a9": {"jobId": "785832c4-e2a6-43d1-a64a-53d3148f18a9", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.385768Z", "updated": "2026-02-23T22:03:01.388980Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "d42e8fc4-1023-453c-9aae-8c33141ca838": {"jobId": "d42e8fc4-1023-453c-9aae-8c33141ca838", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.501817Z", "updated": "2026-02-23T22:03:01.501817Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "6b0eb5cb-57c9-4945-88b8-029123af9a2d": {"jobId": "6b0eb5cb-57c9-4945-88b8-029123af9a2d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.649712Z", "updated": "2026-02-23T22:03:01.649712Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "2187b12b-1dc3-486c-906f-5d5d79a650b4": {"jobId": "2187b12b-1dc3-486c-906f-5d5d79a650b4", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:01.863133Z", "updated": "2026-02-23T22:03:01.863133Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "e7b8935b-cdf0-40e4-a12c-3e0cb94e7fe1": {"jobId": "e7b8935b-cdf0-40e4-a12c-3e0cb94e7fe1", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:32.137243Z", "updated": "2026-02-23T22:03:32.137243Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "589b3193-0684-43bf-83ce-164e22b5866d": {"jobId": "589b3193-0684-43bf-83ce-164e22b5866d", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:32.153159Z", "updated": "2026-02-23T22:03:32.153159Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "1a897341-a4d7-4ed8-827e-a8c70e12743b": {"jobId": "1a897341-a4d7-4ed8-827e-a8c70e12743b", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.420141Z", "updated": "2026-02-23T22:03:34.420141Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "0a34d07d-f3e2-4ce4-9ed1-2e068d448d65": {"jobId": "0a34d07d-f3e2-4ce4-9ed1-2e068d448d65", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.435985Z", "updated": "2026-02-23T22:03:34.435985Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "ba464088-1fa7-46b4-839e-8a6daceae41e": {"jobId": "ba464088-1fa7-46b4-839e-8a6daceae41e", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.570403Z", "updated": "2026-02-23T22:03:34.570403Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "6b2d03e9-9e31-42f4-80f4-8d1b27442c78": {"jobId": "6b2d03e9-9e31-42f4-80f4-8d1b27442c78", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.695656Z", "updated": "2026-02-23T22:03:34.695656Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "5d45d9ce-795c-4615-b973-33cc358c1327": {"jobId": "5d45d9ce-795c-4615-b973-33cc358c1327", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.805109Z", "updated": "2026-02-23T22:03:34.805109Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "4c2dae6b-dbff-4591-91ca-5a679a4cd52f": {"jobId": "4c2dae6b-dbff-4591-91ca-5a679a4cd52f", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:03:34.833666Z", "updated": "2026-02-23T22:03:34.836522Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "c2c62770-2bef-4f4f-9118-84b217b504f0": {"jobId": "c2c62770-2bef-4f4f-9118-84b217b504f0", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.846117Z", "updated": "2026-02-23T22:03:34.849465Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "d6098fe4-5916-4d3b-b540-de1076f51c8d": {"jobId": "d6098fe4-5916-4d3b-b540-de1076f51c8d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:34.960970Z", "updated": "2026-02-23T22:03:34.960970Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "2462ff1e-4093-490c-bef6-eacb3b094e1f": {"jobId": "2462ff1e-4093-490c-bef6-eacb3b094e1f", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:35.106651Z", "updated": "2026-02-23T22:03:35.106651Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "b7920d20-1954-44e2-81d2-e76bf4f03e90": {"jobId": "b7920d20-1954-44e2-81d2-e76bf4f03e90", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:03:35.319641Z", "updated": "2026-02-23T22:03:35.319641Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "446976dc-2fba-4de9-b94d-9d066fdc0c4e": {"jobId": "446976dc-2fba-4de9-b94d-9d066fdc0c4e", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:06.462908Z", "updated": "2026-02-23T22:04:06.462908Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "0ed8ec84-c466-41e3-9591-0553c77975a9": {"jobId": "0ed8ec84-c466-41e3-9591-0553c77975a9", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:06.479128Z", "updated": "2026-02-23T22:04:06.479128Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "b78441e3-a9f1-437c-a279-15518692ac62": {"jobId": "b78441e3-a9f1-437c-a279-15518692ac62", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:08.742026Z", "updated": "2026-02-23T22:04:08.742026Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "69a12038-bcd5-4e8b-a545-0c6f6d3d2a52": {"jobId": "69a12038-bcd5-4e8b-a545-0c6f6d3d2a52", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:08.756654Z", "updated": "2026-02-23T22:04:08.756654Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "41bac021-d432-437f-85dc-bba5cb0757a6": {"jobId": "41bac021-d432-437f-85dc-bba5cb0757a6", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:08.885064Z", "updated": "2026-02-23T22:04:08.885064Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "5442f975-e3e8-4627-9737-b0ed509543c8": {"jobId": "5442f975-e3e8-4627-9737-b0ed509543c8", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:09.010488Z", "updated": "2026-02-23T22:04:09.010488Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "5f68bea6-8dd8-421e-98d7-0cc70d496462": {"jobId": "5f68bea6-8dd8-421e-98d7-0cc70d496462", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:09.122398Z", "updated": "2026-02-23T22:04:09.122398Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "ebf4ee34-fb19-4872-9701-6ecafe5f1fd4": {"jobId": "ebf4ee34-fb19-4872-9701-6ecafe5f1fd4", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:04:09.153630Z", "updated": "2026-02-23T22:04:09.157440Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "67480820-6c57-4fd5-a991-1f7ddfd0ee56": {"jobId": "67480820-6c57-4fd5-a991-1f7ddfd0ee56", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:09.167290Z", "updated": "2026-02-23T22:04:09.171405Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "fb84e869-28c5-4187-b963-62a0f0ddb0d0": {"jobId": "fb84e869-28c5-4187-b963-62a0f0ddb0d0", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:09.286819Z", "updated": "2026-02-23T22:04:09.286819Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "5d9f7eba-2303-4efa-aa2c-30f93fc6971a": {"jobId": "5d9f7eba-2303-4efa-aa2c-30f93fc6971a", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:09.471642Z", "updated": "2026-02-23T22:04:09.471642Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "697f0d2d-d86a-4bfe-8dac-aa80a6b5df7f": {"jobId": "697f0d2d-d86a-4bfe-8dac-aa80a6b5df7f", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:04:09.691847Z", "updated": "2026-02-23T22:04:09.691847Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "bab696ee-5483-4c82-bf44-e457c5f0ea69": {"jobId": "bab696ee-5483-4c82-bf44-e457c5f0ea69", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:23.547932Z", "updated": "2026-02-23T22:05:23.547932Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "370b5752-bb53-44e5-b373-96d014ead43f": {"jobId": "370b5752-bb53-44e5-b373-96d014ead43f", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:23.564772Z", "updated": "2026-02-23T22:05:23.564772Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "d9d78be7-a46e-4236-a815-f65e42cab9fe": {"jobId": "d9d78be7-a46e-4236-a815-f65e42cab9fe", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:25.819681Z", "updated": "2026-02-23T22:05:25.819681Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "d9248c76-d0b1-4107-8739-d26e01dd859c": {"jobId": "d9248c76-d0b1-4107-8739-d26e01dd859c", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:25.835593Z", "updated": "2026-02-23T22:05:25.835593Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "23ee0b0c-c1f0-4be4-83d3-c82e8fe98ae7": {"jobId": "23ee0b0c-c1f0-4be4-83d3-c82e8fe98ae7", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:25.939376Z", "updated": "2026-02-23T22:05:25.939376Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "48b4c768-bbaf-47a8-aeb7-44e697067cb9": {"jobId": "48b4c768-bbaf-47a8-aeb7-44e697067cb9", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:26.102926Z", "updated": "2026-02-23T22:05:26.102926Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "052828cb-fe04-4107-8168-bf626dc3d2d4": {"jobId": "052828cb-fe04-4107-8168-bf626dc3d2d4", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:26.213041Z", "updated": "2026-02-23T22:05:26.213041Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "057a6900-c6e4-4ee7-b392-16cc852a7beb": {"jobId": "057a6900-c6e4-4ee7-b392-16cc852a7beb", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:05:26.244912Z", "updated": "2026-02-23T22:05:26.249375Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "f07f0036-8f43-4d12-8811-7a3e6603228b": {"jobId": "f07f0036-8f43-4d12-8811-7a3e6603228b", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:26.260482Z", "updated": "2026-02-23T22:05:26.265682Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "8eee9f98-bab7-43d9-a8ea-6d7d0f8c5bff": {"jobId": "8eee9f98-bab7-43d9-a8ea-6d7d0f8c5bff", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:26.380473Z", "updated": "2026-02-23T22:05:26.380473Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "b232896b-2721-46b8-abc8-9761a40f9d22": {"jobId": "b232896b-2721-46b8-abc8-9761a40f9d22", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:26.527434Z", "updated": "2026-02-23T22:05:26.527434Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "1f2bf79d-8181-490d-a45e-9438e38b45c6": {"jobId": "1f2bf79d-8181-490d-a45e-9438e38b45c6", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:05:26.742199Z", "updated": "2026-02-23T22:05:26.742199Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "6df02f8b-df42-41dd-b987-a06dbeaf5539": {"jobId": "6df02f8b-df42-41dd-b987-a06dbeaf5539", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:17.263363Z", "updated": "2026-02-23T22:06:17.263363Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "8ea3e465-f7a8-4b71-8375-38f65fcf63ca": {"jobId": "8ea3e465-f7a8-4b71-8375-38f65fcf63ca", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:17.295123Z", "updated": "2026-02-23T22:06:17.295123Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "444c5c43-cbdd-409f-b180-e8b339f8ef11": {"jobId": "444c5c43-cbdd-409f-b180-e8b339f8ef11", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:19.520179Z", "updated": "2026-02-23T22:06:19.520179Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "ebc38efc-530c-444c-bb0f-a9b22912944c": {"jobId": "ebc38efc-530c-444c-bb0f-a9b22912944c", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:19.536100Z", "updated": "2026-02-23T22:06:19.536100Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "973f8a9e-f220-436f-91b2-6507e96b615a": {"jobId": "973f8a9e-f220-436f-91b2-6507e96b615a", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:19.666645Z", "updated": "2026-02-23T22:06:19.666645Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "ab19876c-d13d-4c37-82f5-af575b863ae9": {"jobId": "ab19876c-d13d-4c37-82f5-af575b863ae9", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:19.795408Z", "updated": "2026-02-23T22:06:19.795408Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "bb27b64c-2cda-41ab-8e2b-b23ff462de71": {"jobId": "bb27b64c-2cda-41ab-8e2b-b23ff462de71", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:19.907708Z", "updated": "2026-02-23T22:06:19.907708Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "d0cd71f3-2aff-4345-8804-6451bd9d78bd": {"jobId": "d0cd71f3-2aff-4345-8804-6451bd9d78bd", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:06:19.940310Z", "updated": "2026-02-23T22:06:19.945539Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "4c120e2c-3b8e-471c-a944-d4fe46e6544d": {"jobId": "4c120e2c-3b8e-471c-a944-d4fe46e6544d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:19.965760Z", "updated": "2026-02-23T22:06:19.972003Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "a0138e99-87f4-4213-b003-53e4435d2191": {"jobId": "a0138e99-87f4-4213-b003-53e4435d2191", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:20.085244Z", "updated": "2026-02-23T22:06:20.085244Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "67ffa0a9-35fe-4780-b5e7-17cebd8321c3": {"jobId": "67ffa0a9-35fe-4780-b5e7-17cebd8321c3", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:20.234581Z", "updated": "2026-02-23T22:06:20.234581Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "1640da1c-e8fe-4aaf-b8d6-5f4441a05f4d": {"jobId": "1640da1c-e8fe-4aaf-b8d6-5f4441a05f4d", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:06:20.456531Z", "updated": "2026-02-23T22:06:20.456531Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "1bea47ea-b78c-4c37-ad4a-4bc4731dc493": {"jobId": "1bea47ea-b78c-4c37-ad4a-4bc4731dc493", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:07.539027Z", "updated": "2026-02-23T22:07:07.539027Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "1277fe9e-4a99-4014-8c55-66804e616440": {"jobId": "1277fe9e-4a99-4014-8c55-66804e616440", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:07.558215Z", "updated": "2026-02-23T22:07:07.558215Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "267b8300-765d-40e3-9ec2-5d6478f47892": {"jobId": "267b8300-765d-40e3-9ec2-5d6478f47892", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:09.804460Z", "updated": "2026-02-23T22:07:09.804460Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "cdefce12-bd1d-4828-a27d-0960d104c6fa": {"jobId": "cdefce12-bd1d-4828-a27d-0960d104c6fa", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:09.821632Z", "updated": "2026-02-23T22:07:09.821632Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "092d45de-2992-4688-9eea-5e47bef48651": {"jobId": "092d45de-2992-4688-9eea-5e47bef48651", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:09.928469Z", "updated": "2026-02-23T22:07:09.928469Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "55481550-c20d-404f-888b-972569e1a21d": {"jobId": "55481550-c20d-404f-888b-972569e1a21d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:10.096984Z", "updated": "2026-02-23T22:07:10.096984Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "db8c46c0-4252-4091-89b6-7bcac3da7ab2": {"jobId": "db8c46c0-4252-4091-89b6-7bcac3da7ab2", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:10.207616Z", "updated": "2026-02-23T22:07:10.207616Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "36ad61d8-3b1a-4fa3-bc58-b801c12e6980": {"jobId": "36ad61d8-3b1a-4fa3-bc58-b801c12e6980", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:07:10.241678Z", "updated": "2026-02-23T22:07:10.247863Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "7c89eaf6-cad3-42bf-82b7-0159b289ab4d": {"jobId": "7c89eaf6-cad3-42bf-82b7-0159b289ab4d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:10.260512Z", "updated": "2026-02-23T22:07:10.267134Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "07711cbe-a49b-43ec-b9f5-a47ff809a637": {"jobId": "07711cbe-a49b-43ec-b9f5-a47ff809a637", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:10.381958Z", "updated": "2026-02-23T22:07:10.381958Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "b60ed6da-a124-49f0-a4ac-d52c8d79cb70": {"jobId": "b60ed6da-a124-49f0-a4ac-d52c8d79cb70", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:10.534954Z", "updated": "2026-02-23T22:07:10.534954Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "b7198527-3cd1-4105-ad05-e96e3eb2dc0b": {"jobId": "b7198527-3cd1-4105-ad05-e96e3eb2dc0b", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:07:10.756754Z", "updated": "2026-02-23T22:07:10.756754Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "8c44185b-b5d3-48be-9786-fd8949a4eec0": {"jobId": "8c44185b-b5d3-48be-9786-fd8949a4eec0", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:23.056931Z", "updated": "2026-02-23T22:08:23.056931Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "159d008c-2cbe-43dc-9da0-d476df8f8912": {"jobId": "159d008c-2cbe-43dc-9da0-d476df8f8912", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:23.078203Z", "updated": "2026-02-23T22:08:23.078203Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "4fefa042-d0f2-40d5-878e-65410620d408": {"jobId": "4fefa042-d0f2-40d5-878e-65410620d408", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.348486Z", "updated": "2026-02-23T22:08:25.348486Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "a47f73c7-ce37-48c9-af73-11760b35e296": {"jobId": "a47f73c7-ce37-48c9-af73-11760b35e296", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.366760Z", "updated": "2026-02-23T22:08:25.366760Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "9be31308-883e-4f46-927f-8faf8a43795d": {"jobId": "9be31308-883e-4f46-927f-8faf8a43795d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.476319Z", "updated": "2026-02-23T22:08:25.476319Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "b10443b2-5b5c-4a8a-9d3d-7fba142e87c8": {"jobId": "b10443b2-5b5c-4a8a-9d3d-7fba142e87c8", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.648456Z", "updated": "2026-02-23T22:08:25.648456Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "1df779b8-aa1c-439f-a5d7-3525fb54dc77": {"jobId": "1df779b8-aa1c-439f-a5d7-3525fb54dc77", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.758845Z", "updated": "2026-02-23T22:08:25.758845Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "7dd93d56-a789-4e5e-9f4b-b34bdc9edc33": {"jobId": "7dd93d56-a789-4e5e-9f4b-b34bdc9edc33", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:08:25.793139Z", "updated": "2026-02-23T22:08:25.800459Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "2da98086-c89d-49cb-bac4-5a2f63c02d3b": {"jobId": "2da98086-c89d-49cb-bac4-5a2f63c02d3b", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.814740Z", "updated": "2026-02-23T22:08:25.822762Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "e3f2a3ef-efb8-4a71-a892-437463f84e13": {"jobId": "e3f2a3ef-efb8-4a71-a892-437463f84e13", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:25.940926Z", "updated": "2026-02-23T22:08:25.940926Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "c8e8832f-a05e-4ae1-89bc-418391552a31": {"jobId": "c8e8832f-a05e-4ae1-89bc-418391552a31", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:26.094254Z", "updated": "2026-02-23T22:08:26.094254Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "3491be0a-69f9-4f9a-924c-79fbcb7404a5": {"jobId": "3491be0a-69f9-4f9a-924c-79fbcb7404a5", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:08:26.312951Z", "updated": "2026-02-23T22:08:26.312951Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "b78a5896-d88d-4592-bc98-36dd20426679": {"jobId": "b78a5896-d88d-4592-bc98-36dd20426679", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:08.677668Z", "updated": "2026-02-23T22:10:08.677668Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "c842962b-c87d-4787-bbed-499da0a0e659": {"jobId": "c842962b-c87d-4787-bbed-499da0a0e659", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:08.698047Z", "updated": "2026-02-23T22:10:08.698047Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "9ffc08f6-24de-4744-9f55-5416c269c284": {"jobId": "9ffc08f6-24de-4744-9f55-5416c269c284", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:10.975367Z", "updated": "2026-02-23T22:10:10.975367Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "33140241-39cf-4963-9a9f-c4068665dbdb": {"jobId": "33140241-39cf-4963-9a9f-c4068665dbdb", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:10.995315Z", "updated": "2026-02-23T22:10:10.995315Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "51621767-7943-48df-a074-9f29f6c2374d": {"jobId": "51621767-7943-48df-a074-9f29f6c2374d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.106175Z", "updated": "2026-02-23T22:10:11.106175Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "d3119749-3e84-489c-b36c-de752ff0f44f": {"jobId": "d3119749-3e84-489c-b36c-de752ff0f44f", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.269887Z", "updated": "2026-02-23T22:10:11.269887Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "48cc3a56-6df8-42b6-a1cd-ab380a687d17": {"jobId": "48cc3a56-6df8-42b6-a1cd-ab380a687d17", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.379628Z", "updated": "2026-02-23T22:10:11.379628Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "672705fa-17df-48d3-9dfe-89edd3033977": {"jobId": "672705fa-17df-48d3-9dfe-89edd3033977", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:10:11.415532Z", "updated": "2026-02-23T22:10:11.423246Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "62523329-5a27-4557-ae52-b3a3668cf8b9": {"jobId": "62523329-5a27-4557-ae52-b3a3668cf8b9", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.438093Z", "updated": "2026-02-23T22:10:11.447351Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "3bc40154-66d2-44b8-bf8a-3c931d7ce92a": {"jobId": "3bc40154-66d2-44b8-bf8a-3c931d7ce92a", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.565043Z", "updated": "2026-02-23T22:10:11.565043Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "aa3f467e-d50a-4120-b9b0-0e2d121f169e": {"jobId": "aa3f467e-d50a-4120-b9b0-0e2d121f169e", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.717150Z", "updated": "2026-02-23T22:10:11.717150Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "d59d5ce4-e5a1-4087-91df-f5f957b056ef": {"jobId": "d59d5ce4-e5a1-4087-91df-f5f957b056ef", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:10:11.933145Z", "updated": "2026-02-23T22:10:11.933145Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "277592bc-6939-4806-af05-87c7bf533d5f": {"jobId": "277592bc-6939-4806-af05-87c7bf533d5f", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:24.503021Z", "updated": "2026-02-23T22:12:24.503021Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "7006c679-b227-4022-9c35-3e2cfc85b0f4": {"jobId": "7006c679-b227-4022-9c35-3e2cfc85b0f4", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:24.525639Z", "updated": "2026-02-23T22:12:24.525639Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "8b551dab-da4c-49ad-99ad-beafe80eebb1": {"jobId": "8b551dab-da4c-49ad-99ad-beafe80eebb1", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:26.896434Z", "updated": "2026-02-23T22:12:26.896434Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "a771c412-cd8d-481f-86c5-60973c502cf2": {"jobId": "a771c412-cd8d-481f-86c5-60973c502cf2", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:26.917174Z", "updated": "2026-02-23T22:12:26.917174Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "895fa56d-a56a-45a0-bcac-1ba7cb072f9d": {"jobId": "895fa56d-a56a-45a0-bcac-1ba7cb072f9d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.031192Z", "updated": "2026-02-23T22:12:27.031192Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "b878b07e-4d0c-4452-8ad0-b7d2e014b415": {"jobId": "b878b07e-4d0c-4452-8ad0-b7d2e014b415", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.214706Z", "updated": "2026-02-23T22:12:27.214706Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "4c59c2e2-a55c-475e-8f49-8060b0070c15": {"jobId": "4c59c2e2-a55c-475e-8f49-8060b0070c15", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.331969Z", "updated": "2026-02-23T22:12:27.331969Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "743ddcec-4c43-49cf-a3d7-a79fd07d203c": {"jobId": "743ddcec-4c43-49cf-a3d7-a79fd07d203c", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:12:27.370127Z", "updated": "2026-02-23T22:12:27.378919Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "048b97d2-321b-412b-8add-8bfc73947d10": {"jobId": "048b97d2-321b-412b-8add-8bfc73947d10", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.395188Z", "updated": "2026-02-23T22:12:27.405120Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "faaa0822-0996-42aa-9b4a-09db3b38b253": {"jobId": "faaa0822-0996-42aa-9b4a-09db3b38b253", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.528860Z", "updated": "2026-02-23T22:12:27.528860Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "2f62167a-3bab-496e-b408-f1078f023253": {"jobId": "2f62167a-3bab-496e-b408-f1078f023253", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.680510Z", "updated": "2026-02-23T22:12:27.680510Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "c60578bb-1943-4a06-ba54-ef1f73aa7a31": {"jobId": "c60578bb-1943-4a06-ba54-ef1f73aa7a31", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:12:27.896422Z", "updated": "2026-02-23T22:12:27.896422Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "ddd7182d-c5e7-4708-9d27-6c44c59101e6": {"jobId": "ddd7182d-c5e7-4708-9d27-6c44c59101e6", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:52.476895Z", "updated": "2026-02-23T22:13:52.476895Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "dce96d73-0dd2-433a-ab9f-50708fd76735": {"jobId": "dce96d73-0dd2-433a-ab9f-50708fd76735", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:52.499440Z", "updated": "2026-02-23T22:13:52.499440Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "efadd460-9f5d-40e6-b6d2-7f962b324388": {"jobId": "efadd460-9f5d-40e6-b6d2-7f962b324388", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:54.763632Z", "updated": "2026-02-23T22:13:54.763632Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "9cf3a3f0-f611-41c1-9deb-94268951ce28": {"jobId": "9cf3a3f0-f611-41c1-9deb-94268951ce28", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:54.785857Z", "updated": "2026-02-23T22:13:54.785857Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "245ec5ef-e750-45ae-8a6e-4b05fd695da7": {"jobId": "245ec5ef-e750-45ae-8a6e-4b05fd695da7", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:54.893765Z", "updated": "2026-02-23T22:13:54.893765Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "dc3129a8-db7b-4354-bcb5-3beb9df098af": {"jobId": "dc3129a8-db7b-4354-bcb5-3beb9df098af", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:55.067514Z", "updated": "2026-02-23T22:13:55.067514Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "421d8036-8bb8-4d19-8ab2-16b12114c03f": {"jobId": "421d8036-8bb8-4d19-8ab2-16b12114c03f", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:55.183339Z", "updated": "2026-02-23T22:13:55.183339Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "1f0946fe-d7cf-4ff5-ae38-ea332be556f8": {"jobId": "1f0946fe-d7cf-4ff5-ae38-ea332be556f8", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:13:55.221492Z", "updated": "2026-02-23T22:13:55.231516Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "d407d400-5ed4-4dee-9d95-749ef70bfa2d": {"jobId": "d407d400-5ed4-4dee-9d95-749ef70bfa2d", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:55.247933Z", "updated": "2026-02-23T22:13:55.258441Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "89f1748d-a2ff-4fb4-bad4-034af929932e": {"jobId": "89f1748d-a2ff-4fb4-bad4-034af929932e", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:55.373387Z", "updated": "2026-02-23T22:13:55.373387Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "fef322d8-8a06-4091-b962-2f6f616cbb71": {"jobId": "fef322d8-8a06-4091-b962-2f6f616cbb71", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:55.536523Z", "updated": "2026-02-23T22:13:55.536523Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "200b0ab7-b566-4c98-9639-9c019adc4af5": {"jobId": "200b0ab7-b566-4c98-9639-9c019adc4af5", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:13:55.767251Z", "updated": "2026-02-23T22:13:55.767251Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "9de477f4-e0ab-44c3-8b0d-a785df5ace6b": {"jobId": "9de477f4-e0ab-44c3-8b0d-a785df5ace6b", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:37.450246Z", "updated": "2026-02-23T22:15:37.450246Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "2716eab1-7e96-4ecc-a47b-b12890300ed0": {"jobId": "2716eab1-7e96-4ecc-a47b-b12890300ed0", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:37.473495Z", "updated": "2026-02-23T22:15:37.473495Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "affe6d5d-97ce-4762-b4dc-18a8c62bd933": {"jobId": "affe6d5d-97ce-4762-b4dc-18a8c62bd933", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:39.721512Z", "updated": "2026-02-23T22:15:39.721512Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "5d6a4802-d6fc-4e15-be23-cb55cd6d43fa": {"jobId": "5d6a4802-d6fc-4e15-be23-cb55cd6d43fa", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:39.743320Z", "updated": "2026-02-23T22:15:39.743320Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "881249ed-7649-49a0-a8d2-ea5d453387b2": {"jobId": "881249ed-7649-49a0-a8d2-ea5d453387b2", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:39.852155Z", "updated": "2026-02-23T22:15:39.852155Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "09078395-6734-4aa0-804d-342d533f5a08": {"jobId": "09078395-6734-4aa0-804d-342d533f5a08", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:40.016212Z", "updated": "2026-02-23T22:15:40.016212Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "ac76a665-136a-4730-9228-dceb0a828f85": {"jobId": "ac76a665-136a-4730-9228-dceb0a828f85", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:40.130039Z", "updated": "2026-02-23T22:15:40.130039Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "101a1fa5-3a6b-4923-8b3d-08497fc2cde3": {"jobId": "101a1fa5-3a6b-4923-8b3d-08497fc2cde3", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:15:40.171547Z", "updated": "2026-02-23T22:15:40.182169Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "90649d63-8edc-4d9e-aac5-1b3a6fd2f954": {"jobId": "90649d63-8edc-4d9e-aac5-1b3a6fd2f954", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:40.200090Z", "updated": "2026-02-23T22:15:40.211787Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "a0aa8517-0ec2-4b4a-86c0-5645c4077a6b": {"jobId": "a0aa8517-0ec2-4b4a-86c0-5645c4077a6b", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:40.332893Z", "updated": "2026-02-23T22:15:40.332893Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "e3dee3c1-f3d9-4340-a412-48964442a29b": {"jobId": "e3dee3c1-f3d9-4340-a412-48964442a29b", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:40.497073Z", "updated": "2026-02-23T22:15:40.497073Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "a44b6fc3-3b43-4b6c-ac09-4c2bbd973e37": {"jobId": "a44b6fc3-3b43-4b6c-ac09-4c2bbd973e37", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:15:40.733179Z", "updated": "2026-02-23T22:15:40.733179Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "c102da24-1bac-4567-af7f-39a0eff270f4": {"jobId": "c102da24-1bac-4567-af7f-39a0eff270f4", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:53.998689Z", "updated": "2026-02-23T22:16:53.998689Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "7b2b7e4b-b1c1-45d7-a382-78b3f72cb077": {"jobId": "7b2b7e4b-b1c1-45d7-a382-78b3f72cb077", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:54.024575Z", "updated": "2026-02-23T22:16:54.024575Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "1a8a2412-5b27-40da-a8f0-94f680e026b4": {"jobId": "1a8a2412-5b27-40da-a8f0-94f680e026b4", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.296999Z", "updated": "2026-02-23T22:16:56.296999Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "ec28d8a2-6234-471f-b96f-7c1731d18a0f": {"jobId": "ec28d8a2-6234-471f-b96f-7c1731d18a0f", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.320203Z", "updated": "2026-02-23T22:16:56.320203Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "0dc16554-7eaf-497c-9a1e-4efc286ba872": {"jobId": "0dc16554-7eaf-497c-9a1e-4efc286ba872", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.432278Z", "updated": "2026-02-23T22:16:56.432278Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "8e276ac9-189f-432d-adec-b7ab74ea6590": {"jobId": "8e276ac9-189f-432d-adec-b7ab74ea6590", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.602095Z", "updated": "2026-02-23T22:16:56.602095Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "bc4505e6-8dbf-4d87-ae54-43e5b714a4e9": {"jobId": "bc4505e6-8dbf-4d87-ae54-43e5b714a4e9", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.720377Z", "updated": "2026-02-23T22:16:56.720377Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "4e30a788-c134-463a-bdbb-f88b4b5615c9": {"jobId": "4e30a788-c134-463a-bdbb-f88b4b5615c9", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:16:56.760958Z", "updated": "2026-02-23T22:16:56.772441Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "336d659f-b4f7-4a49-a7d0-a5ec78dc25d8": {"jobId": "336d659f-b4f7-4a49-a7d0-a5ec78dc25d8", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.791195Z", "updated": "2026-02-23T22:16:56.803752Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "9a34fb59-d815-4107-9c6b-d3207704d83a": {"jobId": "9a34fb59-d815-4107-9c6b-d3207704d83a", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:56.926163Z", "updated": "2026-02-23T22:16:56.926163Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "56c4347f-b16f-40db-9f64-b0239711faac": {"jobId": "56c4347f-b16f-40db-9f64-b0239711faac", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:57.053777Z", "updated": "2026-02-23T22:16:57.053777Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "fbe518c3-121e-4097-8a28-322ddee317e7": {"jobId": "fbe518c3-121e-4097-8a28-322ddee317e7", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:16:57.304091Z", "updated": "2026-02-23T22:16:57.304091Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "cba3a8e8-2dd3-48c3-ad24-822320428aef": {"jobId": "cba3a8e8-2dd3-48c3-ad24-822320428aef", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:06.883459Z", "updated": "2026-02-23T22:18:06.883459Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "40e9fb81-f7f8-48c9-a83c-bf1ce068c960": {"jobId": "40e9fb81-f7f8-48c9-a83c-bf1ce068c960", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:06.910504Z", "updated": "2026-02-23T22:18:06.910504Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "33ff2a20-5a20-4bf0-a70c-dda4f52753e3": {"jobId": "33ff2a20-5a20-4bf0-a70c-dda4f52753e3", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.164136Z", "updated": "2026-02-23T22:18:09.164136Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "798f2a1a-3d44-4778-9552-5eef336a799e": {"jobId": "798f2a1a-3d44-4778-9552-5eef336a799e", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.186896Z", "updated": "2026-02-23T22:18:09.186896Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "426ebad3-da3e-4748-93c0-f7286de43ce8": {"jobId": "426ebad3-da3e-4748-93c0-f7286de43ce8", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.295646Z", "updated": "2026-02-23T22:18:09.295646Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "4eb5c271-dee4-4f16-9740-141f3ac0e5fa": {"jobId": "4eb5c271-dee4-4f16-9740-141f3ac0e5fa", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.470972Z", "updated": "2026-02-23T22:18:09.470972Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "bef54659-7bfc-4c67-b172-1722edcea9cb": {"jobId": "bef54659-7bfc-4c67-b172-1722edcea9cb", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.590918Z", "updated": "2026-02-23T22:18:09.590918Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "2ad476b8-a2a6-4783-948e-42087f50a7e4": {"jobId": "2ad476b8-a2a6-4783-948e-42087f50a7e4", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-23T22:18:09.635442Z", "updated": "2026-02-23T22:18:09.647649Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "81cfc65f-344c-43da-a0ac-d002ec25c0ed": {"jobId": "81cfc65f-344c-43da-a0ac-d002ec25c0ed", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.667508Z", "updated": "2026-02-23T22:18:09.680422Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "50f69ac8-ca39-4892-bf3c-2921221183b8": {"jobId": "50f69ac8-ca39-4892-bf3c-2921221183b8", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.799866Z", "updated": "2026-02-23T22:18:09.799866Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "503e7d84-be48-4fa8-a809-ce44b23325ab": {"jobId": "503e7d84-be48-4fa8-a809-ce44b23325ab", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:09.925723Z", "updated": "2026-02-23T22:18:09.925723Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "c0e21843-e0a2-42a1-806f-faf8b325ab93": {"jobId": "c0e21843-e0a2-42a1-806f-faf8b325ab93", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-23T22:18:10.182289Z", "updated": "2026-02-23T22:18:10.182289Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}, "7f5e71ec-f2ef-48f4-96ee-8fbc321e746c": {"jobId": "7f5e71ec-f2ef-48f4-96ee-8fbc321e746c", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:13.899162Z", "updated": "2026-02-24T11:01:13.899162Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "744622fe-029a-40f3-8da0-72ca67a1b965": {"jobId": "744622fe-029a-40f3-8da0-72ca67a1b965", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:13.927623Z", "updated": "2026-02-24T11:01:13.927623Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 1, "deleted": 0, "dryRun": true}, "features": [{"type": "Feature", "id": "org-demo", "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, "properties": {"orgUnit": "org-demo", "value": 4.0}}]}}, "cbcb748c-e2d3-41c8-802c-7fbe6f071ffe": {"jobId": "cbcb748c-e2d3-41c8-802c-7fbe6f071ffe", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:16.564177Z", "updated": "2026-02-24T11:01:16.564177Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 31.0}}]}}, "53534dbe-524f-4057-b501-7aeff92d944e": {"jobId": "53534dbe-524f-4057-b501-7aeff92d944e", "processId": "xclim-warm-days", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:16.587969Z", "updated": "2026-02-24T11:01:16.587969Z", "inputs": {"datasetId": "era5-land-daily", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 35.0, "unit": "degC"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 0.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "era5-land-daily", "indicator": "xclim-warm-days", "parameter": "2m_temperature", "start": "2026-01-01", "end": "2026-01-31", "source": "synthetic-fallback", "threshold": {"value": 35.0, "unit": "degC"}, "value": 3.0}}]}}, "71c380af-7721-47fe-93fc-34a941e09c7f": {"jobId": "71c380af-7721-47fe-93fc-34a941e09c7f", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:16.697073Z", "updated": "2026-02-24T11:01:16.697073Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": false}}, "outputs": {"importSummary": {"imported": 3, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": false, "source": "dhis2"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "91cbfa60-81bf-4151-b6bd-41cfaf45dde1": {"jobId": "91cbfa60-81bf-4151-b6bd-41cfaf45dde1", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:16.873542Z", "updated": "2026-02-24T11:01:16.873542Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "8da9f4b3-3858-43ff-a0f4-c9e4c16cc4c6": {"jobId": "8da9f4b3-3858-43ff-a0f4-c9e4c16cc4c6", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:16.997135Z", "updated": "2026-02-24T11:01:16.997135Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "8e3f5696-b9e8-4f4b-ad89-8db4d6e5831e": {"jobId": "8e3f5696-b9e8-4f4b-ad89-8db4d6e5831e", "processId": "eo-aggregate-import", "status": "queued", "progress": 0, "created": "2026-02-24T11:01:17.043907Z", "updated": "2026-02-24T11:01:17.057770Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-1"}}, "df874e3d-9622-4597-9bb4-fd450c7a18a8": {"jobId": "df874e3d-9622-4597-9bb4-fd450c7a18a8", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:17.079347Z", "updated": "2026-02-24T11:01:17.093680Z", "inputs": {"dhis2": {"dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 0, "deleted": 0, "dryRun": true}, "features": []}, "execution": {"source": "prefect", "flowRunId": "flow-run-2", "state": {"type": "COMPLETED", "name": "Completed"}}}, "30fe9414-3c22-4ec1-bad6-19a2890bfaf2": {"jobId": "30fe9414-3c22-4ec1-bad6-19a2890bfaf2", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:17.214544Z", "updated": "2026-02-24T11:01:17.214544Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "855c697c-e377-44d9-b1d2-889a26d0b345": {"jobId": "855c697c-e377-44d9-b1d2-889a26d0b345", "processId": "eo-aggregate-import", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:17.339216Z", "updated": "2026-02-24T11:01:17.339216Z", "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2086}}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.1105}}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "datetime": "2026-01-31T00:00:00Z", "aggregation": "mean", "source": "dhis2eo", "values": {"precip": 0.2383}}}]}}, "59d70a5a-1a33-47e4-9ef6-1abfe715be46": {"jobId": "59d70a5a-1a33-47e4-9ef6-1abfe715be46", "processId": "xclim-cdd", "status": "succeeded", "progress": 100, "created": "2026-02-24T11:01:17.604881Z", "updated": "2026-02-24T11:01:17.604881Z", "inputs": {"datasetId": "chirps-daily", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "orgUnitLevel": 2, "threshold": {"value": 1.0, "unit": "mm/day"}, "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "outputs": {"importSummary": {"imported": 0, "updated": 0, "ignored": 3, "deleted": 0, "dryRun": true, "source": "dry-run"}, "features": [{"type": "Feature", "id": "O6uvpzGd5pu", "geometry": {"type": "Polygon", "coordinates": [[[-11.64, 8.42], [-11.5, 8.42], [-11.5, 8.55], [-11.64, 8.55], [-11.64, 8.42]]]}, "properties": {"orgUnit": "O6uvpzGd5pu", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}, {"type": "Feature", "id": "fdc6uOvgoji", "geometry": {"type": "Polygon", "coordinates": [[[-13.3, 8.8], [-13.1, 8.8], [-13.1, 9.0], [-13.3, 9.0], [-13.3, 8.8]]]}, "properties": {"orgUnit": "fdc6uOvgoji", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 13.0}}, {"type": "Feature", "id": "lc3eMKXaEfw", "geometry": {"type": "Polygon", "coordinates": [[[-12.4, 7.0], [-12.1, 7.0], [-12.1, 7.25], [-12.4, 7.25], [-12.4, 7.0]]]}, "properties": {"orgUnit": "lc3eMKXaEfw", "datasetId": "chirps-daily", "indicator": "xclim-cdd", "parameter": "precip", "start": "2026-01-01", "end": "2026-01-31", "source": "dhis2eo", "threshold": {"value": 1.0, "unit": "mm/day"}, "value": 26.0}}]}}} \ No newline at end of file diff --git a/.cache/state/schedules.json b/.cache/state/schedules.json new file mode 100644 index 0000000..e984f0a --- /dev/null +++ b/.cache/state/schedules.json @@ -0,0 +1 @@ +{"5cfbf7d5-2c31-493e-aa81-ceb20d13db0e": {"scheduleId": "5cfbf7d5-2c31-493e-aa81-ceb20d13db0e", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T21:59:01.631345Z", "updated": "2026-02-23T21:59:01.633759Z", "lastRunAt": "2026-02-23T21:59:01.633759Z", "lastRunJobId": "fd1d2a89-519d-4ead-afb4-a76ef18cdf6d"}, "b19d087c-73de-4421-88d5-9bc6c29d166b": {"scheduleId": "b19d087c-73de-4421-88d5-9bc6c29d166b", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T21:59:01.640858Z", "updated": "2026-02-23T21:59:01.640858Z", "lastRunAt": null, "lastRunJobId": null}, "195d2328-245f-4573-8e5c-a0d18bb6e027": {"scheduleId": "195d2328-245f-4573-8e5c-a0d18bb6e027", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T21:59:01.648433Z", "updated": "2026-02-23T21:59:01.648433Z", "lastRunAt": null, "lastRunJobId": null}, "0c2f0ab1-d72f-4a85-b0bb-1806c151de48": {"scheduleId": "0c2f0ab1-d72f-4a85-b0bb-1806c151de48", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T21:59:01.655548Z", "updated": "2026-02-23T21:59:01.658990Z", "lastRunAt": "2026-02-23T21:59:01.658990Z", "lastRunJobId": "9ed82292-1c23-4a6f-8077-561c8d431e5a"}, "4daf8538-91de-43c1-ab4f-717d5390230a": {"scheduleId": "4daf8538-91de-43c1-ab4f-717d5390230a", "processId": "workflow", "workflowId": "70a148c9-4c30-4ff4-bdc1-d793853cca5b", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T21:59:01.676128Z", "updated": "2026-02-23T21:59:01.679007Z", "lastRunAt": "2026-02-23T21:59:01.679007Z", "lastRunJobId": "8337900a-0fc6-4f53-a13e-89540fb03d2e"}, "287ad6fc-135a-427b-8c6e-68bedf3a0434": {"scheduleId": "287ad6fc-135a-427b-8c6e-68bedf3a0434", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:01.254541Z", "updated": "2026-02-23T22:03:01.349695Z", "lastRunAt": "2026-02-23T22:03:01.349695Z", "lastRunJobId": "8ae1afa0-88d9-493a-89e7-d118c6651e43"}, "85e67015-5094-4cee-9ff7-5b25276d71d4": {"scheduleId": "85e67015-5094-4cee-9ff7-5b25276d71d4", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:01.357868Z", "updated": "2026-02-23T22:03:01.357868Z", "lastRunAt": null, "lastRunJobId": null}, "522c7ff1-709f-487a-b6e7-8e3c258f8b3e": {"scheduleId": "522c7ff1-709f-487a-b6e7-8e3c258f8b3e", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:01.366100Z", "updated": "2026-02-23T22:03:01.366100Z", "lastRunAt": null, "lastRunJobId": null}, "ccad9e3e-cfd3-4ca2-8d82-9ab18dc746d2": {"scheduleId": "ccad9e3e-cfd3-4ca2-8d82-9ab18dc746d2", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:01.373832Z", "updated": "2026-02-23T22:03:01.379077Z", "lastRunAt": "2026-02-23T22:03:01.379077Z", "lastRunJobId": "fef7a2ef-eda6-42b0-a95d-da21341ab899"}, "26a09350-cc9a-4070-b44c-35a9290d3382": {"scheduleId": "26a09350-cc9a-4070-b44c-35a9290d3382", "processId": "workflow", "workflowId": "5919d4fa-644f-4ffc-8541-d258e987b379", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:03:01.400343Z", "updated": "2026-02-23T22:03:01.504385Z", "lastRunAt": "2026-02-23T22:03:01.504385Z", "lastRunJobId": "d42e8fc4-1023-453c-9aae-8c33141ca838"}, "e002f589-33e2-4689-ab75-55ad752772aa": {"scheduleId": "e002f589-33e2-4689-ab75-55ad752772aa", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:34.709891Z", "updated": "2026-02-23T22:03:34.808021Z", "lastRunAt": "2026-02-23T22:03:34.808021Z", "lastRunJobId": "5d45d9ce-795c-4615-b973-33cc358c1327"}, "4d02c489-5f53-4ee8-a340-f287e39e8dda": {"scheduleId": "4d02c489-5f53-4ee8-a340-f287e39e8dda", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:34.816852Z", "updated": "2026-02-23T22:03:34.816852Z", "lastRunAt": null, "lastRunJobId": null}, "b307747d-4327-4a51-9d58-f8b54b25fe38": {"scheduleId": "b307747d-4327-4a51-9d58-f8b54b25fe38", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:34.824292Z", "updated": "2026-02-23T22:03:34.824292Z", "lastRunAt": null, "lastRunJobId": null}, "2ee41b9f-753d-449b-ae0c-cb6dc0c4a4bd": {"scheduleId": "2ee41b9f-753d-449b-ae0c-cb6dc0c4a4bd", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:03:34.831948Z", "updated": "2026-02-23T22:03:34.839113Z", "lastRunAt": "2026-02-23T22:03:34.839113Z", "lastRunJobId": "4c2dae6b-dbff-4591-91ca-5a679a4cd52f"}, "8f76daef-6165-483f-8254-a4b61c506d8c": {"scheduleId": "8f76daef-6165-483f-8254-a4b61c506d8c", "processId": "workflow", "workflowId": "90ee914e-925d-4775-bbdb-8489f79e6b2d", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:03:34.860159Z", "updated": "2026-02-23T22:03:34.964368Z", "lastRunAt": "2026-02-23T22:03:34.964368Z", "lastRunJobId": "d6098fe4-5916-4d3b-b540-de1076f51c8d"}, "2adddf18-8041-4049-91ca-e82706bb4bd5": {"scheduleId": "2adddf18-8041-4049-91ca-e82706bb4bd5", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:04:09.025807Z", "updated": "2026-02-23T22:04:09.126220Z", "lastRunAt": "2026-02-23T22:04:09.126220Z", "lastRunJobId": "5f68bea6-8dd8-421e-98d7-0cc70d496462"}, "4ced4308-4f5c-4c9d-8ee0-7bf0fdbebe7d": {"scheduleId": "4ced4308-4f5c-4c9d-8ee0-7bf0fdbebe7d", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:04:09.135097Z", "updated": "2026-02-23T22:04:09.135097Z", "lastRunAt": null, "lastRunJobId": null}, "c0109e78-5124-4a5c-9ebd-f715c0d63e10": {"scheduleId": "c0109e78-5124-4a5c-9ebd-f715c0d63e10", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:04:09.143459Z", "updated": "2026-02-23T22:04:09.143459Z", "lastRunAt": null, "lastRunJobId": null}, "e9be5417-17c2-4335-8e16-696ef8554757": {"scheduleId": "e9be5417-17c2-4335-8e16-696ef8554757", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:04:09.151811Z", "updated": "2026-02-23T22:04:09.160932Z", "lastRunAt": "2026-02-23T22:04:09.160932Z", "lastRunJobId": "ebf4ee34-fb19-4872-9701-6ecafe5f1fd4"}, "d1f00049-3f7b-4f65-bab9-4af6b547f867": {"scheduleId": "d1f00049-3f7b-4f65-bab9-4af6b547f867", "processId": "workflow", "workflowId": "364063da-5825-4303-9736-51baf8692530", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:04:09.183145Z", "updated": "2026-02-23T22:04:09.291053Z", "lastRunAt": "2026-02-23T22:04:09.291053Z", "lastRunJobId": "fb84e869-28c5-4187-b963-62a0f0ddb0d0"}, "687d73de-f481-4b7a-8f23-454efb712e04": {"scheduleId": "687d73de-f481-4b7a-8f23-454efb712e04", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:05:26.119079Z", "updated": "2026-02-23T22:05:26.217653Z", "lastRunAt": "2026-02-23T22:05:26.217653Z", "lastRunJobId": "052828cb-fe04-4107-8168-bf626dc3d2d4"}, "fb1bc1c1-fb12-4c5c-a973-4bfdbf6fe5cb": {"scheduleId": "fb1bc1c1-fb12-4c5c-a973-4bfdbf6fe5cb", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:05:26.226313Z", "updated": "2026-02-23T22:05:26.226313Z", "lastRunAt": null, "lastRunJobId": null}, "02d9f727-8c86-4c7a-961c-2c4bb8608736": {"scheduleId": "02d9f727-8c86-4c7a-961c-2c4bb8608736", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:05:26.234478Z", "updated": "2026-02-23T22:05:26.234478Z", "lastRunAt": null, "lastRunJobId": null}, "dff69560-5acc-47cf-a051-8eeb011f2db7": {"scheduleId": "dff69560-5acc-47cf-a051-8eeb011f2db7", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:05:26.242902Z", "updated": "2026-02-23T22:05:26.253967Z", "lastRunAt": "2026-02-23T22:05:26.253967Z", "lastRunJobId": "057a6900-c6e4-4ee7-b392-16cc852a7beb"}, "51f94cf2-b5b6-49a7-bf0c-a537a6aa1b56": {"scheduleId": "51f94cf2-b5b6-49a7-bf0c-a537a6aa1b56", "processId": "workflow", "workflowId": "9be46657-dc95-4197-b520-034877ad53fd", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:05:26.278557Z", "updated": "2026-02-23T22:05:26.385589Z", "lastRunAt": "2026-02-23T22:05:26.385589Z", "lastRunJobId": "8eee9f98-bab7-43d9-a8ea-6d7d0f8c5bff"}, "af06959e-30ef-49e3-93c8-25e9f3d50313": {"scheduleId": "af06959e-30ef-49e3-93c8-25e9f3d50313", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:06:19.812584Z", "updated": "2026-02-23T22:06:19.913115Z", "lastRunAt": "2026-02-23T22:06:19.913115Z", "lastRunJobId": "bb27b64c-2cda-41ab-8e2b-b23ff462de71"}, "bd946b5d-305a-45c4-8869-d4d1d25e6a31": {"scheduleId": "bd946b5d-305a-45c4-8869-d4d1d25e6a31", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:06:19.921791Z", "updated": "2026-02-23T22:06:19.921791Z", "lastRunAt": null, "lastRunJobId": null}, "4fcde89a-32f2-445e-84f0-dfa7255070d9": {"scheduleId": "4fcde89a-32f2-445e-84f0-dfa7255070d9", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:06:19.929875Z", "updated": "2026-02-23T22:06:19.929875Z", "lastRunAt": null, "lastRunJobId": null}, "90677ea1-0ed0-4fda-9b94-8b2fcad101d2": {"scheduleId": "90677ea1-0ed0-4fda-9b94-8b2fcad101d2", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:06:19.938256Z", "updated": "2026-02-23T22:06:19.950940Z", "lastRunAt": "2026-02-23T22:06:19.950940Z", "lastRunJobId": "d0cd71f3-2aff-4345-8804-6451bd9d78bd"}, "19b99ee9-39ff-48d6-ba66-fc4ab92863ae": {"scheduleId": "19b99ee9-39ff-48d6-ba66-fc4ab92863ae", "processId": "workflow", "workflowId": "5b2d0e9f-a83b-46a1-bac0-93d7693df940", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:06:19.985953Z", "updated": "2026-02-23T22:06:20.091324Z", "lastRunAt": "2026-02-23T22:06:20.091324Z", "lastRunJobId": "a0138e99-87f4-4213-b003-53e4435d2191"}, "82e3bc02-1c6a-4b9b-b2d4-4b8e101fe724": {"scheduleId": "82e3bc02-1c6a-4b9b-b2d4-4b8e101fe724", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:07:10.115363Z", "updated": "2026-02-23T22:07:10.213898Z", "lastRunAt": "2026-02-23T22:07:10.213898Z", "lastRunJobId": "db8c46c0-4252-4091-89b6-7bcac3da7ab2"}, "ee39e6f6-db58-4b73-bdf0-2489b8bd875d": {"scheduleId": "ee39e6f6-db58-4b73-bdf0-2489b8bd875d", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:07:10.222341Z", "updated": "2026-02-23T22:07:10.222341Z", "lastRunAt": null, "lastRunJobId": null}, "4fe2da51-f1e5-4fb9-af57-60b7aef990b3": {"scheduleId": "4fe2da51-f1e5-4fb9-af57-60b7aef990b3", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:07:10.231146Z", "updated": "2026-02-23T22:07:10.231146Z", "lastRunAt": null, "lastRunJobId": null}, "675b3b23-e03f-424d-b9e8-0a1fdcb0cf21": {"scheduleId": "675b3b23-e03f-424d-b9e8-0a1fdcb0cf21", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:07:10.239832Z", "updated": "2026-02-23T22:07:10.253859Z", "lastRunAt": "2026-02-23T22:07:10.253859Z", "lastRunJobId": "36ad61d8-3b1a-4fa3-bc58-b801c12e6980"}, "91b646d5-d79d-47fe-899b-8267ad579745": {"scheduleId": "91b646d5-d79d-47fe-899b-8267ad579745", "processId": "workflow", "workflowId": "99f60372-418b-4205-b3e1-0f95b8c47ec3", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:07:10.281216Z", "updated": "2026-02-23T22:07:10.388949Z", "lastRunAt": "2026-02-23T22:07:10.388949Z", "lastRunJobId": "07711cbe-a49b-43ec-b9f5-a47ff809a637"}, "d8aef236-4b7d-40db-b379-b24e9b78b458": {"scheduleId": "d8aef236-4b7d-40db-b379-b24e9b78b458", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:08:25.668009Z", "updated": "2026-02-23T22:08:25.765997Z", "lastRunAt": "2026-02-23T22:08:25.765997Z", "lastRunJobId": "1df779b8-aa1c-439f-a5d7-3525fb54dc77"}, "c563a474-3ee3-4622-bb9e-2b0216a1bc6a": {"scheduleId": "c563a474-3ee3-4622-bb9e-2b0216a1bc6a", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:08:25.774336Z", "updated": "2026-02-23T22:08:25.774336Z", "lastRunAt": null, "lastRunJobId": null}, "c33608bb-5677-4e2b-8cd4-1953c538d4ba": {"scheduleId": "c33608bb-5677-4e2b-8cd4-1953c538d4ba", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:08:25.782572Z", "updated": "2026-02-23T22:08:25.782572Z", "lastRunAt": null, "lastRunJobId": null}, "f7b1e808-e37d-4524-95da-bcd725fee8f2": {"scheduleId": "f7b1e808-e37d-4524-95da-bcd725fee8f2", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:08:25.791049Z", "updated": "2026-02-23T22:08:25.807702Z", "lastRunAt": "2026-02-23T22:08:25.807702Z", "lastRunJobId": "7dd93d56-a789-4e5e-9f4b-b34bdc9edc33"}, "730b4b89-b173-454b-8266-3e926815ac8b": {"scheduleId": "730b4b89-b173-454b-8266-3e926815ac8b", "processId": "workflow", "workflowId": "d122ebc8-c984-41dc-a867-25507acedb91", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:08:25.838704Z", "updated": "2026-02-23T22:08:25.949015Z", "lastRunAt": "2026-02-23T22:08:25.949015Z", "lastRunJobId": "e3f2a3ef-efb8-4a71-a892-437463f84e13"}, "610d77ed-461b-4e3e-b435-a6f66758894b": {"scheduleId": "610d77ed-461b-4e3e-b435-a6f66758894b", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:10:11.289695Z", "updated": "2026-02-23T22:10:11.387875Z", "lastRunAt": "2026-02-23T22:10:11.387875Z", "lastRunJobId": "48cc3a56-6df8-42b6-a1cd-ab380a687d17"}, "8dc5d3d9-29bb-49bc-b6f8-d59852317d84": {"scheduleId": "8dc5d3d9-29bb-49bc-b6f8-d59852317d84", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:10:11.396699Z", "updated": "2026-02-23T22:10:11.396699Z", "lastRunAt": null, "lastRunJobId": null}, "beddb641-b328-4b72-b140-311e8191bf8a": {"scheduleId": "beddb641-b328-4b72-b140-311e8191bf8a", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:10:11.405166Z", "updated": "2026-02-23T22:10:11.405166Z", "lastRunAt": null, "lastRunJobId": null}, "a6b28e0a-a35e-4a34-b71f-a90d035603bc": {"scheduleId": "a6b28e0a-a35e-4a34-b71f-a90d035603bc", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:10:11.413250Z", "updated": "2026-02-23T22:10:11.431385Z", "lastRunAt": "2026-02-23T22:10:11.431385Z", "lastRunJobId": "672705fa-17df-48d3-9dfe-89edd3033977"}, "623057f2-056c-45c2-8f76-103c68bb1184": {"scheduleId": "623057f2-056c-45c2-8f76-103c68bb1184", "processId": "workflow", "workflowId": "8615178b-17c8-434f-842b-e9f13b745301", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:10:11.463710Z", "updated": "2026-02-23T22:10:11.573987Z", "lastRunAt": "2026-02-23T22:10:11.573987Z", "lastRunJobId": "3bc40154-66d2-44b8-bf8a-3c931d7ce92a"}, "622341b1-38a3-4af0-858a-2300012d59a7": {"scheduleId": "622341b1-38a3-4af0-858a-2300012d59a7", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:12:27.237291Z", "updated": "2026-02-23T22:12:27.340850Z", "lastRunAt": "2026-02-23T22:12:27.340850Z", "lastRunJobId": "4c59c2e2-a55c-475e-8f49-8060b0070c15"}, "99925ea5-18a8-424a-b7da-d73bd335dc75": {"scheduleId": "99925ea5-18a8-424a-b7da-d73bd335dc75", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:12:27.350371Z", "updated": "2026-02-23T22:12:27.350371Z", "lastRunAt": null, "lastRunJobId": null}, "c67b9ab1-0dc1-48a5-aec1-20489807497a": {"scheduleId": "c67b9ab1-0dc1-48a5-aec1-20489807497a", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:12:27.359375Z", "updated": "2026-02-23T22:12:27.359375Z", "lastRunAt": null, "lastRunJobId": null}, "3cb199b7-50a0-4b65-8b2e-426a1d50fcc9": {"scheduleId": "3cb199b7-50a0-4b65-8b2e-426a1d50fcc9", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:12:27.367947Z", "updated": "2026-02-23T22:12:27.387899Z", "lastRunAt": "2026-02-23T22:12:27.387899Z", "lastRunJobId": "743ddcec-4c43-49cf-a3d7-a79fd07d203c"}, "ccec39ce-7b47-4fba-b991-a03f2a76fa91": {"scheduleId": "ccec39ce-7b47-4fba-b991-a03f2a76fa91", "processId": "workflow", "workflowId": "696e988e-0e0d-4e2a-8bd2-9e6109ae798d", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:12:27.422585Z", "updated": "2026-02-23T22:12:27.538547Z", "lastRunAt": "2026-02-23T22:12:27.538547Z", "lastRunJobId": "faaa0822-0996-42aa-9b4a-09db3b38b253"}, "8c5aa333-1fbe-4def-871c-67846973a0cd": {"scheduleId": "8c5aa333-1fbe-4def-871c-67846973a0cd", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:13:55.089768Z", "updated": "2026-02-23T22:13:55.193088Z", "lastRunAt": "2026-02-23T22:13:55.193088Z", "lastRunJobId": "421d8036-8bb8-4d19-8ab2-16b12114c03f"}, "4b60855e-b4f1-47a4-8212-757bede1eae9": {"scheduleId": "4b60855e-b4f1-47a4-8212-757bede1eae9", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:13:55.201948Z", "updated": "2026-02-23T22:13:55.201948Z", "lastRunAt": null, "lastRunJobId": null}, "56c7219c-2f65-4ecf-9c3b-2637a3721373": {"scheduleId": "56c7219c-2f65-4ecf-9c3b-2637a3721373", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:13:55.210517Z", "updated": "2026-02-23T22:13:55.210517Z", "lastRunAt": null, "lastRunJobId": null}, "f097970d-3aea-499b-8378-11f1a649efe6": {"scheduleId": "f097970d-3aea-499b-8378-11f1a649efe6", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:13:55.219281Z", "updated": "2026-02-23T22:13:55.241108Z", "lastRunAt": "2026-02-23T22:13:55.241108Z", "lastRunJobId": "1f0946fe-d7cf-4ff5-ae38-ea332be556f8"}, "73ed1631-fa29-4acd-a093-d6f844acda2c": {"scheduleId": "73ed1631-fa29-4acd-a093-d6f844acda2c", "processId": "workflow", "workflowId": "76b22e29-d03d-41cc-8afd-1f6d98c904b8", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:13:55.276114Z", "updated": "2026-02-23T22:13:55.383703Z", "lastRunAt": "2026-02-23T22:13:55.383703Z", "lastRunJobId": "89f1748d-a2ff-4fb4-bad4-034af929932e"}, "719443e3-b9b8-4c98-8513-4eb9f398a68f": {"scheduleId": "719443e3-b9b8-4c98-8513-4eb9f398a68f", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:15:40.039255Z", "updated": "2026-02-23T22:15:40.141384Z", "lastRunAt": "2026-02-23T22:15:40.141384Z", "lastRunJobId": "ac76a665-136a-4730-9228-dceb0a828f85"}, "ac016e84-b71a-4570-b097-66e7f56601fd": {"scheduleId": "ac016e84-b71a-4570-b097-66e7f56601fd", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:15:40.150909Z", "updated": "2026-02-23T22:15:40.150909Z", "lastRunAt": null, "lastRunJobId": null}, "9fef80ef-ee87-4a54-84da-1e49be0823d5": {"scheduleId": "9fef80ef-ee87-4a54-84da-1e49be0823d5", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:15:40.160032Z", "updated": "2026-02-23T22:15:40.160032Z", "lastRunAt": null, "lastRunJobId": null}, "bb9b0350-2f4a-4cc3-8d80-2728f5d5f790": {"scheduleId": "bb9b0350-2f4a-4cc3-8d80-2728f5d5f790", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:15:40.169187Z", "updated": "2026-02-23T22:15:40.192890Z", "lastRunAt": "2026-02-23T22:15:40.192890Z", "lastRunJobId": "101a1fa5-3a6b-4923-8b3d-08497fc2cde3"}, "c80ba67b-279a-4402-93a1-4f338959a04e": {"scheduleId": "c80ba67b-279a-4402-93a1-4f338959a04e", "processId": "workflow", "workflowId": "f94af166-6944-45d6-b972-4500feec0eec", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:15:40.230848Z", "updated": "2026-02-23T22:15:40.344231Z", "lastRunAt": "2026-02-23T22:15:40.344231Z", "lastRunJobId": "a0aa8517-0ec2-4b4a-86c0-5645c4077a6b"}, "58efaf46-a456-42c4-b649-ea1108f15ea9": {"scheduleId": "58efaf46-a456-42c4-b649-ea1108f15ea9", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:16:56.626648Z", "updated": "2026-02-23T22:16:56.732055Z", "lastRunAt": "2026-02-23T22:16:56.732055Z", "lastRunJobId": "bc4505e6-8dbf-4d87-ae54-43e5b714a4e9"}, "b7e47eb8-a5c3-4b5f-995d-d11c80819ecb": {"scheduleId": "b7e47eb8-a5c3-4b5f-995d-d11c80819ecb", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:16:56.741260Z", "updated": "2026-02-23T22:16:56.741260Z", "lastRunAt": null, "lastRunJobId": null}, "b6886dd9-7b18-403b-ab39-f08b82630146": {"scheduleId": "b6886dd9-7b18-403b-ab39-f08b82630146", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:16:56.750007Z", "updated": "2026-02-23T22:16:56.750007Z", "lastRunAt": null, "lastRunJobId": null}, "387c67e0-fadf-4c98-80af-68be8fb71cd1": {"scheduleId": "387c67e0-fadf-4c98-80af-68be8fb71cd1", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:16:56.758467Z", "updated": "2026-02-23T22:16:56.783858Z", "lastRunAt": "2026-02-23T22:16:56.783858Z", "lastRunJobId": "4e30a788-c134-463a-bdbb-f88b4b5615c9"}, "ec048ce8-ccdf-4414-8f1e-900235643423": {"scheduleId": "ec048ce8-ccdf-4414-8f1e-900235643423", "processId": "workflow", "workflowId": "ba8a6537-d7a7-4062-b169-bc65113cbb8c", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:16:56.823648Z", "updated": "2026-02-23T22:16:56.938637Z", "lastRunAt": "2026-02-23T22:16:56.938637Z", "lastRunJobId": "9a34fb59-d815-4107-9c6b-d3207704d83a"}, "456d431a-5464-40fd-886c-85a6638ae141": {"scheduleId": "456d431a-5464-40fd-886c-85a6638ae141", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:18:09.497375Z", "updated": "2026-02-23T22:18:09.603709Z", "lastRunAt": "2026-02-23T22:18:09.603709Z", "lastRunJobId": "bef54659-7bfc-4c67-b172-1722edcea9cb"}, "6f27c166-ad26-4103-b4fd-0c04d4937464": {"scheduleId": "6f27c166-ad26-4103-b4fd-0c04d4937464", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:18:09.614239Z", "updated": "2026-02-23T22:18:09.614239Z", "lastRunAt": null, "lastRunJobId": null}, "86fb93ec-4d8e-496c-8359-ddb8ac1bccb3": {"scheduleId": "86fb93ec-4d8e-496c-8359-ddb8ac1bccb3", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:18:09.623851Z", "updated": "2026-02-23T22:18:09.623851Z", "lastRunAt": null, "lastRunJobId": null}, "75f754a7-5bbe-40d9-b05e-26f4b53dac7f": {"scheduleId": "75f754a7-5bbe-40d9-b05e-26f4b53dac7f", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-23T22:18:09.632760Z", "updated": "2026-02-23T22:18:09.660081Z", "lastRunAt": "2026-02-23T22:18:09.660081Z", "lastRunJobId": "2ad476b8-a2a6-4783-948e-42087f50a7e4"}, "c87d04b7-479a-4038-aa91-5a3806e7ba95": {"scheduleId": "c87d04b7-479a-4038-aa91-5a3806e7ba95", "processId": "workflow", "workflowId": "5a390470-b37d-45b2-9633-8435c0167424", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-23T22:18:09.701252Z", "updated": "2026-02-23T22:18:09.813071Z", "lastRunAt": "2026-02-23T22:18:09.813071Z", "lastRunJobId": "50f69ac8-ca39-4892-bf3c-2921221183b8"}, "78df810b-b129-4660-9186-e2a6ba77293f": {"scheduleId": "78df810b-b129-4660-9186-e2a6ba77293f", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-24T11:01:16.901478Z", "updated": "2026-02-24T11:01:17.011463Z", "lastRunAt": "2026-02-24T11:01:17.011463Z", "lastRunJobId": "8da9f4b3-3858-43ff-a0f4-c9e4c16cc4c6"}, "8beadc46-40a6-4a61-be68-bc5388857bcf": {"scheduleId": "8beadc46-40a6-4a61-be68-bc5388857bcf", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-24T11:01:17.020819Z", "updated": "2026-02-24T11:01:17.020819Z", "lastRunAt": null, "lastRunJobId": null}, "f6fdab58-6f13-449b-b44e-a5034e6400ea": {"scheduleId": "f6fdab58-6f13-449b-b44e-a5034e6400ea", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-24T11:01:17.031472Z", "updated": "2026-02-24T11:01:17.031472Z", "lastRunAt": null, "lastRunJobId": null}, "8ab6d636-1289-431f-9338-637ca91ede80": {"scheduleId": "8ab6d636-1289-431f-9338-637ca91ede80", "processId": "eo-aggregate-import", "workflowId": null, "name": "nightly-precip-import", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "start": null, "end": null, "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dataElementMap": null, "dryRun": true}}, "created": "2026-02-24T11:01:17.041326Z", "updated": "2026-02-24T11:01:17.071524Z", "lastRunAt": "2026-02-24T11:01:17.071524Z", "lastRunJobId": "8e3f5696-b9e8-4f4b-ad89-8db4d6e5831e"}, "77c9d9f5-ddf6-4771-8100-94bed3ae0c20": {"scheduleId": "77c9d9f5-ddf6-4771-8100-94bed3ae0c20", "processId": "workflow", "workflowId": "a1ef8b41-460c-40ba-a219-aef155d1e67e", "name": "nightly-workflow", "cron": "0 0 * * *", "timezone": "UTC", "enabled": true, "inputs": null, "created": "2026-02-24T11:01:17.115211Z", "updated": "2026-02-24T11:01:17.229533Z", "lastRunAt": "2026-02-24T11:01:17.229533Z", "lastRunJobId": "30fe9414-3c22-4ec1-bad6-19a2890bfaf2"}} \ No newline at end of file diff --git a/.cache/state/workflows.json b/.cache/state/workflows.json new file mode 100644 index 0000000..29ae554 --- /dev/null +++ b/.cache/state/workflows.json @@ -0,0 +1 @@ +{"70a148c9-4c30-4ff4-bdc1-d793853cca5b": {"workflowId": "70a148c9-4c30-4ff4-bdc1-d793853cca5b", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T21:59:01.674872Z", "updated": "2026-02-23T21:59:01.678721Z", "lastRunAt": "2026-02-23T21:59:01.678721Z", "lastRunJobIds": ["8337900a-0fc6-4f53-a13e-89540fb03d2e"]}, "5919d4fa-644f-4ffc-8541-d258e987b379": {"workflowId": "5919d4fa-644f-4ffc-8541-d258e987b379", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:03:01.398456Z", "updated": "2026-02-23T22:03:01.503931Z", "lastRunAt": "2026-02-23T22:03:01.503931Z", "lastRunJobIds": ["d42e8fc4-1023-453c-9aae-8c33141ca838"]}, "90ee914e-925d-4775-bbdb-8489f79e6b2d": {"workflowId": "90ee914e-925d-4775-bbdb-8489f79e6b2d", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:03:34.858713Z", "updated": "2026-02-23T22:03:34.964026Z", "lastRunAt": "2026-02-23T22:03:34.964026Z", "lastRunJobIds": ["d6098fe4-5916-4d3b-b540-de1076f51c8d"]}, "364063da-5825-4303-9736-51baf8692530": {"workflowId": "364063da-5825-4303-9736-51baf8692530", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:04:09.181686Z", "updated": "2026-02-23T22:04:09.290683Z", "lastRunAt": "2026-02-23T22:04:09.290683Z", "lastRunJobIds": ["fb84e869-28c5-4187-b963-62a0f0ddb0d0"]}, "9be46657-dc95-4197-b520-034877ad53fd": {"workflowId": "9be46657-dc95-4197-b520-034877ad53fd", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:05:26.277030Z", "updated": "2026-02-23T22:05:26.385208Z", "lastRunAt": "2026-02-23T22:05:26.385208Z", "lastRunJobIds": ["8eee9f98-bab7-43d9-a8ea-6d7d0f8c5bff"]}, "5b2d0e9f-a83b-46a1-bac0-93d7693df940": {"workflowId": "5b2d0e9f-a83b-46a1-bac0-93d7693df940", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:06:19.984497Z", "updated": "2026-02-23T22:06:20.090912Z", "lastRunAt": "2026-02-23T22:06:20.090912Z", "lastRunJobIds": ["a0138e99-87f4-4213-b003-53e4435d2191"]}, "99f60372-418b-4205-b3e1-0f95b8c47ec3": {"workflowId": "99f60372-418b-4205-b3e1-0f95b8c47ec3", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:07:10.279699Z", "updated": "2026-02-23T22:07:10.388391Z", "lastRunAt": "2026-02-23T22:07:10.388391Z", "lastRunJobIds": ["07711cbe-a49b-43ec-b9f5-a47ff809a637"]}, "d122ebc8-c984-41dc-a867-25507acedb91": {"workflowId": "d122ebc8-c984-41dc-a867-25507acedb91", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:08:25.837044Z", "updated": "2026-02-23T22:08:25.948539Z", "lastRunAt": "2026-02-23T22:08:25.948539Z", "lastRunJobIds": ["e3f2a3ef-efb8-4a71-a892-437463f84e13"]}, "8615178b-17c8-434f-842b-e9f13b745301": {"workflowId": "8615178b-17c8-434f-842b-e9f13b745301", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:10:11.462031Z", "updated": "2026-02-23T22:10:11.573452Z", "lastRunAt": "2026-02-23T22:10:11.573452Z", "lastRunJobIds": ["3bc40154-66d2-44b8-bf8a-3c931d7ce92a"]}, "696e988e-0e0d-4e2a-8bd2-9e6109ae798d": {"workflowId": "696e988e-0e0d-4e2a-8bd2-9e6109ae798d", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:12:27.421003Z", "updated": "2026-02-23T22:12:27.537998Z", "lastRunAt": "2026-02-23T22:12:27.537998Z", "lastRunJobIds": ["faaa0822-0996-42aa-9b4a-09db3b38b253"]}, "76b22e29-d03d-41cc-8afd-1f6d98c904b8": {"workflowId": "76b22e29-d03d-41cc-8afd-1f6d98c904b8", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:13:55.274478Z", "updated": "2026-02-23T22:13:55.383149Z", "lastRunAt": "2026-02-23T22:13:55.383149Z", "lastRunJobIds": ["89f1748d-a2ff-4fb4-bad4-034af929932e"]}, "f94af166-6944-45d6-b972-4500feec0eec": {"workflowId": "f94af166-6944-45d6-b972-4500feec0eec", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:15:40.229164Z", "updated": "2026-02-23T22:15:40.343668Z", "lastRunAt": "2026-02-23T22:15:40.343668Z", "lastRunJobIds": ["a0aa8517-0ec2-4b4a-86c0-5645c4077a6b"]}, "ba8a6537-d7a7-4062-b169-bc65113cbb8c": {"workflowId": "ba8a6537-d7a7-4062-b169-bc65113cbb8c", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:16:56.821962Z", "updated": "2026-02-23T22:16:56.938046Z", "lastRunAt": "2026-02-23T22:16:56.938046Z", "lastRunJobIds": ["9a34fb59-d815-4107-9c6b-d3207704d83a"]}, "5a390470-b37d-45b2-9633-8435c0167424": {"workflowId": "5a390470-b37d-45b2-9633-8435c0167424", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-23T22:18:09.699497Z", "updated": "2026-02-23T22:18:09.812445Z", "lastRunAt": "2026-02-23T22:18:09.812445Z", "lastRunJobIds": ["50f69ac8-ca39-4892-bf3c-2921221183b8"]}, "a1ef8b41-460c-40ba-a219-aef155d1e67e": {"workflowId": "a1ef8b41-460c-40ba-a219-aef155d1e67e", "name": "scheduled-workflow", "steps": [{"name": "aggregate", "processId": "eo-aggregate-import", "payload": {"inputs": {"datasetId": "chirps-daily", "parameters": ["precip"], "datetime": "2026-01-31T00:00:00Z", "orgUnitLevel": 2, "aggregation": "mean", "dhis2": {"dataElementId": "abc123", "dryRun": true}}}}], "created": "2026-02-24T11:01:17.113543Z", "updated": "2026-02-24T11:01:17.228929Z", "lastRunAt": "2026-02-24T11:01:17.228929Z", "lastRunJobIds": ["30fe9414-3c22-4ec1-bad6-19a2890bfaf2"]}} \ No newline at end of file diff --git a/.cache/xclim/chirps-daily/precip/precip_20260101_20260131_2026-01.nc b/.cache/xclim/chirps-daily/precip/precip_20260101_20260131_2026-01.nc new file mode 100644 index 0000000..94d2196 Binary files /dev/null and b/.cache/xclim/chirps-daily/precip/precip_20260101_20260131_2026-01.nc differ diff --git a/.cache/xclim/chirps-daily/precip/precip_20260131_20260131_2026-01.nc b/.cache/xclim/chirps-daily/precip/precip_20260131_20260131_2026-01.nc new file mode 100644 index 0000000..94d2196 Binary files /dev/null and b/.cache/xclim/chirps-daily/precip/precip_20260131_20260131_2026-01.nc differ diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a296be0..8b6aaba 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,6 +8,24 @@ - DHIS2 Maps app and DHIS2 Climate app are primary consumers of `eo-api`. - `eo-api` should replace functionality currently sourced via Google Earth Engine for these apps. +## Current implemented baseline (keep in sync) + +- Dataset discovery is implemented via: + - `GET /collections` + - `GET /collections/{collectionId}` +- OGC API - Coverages baseline is implemented via: + - `GET /collections/{collectionId}/coverage` +- Collections and coverages are split into separate endpoint modules: + - `eoapi/endpoints/collections.py` + - `eoapi/endpoints/coverages.py` +- Shared endpoint constants/errors live in: + - `eoapi/endpoints/constants.py` + - `eoapi/endpoints/errors.py` +- Dataset metadata is file-driven from `eoapi/datasets//.yaml` and validated by Pydantic (`eoapi/datasets/catalog.py`). +- Dataset-specific source resolver logic lives in `eoapi/datasets//resolver.py`. +- Dataset validation command is available via `make validate-datasets`. +- Tests currently include endpoint error contract tests and run via `make test`. + ## Product priorities - Favor end-to-end data flow correctness over feature breadth. @@ -41,6 +59,8 @@ - For async/long-running operations, expose job status instead of blocking calls. - Include stable identifiers for datasets, processes, and executions. - Treat Maps app and Climate app contracts as first-class compatibility targets. +- For collections/coverages, prefer OGC API - Common and OGC API - Coverages compatible response structures and link relations. +- Keep collections and coverages handlers decoupled, with shared helpers/constants in dedicated modules. ## Geospatial/data handling guidance @@ -86,3 +106,5 @@ - Update docs when adding endpoints, process parameters, or output schema changes. - Include example requests/responses for new process execution paths. +- Keep `README.md` concise and place endpoint examples in `API_EXAMPLES.md`. +- Keep dataset schema documentation in `eoapi/datasets/README.md`. diff --git a/.github/skills/eo-pipeline-orchestration/SKILL.md b/.github/skills/eo-pipeline-orchestration/SKILL.md index 6fd10da..ae464b2 100644 --- a/.github/skills/eo-pipeline-orchestration/SKILL.md +++ b/.github/skills/eo-pipeline-orchestration/SKILL.md @@ -6,12 +6,14 @@ description: Define scheduled and custom EO data pipelines with optional pre/pos # EO Pipeline Orchestration ## Use this skill when + - Creating recurring ingestion workflows - Adding custom pre/post-processing pipeline steps - Designing orchestration handoffs (Airflow/Prefect) ## Canonical pipeline stages -1. Discover dataset and validate metadata + +1. Discover dataset and validate metadata (from `eoapi/datasets//.yaml` definitions) 2. Extract data for time/area window 3. Cache source/intermediate artifacts as files when needed 4. Transform and harmonize units/CRS @@ -21,6 +23,7 @@ description: Define scheduled and custom EO data pipelines with optional pre/pos 8. Trigger import or publish for downstream ingestion ## Orchestration guidance + - Treat each stage as an idempotent task where possible - Persist execution metadata and lineage - Use retries/backoff for transient provider failures @@ -32,10 +35,13 @@ description: Define scheduled and custom EO data pipelines with optional pre/pos - Promote reusable orchestration helpers to upstream libraries when they are broadly applicable beyond `eo-api` ## Data integrity checks + - Nodata handling rules are explicit - CRS mismatches are detected and resolved deterministically - Aggregation method and temporal windows are logged +- Dataset definition schema is validated before runs (e.g. `make validate-datasets`) ## MVP constraints + - Prefer simple, inspectable DAGs over highly dynamic graphs - Prioritize reliable daily scheduled runs for climate and population flows diff --git a/.github/skills/eo-process-api-design/SKILL.md b/.github/skills/eo-process-api-design/SKILL.md index 1580080..706f06b 100644 --- a/.github/skills/eo-process-api-design/SKILL.md +++ b/.github/skills/eo-process-api-design/SKILL.md @@ -6,16 +6,32 @@ description: Design OGC API - Processes style EO execution endpoints and request # EO Process API Design ## Use this skill when + - Adding or modifying process execution endpoints - Designing dataset/process discovery and execution contracts - Defining long-running execution behavior and status tracking -## Required endpoint baseline -- `GET /processes` -- `GET /processes/{process-id}` -- `POST /processes/{process-id}/execution` +## Current API baseline in this repo + +- Dataset discovery: + - `GET /collections` + - `GET /collections/{collectionId}` +- Coverage retrieval: + - `GET /collections/{collectionId}/coverage` +- Process execution endpoints (target baseline): + - `GET /processes` + - `GET /processes/{process-id}` + - `POST /processes/{process-id}/execution` + +## OGC endpoint guidance + +- For collections, follow OGC API - Common response structures (`extent`, `links`, stable IDs). +- For coverage responses, follow OGC API - Coverages/CoverageJSON-compatible structures (`domain`, `parameters`, `ranges`). +- Validate query parameters and return structured `InvalidParameterValue` errors. +- Return `NotFound` for unknown collections/processes. ## Design rules + - Keep resource names consistent and stable - Use explicit IDs for process, execution, dataset - Return structured validation errors with actionable messages @@ -23,6 +39,7 @@ description: Design OGC API - Processes style EO execution endpoints and request - Keep response schemas backward compatible ## EO/DHIS2 checks + - Document CRS assumptions and aggregation method semantics - Include preview/dry-run support where feasible - Use `dhis2-python-client` for DHIS2 Web API operations @@ -33,6 +50,7 @@ description: Design OGC API - Processes style EO execution endpoints and request - Prioritize process capabilities that replace current Google Earth Engine-backed functionality ## Output checklist + - Endpoint contract - Example request/response - Error model diff --git a/.github/skills/prd-authoring/SKILL.md b/.github/skills/prd-authoring/SKILL.md index 5ec2131..42e42ba 100644 --- a/.github/skills/prd-authoring/SKILL.md +++ b/.github/skills/prd-authoring/SKILL.md @@ -35,6 +35,11 @@ description: Draft or refine PRDs for DHIS2 EO API features with clear MVP scope - Includes compatibility and migration implications for `maps-app` and `climate-app` - Explicitly identifies which Google Earth Engine-dependent behavior is replaced by `eo-api` - Calls out standards alignment (OGC/STAC) when applicable +- Reflects current implemented baseline where relevant: + - `/collections`, `/collections/{collectionId}` + - `/collections/{collectionId}/coverage` + - File-driven dataset metadata in `eoapi/datasets//.yaml` validated by Pydantic + - Validation/test workflow via `make validate-datasets` and `make test` ## Quality bar diff --git a/.gitignore b/.gitignore index 670a936..c26a1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__/ .venv/ +*.egg-info/ diff --git a/API_EXAMPLES.md b/API_EXAMPLES.md new file mode 100644 index 0000000..c25d58e --- /dev/null +++ b/API_EXAMPLES.md @@ -0,0 +1,433 @@ +# API Examples + +Base URL (local): + +http://127.0.0.1:8000 + +OGC landing page: + +http://127.0.0.1:8000/ + +```bash +curl "http://127.0.0.1:8000/" +``` + +## Landing page runtime summary + +Operator-focused view: + +```bash +curl "http://127.0.0.1:8000/" | jq '.runtime' +``` + +Example runtime payload: + +```json +{ + "cors": { + "mode": "wildcard", + "origins": 1 + }, + "apiKeyRequired": false, + "dhis2": { + "configured": false, + "host": "unset", + "authMode": "none" + }, + "state": { + "persistenceEnabled": true, + "directory": ".cache/state" + }, + "internalScheduler": { + "enabled": true + } +} +``` + +## Collections (`/collections`) + +Conformance declaration: + +http://127.0.0.1:8000/conformance + +```bash +curl "http://127.0.0.1:8000/conformance" +``` + +Note: values used in `range-subset` and `parameter-name` must match keys in `eoapi/datasets//.yaml` under `parameters`. + +List collections: + +http://127.0.0.1:8000/collections + +```bash +curl "http://127.0.0.1:8000/collections" +``` + +Get CHIRPS collection: + +http://127.0.0.1:8000/collections/chirps-daily + +```bash +curl "http://127.0.0.1:8000/collections/chirps-daily" +``` + +Get ERA5-Land collection: + +http://127.0.0.1:8000/collections/era5-land-daily + +Collections in this section correspond to OGC API - Common collection discovery endpoints. + +Get CHIRPS coverage (default extent/time): + +http://127.0.0.1:8000/collections/chirps-daily/coverage + +```bash +curl "http://127.0.0.1:8000/collections/chirps-daily/coverage" +``` + +Get CHIRPS coverage for a specific datetime and bbox: + +http://127.0.0.1:8000/collections/chirps-daily/coverage?datetime=2026-01-31T00:00:00Z&bbox=30,-5,35,2 + +```bash +curl "http://127.0.0.1:8000/collections/chirps-daily/coverage?datetime=2026-01-31T00:00:00Z&bbox=30,-5,35,2" +``` + +Get ERA5-Land coverage for a range-subset parameter: + +http://127.0.0.1:8000/collections/era5-land-daily/coverage?range-subset=2m_temperature + +```bash +curl "http://127.0.0.1:8000/collections/era5-land-daily/coverage?range-subset=2m_temperature" +``` + +Get CHIRPS EDR position query: + +http://127.0.0.1:8000/collections/chirps-daily/position?coords=POINT(30%20-1)&datetime=2026-01-31T00:00:00Z¶meter-name=precip + +```bash +curl "http://127.0.0.1:8000/collections/chirps-daily/position?coords=POINT(30%20-1)&datetime=2026-01-31T00:00:00Z¶meter-name=precip" +``` + +Get ERA5-Land EDR position query: + +http://127.0.0.1:8000/collections/era5-land-daily/position?coords=POINT(36.8%20-1.3)¶meter-name=2m_temperature + +```bash +curl "http://127.0.0.1:8000/collections/era5-land-daily/position?coords=POINT(36.8%20-1.3)¶meter-name=2m_temperature" +``` + +Get CHIRPS EDR area query: + +http://127.0.0.1:8000/collections/chirps-daily/area?bbox=30,-5,35,2&datetime=2026-01-31T00:00:00Z¶meter-name=precip + +```bash +curl "http://127.0.0.1:8000/collections/chirps-daily/area?bbox=30,-5,35,2&datetime=2026-01-31T00:00:00Z¶meter-name=precip" +``` + +Get ERA5-Land EDR area query: + +http://127.0.0.1:8000/collections/era5-land-daily/area?bbox=36,-2,38,0¶meter-name=2m_temperature + +```bash +curl "http://127.0.0.1:8000/collections/era5-land-daily/area?bbox=36,-2,38,0¶meter-name=2m_temperature" +``` + +## Features (`/features`) + +List feature collections: + +http://127.0.0.1:8000/features + +```bash +curl "http://127.0.0.1:8000/features" +``` + +Get DHIS2 org unit features (level 2): + +http://127.0.0.1:8000/features/dhis2-org-units/items?level=2 + +```bash +curl "http://127.0.0.1:8000/features/dhis2-org-units/items?level=2" +``` + +Get DHIS2 org unit features filtered by bbox: + +http://127.0.0.1:8000/features/dhis2-org-units/items?level=2&bbox=-13,8,-11,9 + +```bash +curl "http://127.0.0.1:8000/features/dhis2-org-units/items?level=2&bbox=-13,8,-11,9" +``` + +## Processes (`/processes`) + +List processes: + +http://127.0.0.1:8000/processes + +```bash +curl "http://127.0.0.1:8000/processes" +``` + +Describe aggregate-import process: + +http://127.0.0.1:8000/processes/eo-aggregate-import + +```bash +curl "http://127.0.0.1:8000/processes/eo-aggregate-import" +``` + +Execute aggregate-import process (dry-run): + +http://127.0.0.1:8000/processes/eo-aggregate-import/execution + +```bash +curl -X POST "http://127.0.0.1:8000/processes/eo-aggregate-import/execution" \ + -H "Content-Type: application/json" \ + -d '{ + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + }' +``` + +Execute xclim CDD process (dry-run): + +http://127.0.0.1:8000/processes/xclim-cdd/execution + +```bash +curl -X POST "http://127.0.0.1:8000/processes/xclim-cdd/execution" \ + -H "Content-Type: application/json" \ + -d '{ + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 1.0, "unit": "mm/day" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + }' +``` + +Execute xclim warm-days process (dry-run): + +http://127.0.0.1:8000/processes/xclim-warm-days/execution + +```bash +curl -X POST "http://127.0.0.1:8000/processes/xclim-warm-days/execution" \ + -H "Content-Type: application/json" \ + -d '{ + "inputs": { + "datasetId": "era5-land-daily", + "parameter": "2m_temperature", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 35.0, "unit": "degC" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + }' +``` + +Check job status (replace ``): + +http://127.0.0.1:8000/jobs/ + +```bash +curl "http://127.0.0.1:8000/jobs/" +``` + +Get aggregated result features from a job (replace ``): + +http://127.0.0.1:8000/features/aggregated-results/items?jobId= + +```bash +curl "http://127.0.0.1:8000/features/aggregated-results/items?jobId=" +``` + +## Workflows (`/workflows`) + +Create a custom workflow with two steps: + +http://127.0.0.1:8000/workflows + +```bash +curl -X POST "http://127.0.0.1:8000/workflows" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "climate-indicators-workflow", + "steps": [ + { + "name": "aggregate-precip", + "processId": "eo-aggregate-import", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + } + }, + { + "name": "cdd", + "processId": "xclim-cdd", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 1.0, "unit": "mm/day" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + } + } + ] + }' +``` + +List workflows: + +http://127.0.0.1:8000/workflows + +```bash +curl "http://127.0.0.1:8000/workflows" +``` + +Run a workflow immediately (replace ``): + +http://127.0.0.1:8000/workflows//run + +```bash +curl -X POST "http://127.0.0.1:8000/workflows//run" +``` + +## Schedules (`/schedules`) + +Schedules allow user-defined recurring execution of `eo-aggregate-import` (for example nightly imports). + +Create a nightly schedule: + +http://127.0.0.1:8000/schedules + +```bash +curl -X POST "http://127.0.0.1:8000/schedules" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "nightly-precip-import", + "cron": "0 0 * * *", + "timezone": "UTC", + "enabled": true, + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + }' +``` + +Create a workflow-target schedule (replace ``): + +http://127.0.0.1:8000/schedules + +```bash +curl -X POST "http://127.0.0.1:8000/schedules" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "nightly-workflow-run", + "cron": "0 0 * * *", + "timezone": "UTC", + "enabled": true, + "workflowId": "" + }' +``` + +List schedules: + +http://127.0.0.1:8000/schedules + +```bash +curl "http://127.0.0.1:8000/schedules" +``` + +Run a schedule immediately (replace ``): + +http://127.0.0.1:8000/schedules//run + +```bash +curl -X POST "http://127.0.0.1:8000/schedules//run" +``` + +Trigger schedule from orchestrator callback (replace ``): + +http://127.0.0.1:8000/schedules//callback + +Requires server env var `EOAPI_SCHEDULER_TOKEN` and header `X-Scheduler-Token`. + +```bash +curl -X POST "http://127.0.0.1:8000/schedules//callback" \ + -H "X-Scheduler-Token: " +``` + +## COG (`/cog`) + +COG info: + +http://127.0.0.1:8000/cog/info?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif + +```bash +curl "http://127.0.0.1:8000/cog/info?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif" +``` + +COG preview: + +http://127.0.0.1:8000/cog/preview.png?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif&max_size=2048&colormap_name=delta + +```bash +curl -o chirps-preview.png "http://127.0.0.1:8000/cog/preview.png?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif&max_size=2048&colormap_name=delta" +``` + +Tile: + +http://127.0.0.1:8000/cog/tiles/WebMercatorQuad/4/5/5.png?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif&colormap_name=delta + +```bash +curl -o chirps-tile.png "http://127.0.0.1:8000/cog/tiles/WebMercatorQuad/4/5/5.png?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif&colormap_name=delta" +``` + +CHIRPS COG test file: + +https://data.chc.ucsb.edu/products/CHIRPS/v3.0/daily/final/rnl/2026/chirps-v3.0.rnl.2026.01.31.tif diff --git a/Makefile b/Makefile index 7594dc0..d04d907 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,9 @@ sync: run: uv run uvicorn main:app --reload + +validate-datasets: + uv run python scripts/validate_datasets.py + +test: + uv run --with pytest pytest -q diff --git a/ORCHESTRATION_DECISION.md b/ORCHESTRATION_DECISION.md new file mode 100644 index 0000000..9b2e6c4 --- /dev/null +++ b/ORCHESTRATION_DECISION.md @@ -0,0 +1,80 @@ +# Orchestration Decision Scorecard + +## Purpose + +Select the primary orchestrator for eo-api scheduled and long-running execution. + +Options compared: + +- Prefect +- Airflow +- Dagster +- Temporal +- Argo Workflows +- Internal scheduler only + +## Scoring model + +- Scale: 1 (weak) to 5 (strong) +- Weighted score = score × weight +- Weights reflect current eo-api phase (MVP to near-production) + +| Criterion | Weight | +| ----------------------------- | ------: | +| API-triggered integration fit | 20 | +| Time to production | 15 | +| Operations overhead | 15 | +| Reliability and retries | 15 | +| Governance and auditability | 10 | +| Developer productivity | 10 | +| Scalability and parallelism | 10 | +| Cost predictability | 5 | +| **Total** | **100** | + +## Results + +| Option | API fit (20) | Time (15) | Ops (15) | Reliability (15) | Governance (10) | Dev speed (10) | Scale (10) | Cost (5) | **Weighted total / 100** | +| --------------------------- | -----------: | --------: | -------: | ---------------: | --------------: | -------------: | ---------: | -------: | -----------------------: | +| **Prefect** | 5 | 5 | 4 | 4 | 3 | 5 | 4 | 4 | **87** | +| **Dagster** | 3 | 3 | 3 | 4 | 4 | 4 | 4 | 3 | **68** | +| **Airflow** | 3 | 3 | 2 | 4 | 5 | 3 | 5 | 3 | **67** | +| **Temporal** | 4 | 2 | 3 | 5 | 3 | 2 | 5 | 3 | **66** | +| **Argo Workflows** | 3 | 2 | 3 | 4 | 3 | 2 | 5 | 3 | **61** | +| **Internal scheduler only** | 2 | 5 | 5 | 2 | 1 | 4 | 2 | 5 | **59** | + +## Recommendation + +### Primary now + +- **Use Prefect as the primary orchestrator** for eo-api. +- Keep the internal scheduler enabled as fallback for simple local/dev operation. + +### Why + +- Strongest fit with current API-driven execution design. +- Lowest implementation friction in this repository today. +- Good reliability and operational controls without Airflow-level overhead. + +## Re-evaluation triggers + +Re-run this decision if one or more of these become true: + +- Organization mandates a central Airflow platform for all production data pipelines. +- Multi-team governance, strict lineage, or compliance reporting dominates requirements. +- Workflow complexity shifts to very long-lived, stateful business processes. +- eo-api run volume and fan-out exceed current orchestration/operator capacity. + +## Practical adoption plan + +1. Keep Prefect as default orchestration backend. +2. Preserve adapter-style boundary in eo-api so alternate orchestrators can be added. +3. Review quarterly using real metrics: + - schedule success rate + - mean time to recovery + - operator effort per week + - backfill effort and failure rate + +## Notes + +- Scores are tuned for current project context, not universal. +- If enterprise governance is weighted higher than implementation speed, Airflow can become the preferred option. diff --git a/PRD.md b/PRD.md index 43292fe..84a45d6 100644 --- a/PRD.md +++ b/PRD.md @@ -1,6 +1,7 @@ # DHIS2 EO API — Product Requirements Document (PRD) ## 1) Overview + DHIS2 EO API is a no-code geospatial data integration platform that enables users to discover, fetch, process, harmonize, and load earth observation and related datasets into DHIS2 and the CHAP Modelling Platform. DHIS2 Maps app and DHIS2 Climate app are core downstream consumers of this API, with `eo-api` replacing functionality currently sourced from Google Earth Engine. @@ -8,7 +9,9 @@ DHIS2 Maps app and DHIS2 Climate app are core downstream consumers of this API, This PRD defines the MVP scope for the hackathon and a near-term path toward a production-ready platform. ## 2) Problem Statement + Current EO data workflows are fragmented across tools and scripts, making them hard to repeat, schedule, and maintain. Teams need a unified API that supports: + - Dataset discovery - On-demand processing - Aggregation to DHIS2 org units @@ -16,6 +19,7 @@ Current EO data workflows are fragmented across tools and scripts, making them h - Optional custom pre/post-processing ## 3) Goals + - Provide a unified API for EO data retrieval and processing. - Enable no-code workflows comparable to DHIS2 Climate Tools, using existing DHIS2 EO libraries. - Support map-serving and analytics use cases without Google Earth Engine lock-in. @@ -23,18 +27,22 @@ Current EO data workflows are fragmented across tools and scripts, making them h - Deliver an MVP that proves core end-to-end flows during the hackathon. ## 4) Non-Goals (MVP) + - Full enterprise IAM/SSO implementation. - Comprehensive billing/tenancy model. - Complete OGC suite compliance beyond selected process endpoints. - Replacing all existing tooling in one release. ## 5) Users and Key User Stories + ### Primary users + - DHIS2 implementers and analysts - Climate/health data teams - GIS/data engineering teams ### User stories + - User A: Import daily temperature and precipitation into DHIS2 on a user-defined schedule, aggregated to org units. - User B: Import annual population data, automatically aggregated to org units. - User C: Visualize high-resolution population data in DHIS2 Maps with meaningful density-based styles. @@ -42,11 +50,14 @@ Current EO data workflows are fragmented across tools and scripts, making them h - User E: Add custom pre/post-processing (e.g., consecutive rainy days) before import. ## 6) Functional Requirements + ### FR1 — Unified data/process API + - Expose a single API surface for dataset listing, request submission, processing, and retrieval. - Use `dhis2eo` as the default EO/climate processing integration library. ### FR2 — Core process execution + - Support process discovery and execution using an OGC API - Processes-aligned model. - Minimum endpoints: - `GET /processes` @@ -54,6 +65,7 @@ Current EO data workflows are fragmented across tools and scripts, making them h - `POST /processes/{process-id}/execution` ### FR3 — Raster and tiling capabilities + - Support on-the-fly image tiling and styling from raster sources (COGs/Zarr where feasible). - Enable: - Value retrieval at a location @@ -61,30 +73,54 @@ Current EO data workflows are fragmented across tools and scripts, making them h - Ensure parity for the core map/preview capabilities currently implemented with Google Earth Engine for DHIS2 Maps and Climate clients. ### FR4 — Dataset discovery endpoint + - Provide endpoint(s) to list available datasets for DHIS2 Maps and related clients. - Prefer STAC-compatible metadata where possible. +### FR10 — OGC Collections and Coverages baseline + +- Implement OGC API - Common collection discovery endpoints: + - `GET /collections` + - `GET /collections/{collectionId}` +- Implement OGC API - Coverages baseline endpoint: + - `GET /collections/{collectionId}/coverage` +- Support key coverage query parameters (`bbox`, `datetime`, `range-subset`, `f`) with structured validation errors. + +### FR11 — File-driven dataset catalog + +- Store dataset definitions with one folder per dataset in `eoapi/datasets//`. +- Store metadata as `eoapi/datasets//.yaml`. +- Store dataset source resolver logic as `eoapi/datasets//resolver.py`. +- Validate dataset definitions via Pydantic before use. +- Keep dataset schema and examples documented in `eoapi/datasets/README.md`. + ### FR8 — Consumer app compatibility + - API contracts and outputs must support DHIS2 Maps app (https://github.com/dhis2/maps-app) and DHIS2 Climate app (https://github.com/dhis2/climate-app). - Changes to EO API contracts should be assessed for impact on both apps before release. ### FR5 — Scheduling and orchestration + - Allow fixed-interval runs for recurring ingestion. - Support simple pipeline orchestration with optional pre/post-processing step hooks. ### FR6 — DHIS2 integration + - Push processed/aggregated outputs to DHIS2 database through the DHIS2 Web API. - Use `dhis2-python-client` as the default DHIS2 Web API integration library. ### FR9 — Caching and configuration storage + - Support file-based caching of downloaded and/or intermediate processed data when needed for performance, resiliency, or replay. - Support use of DHIS2 Data Store for storing EO API configuration metadata where appropriate. ### FR7 — Upstream library evolution + - Treat `dhis2-python-client` and `dhis2eo` as strategic dependencies that can be changed to fulfill `eo-api` requirements. - When required functionality is missing, define and implement upstream changes rather than introducing long-term local forks in `eo-api`. ## 7) Non-Functional Requirements + - Reliability: Handle simultaneous and long-running requests. - Extensibility: Process catalog must support adding new EO pipelines with minimal API changes. - Interoperability: Align with open geospatial standards where practical (OGC API, STAC). @@ -93,6 +129,7 @@ Current EO data workflows are fragmented across tools and scripts, making them h - Dependency sustainability: Upstream contributions to `dhis2-python-client` and `dhis2eo` should preserve backward compatibility where feasible. ## 8) Proposed Technical Direction + - API framework: FastAPI - Process API: pygeoapi (OGC API - Processes) - Raster/tile API: TiTiler (`/cog/*`, `/stac/*`) @@ -105,25 +142,34 @@ Current EO data workflows are fragmented across tools and scripts, making them h - Deployment: Docker-based services ## 9) MVP Scope (Hackathon) + ### In scope + - Draft unified API interface +- OGC collection discovery (`/collections`, `/collections/{collectionId}`) +- OGC coverage baseline (`/collections/{collectionId}/coverage`) for gridded datasets (CHIRPS, ERA5-Land) +- File-driven dataset definitions (`eoapi/datasets//.yaml`) with schema validation - Process catalog + at least one executable process flow - Climate variable import path (temperature/precipitation) aggregated to org units - Preview of data for org unit - Basic scheduled run capability (or simulated scheduler integration) ### Out of scope + - Full production-grade authz/authn stack - Broad catalog of EO sources - Advanced UI beyond thin client integration points ## 10) Success Metrics + - Time-to-first-ingestion: User can configure and run first import quickly. - Pipeline success rate for scheduled runs. - Latency for preview and aggregation requests within acceptable operational bounds. - Number of reusable process definitions in the process catalog. +- Dataset onboarding time: time to add a new dataset definition YAML and expose it via `/collections`. ## 11) Google Earth Engine Parity Checklist + - [ ] On-the-fly raster tiling available via EO API endpoints for Maps/Climate use cases. - [ ] Styling controls exposed for map visualization workflows used by DHIS2 Maps app. - [ ] Value retrieval at a single location (point query) supported for climate/map inspection. @@ -136,27 +182,35 @@ Current EO data workflows are fragmented across tools and scripts, making them h - [ ] Core Climate app workflows run without Google Earth Engine dependency. ### Parity verification criteria + - Functional parity: Equivalent user-visible outcome for each checklist item in Maps and Climate integrations. - Data parity: Results are within agreed tolerance for value/aggregation comparisons. - Operational parity: Throughput and latency are acceptable for expected production usage. ## 12) Risks and Open Questions + - Should TiTiler and EO API run in same container or separate services? - Should openEO be explored as a strategic integration path? - What level of OGC API - Processes compliance is required for MVP vs production? ## 13) Upstream Contribution Strategy + - Preferred path: implement missing cross-project capabilities in `dhis2-python-client` and/or `dhis2eo`, then consume released versions in `eo-api`. - Avoid permanent private forks; use short-lived patches only when release timing requires temporary workarounds. - Track upstream gaps as explicit requirements with owner, milestone, and compatibility impact. ## 14) Milestones + 1. Domain and technology landscape assessment 2. Unified API draft finalized 3. MVP process flow implemented 4. End-to-end demo: dataset → process → aggregated output → DHIS2 import path ## 15) Acceptance Criteria (MVP) + +- `/collections` and `/collections/{collectionId}` return OGC-compatible collection metadata for configured datasets. +- `/collections/{collectionId}/coverage` returns OGC Coverages-compatible response structures for supported datasets. +- Dataset definitions are managed as YAML files and validated successfully before runtime use. - A user can discover available process(es) and execute one through API. - A climate dataset can be previewed and aggregated to org units. - A recurring run can be configured and triggered on schedule. diff --git a/PREFECT_INTEGRATION.md b/PREFECT_INTEGRATION.md new file mode 100644 index 0000000..d8eed2e --- /dev/null +++ b/PREFECT_INTEGRATION.md @@ -0,0 +1,150 @@ +# Prefect Integration (Minimal Design) + +This document proposes a minimal way to integrate Prefect orchestration into `eo-api` for no-code scheduled and long-running EO workflows. + +**Constraint:** This project uses only open-source Prefect (self-hosted). Paid managed services are out of scope. + +## 1) Scope and intent + +- Keep `eo-api` as API/control plane. +- Use Prefect as orchestration plane for: + - API-triggered process executions + - recurring schedules (daily/monthly/yearly) +- Preserve current OGC-aligned API contracts and existing `/jobs/{jobId}` model. +- Reuse `dhis2eo` and `dhis2-client` inside flow/task steps. + +## 2) Why Prefect for this project + +- Python-native flow definitions align with current FastAPI codebase. +- Lower operational overhead than Airflow for early-stage product development. +- Good fit for API-triggered runs and dynamic parameters from process execution requests. +- Built-in state model can be translated cleanly into API job states. + +## 3) Minimal architecture + +Components: + +- `eo-api` (FastAPI) + - validates process execution payloads + - creates stable `jobId` + - triggers Prefect deployment/flow run + - exposes status/result via `/jobs/{jobId}` +- Self-hosted Prefect server + workers (open-source deployment) + - executes EO pipeline flows +- Shared storage/cache + - intermediate EO files and result artifacts +- State mapping store + - maps `jobId` ↔ `flow_run_id` + +Suggested mapping fields: + +- `jobId` (API) +- `flowName` (Prefect flow/deployment) +- `flowRunId` (Prefect) +- `status` (`queued|running|succeeded|failed`) +- `progress` (0–100) +- `resultRef` (artifact path/link) + +## 4) Flow boundary and task contract + +Recommended flow name: + +- `eo_aggregate_import_v1` + +Recommended tasks: + +1. `validate_inputs` +2. `load_org_units` +3. `extract_source_data` +4. `aggregate_to_org_units` +5. `build_dhis2_payload` +6. `import_to_dhis2` +7. `publish_results` + +Task IO contract: + +- Inputs: `jobId`, `datasetId`, `parameters`, `datetime/start/end`, `orgUnitLevel`, `aggregation`, `dhis2` options +- Outputs (JSON-serializable): + - `orgUnitCount` + - `rowCount` + - `payloadPreviewPath` + - `importSummary` + - `resultFeaturesPath` + +## 5) API interaction model + +### Trigger from process endpoint + +- Endpoint: `POST /processes/eo-aggregate-import/execution` +- Behavior: + - create `jobId` + - submit Prefect flow run with request inputs + - persist `jobId` ↔ `flowRunId` + - return `202` with monitor/result links + +### Monitor through job endpoint + +- Endpoint: `GET /jobs/{jobId}` +- Behavior: + - lookup `flowRunId` + - fetch Prefect state + - map state to API status + - return status/progress/import summary/result links + +State mapping (minimal): + +- `queued` → Prefect: `Scheduled`, `Pending` +- `running` → Prefect: `Running` +- `succeeded` → Prefect: `Completed` +- `failed` → Prefect: `Failed`, `Crashed`, `Cancelled` + +## 6) Scheduling model + +Use Prefect deployments/schedules for: + +- daily ERA5-Land import +- daily CHIRPS import +- yearly WorldPop refresh + +Each schedule should support parameterized deployment inputs (dataset, period, org unit level, dryRun). + +## 7) Operational controls + +- Retries/backoff per task for transient failures (provider/network). +- Task and flow-level timeouts. +- Idempotent import guardrails (same period/org unit reruns do not duplicate). +- Structured artifacts for payload previews and import summaries. +- `dryRun=true` default for safety in no-code workflows. + +## 8) Security and secret handling + +- Store credentials in self-hosted Prefect blocks/secret store or environment-managed secret backend. +- Do not log sensitive credentials in flow/task logs. +- Redact sensitive fields in API and worker logs. + +## 9) Suggested rollout phases + +Phase 1 (minimal): + +- one Prefect-backed flow for `eo-aggregate-import` +- API trigger + `/jobs/{jobId}` polling +- one scheduled daily ERA5 dry-run deployment + +Phase 2: + +- CHIRPS and WorldPop schedules +- richer progress reporting per step +- payload preview retrieval endpoint + +Phase 3: + +- multi-tenant schedule configuration +- stronger alerting/notification integration +- policy-based retries and escalation + +## 10) Fit with current eo-api direction + +- Supports no-code scheduled workflows without overloading request handlers. +- Keeps endpoint contracts stable for DHIS2 Maps and Climate app paths. +- Aligns with product priority on end-to-end correctness and reproducibility. +- Preserves strategic library usage (`dhis2eo`, `dhis2-client`) with clear task boundaries. diff --git a/PRESENTATION.md b/PRESENTATION.md new file mode 100644 index 0000000..913acdd --- /dev/null +++ b/PRESENTATION.md @@ -0,0 +1,303 @@ +--- +marp: true +theme: default +paginate: true +title: DHIS2 EO API +--- + +# DHIS2 EO API + +## Unified Earth Observation processing for DHIS2 and CHAP + +- Standards-aligned geospatial API for discovery, processing, and ingestion +- Replaces key Google Earth Engine-dependent integration paths +- Built for no-code-friendly operational workflows + +--- + +# Why this API exists + +## Problem + +- EO and climate workflows are often fragmented across scripts and tools +- Hard to repeat, schedule, validate, and operationalize at scale + +## Goal + +- One API surface for: + - Dataset discovery + - EO processing + - Aggregation to DHIS2 org units + - Import-ready outputs + - Scheduled recurring runs + +--- + +# Who it serves + +- DHIS2 implementers and analysts +- Climate and health data teams +- GIS and data engineering teams +- DHIS2 Maps app and DHIS2 Climate app as primary consumers + +--- + +# Architecture at a glance + +```mermaid +flowchart LR + A[Client / Maps / Climate / Scripts] --> B[FastAPI EO API] + B --> C[Dataset Catalog YAML] + B --> D[EO Processing via dhis2eo + xclim] + B --> E[DHIS2 Web API] + B --> F[Prefect Optional] + B --> G[External OGC Providers Optional] + B --> H[State Store JSON files] +``` + +--- + +# Technology choices + +- API framework: FastAPI +- OGC alignment: pygeoapi patterns +- Raster endpoints: TiTiler COG routes +- EO processing: dhis2eo +- Climate indices: xclim +- Orchestration: Prefect (optional), internal scheduler (optional) +- Persistence: JSON state files (jobs, schedules, workflows) + +--- + +# Core resources and concepts + +- Collections: discover available datasets +- Coverage and EDR queries: inspect raster values and areas +- Processes: executable units of work +- Jobs: async run status and results +- Workflows: multi-step process pipelines +- Schedules: recurring execution of process inputs or workflows + +--- + +# OGC APIs used: what, how, why + +Used in this API: + +- **OGC API - Common**: landing page + collection discovery (`/`, `/collections`) +- **OGC API - Coverages**: gridded raster access (`/collections/{id}/coverage`) +- **OGC API - EDR**: direct query patterns for point/area extraction (`/position`, `/area`) +- **OGC API - Features**: vector collections for org units and outputs (`/features/*`) +- **OGC API - Processes (style)**: process catalog + execution (`/processes/*`) + +How they relate: + +- `Common` provides dataset identities and link relations +- `Coverages` + `EDR` are complementary views over the same collections +- `Processes` consume collection-based inputs and emit job/output links +- `Features` carries both input geometries and process outputs + +Why this mix: + +- Interoperability with geospatial tools and ecosystem clients +- Predictable contracts for DHIS2 Maps/Climate integration +- Composable workflow model (discover → query → process → import) + +--- + +# OGC standards stack (visual) + +```mermaid +flowchart TB + A[OGC API - Common\nDiscovery + Links + Collections] --> B1[OGC API - Coverages\nGridded raster queries] + A --> B2[OGC API - EDR\nPoint/area extraction patterns] + A --> B3[OGC API - Features\nVector collections + outputs] + B1 --> C[OGC API - Processes style\nExecution + jobs + outputs] + B2 --> C + B3 --> C +``` + +- `Common` anchors identities and link relations. +- `Coverages`, `EDR`, and `Features` provide complementary data-access paths. +- `Processes` orchestrates execution over those resources and produces import-ready outputs. + +--- + +# Dataset model + +- File-driven catalog under eoapi/datasets +- One folder per dataset +- YAML metadata validated by Pydantic +- Dataset-specific resolver logic per source +- Enables adding datasets with minimal endpoint changes + +--- + +# Endpoint map + +## Discovery and standards + +- GET / +- GET /conformance +- GET /collections +- GET /collections/{collectionId} + +## Data access + +- GET /collections/{collectionId}/coverage +- GET /collections/{collectionId}/position +- GET /collections/{collectionId}/area +- GET /features +- GET /features/{collectionId}/items + +## Execution + +- GET /processes +- GET /processes/{processId} +- POST /processes/{processId}/execution +- GET /jobs/{jobId} + +## Orchestration + +- CRUD /workflows + POST /workflows/{workflowId}/run +- CRUD /schedules + POST /schedules/{scheduleId}/run + +--- + +# How execution works + +1. Client posts process inputs +2. API validates payload and dataset/parameter contracts +3. EO extraction and aggregation run (dhis2eo first, fallback if needed) +4. Output converted to import-ready dataValues +5. DHIS2 import is executed or simulated (dry run) +6. Job status and outputs are persisted and queryable + +--- + +# Supported process catalog + +- eo-aggregate-import +- xclim-cdd (consecutive dry days) +- xclim-cwd (consecutive wet days) +- xclim-warm-days + +Each process produces: + +- Import summary +- Feature outputs +- Traceable job status + +--- + +# DHIS2 integration behavior + +- Live integration enabled via environment variables +- Org units can be fetched directly from DHIS2 +- Process outputs are converted to DHIS2 dataValueSets payloads +- Dry-run mode validates and previews without committing writes +- Safe fallback behavior for local/dev when DHIS2 is not configured + +--- + +# Workflow orchestration + +- Workflows store ordered process steps +- Each step executes using the same process contracts +- Run returns all step job IDs for traceability +- Schedules can target: + - Process inputs + - Or a saved workflow ID + +--- + +# Scheduling options + +## Internal scheduler + +- Poll-based cron worker built into API process +- Runs enabled schedules at due times + +## Prefect integration + +- Optional offloading of schedule runs to Prefect deployments +- Job status can sync from Prefect flow states + +--- + +# External OGC federation + +- Merge external providers into local collections catalog +- Federated collection IDs: ext:{providerId}:{sourceCollectionId} +- Proxy coverage and EDR operations to upstream services +- Per-provider controls: + - Auth headers and API keys + - Timeouts and retries + - Allowed operations (coverage, position, area) + +--- + +# Runtime operations and safety + +- Persistent state for jobs, schedules, workflows +- Runtime summary exposed on landing page for operators +- Configurable CORS origins +- Optional API key guard for write routes +- Clear endpoint error contracts (NotFound, InvalidParameterValue, etc.) + +--- + +# Typical demo flow + +1. Discover datasets with GET /collections +2. Inspect one dataset with GET /collections/{id} +3. Preview data using coverage/position/area +4. Execute eo-aggregate-import in dry run +5. Check job via GET /jobs/{jobId} +6. View features via GET /features/aggregated-results/items?jobId=... +7. Create workflow and run it +8. Add schedule for recurring execution + +--- + +# What is production-ready now + +- Core API surface and contracts +- Multi-process execution including xclim +- Workflow and schedule orchestration +- Optional Prefect integration +- External OGC federation with hardening controls +- Durable local state persistence + +--- + +# Next steps to scale further + +- Add metrics dashboards and alerting around scheduler/import failures +- Expand DHIS2 mapping options for multi-data-element scenarios +- Strengthen deployment profile (container, secrets, env templates) +- Add comparison benchmarks against existing GEE-based pipelines + +--- + +# Quick start for this demo + +- Install dependencies: make sync +- Run API: make run +- Open docs: /docs +- Open examples: API_EXAMPLES.md +- Open demo UI: /example-app + +--- + +# Closing + +DHIS2 EO API provides a practical, standards-aligned bridge from EO data to DHIS2 operations: + +- Discover +- Process +- Validate +- Schedule +- Import + +All through one coherent API. diff --git a/PRESENTATION_EXECUTIVE.md b/PRESENTATION_EXECUTIVE.md new file mode 100644 index 0000000..d9c13ff --- /dev/null +++ b/PRESENTATION_EXECUTIVE.md @@ -0,0 +1,176 @@ +--- +marp: true +theme: default +paginate: true +title: DHIS2 EO API — Executive Brief +--- + +# DHIS2 EO API + +## Executive Brief (10 slides) + +- Unified API for Earth Observation data into DHIS2 and CHAP +- Standards-aligned, operational, and no-code-friendly +- Built to reduce dependence on fragmented scripts and GEE-only paths + +--- + +# 1) Why this matters + +- Climate and EO workflows are often brittle and hard to repeat +- Teams need one reliable interface for discovery, processing, and import +- This API turns EO pipelines into auditable, schedulable operations + +--- + +# 2) What the API delivers + +- Dataset discovery (`/collections`) +- Data preview (`/coverage`, `/position`, `/area`) +- Process execution (`/processes/*/execution`) +- Async job tracking (`/jobs/{jobId}`) +- Workflow + schedule orchestration (`/workflows`, `/schedules`) + +--- + +# 3) How it works end-to-end + +```mermaid +flowchart LR + A[Client / DHIS2 Apps] --> B[EO API] + B --> C[Dataset catalog + resolvers] + B --> D[dhis2eo + xclim processing] + B --> E[Import-ready dataValues] + E --> F[DHIS2 Web API] + B --> G[Jobs/Workflows/Schedules state] +``` + +--- + +# 4) OGC APIs used: what, how, why + +Used in this API: + +- **OGC API - Common**: landing page + collection discovery (`/`, `/collections`) +- **OGC API - Coverages**: gridded raster access (`/collections/{id}/coverage`) +- **OGC API - EDR**: direct query patterns for point/area extraction (`/position`, `/area`) +- **OGC API - Features**: vector collections for org units and outputs (`/features/*`) +- **OGC API - Processes (style)**: process catalog + execution (`/processes/*`) + +How they relate: + +- `Common` provides dataset identities and links +- `Coverages` + `EDR` are complementary views over the same collections +- `Processes` consume collection-driven inputs and publish job/output links +- `Features` carries both input geometries (org units) and process outputs + +Why this mix: + +- Interoperability with geospatial tools and clients +- Predictable contracts for DHIS2 Maps/Climate integration +- Composable architecture (discover → query → process → import) + +--- + +# OGC standards stack (visual) + +```mermaid +flowchart TB + A[OGC API - Common\nDiscovery + Links + Collections] --> B1[OGC API - Coverages\nGridded raster queries] + A --> B2[OGC API - EDR\nPoint/area extraction patterns] + A --> B3[OGC API - Features\nVector collections + outputs] + B1 --> C[OGC API - Processes style\nExecution + jobs + outputs] + B2 --> C + B3 --> C +``` + +- `Common` anchors identities and link relations. +- `Coverages`, `EDR`, and `Features` provide complementary data-access paths. +- `Processes` orchestrates execution over those resources and produces import-ready outputs. + +--- + +# 5) Process capabilities today + +- `eo-aggregate-import` +- `xclim-cdd` +- `xclim-cwd` +- `xclim-warm-days` + +Outputs include: + +- Import summary +- Feature-level results +- Traceable job state + +--- + +# 6) Orchestration options + +- Saved multi-step workflows (`/workflows`) +- Recurring schedules (`/schedules`) +- Internal cron worker for built-in recurring runs +- Optional Prefect integration for external orchestration and status sync + +--- + +# 6b) Orchestrator scorecard (weighted) + +Scoring basis: + +- 1–5 score scale across API fit, time to production, ops overhead, reliability, governance, dev speed, scale, and cost. + +| Option | Weighted total (/100) | +| ----------------------- | --------------------: | +| Prefect | **87** | +| Dagster | 68 | +| Airflow | 67 | +| Temporal | 66 | +| Argo Workflows | 61 | +| Internal scheduler only | 59 | + +Decision for current phase: + +- Choose **Prefect** as primary orchestrator. +- Keep internal scheduler as fallback. +- Re-evaluate if enterprise governance constraints become dominant. + +--- + +# 7) Operational resilience + +- Durable local persistence for jobs/schedules/workflows +- Runtime summary at landing page (`GET /`) for operator visibility +- Configurable CORS and optional API-key write protection +- Clear error contracts for fast debugging + +--- + +# 8) Federation and extensibility + +- External OGC providers can be merged into local catalog +- Provider-level auth, timeout, retry, and operation toggles +- File-driven dataset onboarding keeps extension cost low + +--- + +# 9) Demo storyline (5 minutes) + +1. Discover datasets (`GET /collections`) +2. Preview climate data (`/coverage` or `/position`) +3. Execute dry-run import process +4. Track job + inspect feature results +5. Run workflow and attach schedule + +--- + +# 10) Key takeaway + +DHIS2 EO API is now a practical integration backbone for EO/climate operations: + +- Discover confidently +- Process consistently +- Schedule reliably +- Import safely into DHIS2 + +Ready for incremental production hardening and broader dataset/process expansion. diff --git a/README.md b/README.md index 9265ace..816c3c2 100644 --- a/README.md +++ b/README.md @@ -38,31 +38,156 @@ uvicorn main:app --reload - `make sync` — install dependencies with uv - `make run` — start the app with uv +- `make validate-datasets` — validate all dataset YAML files against the Pydantic schema +- `make test` — run Python tests with pytest -Root endpoint: +## Example frontend app -http://127.0.0.1:8000/ -> Welcome to DHIS2 EO API +A minimal browser UI is available at: -Docs: +- `http://127.0.0.1:8000/example-app` -http://127.0.0.1:8000/docs +This app demonstrates creating and running scheduled imports (for example nightly precipitation/temperature imports aggregated to org units) using the API endpoints. -Examples: +## Documentation -COG info: +- API usage examples: [`API_EXAMPLES.md`](API_EXAMPLES.md) + - Runtime summary example for `GET /`: [`API_EXAMPLES.md#landing-page-runtime-summary`](API_EXAMPLES.md#landing-page-runtime-summary) +- Project presentation deck: [`PRESENTATION.md`](PRESENTATION.md) +- Executive presentation deck (10 slides): [`PRESENTATION_EXECUTIVE.md`](PRESENTATION_EXECUTIVE.md) +- Orchestration tool decision scorecard: [`ORCHESTRATION_DECISION.md`](ORCHESTRATION_DECISION.md) +- Dataset schema and resolver conventions: [`eoapi/datasets/README.md`](eoapi/datasets/README.md) +- Product requirements and scope: [`PRD.md`](PRD.md) +- Prefect orchestration design: [`PREFECT_INTEGRATION.md`](PREFECT_INTEGRATION.md) +- xclim indicator integration design: [`XCLIM_INTEGRATION.md`](XCLIM_INTEGRATION.md) +- Repository coding guidance for AI edits: [`.github/copilot-instructions.md`](.github/copilot-instructions.md) -http://127.0.0.1:8000/cog/info?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif +## Prefect runtime configuration -COG preview: +To enable Prefect-backed schedule runs (`POST /schedules/{scheduleId}/run`), set: -http://127.0.0.1:8000/cog/preview.png?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif&max_size=2048&colormap_name=delta +- `EOAPI_PREFECT_ENABLED=true` +- `EOAPI_PREFECT_API_URL=` +- `EOAPI_PREFECT_DEPLOYMENT_ID=` +- `EOAPI_PREFECT_API_KEY=` -Tile: +If Prefect is disabled (or unavailable), schedule runs fall back to local in-process execution. -http://127.0.0.1:8000/cog/tiles/WebMercatorQuad/4/5/5.png?url=https%3A%2F%2Fdata.chc.ucsb.edu%2Fproducts%2FCHIRPS%2Fv3.0%2Fdaily%2Ffinal%2Frnl%2F2026%2Fchirps-v3.0.rnl.2026.01.31.tif&colormap_name=delta +## Internal scheduler runtime ---- +Recurring schedules can also run from an internal cron worker in this API process. -CHIRPS COG test file: +- `EOAPI_INTERNAL_SCHEDULER_ENABLED` (optional, default `true`) +- `EOAPI_INTERNAL_SCHEDULER_POLL_SECONDS` (optional, default `30`) -https://data.chc.ucsb.edu/products/CHIRPS/v3.0/daily/final/rnl/2026/chirps-v3.0.rnl.2026.01.31.tif +## DHIS2 runtime configuration + +To enable live DHIS2 org-unit retrieval and `dataValueSets` import: + +- `EOAPI_DHIS2_BASE_URL=` +- Either `EOAPI_DHIS2_TOKEN=` +- Or `EOAPI_DHIS2_USERNAME=` and `EOAPI_DHIS2_PASSWORD=` +- Optional: `EOAPI_DHIS2_TIMEOUT_SECONDS=20` + +Behavior: + +- If DHIS2 is configured, org units are fetched from DHIS2 for `/features/dhis2-org-units/items`. +- If DHIS2 is not configured or unavailable, built-in sample org units are used as fallback. +- Process imports (`dryRun=false`) send `dataValues` to DHIS2 `POST /api/dataValueSets`. +- Process imports (`dryRun=true`) are validated locally and return import-ready payload summaries without writing to DHIS2. + +## State persistence + +Jobs, schedules, and workflows now persist to JSON state files. + +- `EOAPI_STATE_DIR` (optional, default `.cache/state`) +- `EOAPI_STATE_PERSIST` (optional, default `true`; set `false` to disable persistence) + +## API security and CORS + +- `EOAPI_CORS_ORIGINS` (optional, default `*`) comma-separated allowed origins +- `EOAPI_API_KEY` (optional): if set, write operations (`POST`, `PATCH`, `PUT`, `DELETE`) require `X-API-Key` header + +## External OGC federation (collections) + +To merge external OGC API - Common collections into local `/collections`, set: + +- `EOAPI_EXTERNAL_OGC_SERVICES=` + +Example: + +```json +[ + { + "id": "demo-provider", + "title": "Demo OGC Provider", + "url": "https://example-ogc.test", + "headers": { + "X-Client-Id": "eo-api" + }, + "apiKeyEnv": "DEMO_OGC_API_KEY", + "authScheme": "Bearer", + "timeoutSeconds": 20, + "retries": 1, + "operations": ["coverage", "position"] + } +] +``` + +Provider config fields: + +- `headers` (optional): static headers to include in upstream requests. +- `apiKeyEnv` (optional): environment variable name containing upstream API key/token. +- `authScheme` (optional, default `Bearer`): used in `Authorization` header as ` `; use `none` to send token without a scheme. +- `timeoutSeconds` (optional, default `20`): request timeout per upstream call. +- `retries` (optional, default `0`): number of retries for transient upstream/network failures. +- `operations` (optional): explicit allowlist for proxied operations; supported values are `coverage`, `position`, `area`. If omitted, all operations are allowed. If any unknown value is provided, that provider config is rejected. + +Current behavior: + +- `/collections` returns local + external collections. +- External collections use federated IDs: `ext::`. +- `/collections/{collectionId}` supports those federated external IDs. +- `/collections/{collectionId}/coverage` proxies to upstream for federated IDs. +- `/collections/{collectionId}/position` and `/collections/{collectionId}/area` proxy to upstream for federated IDs. + +## Workflow JSON schema (MVP) + +Custom workflows are created via `POST /workflows` using this shape: + +```json +{ + "name": "climate-indicators-workflow", + "steps": [ + { + "name": "aggregate-step", + "processId": "eo-aggregate-import", + "payload": { + "inputs": { "...": "process-specific inputs" } + } + }, + { + "name": "indicator-step", + "processId": "xclim-cdd", + "payload": { + "inputs": { "...": "process-specific inputs" } + } + } + ] +} +``` + +Notes: + +- `steps` run sequentially in the order provided. +- `processId` must match an existing process ID from `GET /processes`. +- `payload.inputs` must match the target process execution contract. +- Schedules can target either aggregate-import inputs or a `workflowId`. + +## Dataset definitions + +Collection metadata for `/collections` is defined in YAML files under `eoapi/datasets/`. + +Each dataset uses `eoapi/datasets//.yaml` with matching resolver code in `eoapi/datasets//resolver.py`. + +For schema details, examples, and current dataset files, see [`eoapi/datasets/README.md`](eoapi/datasets/README.md). diff --git a/XCLIM_INTEGRATION.md b/XCLIM_INTEGRATION.md new file mode 100644 index 0000000..c03b5bc --- /dev/null +++ b/XCLIM_INTEGRATION.md @@ -0,0 +1,281 @@ +# xclim Integration (Implemented Baseline + Next Steps) + +This document captures the implemented baseline for `xclim` integration in `eo-api` and the next phases toward broader indicator coverage and orchestration hardening. + +## 0) Current baseline status + +Implemented in API: + +- Process IDs exposed under `/processes`: + - `xclim-cdd` + - `xclim-cwd` + - `xclim-warm-days` +- Execution endpoint support: + - `POST /processes/{processId}/execution` +- Job monitoring reuse: + - `GET /jobs/{jobId}` + +Current execution behavior: + +- Uses lazy imports of `xclim`, `xarray`, `numpy`, and `pandas`. +- Validates required inputs (dataset/parameter/start/end/threshold/orgUnitLevel). +- Requires explicit units (precipitation in mm-based units, warm-days in Celsius units). +- Extracts EO source data through `dhis2eo` download helpers: + - CHIRPS: `dhis2eo.data.chc.chirps3.daily.download` + - ERA5-Land: `dhis2eo.data.cds.era5_land.hourly.download` +- Opens downloaded NetCDF outputs with `xarray`, derives daily series, and computes per-org-unit indicators. +- Produces import-ready feature payload shape + `importSummary` using existing job/result patterns. +- Includes deterministic synthetic fallback only when EO extraction fails at runtime (for example provider or credential issues). + +Runtime cache: + +- Downloaded EO files are cached under `EOAPI_XCLIM_CACHE_DIR` (default: `.cache/xclim`). + +## 1) Goals + +- Add standards-based indicator processes on top of existing EO ingestion workflows. +- Keep current raw import flows unchanged. +- Expose indicators through OGC API - Processes style endpoints. +- Produce DHIS2-ready outputs via existing `dhis2-client` integration path. + +## 2) Why xclim here + +- Provides standardized climate indices with explicit semantics and parameters. +- Reduces one-off custom indicator logic. +- Works directly with `xarray`, which fits existing EO data handling patterns. + +## 3) Scope for first phase (2–3 indicators) + +Start with indicators that map directly to climate-health use cases: + +1. **Consecutive Dry Days (CDD)** + - Typical use: drought stress monitoring. + - Inputs: daily precipitation, threshold (for example 1 mm/day), time range. + +2. **Consecutive Wet Days (CWD)** + - Typical use: flood/vector suitability signal. + - Inputs: daily precipitation, threshold, time range. + +3. **Warm Days Above Threshold** + - Typical use: heat stress indicator. + - Inputs: daily 2m temperature, threshold (for example 35°C), time range. + +These three provide immediate value while keeping compute and validation scope manageable. + +## 4) Process catalog additions + +Process IDs (implemented): + +- `xclim-cdd` +- `xclim-cwd` +- `xclim-warm-days` + +Endpoints (implemented): + +- `GET /processes` +- `GET /processes/{processId}` +- `POST /processes/{processId}/execution` +- `GET /jobs/{jobId}` + +## 5) Proposed execution contract + +Common request fields: + +- `datasetId` (`chirps-daily` or `era5-land-daily`) +- `parameter` (`precip` or `2m_temperature`) +- `start`, `end` +- `orgUnitLevel` or `featureCollectionId` +- `threshold` (unit explicit) +- `aggregation` (usually `sum` or `mean` for post-index rollups) +- `dhis2`: + - `dataElementId` + - `dryRun` (default `true`) + +Common output fields: + +- `indicatorName` +- `period` +- `orgUnitCount` +- `importSummary` +- link to result features and payload preview + +## 6) Data and unit handling requirements + +- Require explicit units in process inputs (for example `degC`, `mm/day`). +- Reject ambiguous or missing units. +- Avoid silent conversions. +- Document assumptions in process descriptions. + +Unit examples: + +- ERA5-Land temperature is often Kelvin in source workflows; convert explicitly before threshold logic. +- Precipitation threshold units must match transformed daily precipitation units. + +## 7) Pipeline placement + +Insert `xclim` after EO extraction and before DHIS2 payload generation: + +1. Extract EO source data (`dhis2eo`) +2. Harmonize units/CRS/time axis +3. Compute `xclim` indicator on gridded data +4. Aggregate indicator result to org units +5. Build DHIS2 payload +6. Import (or dry-run) + +This keeps EO extraction concerns separate from indicator logic. + +## 8) Validation strategy + +For each indicator process, add deterministic tests for: + +- threshold boundary behavior +- missing/nodata handling +- unit mismatch rejection +- expected period formatting for DHIS2 payloads +- process contract errors (`InvalidParameterValue`, `NotFound`) + +## 9) Incremental rollout + +Phase 1: + +- add `xclim-cdd` and `xclim-warm-days` +- dry-run imports only by default +- process contract + deterministic tests + +Phase 2: + +- add `xclim-cwd` +- enable scheduled runs through existing schedule/orchestrator path +- add payload preview endpoint if not already enabled + +Phase 3: + +- evaluate additional indices (for example hot spell duration, percentile-based extremes) +- add bias-adjustment entry points where needed + +## 10) Example process payloads + +### `xclim-cdd` + +```json +{ + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 1.0, "unit": "mm/day" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } +} +``` + +### `xclim-warm-days` + +```json +{ + "inputs": { + "datasetId": "era5-land-daily", + "parameter": "2m_temperature", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 35.0, "unit": "degC" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } +} +``` + +## 11) Dependency and packaging note + +`xclim` is now added as a project dependency. Keep CI/environment checks focused on compatibility across the `xclim` + `xarray` + `numpy` stack. + +## 12) Copy/paste workflow example + +Use this to create a custom workflow that runs aggregate import first, then CDD and warm-days indicators. + +```json +{ + "name": "climate-indicators-monthly-workflow", + "steps": [ + { + "name": "aggregate-precip", + "processId": "eo-aggregate-import", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + } + }, + { + "name": "cdd", + "processId": "xclim-cdd", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 1.0, "unit": "mm/day" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + } + }, + { + "name": "warm-days", + "processId": "xclim-warm-days", + "payload": { + "inputs": { + "datasetId": "era5-land-daily", + "parameter": "2m_temperature", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": { "value": 35.0, "unit": "degC" }, + "dhis2": { + "dataElementId": "", + "dryRun": true + } + } + } + } + ] +} +``` + +Suggested API flow: + +1. `POST /workflows` with the JSON above. +2. `POST /workflows/{workflowId}/run` for immediate execution. +3. `POST /schedules` with `workflowId` for recurring execution. + +Workflow-target schedule payload example: + +```json +{ + "name": "nightly-climate-workflow", + "cron": "0 0 * * *", + "timezone": "UTC", + "enabled": true, + "workflowId": "" +} +``` diff --git a/eoapi/__init__.py b/eoapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eoapi/datasets/README.md b/eoapi/datasets/README.md new file mode 100644 index 0000000..bf1e018 --- /dev/null +++ b/eoapi/datasets/README.md @@ -0,0 +1,98 @@ +# Dataset metadata + +This folder contains dataset definitions used by the `/collections` endpoints. + +Definitions are loaded from dataset-specific subfolders and validated by the `DatasetDefinition` Pydantic model in `eoapi/datasets/catalog.py`. + +## Folder layout + +Each dataset has its own folder named after the dataset ID: + +- `eoapi/datasets//.yaml` +- `eoapi/datasets//resolver.py` + +Example: + +- `eoapi/datasets/chirps-daily/chirps-daily.yaml` +- `eoapi/datasets/chirps-daily/resolver.py` + +## Required schema + +- `id` (string): unique collection identifier used in `/collections/{id}` +- `title` (string): human-readable collection title +- `description` (string): collection description +- `spatial_bbox` (array of 4 numbers): `[minx, miny, maxx, maxy]` in CRS84 +- `temporal_interval` (array of 2 values): `[start_iso8601, end_iso8601_or_null]` + +## Optional schema + +- `keywords` (array of strings): tags used for discovery +- `parameters` (object): shared parameter definitions used by both Coverages and EDR endpoints + +### `parameters` object shape + +Each key is a parameter ID (for example `precip` or `2m_temperature`) and value is a CoverageJSON/EDR-compatible parameter object, for example: + +```yaml +parameters: + precip: + type: Parameter + description: + en: Daily precipitation + unit: + label: + en: mm/day + observedProperty: + label: + en: Precipitation +``` + +## Example + +```yaml +id: my-dataset-daily +title: My Dataset Daily +description: Daily gridded variable for demonstration. +keywords: + - climate + - precipitation + - coverage +spatial_bbox: + - -180.0 + - -90.0 + - 180.0 + - 90.0 +temporal_interval: + - 2000-01-01T00:00:00Z + - null +parameters: + precip: + type: Parameter + description: + en: Daily precipitation + unit: + label: + en: mm/day + observedProperty: + label: + en: Precipitation +``` + +## Current definitions + +- `chirps-daily/chirps-daily.yaml` +- `era5-land-daily/era5-land-daily.yaml` + +## Adding a new dataset resolver module + +When adding a new dataset, create a folder under `eoapi/datasets/` named exactly as the dataset ID and include a `resolver.py` module for dataset-specific source integration logic. + +Expected resolver functions in the module: + +- `coverage_source(datetime_value, parameters, bbox)` +- `position_source(datetime_value, parameters, coords)` +- `area_source(datetime_value, parameters, bbox)` + +These should follow the shared resolver contracts in `eoapi/datasets/base.py`. + +Resolver registration is automatic via `eoapi/datasets/resolvers.py`, which scans dataset folders and loads `resolver.py` by dataset ID. diff --git a/eoapi/datasets/__init__.py b/eoapi/datasets/__init__.py new file mode 100644 index 0000000..46f32d0 --- /dev/null +++ b/eoapi/datasets/__init__.py @@ -0,0 +1,18 @@ +from .catalog import DatasetDefinition, load_datasets +from .base import AreaResolver, BBox, CoverageResolver, ParameterMap, Point, PositionResolver, SourcePayload +from .resolvers import area_resolvers, coverage_resolvers, position_resolvers + +__all__ = [ + "DatasetDefinition", + "load_datasets", + "ParameterMap", + "BBox", + "Point", + "SourcePayload", + "CoverageResolver", + "PositionResolver", + "AreaResolver", + "coverage_resolvers", + "position_resolvers", + "area_resolvers", +] diff --git a/eoapi/datasets/base.py b/eoapi/datasets/base.py new file mode 100644 index 0000000..2a37cd3 --- /dev/null +++ b/eoapi/datasets/base.py @@ -0,0 +1,33 @@ +from typing import Protocol, TypeAlias + +ParameterMap: TypeAlias = dict[str, dict] +BBox: TypeAlias = tuple[float, float, float, float] +Point: TypeAlias = tuple[float, float] +SourcePayload: TypeAlias = dict + + +class CoverageResolver(Protocol): + def __call__( + self, + datetime_value: str, + parameters: ParameterMap, + bbox: BBox, + ) -> SourcePayload: ... + + +class PositionResolver(Protocol): + def __call__( + self, + datetime_value: str, + parameters: ParameterMap, + coords: Point, + ) -> SourcePayload: ... + + +class AreaResolver(Protocol): + def __call__( + self, + datetime_value: str, + parameters: ParameterMap, + bbox: BBox, + ) -> SourcePayload: ... diff --git a/eoapi/datasets/catalog.py b/eoapi/datasets/catalog.py new file mode 100644 index 0000000..aae4019 --- /dev/null +++ b/eoapi/datasets/catalog.py @@ -0,0 +1,65 @@ +from datetime import datetime +from functools import lru_cache +from pathlib import Path + +from pydantic import BaseModel, Field, field_validator +import yaml + +DATASETS_DIR = Path(__file__).resolve().parent + + +class DatasetDefinition(BaseModel): + id: str + title: str + description: str + keywords: list[str] = Field(default_factory=list) + spatial_bbox: tuple[float, float, float, float] + temporal_interval: tuple[str, str | None] + parameters: dict[str, dict] = Field(default_factory=dict) + + @field_validator("temporal_interval", mode="before") + @classmethod + def normalize_temporal_interval(cls, value): + if not isinstance(value, (list, tuple)) or len(value) != 2: + return value + + normalized: list[str | None] = [] + for item in value: + if isinstance(item, datetime): + iso_value = item.isoformat() + if iso_value.endswith("+00:00"): + iso_value = iso_value.replace("+00:00", "Z") + normalized.append(iso_value) + else: + normalized.append(item) + + return normalized + + +@lru_cache(maxsize=1) +def load_datasets() -> dict[str, DatasetDefinition]: + datasets: dict[str, DatasetDefinition] = {} + + dataset_dirs = sorted(path for path in DATASETS_DIR.iterdir() if path.is_dir()) + for dataset_dir in dataset_dirs: + yaml_candidates = [dataset_dir / f"{dataset_dir.name}.yml", dataset_dir / f"{dataset_dir.name}.yaml"] + dataset_file = next((candidate for candidate in yaml_candidates if candidate.exists()), None) + if dataset_file is None: + fallback_files = sorted(dataset_dir.glob("*.yml")) + sorted(dataset_dir.glob("*.yaml")) + if not fallback_files: + continue + dataset_file = fallback_files[0] + + with dataset_file.open("r", encoding="utf-8") as file_handle: + payload = yaml.safe_load(file_handle) or {} + + dataset = DatasetDefinition.model_validate(payload) + if dataset.id != dataset_dir.name: + raise RuntimeError( + f"Dataset id '{dataset.id}' must match dataset folder name '{dataset_dir.name}'" + ) + if dataset.id in datasets: + raise RuntimeError(f"Duplicate dataset id found: {dataset.id}") + datasets[dataset.id] = dataset + + return datasets diff --git a/eoapi/datasets/chirps-daily/chirps-daily.yaml b/eoapi/datasets/chirps-daily/chirps-daily.yaml new file mode 100644 index 0000000..7a2ff24 --- /dev/null +++ b/eoapi/datasets/chirps-daily/chirps-daily.yaml @@ -0,0 +1,28 @@ +id: chirps-daily +title: CHIRPS Daily Precipitation +description: Daily precipitation from CHIRPS as a gridded coverage dataset. +keywords: + - CHIRPS + - precipitation + - rainfall + - coverage + - raster +spatial_bbox: + - -180.0 + - -60.0 + - 180.0 + - 60.0 +temporal_interval: + - 1981-01-01T00:00:00Z + - null +parameters: + precip: + type: Parameter + description: + en: Daily precipitation + unit: + label: + en: mm/day + observedProperty: + label: + en: Precipitation diff --git a/eoapi/datasets/chirps-daily/resolver.py b/eoapi/datasets/chirps-daily/resolver.py new file mode 100644 index 0000000..f35dc04 --- /dev/null +++ b/eoapi/datasets/chirps-daily/resolver.py @@ -0,0 +1,58 @@ +from datetime import date + +from dhis2eo.data.chc.chirps3 import daily as chirps3_daily + +from eoapi.datasets.base import BBox, ParameterMap, Point, SourcePayload +from eoapi.endpoints.errors import invalid_parameter + + +def _parse_sample_day(datetime_value: str) -> date: + try: + return date.fromisoformat(datetime_value[:10]) + except ValueError as exc: + raise invalid_parameter("datetime must be an ISO 8601 date or datetime") from exc + + +def coverage_source( + datetime_value: str, + parameters: ParameterMap, + bbox: BBox, +) -> SourcePayload: + sample_day = _parse_sample_day(datetime_value) + return { + "backend": "dhis2eo.data.chc.chirps3.daily", + "resolver": "url_for_day", + "source_url": chirps3_daily.url_for_day(sample_day), + "bbox": list(bbox), + "variables": list(parameters.keys()), + } + + +def position_source( + datetime_value: str, + parameters: ParameterMap, + coords: Point, +) -> SourcePayload: + sample_day = _parse_sample_day(datetime_value) + return { + "backend": "dhis2eo.data.chc.chirps3.daily", + "resolver": "url_for_day", + "source_url": chirps3_daily.url_for_day(sample_day), + "point": [coords[0], coords[1]], + "variables": list(parameters.keys()), + } + + +def area_source( + datetime_value: str, + parameters: ParameterMap, + bbox: BBox, +) -> SourcePayload: + sample_day = _parse_sample_day(datetime_value) + return { + "backend": "dhis2eo.data.chc.chirps3.daily", + "resolver": "url_for_day", + "source_url": chirps3_daily.url_for_day(sample_day), + "bbox": list(bbox), + "variables": list(parameters.keys()), + } diff --git a/eoapi/datasets/era5-land-daily/era5-land-daily.yaml b/eoapi/datasets/era5-land-daily/era5-land-daily.yaml new file mode 100644 index 0000000..d0f5b46 --- /dev/null +++ b/eoapi/datasets/era5-land-daily/era5-land-daily.yaml @@ -0,0 +1,38 @@ +id: era5-land-daily +title: ERA5-Land Daily Climate +description: Daily ERA5-Land variables as a gridded coverage dataset. +keywords: + - ERA5-Land + - temperature + - soil moisture + - coverage + - raster +spatial_bbox: + - -180.0 + - -90.0 + - 180.0 + - 90.0 +temporal_interval: + - 1950-01-01T00:00:00Z + - null +parameters: + 2m_temperature: + type: Parameter + description: + en: 2m air temperature + unit: + label: + en: K + observedProperty: + label: + en: 2m temperature + total_precipitation: + type: Parameter + description: + en: Total precipitation + unit: + label: + en: m + observedProperty: + label: + en: Total precipitation diff --git a/eoapi/datasets/era5-land-daily/resolver.py b/eoapi/datasets/era5-land-daily/resolver.py new file mode 100644 index 0000000..be7a271 --- /dev/null +++ b/eoapi/datasets/era5-land-daily/resolver.py @@ -0,0 +1,55 @@ +from dhis2eo.data.cds.era5_land import hourly as era5_land_hourly +from dhis2eo.data.cds.era5_land import monthly as era5_land_monthly + +from eoapi.datasets.base import BBox, ParameterMap, Point, SourcePayload + + +def coverage_source( + datetime_value: str, + parameters: ParameterMap, + bbox: BBox, +) -> SourcePayload: + return { + "backend": "dhis2eo.data.cds.era5_land", + "resolver": [ + era5_land_hourly.download.__name__, + era5_land_monthly.download.__name__, + ], + "variables": list(parameters.keys()), + "bbox": list(bbox), + "note": "ERA5-Land source data is resolved via CDS download workflows in dhis2eo.", + } + + +def position_source( + datetime_value: str, + parameters: ParameterMap, + coords: Point, +) -> SourcePayload: + return { + "backend": "dhis2eo.data.cds.era5_land", + "resolver": [ + era5_land_hourly.download.__name__, + era5_land_monthly.download.__name__, + ], + "variables": list(parameters.keys()), + "point": [coords[0], coords[1]], + "note": "ERA5-Land point query is resolved through CDS workflows in dhis2eo.", + } + + +def area_source( + datetime_value: str, + parameters: ParameterMap, + bbox: BBox, +) -> SourcePayload: + return { + "backend": "dhis2eo.data.cds.era5_land", + "resolver": [ + era5_land_hourly.download.__name__, + era5_land_monthly.download.__name__, + ], + "variables": list(parameters.keys()), + "bbox": list(bbox), + "note": "ERA5-Land area query is resolved through CDS workflows in dhis2eo.", + } diff --git a/eoapi/datasets/resolvers.py b/eoapi/datasets/resolvers.py new file mode 100644 index 0000000..e4a7b41 --- /dev/null +++ b/eoapi/datasets/resolvers.py @@ -0,0 +1,62 @@ +from functools import lru_cache +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +from types import ModuleType + +from eoapi.datasets.base import AreaResolver, CoverageResolver, PositionResolver +from eoapi.datasets.catalog import DATASETS_DIR + + +def _load_module(module_name: str, module_path: Path) -> ModuleType: + spec = spec_from_file_location(module_name, module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load resolver module from: {module_path}") + + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@lru_cache(maxsize=1) +def _resolver_modules() -> dict[str, ModuleType]: + modules: dict[str, ModuleType] = {} + + for dataset_dir in sorted(path for path in DATASETS_DIR.iterdir() if path.is_dir()): + resolver_path = dataset_dir / "resolver.py" + if not resolver_path.exists(): + continue + + module_name = f"eoapi.datasets._resolver_{dataset_dir.name.replace('-', '_')}" + modules[dataset_dir.name] = _load_module(module_name, resolver_path) + + return modules + + +@lru_cache(maxsize=1) +def coverage_resolvers() -> dict[str, CoverageResolver]: + resolvers: dict[str, CoverageResolver] = {} + for dataset_id, module in _resolver_modules().items(): + resolver = getattr(module, "coverage_source", None) + if callable(resolver): + resolvers[dataset_id] = resolver + return resolvers + + +@lru_cache(maxsize=1) +def position_resolvers() -> dict[str, PositionResolver]: + resolvers: dict[str, PositionResolver] = {} + for dataset_id, module in _resolver_modules().items(): + resolver = getattr(module, "position_source", None) + if callable(resolver): + resolvers[dataset_id] = resolver + return resolvers + + +@lru_cache(maxsize=1) +def area_resolvers() -> dict[str, AreaResolver]: + resolvers: dict[str, AreaResolver] = {} + for dataset_id, module in _resolver_modules().items(): + resolver = getattr(module, "area_source", None) + if callable(resolver): + resolvers[dataset_id] = resolver + return resolvers diff --git a/eoapi/dhis2_integration.py b/eoapi/dhis2_integration.py new file mode 100644 index 0000000..b370c25 --- /dev/null +++ b/eoapi/dhis2_integration.py @@ -0,0 +1,220 @@ +import json +import os +from datetime import UTC, datetime +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + + +def _base_url() -> str | None: + raw = os.getenv("EOAPI_DHIS2_BASE_URL", "").strip().rstrip("/") + return raw or None + + +def _auth_token() -> str | None: + raw = os.getenv("EOAPI_DHIS2_TOKEN", "").strip() + return raw or None + + +def _basic_credentials() -> tuple[str, str] | None: + username = os.getenv("EOAPI_DHIS2_USERNAME", "").strip() + password = os.getenv("EOAPI_DHIS2_PASSWORD", "").strip() + if username and password: + return (username, password) + return None + + +def dhis2_configured() -> bool: + return _base_url() is not None and (_auth_token() is not None or _basic_credentials() is not None) + + +def _timeout() -> float: + raw = os.getenv("EOAPI_DHIS2_TIMEOUT_SECONDS", "20").strip() + try: + timeout = float(raw) + except ValueError: + timeout = 20.0 + return timeout if timeout > 0 else 20.0 + + +def _auth_headers() -> dict[str, str]: + headers = {"Accept": "application/json", "Content-Type": "application/json"} + token = _auth_token() + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + credentials = _basic_credentials() + if credentials is None: + return headers + + import base64 + + encoded = base64.b64encode(f"{credentials[0]}:{credentials[1]}".encode("utf-8")).decode("ascii") + headers["Authorization"] = f"Basic {encoded}" + return headers + + +def _request_json(method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any] | None: + base = _base_url() + if base is None: + return None + + request = Request( + url=f"{base}{path}", + method=method, + data=json.dumps(payload).encode("utf-8") if payload is not None else None, + ) + for key, value in _auth_headers().items(): + request.add_header(key, value) + + try: + with urlopen(request, timeout=_timeout()) as response: + body = response.read().decode("utf-8") + if not body: + return {} + parsed = json.loads(body) + return parsed if isinstance(parsed, dict) else {} + except (HTTPError, URLError, json.JSONDecodeError): + return None + + +def _extract_import_counts(payload: dict[str, Any] | None, dry_run: bool, fallback_count: int) -> dict[str, Any]: + if not isinstance(payload, dict): + return { + "imported": fallback_count, + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": dry_run, + "source": "simulated", + } + + counts = payload.get("response", {}).get("importCount") + if not isinstance(counts, dict): + counts = payload.get("importCount") + + if not isinstance(counts, dict): + return { + "imported": fallback_count, + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": dry_run, + "source": "simulated", + } + + return { + "imported": int(counts.get("imported", 0)), + "updated": int(counts.get("updated", 0)), + "ignored": int(counts.get("ignored", 0)), + "deleted": int(counts.get("deleted", 0)), + "dryRun": dry_run, + "source": "dhis2", + } + + +def import_data_values_to_dhis2(data_values: list[dict[str, Any]], *, dry_run: bool) -> dict[str, Any]: + if dry_run: + return { + "imported": 0, + "updated": 0, + "ignored": len(data_values), + "deleted": 0, + "dryRun": True, + "source": "dry-run", + } + + if not data_values: + return { + "imported": 0, + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": False, + "source": "dhis2", + } + + if not dhis2_configured(): + return { + "imported": len(data_values), + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": False, + "source": "simulated", + } + + request_payload = { + "dataValues": data_values, + } + response_payload = _request_json( + "POST", + f"/api/dataValueSets?importStrategy=CREATE_AND_UPDATE&dryRun={'true' if dry_run else 'false'}&preheatCache=true&skipAudit=true&async=false", + payload=request_payload, + ) + return _extract_import_counts(response_payload, dry_run=dry_run, fallback_count=len(data_values)) + + +def _to_feature(ou_id: str, name: str, level: int, geometry: dict[str, Any]) -> dict[str, Any] | None: + if not isinstance(geometry, dict) or "type" not in geometry or "coordinates" not in geometry: + return None + + return { + "type": "Feature", + "id": ou_id, + "geometry": geometry, + "properties": { + "name": name, + "level": level, + }, + } + + +def fetch_org_units_from_dhis2(level: int) -> list[dict[str, Any]]: + if not dhis2_configured(): + return [] + + params = urlencode( + { + "fields": "id,name,level,geometry", + "filter": f"level:eq:{level}", + "paging": "false", + } + ) + payload = _request_json("GET", f"/api/organisationUnits?{params}") + if not isinstance(payload, dict): + return [] + + units = payload.get("organisationUnits", []) + if not isinstance(units, list): + return [] + + features: list[dict[str, Any]] = [] + for item in units: + if not isinstance(item, dict): + continue + ou_id = str(item.get("id", "")).strip() + name = str(item.get("name", "")).strip() or ou_id + if not ou_id: + continue + + geometry = item.get("geometry") + feature = _to_feature(ou_id, name, level, geometry) + if feature is not None: + features.append(feature) + + return features + + +def iso_to_dhis2_period(datetime_value: str) -> str: + normalized = datetime_value.strip() + try: + if normalized.endswith("Z"): + parsed = datetime.fromisoformat(normalized.replace("Z", "+00:00")) + else: + parsed = datetime.fromisoformat(normalized) + return parsed.astimezone(UTC).strftime("%Y%m%d") + except ValueError: + return normalized[:10].replace("-", "") diff --git a/eoapi/endpoints/__init__.py b/eoapi/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eoapi/endpoints/collections.py b/eoapi/endpoints/collections.py new file mode 100644 index 0000000..7503a7e --- /dev/null +++ b/eoapi/endpoints/collections.py @@ -0,0 +1,187 @@ +from fastapi import APIRouter, HTTPException, Request + +from pygeoapi import l10n +from pygeoapi.util import url_join +from eoapi.datasets import DatasetDefinition, load_datasets +from eoapi.endpoints.constants import CRS84, OGC_RELTYPES_BASE +from eoapi.endpoints.errors import not_found +from eoapi.external_ogc import get_external_collection, list_external_collections, parse_federated_collection_id +from eoapi.endpoints.coverages import router as coverages_router +from eoapi.endpoints.edr import router as edr_router + +router = APIRouter(tags=["Collections"]) + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _locale_from_request(request: Request) -> str: + requested = request.query_params.get("lang", "en") + locale = l10n.str2locale(requested, silent=True) + return l10n.locale2str(locale) if locale else "en" + + +def _collection_links(request: Request, collection_id: str) -> list[dict]: + base = _base_url(request) + collections_url = url_join(base, "collections") + collection_url = url_join(collections_url, collection_id) + return [ + { + "rel": "self", + "type": "application/json", + "title": "This collection", + "href": collection_url, + }, + { + "rel": "root", + "type": "application/json", + "title": "API root", + "href": url_join(base, "/"), + }, + { + "rel": "parent", + "type": "application/json", + "title": "Collections", + "href": collections_url, + }, + { + "rel": f"{OGC_RELTYPES_BASE}/coverage", + "type": "application/json", + "title": "Collection coverage", + "href": url_join(collection_url, "coverage"), + }, + { + "rel": f"{OGC_RELTYPES_BASE}/data", + "type": "application/json", + "title": "Collection EDR position query", + "href": url_join(collection_url, "position"), + }, + { + "rel": f"{OGC_RELTYPES_BASE}/data", + "type": "application/json", + "title": "Collection EDR area query", + "href": url_join(collection_url, "area"), + }, + ] + + +def _build_collection(request: Request, dataset: DatasetDefinition) -> dict: + locale = _locale_from_request(request) + return { + "id": dataset.id, + "title": l10n.translate(dataset.title, locale), + "description": l10n.translate(dataset.description, locale), + "keywords": dataset.keywords, + "extent": { + "spatial": { + "bbox": [list(dataset.spatial_bbox)], + "crs": CRS84, + }, + "temporal": { + "interval": [list(dataset.temporal_interval)], + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", + }, + }, + "itemType": "coverage", + "crs": [CRS84], + "links": _collection_links(request, dataset.id), + } + + +def _external_collection_links(request: Request, collection_id: str, source_url: str | None) -> list[dict]: + base = _base_url(request) + collections_url = url_join(base, "collections") + collection_url = url_join(collections_url, collection_id) + + links = [ + { + "rel": "self", + "type": "application/json", + "title": "This collection", + "href": collection_url, + }, + { + "rel": "root", + "type": "application/json", + "title": "API root", + "href": url_join(base, "/"), + }, + { + "rel": "parent", + "type": "application/json", + "title": "Collections", + "href": collections_url, + }, + ] + + if source_url: + links.append( + { + "rel": "source", + "type": "application/json", + "title": "Upstream collection", + "href": source_url, + } + ) + + return links + + +def _normalize_external_collection(request: Request, collection: dict) -> dict: + federation = collection.get("federation", {}) if isinstance(collection.get("federation"), dict) else {} + source_url = None + provider_url = federation.get("providerUrl") + source_collection_id = federation.get("sourceCollectionId") + if isinstance(provider_url, str) and isinstance(source_collection_id, str): + source_url = url_join(provider_url.rstrip("/"), "collections", source_collection_id) + + return { + **collection, + "links": _external_collection_links(request, collection["id"], source_url), + } + + +@router.get("/collections") +def get_collections(request: Request) -> dict: + base = _base_url(request) + collections_url = url_join(base, "collections") + datasets = load_datasets() + + local_collections = [_build_collection(request, dataset) for dataset in datasets.values()] + external_collections = [_normalize_external_collection(request, item) for item in list_external_collections()] + + return { + "collections": local_collections + external_collections, + "links": [ + { + "rel": "self", + "type": "application/json", + "title": "This document", + "href": collections_url, + }, + { + "rel": "root", + "type": "application/json", + "title": "API root", + "href": url_join(base, "/"), + }, + ], + } + + +@router.get("/collections/{collectionId}") +def get_collection(collectionId: str, request: Request) -> dict: + if parse_federated_collection_id(collectionId) is not None: + external_collection = get_external_collection(collectionId) + if external_collection is None: + raise not_found("Collection", collectionId) + return _normalize_external_collection(request, external_collection) + + dataset = load_datasets().get(collectionId) + if dataset is None: + raise not_found("Collection", collectionId) + return _build_collection(request, dataset) + + +router.include_router(coverages_router, tags=["Collections"]) +router.include_router(edr_router, tags=["Collections"]) diff --git a/eoapi/endpoints/conformance.py b/eoapi/endpoints/conformance.py new file mode 100644 index 0000000..a7a4b64 --- /dev/null +++ b/eoapi/endpoints/conformance.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, Request + +from pygeoapi.util import url_join + +router = APIRouter(tags=["Conformance"]) + +CONFORMANCE_CLASSES = [ + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json", + "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections", + "http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/coveragejson", + "http://www.opengis.net/spec/ogcapi-edr-1/1.1/conf/core", + "http://www.opengis.net/spec/ogcapi-edr-1/1.1/conf/position", + "http://www.opengis.net/spec/ogcapi-edr-1/1.1/conf/area", + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson", + "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/json", +] + + +@router.get("/conformance") +def get_conformance(request: Request) -> dict: + base = str(request.base_url).rstrip("/") + return { + "conformsTo": CONFORMANCE_CLASSES, + "links": [ + { + "rel": "self", + "type": "application/json", + "title": "Conformance declaration", + "href": url_join(base, "conformance"), + }, + { + "rel": "root", + "type": "application/json", + "title": "API root", + "href": url_join(base, "/"), + }, + ], + } diff --git a/eoapi/endpoints/constants.py b/eoapi/endpoints/constants.py new file mode 100644 index 0000000..6f2a65d --- /dev/null +++ b/eoapi/endpoints/constants.py @@ -0,0 +1,2 @@ +OGC_RELTYPES_BASE = "http://www.opengis.net/def/rel/ogc/1.0" +CRS84 = "http://www.opengis.net/def/crs/OGC/1.3/CRS84" diff --git a/eoapi/endpoints/coverages.py b/eoapi/endpoints/coverages.py new file mode 100644 index 0000000..dea6a43 --- /dev/null +++ b/eoapi/endpoints/coverages.py @@ -0,0 +1,199 @@ +from fastapi import APIRouter, Query, Request + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.datasets import DatasetDefinition, load_datasets +from eoapi.datasets.base import BBox, CoverageResolver, ParameterMap, SourcePayload +from eoapi.datasets.resolvers import coverage_resolvers +from eoapi.endpoints.constants import CRS84 +from eoapi.endpoints.errors import invalid_parameter, not_found +from eoapi.external_ogc import ( + is_external_operation_enabled, + parse_federated_collection_id, + proxy_external_collection_request, +) + +router = APIRouter() + +COVERAGE_RESOLVERS: dict[str, CoverageResolver] = { + **coverage_resolvers(), +} + + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _parse_bbox( + bbox: str | None, + fallback_bbox: tuple[float, float, float, float], +) -> tuple[float, float, float, float]: + if bbox is None: + return fallback_bbox + + try: + values = tuple(float(part.strip()) for part in bbox.split(",")) + except ValueError as exc: + raise invalid_parameter("bbox must contain 4 comma-separated numbers") from exc + + if len(values) != 4: + raise invalid_parameter("bbox must contain 4 comma-separated numbers") + + return values + + +def _parse_datetime(datetime_value: str | None, fallback_start: str) -> str: + if not datetime_value: + return fallback_start + + if "/" in datetime_value: + start, _, _ = datetime_value.partition("/") + if start in {"", ".."}: + return fallback_start + return start + + return datetime_value + + +def _select_parameters(dataset: DatasetDefinition, range_subset: str | None) -> dict[str, dict]: + available = dataset.parameters + if not available: + raise invalid_parameter(f"No parameters configured for collection '{dataset.id}'") + + if not range_subset: + return available + + requested = [parameter.strip() for parameter in range_subset.split(",") if parameter.strip()] + unknown = [parameter for parameter in requested if parameter not in available] + if unknown: + raise invalid_parameter(f"Unknown range-subset parameter(s): {', '.join(unknown)}") + + return {parameter: available[parameter] for parameter in requested} + + +def _resolve_dhis2eo_source( + collection_id: str, + parameters: ParameterMap, + datetime_value: str, + bbox: BBox, +) -> SourcePayload: + resolver = COVERAGE_RESOLVERS.get(collection_id) + if resolver is not None: + return resolver(datetime_value, parameters, bbox) + + return { + "backend": "dhis2eo", + "bbox": list(bbox), + } + + +def _coverage_links(request: Request, collection_id: str) -> list[dict]: + base = _base_url(request) + collection_url = url_join(base, "collections", collection_id) + coverage_url = url_join(collection_url, "coverage") + return [ + { + "rel": "self", + "type": FORMAT_TYPES[F_JSON], + "title": "Coverage as CoverageJSON", + "href": coverage_url, + }, + { + "rel": "collection", + "type": FORMAT_TYPES[F_JSON], + "title": "Collection metadata", + "href": collection_url, + }, + { + "rel": "root", + "type": FORMAT_TYPES[F_JSON], + "title": "API root", + "href": url_join(base, "/"), + }, + ] + + +@router.get("/collections/{collectionId}/coverage") +def get_collection_coverage( + collectionId: str, + request: Request, + bbox: str | None = None, + datetime_value: str | None = Query(default=None, alias="datetime"), + range_subset: str | None = Query( + default=None, + alias="range-subset", + description="Comma-separated parameter IDs. Must match keys under eoapi/datasets//.yaml -> parameters", + ), + output_format: str = Query(default=F_JSON, alias="f"), +) -> dict: + if parse_federated_collection_id(collectionId) is not None: + operation_enabled = is_external_operation_enabled(collectionId, "coverage") + if operation_enabled is False: + raise invalid_parameter("coverage operation is disabled for this external provider") + proxied = proxy_external_collection_request( + collection_id=collectionId, + operation="coverage", + query_params=list(request.query_params.multi_items()), + ) + if proxied is None: + raise not_found("Collection", collectionId) + return proxied + + if output_format not in {F_JSON, "covjson"}: + raise invalid_parameter("Only f=json and f=covjson are currently supported") + + dataset = load_datasets().get(collectionId) + if dataset is None: + raise not_found("Collection", collectionId) + + bbox_values = _parse_bbox(bbox, dataset.spatial_bbox) + time_value = _parse_datetime(datetime_value, dataset.temporal_interval[0]) + parameters = _select_parameters(dataset, range_subset) + source = _resolve_dhis2eo_source(collectionId, parameters, time_value, bbox_values) + + ranges = { + parameter_name: { + "type": "NdArray", + "dataType": "float", + "axisNames": ["t", "y", "x"], + "shape": [1, 1, 1], + "values": [None], + } + for parameter_name in parameters + } + + return { + "type": "Coverage", + "title": dataset.title, + "description": dataset.description, + "domain": { + "type": "Domain", + "domainType": "Grid", + "axes": { + "x": {"start": bbox_values[0], "stop": bbox_values[2], "num": 2}, + "y": {"start": bbox_values[1], "stop": bbox_values[3], "num": 2}, + "t": {"values": [time_value]}, + }, + "referencing": [ + { + "coordinates": ["x", "y"], + "system": { + "type": "GeographicCRS", + "id": CRS84, + }, + }, + { + "coordinates": ["t"], + "system": { + "type": "TemporalRS", + "calendar": "Gregorian", + }, + }, + ], + }, + "parameters": parameters, + "ranges": ranges, + "links": _coverage_links(request, collectionId), + "source": source, + } diff --git a/eoapi/endpoints/edr.py b/eoapi/endpoints/edr.py new file mode 100644 index 0000000..442e40b --- /dev/null +++ b/eoapi/endpoints/edr.py @@ -0,0 +1,310 @@ +import re + +from fastapi import APIRouter, Query, Request + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.datasets import DatasetDefinition, load_datasets +from eoapi.datasets.base import AreaResolver, BBox, ParameterMap, Point, PositionResolver, SourcePayload +from eoapi.datasets.resolvers import area_resolvers, position_resolvers +from eoapi.endpoints.constants import CRS84 +from eoapi.endpoints.errors import invalid_parameter, not_found +from eoapi.external_ogc import ( + is_external_operation_enabled, + parse_federated_collection_id, + proxy_external_collection_request, +) + +router = APIRouter() + +POSITION_RESOLVERS: dict[str, PositionResolver] = { + **position_resolvers(), +} + +AREA_RESOLVERS: dict[str, AreaResolver] = { + **area_resolvers(), +} + +POINT_PATTERN = re.compile( + r"^POINT\s*\(\s*(?P-?\d+(?:\.\d+)?)\s+(?P-?\d+(?:\.\d+)?)\s*\)$", + re.IGNORECASE, +) + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _parse_point_coords(coords: str) -> tuple[float, float]: + match = POINT_PATTERN.match(coords.strip()) + if not match: + raise invalid_parameter("coords must be a WKT POINT like POINT(30 -1)") + + x = float(match.group("x")) + y = float(match.group("y")) + return (x, y) + + +def _parse_bbox( + bbox: str, +) -> tuple[float, float, float, float]: + try: + values = tuple(float(part.strip()) for part in bbox.split(",")) + except ValueError as exc: + raise invalid_parameter("bbox must contain 4 comma-separated numbers") from exc + + if len(values) != 4: + raise invalid_parameter("bbox must contain 4 comma-separated numbers") + + minx, miny, maxx, maxy = values + if minx >= maxx or miny >= maxy: + raise invalid_parameter("bbox must follow minx,miny,maxx,maxy with min < max") + + return values + + +def _parse_datetime(datetime_value: str | None, fallback_start: str) -> str: + if not datetime_value: + return fallback_start + + if "/" in datetime_value: + start, _, _ = datetime_value.partition("/") + if start in {"", ".."}: + return fallback_start + return start + + return datetime_value + + +def _select_parameters(dataset: DatasetDefinition, parameter_name: str | None) -> dict[str, dict]: + available = dataset.parameters + if not available: + raise invalid_parameter(f"No parameters configured for collection '{dataset.id}'") + + if not parameter_name: + return available + + requested = [parameter.strip() for parameter in parameter_name.split(",") if parameter.strip()] + unknown = [parameter for parameter in requested if parameter not in available] + if unknown: + raise invalid_parameter(f"Unknown parameter-name value(s): {', '.join(unknown)}") + + return {parameter: available[parameter] for parameter in requested} + + +def _resolve_dhis2eo_source( + collection_id: str, + parameters: ParameterMap, + datetime_value: str, + coords: Point, +) -> SourcePayload: + resolver = POSITION_RESOLVERS.get(collection_id) + if resolver is not None: + return resolver(datetime_value, parameters, coords) + + return { + "backend": "dhis2eo", + "point": [coords[0], coords[1]], + } + + +def _resolve_dhis2eo_source_for_bbox( + collection_id: str, + parameters: ParameterMap, + datetime_value: str, + bbox: BBox, +) -> SourcePayload: + resolver = AREA_RESOLVERS.get(collection_id) + if resolver is not None: + return resolver(datetime_value, parameters, bbox) + + return { + "backend": "dhis2eo", + "bbox": list(bbox), + } + + +def _bbox_polygon(bbox: tuple[float, float, float, float]) -> list[list[float]]: + minx, miny, maxx, maxy = bbox + return [ + [minx, miny], + [maxx, miny], + [maxx, maxy], + [minx, maxy], + [minx, miny], + ] + + +def _position_links(request: Request, collection_id: str, coords: str) -> list[dict]: + base = _base_url(request) + collection_url = url_join(base, "collections", collection_id) + position_url = url_join(collection_url, "position") + return [ + { + "rel": "self", + "type": FORMAT_TYPES[F_JSON], + "title": "EDR position query", + "href": f"{position_url}?coords={coords}", + }, + { + "rel": "collection", + "type": FORMAT_TYPES[F_JSON], + "title": "Collection metadata", + "href": collection_url, + }, + { + "rel": "root", + "type": FORMAT_TYPES[F_JSON], + "title": "API root", + "href": url_join(base, "/"), + }, + ] + + +def _area_links(request: Request, collection_id: str, bbox: str) -> list[dict]: + base = _base_url(request) + collection_url = url_join(base, "collections", collection_id) + area_url = url_join(collection_url, "area") + return [ + { + "rel": "self", + "type": FORMAT_TYPES[F_JSON], + "title": "EDR area query", + "href": f"{area_url}?bbox={bbox}", + }, + { + "rel": "collection", + "type": FORMAT_TYPES[F_JSON], + "title": "Collection metadata", + "href": collection_url, + }, + { + "rel": "root", + "type": FORMAT_TYPES[F_JSON], + "title": "API root", + "href": url_join(base, "/"), + }, + ] + + +@router.get("/collections/{collectionId}/position") +def get_collection_position( + collectionId: str, + request: Request, + coords: str = Query(..., description="WKT POINT, e.g. POINT(30 -1)"), + datetime_value: str | None = Query(default=None, alias="datetime"), + parameter_name: str | None = Query( + default=None, + alias="parameter-name", + description="Comma-separated parameter IDs. Must match keys under eoapi/datasets//.yaml -> parameters", + ), + output_format: str = Query(default=F_JSON, alias="f"), +) -> dict: + if parse_federated_collection_id(collectionId) is not None: + operation_enabled = is_external_operation_enabled(collectionId, "position") + if operation_enabled is False: + raise invalid_parameter("position operation is disabled for this external provider") + proxied = proxy_external_collection_request( + collection_id=collectionId, + operation="position", + query_params=list(request.query_params.multi_items()), + ) + if proxied is None: + raise not_found("Collection", collectionId) + return proxied + + if output_format not in {F_JSON, "geojson"}: + raise invalid_parameter("Only f=json and f=geojson are currently supported") + + dataset = load_datasets().get(collectionId) + if dataset is None: + raise not_found("Collection", collectionId) + + point = _parse_point_coords(coords) + time_value = _parse_datetime(datetime_value, dataset.temporal_interval[0]) + parameters = _select_parameters(dataset, parameter_name) + source = _resolve_dhis2eo_source(collectionId, parameters, time_value, point) + + return { + "type": "FeatureCollection", + "title": dataset.title, + "features": [ + { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [point[0], point[1]]}, + "properties": { + "collection": collectionId, + "datetime": time_value, + "crs": CRS84, + "parameters": list(parameters.keys()), + "values": {parameter: None for parameter in parameters.keys()}, + }, + } + ], + "parameters": parameters, + "links": _position_links(request, collectionId, coords), + "source": source, + } + + +@router.get("/collections/{collectionId}/area") +def get_collection_area( + collectionId: str, + request: Request, + bbox: str = Query(..., description="Area bbox as minx,miny,maxx,maxy in CRS84"), + datetime_value: str | None = Query(default=None, alias="datetime"), + parameter_name: str | None = Query( + default=None, + alias="parameter-name", + description="Comma-separated parameter IDs. Must match keys under eoapi/datasets//.yaml -> parameters", + ), + output_format: str = Query(default=F_JSON, alias="f"), +) -> dict: + if parse_federated_collection_id(collectionId) is not None: + operation_enabled = is_external_operation_enabled(collectionId, "area") + if operation_enabled is False: + raise invalid_parameter("area operation is disabled for this external provider") + proxied = proxy_external_collection_request( + collection_id=collectionId, + operation="area", + query_params=list(request.query_params.multi_items()), + ) + if proxied is None: + raise not_found("Collection", collectionId) + return proxied + + if output_format not in {F_JSON, "geojson"}: + raise invalid_parameter("Only f=json and f=geojson are currently supported") + + dataset = load_datasets().get(collectionId) + if dataset is None: + raise not_found("Collection", collectionId) + + area_bbox = _parse_bbox(bbox) + time_value = _parse_datetime(datetime_value, dataset.temporal_interval[0]) + parameters = _select_parameters(dataset, parameter_name) + source = _resolve_dhis2eo_source_for_bbox(collectionId, parameters, time_value, area_bbox) + + return { + "type": "FeatureCollection", + "title": dataset.title, + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [_bbox_polygon(area_bbox)], + }, + "properties": { + "collection": collectionId, + "datetime": time_value, + "crs": CRS84, + "parameters": list(parameters.keys()), + "aggregates": {parameter: None for parameter in parameters.keys()}, + }, + } + ], + "parameters": parameters, + "links": _area_links(request, collectionId, bbox), + "source": source, + } diff --git a/eoapi/endpoints/errors.py b/eoapi/endpoints/errors.py new file mode 100644 index 0000000..2eec2b4 --- /dev/null +++ b/eoapi/endpoints/errors.py @@ -0,0 +1,21 @@ +from fastapi import HTTPException + + +def not_found(resource: str, identifier: str) -> HTTPException: + return HTTPException( + status_code=404, + detail={ + "code": "NotFound", + "description": f"{resource} '{identifier}' not found", + }, + ) + + +def invalid_parameter(description: str) -> HTTPException: + return HTTPException( + status_code=400, + detail={ + "code": "InvalidParameterValue", + "description": description, + }, + ) diff --git a/eoapi/endpoints/features.py b/eoapi/endpoints/features.py new file mode 100644 index 0000000..76c5384 --- /dev/null +++ b/eoapi/endpoints/features.py @@ -0,0 +1,168 @@ +from typing import Any + +from fastapi import APIRouter, Query, Request + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.dhis2_integration import fetch_org_units_from_dhis2 +from eoapi.endpoints.constants import CRS84 +from eoapi.endpoints.errors import invalid_parameter, not_found +from eoapi.jobs import get_job + +router = APIRouter(tags=["Features"]) + + +ORG_UNIT_FEATURES: list[dict[str, Any]] = [ + { + "type": "Feature", + "id": "O6uvpzGd5pu", + "geometry": { + "type": "Polygon", + "coordinates": [[[ -11.64, 8.42], [-11.50, 8.42], [-11.50, 8.55], [-11.64, 8.55], [-11.64, 8.42]]], + }, + "properties": {"name": "Bo", "level": 2}, + }, + { + "type": "Feature", + "id": "fdc6uOvgoji", + "geometry": { + "type": "Polygon", + "coordinates": [[[-13.30, 8.80], [-13.10, 8.80], [-13.10, 9.00], [-13.30, 9.00], [-13.30, 8.80]]], + }, + "properties": {"name": "Bombali", "level": 2}, + }, + { + "type": "Feature", + "id": "lc3eMKXaEfw", + "geometry": { + "type": "Polygon", + "coordinates": [[[-12.40, 7.00], [-12.10, 7.00], [-12.10, 7.25], [-12.40, 7.25], [-12.40, 7.00]]], + }, + "properties": {"name": "Bonthe", "level": 2}, + }, +] + + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _parse_bbox(bbox: str | None) -> tuple[float, float, float, float] | None: + if bbox is None: + return None + + try: + values = tuple(float(part.strip()) for part in bbox.split(",")) + except ValueError as exc: + raise invalid_parameter("bbox must contain 4 comma-separated numbers") from exc + + if len(values) != 4: + raise invalid_parameter("bbox must contain 4 comma-separated numbers") + + minx, miny, maxx, maxy = values + if minx >= maxx or miny >= maxy: + raise invalid_parameter("bbox must follow minx,miny,maxx,maxy with min < max") + + return values + + +def _feature_bbox(feature: dict[str, Any]) -> tuple[float, float, float, float]: + ring = feature["geometry"]["coordinates"][0] + xs = [point[0] for point in ring] + ys = [point[1] for point in ring] + return (min(xs), min(ys), max(xs), max(ys)) + + +def _intersects(a: tuple[float, float, float, float], b: tuple[float, float, float, float]) -> bool: + return not (a[2] < b[0] or a[0] > b[2] or a[3] < b[1] or a[1] > b[3]) + + +def org_unit_items(level: int = 2, bbox: tuple[float, float, float, float] | None = None) -> list[dict[str, Any]]: + dhis2_features = fetch_org_units_from_dhis2(level) + features = dhis2_features or [feature for feature in ORG_UNIT_FEATURES if feature["properties"].get("level") == level] + if bbox is not None: + features = [feature for feature in features if _intersects(_feature_bbox(feature), bbox)] + return features + + +@router.get("/features") +def get_feature_collections(request: Request) -> dict[str, Any]: + base = _base_url(request) + features_url = url_join(base, "features") + return { + "collections": [ + { + "id": "dhis2-org-units", + "title": "DHIS2 organisation units", + "description": "Organisation unit features from DHIS2-compatible structure.", + "itemType": "feature", + "crs": [CRS84], + "links": [ + { + "rel": "items", + "type": FORMAT_TYPES[F_JSON], + "title": "Feature items", + "href": url_join(base, "features", "dhis2-org-units", "items"), + } + ], + }, + { + "id": "aggregated-results", + "title": "Aggregated process results", + "description": "Feature collection generated by process executions.", + "itemType": "feature", + "crs": [CRS84], + "links": [ + { + "rel": "items", + "type": FORMAT_TYPES[F_JSON], + "title": "Feature items", + "href": url_join(base, "features", "aggregated-results", "items"), + } + ], + }, + ], + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": features_url}, + {"rel": "root", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "/")}, + ], + } + + +@router.get("/features/{collectionId}/items") +def get_feature_items( + collectionId: str, + request: Request, + level: int = Query(default=2, ge=1), + bbox: str | None = None, + jobId: str | None = None, +) -> dict[str, Any]: + base = _base_url(request) + items_url = url_join(base, "features", collectionId, "items") + + if collectionId == "dhis2-org-units": + bbox_values = _parse_bbox(bbox) + features = org_unit_items(level=level, bbox=bbox_values) + elif collectionId == "aggregated-results": + if not jobId: + raise invalid_parameter("jobId is required for aggregated-results") + job = get_job(jobId) + if job is None: + raise not_found("Job", jobId) + features = job["outputs"].get("features", []) + else: + raise not_found("Feature collection", collectionId) + + return { + "type": "FeatureCollection", + "timeStamp": jobId or "current", + "numberMatched": len(features), + "numberReturned": len(features), + "features": features, + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": items_url}, + {"rel": "collection", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "features")}, + {"rel": "root", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "/")}, + ], + } diff --git a/eoapi/endpoints/processes.py b/eoapi/endpoints/processes.py new file mode 100644 index 0000000..eb97f71 --- /dev/null +++ b/eoapi/endpoints/processes.py @@ -0,0 +1,837 @@ +from datetime import date +import logging +import os +from pathlib import Path +from typing import Any, Literal + +from fastapi import APIRouter, Request +from pydantic import BaseModel, Field, ValidationError + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.datasets import load_datasets +from eoapi.dhis2_integration import import_data_values_to_dhis2, iso_to_dhis2_period +from eoapi.endpoints.errors import invalid_parameter, not_found +from eoapi.endpoints.features import org_unit_items +from eoapi.jobs import create_job, get_job, update_job +from eoapi.orchestration.prefect import get_flow_run, prefect_enabled, prefect_state_to_job_status + +router = APIRouter(tags=["Processes"]) +logger = logging.getLogger(__name__) + +AGGREGATE_PROCESS_ID = "eo-aggregate-import" +XCLIM_CDD_PROCESS_ID = "xclim-cdd" +XCLIM_CWD_PROCESS_ID = "xclim-cwd" +XCLIM_WARM_DAYS_PROCESS_ID = "xclim-warm-days" + +XCLIM_PROCESS_IDS = { + XCLIM_CDD_PROCESS_ID, + XCLIM_CWD_PROCESS_ID, + XCLIM_WARM_DAYS_PROCESS_ID, +} + +TIME_DIM_CANDIDATES = ("time", "valid_time") +X_DIM_CANDIDATES = ("x", "lon", "longitude") +Y_DIM_CANDIDATES = ("y", "lat", "latitude") + + +class DHIS2ImportOptions(BaseModel): + dataElementId: str = Field(min_length=1) + dataElementMap: dict[str, str] | None = None + dryRun: bool = True + + +class AggregateImportInputs(BaseModel): + datasetId: str = Field(min_length=1) + parameters: list[str] | None = None + datetime: str | None = None + start: str | None = None + end: str | None = None + orgUnitLevel: int = Field(default=2, ge=1) + aggregation: Literal["mean", "sum", "min", "max"] = "mean" + dhis2: DHIS2ImportOptions + + +class ThresholdInput(BaseModel): + value: float + unit: str = Field(min_length=1) + + +class XclimIndicatorInputs(BaseModel): + datasetId: str = Field(min_length=1) + parameter: str = Field(min_length=1) + start: date + end: date + orgUnitLevel: int = Field(default=2, ge=1) + threshold: ThresholdInput + dhis2: DHIS2ImportOptions + + +class ExecuteRequest(BaseModel): + inputs: dict[str, Any] + + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _select_parameters(dataset_id: str, requested: list[str] | None) -> list[str]: + dataset = load_datasets().get(dataset_id) + if dataset is None: + raise not_found("Collection", dataset_id) + + available = list(dataset.parameters.keys()) + if not available: + raise invalid_parameter(f"No parameters configured for collection '{dataset.id}'") + + if not requested: + return available + + unknown = [parameter for parameter in requested if parameter not in dataset.parameters] + if unknown: + raise invalid_parameter(f"Unknown parameter(s): {', '.join(unknown)}") + + return requested + + +def _value_for(index: int, parameter: str, aggregation: str) -> float: + base = (index + 1) * (len(parameter) + 1) + if aggregation == "sum": + return round(base * 1.5, 4) + if aggregation == "min": + return round(base * 0.5, 4) + if aggregation == "max": + return round(base * 2.0, 4) + return round(base * 1.0, 4) + + +def _build_aggregate_data_values( + features: list[dict[str, Any]], + *, + default_data_element_id: str, + data_element_map: dict[str, str] | None, + period: str, +) -> list[dict[str, Any]]: + payload: list[dict[str, Any]] = [] + for feature in features: + org_unit = feature["properties"].get("orgUnit") + values = feature["properties"].get("values", {}) + if not isinstance(org_unit, str) or not isinstance(values, dict): + continue + + for parameter_id, value in values.items(): + mapped_data_element = (data_element_map or {}).get(parameter_id, default_data_element_id) + if not mapped_data_element: + continue + payload.append( + { + "dataElement": mapped_data_element, + "orgUnit": org_unit, + "period": period, + "value": value, + } + ) + + return payload + + +def _build_indicator_data_values( + features: list[dict[str, Any]], + *, + data_element_id: str, + period: str, +) -> list[dict[str, Any]]: + payload: list[dict[str, Any]] = [] + for feature in features: + properties = feature.get("properties", {}) + if not isinstance(properties, dict): + continue + + org_unit = properties.get("orgUnit") + value = properties.get("value") + if not isinstance(org_unit, str) or value is None: + continue + + payload.append( + { + "dataElement": data_element_id, + "orgUnit": org_unit, + "period": period, + "value": value, + } + ) + + return payload + + +def _coerce_date(value: str) -> date: + try: + return date.fromisoformat(value[:10]) + except ValueError as exc: + raise invalid_parameter(f"Invalid date value '{value}'") from exc + + +def _aggregate_series_values(values: list[float], aggregation: str) -> float: + if not values: + return 0.0 + + if aggregation == "sum": + return round(sum(values), 4) + if aggregation == "min": + return round(min(values), 4) + if aggregation == "max": + return round(max(values), 4) + return round(sum(values) / len(values), 4) + + +def _extract_aggregate_values_from_dhis2eo( + *, + dataset_id: str, + parameters: list[str], + start: date, + end: date, + aggregation: str, + org_units: list[dict[str, Any]], +) -> dict[str, dict[str, float]]: + try: + import numpy as np + import pandas as pd + import xarray as xr + except ImportError as exc: + raise RuntimeError("Missing xarray/pandas/numpy dependencies for aggregate extraction") from exc + + values_by_org: dict[str, dict[str, float]] = {feature["id"]: {} for feature in org_units} + + for parameter in parameters: + series_by_org = _extract_org_unit_series( + dataset_id=dataset_id, + parameter=parameter, + start=start, + end=end, + org_units=org_units, + np=np, + pd=pd, + xr=xr, + ) + + for org_id, series in series_by_org.items(): + loaded = series.load() + raw = loaded.values + flat = [float(value) for value in raw.flatten().tolist() if value is not None] + values_by_org.setdefault(org_id, {})[parameter] = _aggregate_series_values(flat, aggregation) + + return values_by_org + + +def _parse_process_inputs(process_id: str, payload: ExecuteRequest) -> AggregateImportInputs | XclimIndicatorInputs: + try: + if process_id == AGGREGATE_PROCESS_ID: + return AggregateImportInputs.model_validate(payload.inputs) + if process_id in XCLIM_PROCESS_IDS: + return XclimIndicatorInputs.model_validate(payload.inputs) + except ValidationError as exc: + raise invalid_parameter(f"Invalid process inputs: {exc.errors()}") from exc + + raise not_found("Process", process_id) + + +def _process_definition(process_id: str, base: str) -> dict[str, Any]: + descriptions = { + AGGREGATE_PROCESS_ID: { + "title": "EO aggregate and DHIS2 import", + "description": "Aggregate EO dataset values to DHIS2 org units and import as dataValueSets.", + }, + XCLIM_CDD_PROCESS_ID: { + "title": "xclim consecutive dry days (CDD)", + "description": "Compute xclim CDD over org units using daily precipitation and import-ready outputs.", + }, + XCLIM_CWD_PROCESS_ID: { + "title": "xclim consecutive wet days (CWD)", + "description": "Compute xclim CWD over org units using daily precipitation and import-ready outputs.", + }, + XCLIM_WARM_DAYS_PROCESS_ID: { + "title": "xclim warm days above threshold", + "description": "Compute number of warm days above threshold over org units and import-ready outputs.", + }, + } + meta = descriptions[process_id] + + return { + "id": process_id, + "title": meta["title"], + "description": meta["description"], + "jobControlOptions": ["async-execute"], + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "processes", process_id)}, + { + "rel": "execute", + "type": FORMAT_TYPES[F_JSON], + "href": url_join(base, "processes", process_id, "execution"), + }, + ], + } + + +def _validate_xclim_inputs(process_id: str, inputs: XclimIndicatorInputs) -> None: + if inputs.start > inputs.end: + raise invalid_parameter("Input 'start' must be before or equal to 'end'") + + datasets = load_datasets() + dataset = datasets.get(inputs.datasetId) + if dataset is None: + raise not_found("Collection", inputs.datasetId) + + if inputs.parameter not in dataset.parameters: + raise invalid_parameter(f"Unknown parameter '{inputs.parameter}' for collection '{inputs.datasetId}'") + + threshold_unit = inputs.threshold.unit.lower() + if process_id in {XCLIM_CDD_PROCESS_ID, XCLIM_CWD_PROCESS_ID} and "mm" not in threshold_unit: + raise invalid_parameter("Precipitation threshold unit must include 'mm' (for example 'mm/day')") + if process_id == XCLIM_WARM_DAYS_PROCESS_ID and ("degc" not in threshold_unit and "c" != threshold_unit): + raise invalid_parameter("Warm-days threshold unit must be Celsius (for example 'degC')") + + +def _run_xclim(process_id: str, inputs: XclimIndicatorInputs) -> dict[str, Any]: + _validate_xclim_inputs(process_id, inputs) + + try: + import numpy as np + import pandas as pd + import xarray as xr + from xclim import indices as xci + except ImportError as exc: + raise invalid_parameter( + "xclim execution dependencies are missing. Install optional runtime dependencies for xclim integration." + ) from exc + + org_units = org_unit_items(level=inputs.orgUnitLevel) + if not org_units: + raise invalid_parameter(f"No org unit features found for level {inputs.orgUnitLevel}") + + threshold = f"{inputs.threshold.value} {inputs.threshold.unit}" + uses_dhis2eo_source = True + try: + org_unit_series = _extract_org_unit_series( + dataset_id=inputs.datasetId, + parameter=inputs.parameter, + start=inputs.start, + end=inputs.end, + org_units=org_units, + np=np, + pd=pd, + xr=xr, + ) + except Exception as exc: + logger.warning("Falling back to synthetic xclim source after extraction failure: %s", exc) + uses_dhis2eo_source = False + org_unit_series = _synthetic_org_unit_series( + process_id=process_id, + start=inputs.start, + end=inputs.end, + org_units=org_units, + np=np, + pd=pd, + xr=xr, + ) + + result_features: list[dict[str, Any]] = [] + + for feature in org_units: + series = org_unit_series.get(feature["id"]) + if series is None: + continue + + if process_id in {XCLIM_CDD_PROCESS_ID, XCLIM_CWD_PROCESS_ID}: + if process_id == XCLIM_CDD_PROCESS_ID: + indicator = xci.maximum_consecutive_dry_days(series, thresh=threshold, freq="YS") + else: + indicator = xci.maximum_consecutive_wet_days(series, thresh=threshold, freq="YS") + else: + indicator = xci.tx_days_above(series, thresh=threshold, freq="YS") + + if "time" in indicator.dims: + scalar = indicator.isel(time=-1).load() + else: + scalar = indicator.load() + value = float(scalar.item()) + + result_features.append( + { + "type": "Feature", + "id": feature["id"], + "geometry": feature["geometry"], + "properties": { + "orgUnit": feature["id"], + "datasetId": inputs.datasetId, + "indicator": process_id, + "parameter": inputs.parameter, + "start": inputs.start.isoformat(), + "end": inputs.end.isoformat(), + "source": "dhis2eo" if uses_dhis2eo_source else "synthetic-fallback", + "threshold": inputs.threshold.model_dump(), + "value": round(value, 4), + }, + } + ) + + period = inputs.end.strftime("%Y%m%d") + data_values = _build_indicator_data_values( + result_features, + data_element_id=inputs.dhis2.dataElementId, + period=period, + ) + import_summary = import_data_values_to_dhis2(data_values, dry_run=inputs.dhis2.dryRun) + outputs = { + "importSummary": import_summary, + "features": result_features, + } + return create_job(process_id, inputs.model_dump(mode="json"), outputs) + + +def _synthetic_org_unit_series( + process_id: str, + start: date, + end: date, + org_units: list[dict[str, Any]], + np: Any, + pd: Any, + xr: Any, +) -> dict[str, Any]: + times = pd.date_range(start.isoformat(), end.isoformat(), freq="D") + if len(times) == 0: + raise invalid_parameter("The selected period did not produce any daily timesteps") + + values_by_org_unit: dict[str, Any] = {} + for index, feature in enumerate(org_units): + if process_id in {XCLIM_CDD_PROCESS_ID, XCLIM_CWD_PROCESS_ID}: + base = 0.5 + (index % 4) + signal = base + (np.arange(len(times)) % 6) * 0.35 + series = xr.DataArray( + signal, + coords={"time": times}, + dims=["time"], + attrs={"units": "mm/day"}, + ) + else: + base = 22.0 + (index % 5) + signal = base + (np.arange(len(times)) % 9) * 1.4 + series = xr.DataArray( + signal, + coords={"time": times}, + dims=["time"], + attrs={"units": "degC"}, + ) + + values_by_org_unit[feature["id"]] = series + + return values_by_org_unit + + +def _extract_org_unit_series( + dataset_id: str, + parameter: str, + start: date, + end: date, + org_units: list[dict[str, Any]], + np: Any, + pd: Any, + xr: Any, +) -> dict[str, Any]: + da = _extract_dataarray_with_dhis2eo(dataset_id, parameter, start, end, org_units, xr) + da = _prepare_dataarray_for_parameter(da, parameter, pd) + + x_dim, y_dim = _spatial_dims(da) + values_by_org_unit: dict[str, Any] = {} + + for feature in org_units: + minx, miny, maxx, maxy = _feature_bounds(feature) + clipped = da + if x_dim: + clipped = _slice_dim(clipped, x_dim, minx, maxx) + if y_dim: + clipped = _slice_dim(clipped, y_dim, miny, maxy) + + if x_dim and y_dim: + series = clipped.mean(dim=[y_dim, x_dim], skipna=True) + elif x_dim: + series = clipped.mean(dim=[x_dim], skipna=True) + elif y_dim: + series = clipped.mean(dim=[y_dim], skipna=True) + else: + series = clipped + + if "time" not in series.dims: + time_values = pd.date_range(start.isoformat(), end.isoformat(), freq="D") + expanded = xr.DataArray( + np.repeat(float(series.item()), len(time_values)), + coords={"time": time_values}, + dims=["time"], + attrs=series.attrs, + ) + series = expanded + + values_by_org_unit[feature["id"]] = series + + return values_by_org_unit + + +def _extract_dataarray_with_dhis2eo( + dataset_id: str, + parameter: str, + start: date, + end: date, + org_units: list[dict[str, Any]], + xr: Any, +) -> Any: + bbox = _org_units_bbox(org_units) + cache_dir = _xclim_cache_dir(dataset_id, parameter) + prefix = f"{parameter}_{start.isoformat()}_{end.isoformat()}".replace("-", "") + + if dataset_id == "chirps-daily": + from dhis2eo.data.chc.chirps3 import daily as chirps_daily + + files = chirps_daily.download( + start=start.isoformat(), + end=end.isoformat(), + bbox=bbox, + dirname=str(cache_dir), + prefix=prefix, + var_name="precip", + overwrite=False, + ) + paths = [str(path) for path in files if Path(path).exists()] + if not paths: + raise RuntimeError("No CHIRPS files were downloaded") + + dataset = xr.open_mfdataset(paths, combine="by_coords") + return _dataset_dataarray(dataset, preferred=[parameter, "precip"]) + + if dataset_id == "era5-land-daily": + from dhis2eo.data.cds.era5_land import hourly as era5_hourly + + variable = "total_precipitation" if parameter == "precip" else parameter + files = era5_hourly.download( + start=start.isoformat(), + end=end.isoformat(), + bbox=bbox, + dirname=str(cache_dir), + prefix=prefix, + variables=[variable], + overwrite=False, + ) + paths = [str(path) for path in files if Path(path).exists()] + if not paths: + raise RuntimeError("No ERA5-Land files were downloaded") + + dataset = xr.open_mfdataset(paths, combine="by_coords") + return _dataset_dataarray(dataset, preferred=[parameter, variable, "t2m", "tp"]) + + raise RuntimeError(f"No dhis2eo extractor configured for dataset '{dataset_id}'") + + +def _dataset_dataarray(dataset: Any, preferred: list[str]) -> Any: + for name in preferred: + if name in dataset.data_vars: + da = dataset[name] + da.name = name + return da + + first_name = next(iter(dataset.data_vars.keys()), None) + if first_name is None: + raise RuntimeError("Downloaded dataset has no data variables") + da = dataset[first_name] + da.name = first_name + return da + + +def _prepare_dataarray_for_parameter(da: Any, parameter: str, pd: Any) -> Any: + time_dim = _time_dim(da) + if time_dim != "time": + da = da.rename({time_dim: "time"}) + + da = da.sortby("time") + units = str(da.attrs.get("units", "")).lower() + + if parameter in {"precip", "total_precipitation"}: + da = da.resample(time="1D").sum(skipna=True) + if units == "m": + da = da * 1000.0 + da.attrs["units"] = "mm/day" + elif "mm" in units: + da.attrs["units"] = "mm/day" + else: + da.attrs["units"] = da.attrs.get("units", "mm/day") + else: + da = da.resample(time="1D").mean(skipna=True) + if units in {"k", "kelvin"}: + da = da - 273.15 + da.attrs["units"] = "degC" + elif units in {"c", "degc", "°c"}: + da.attrs["units"] = "degC" + + return da + + +def _org_units_bbox(org_units: list[dict[str, Any]]) -> tuple[float, float, float, float]: + bounds = [_feature_bounds(feature) for feature in org_units] + minx = min(bound[0] for bound in bounds) + miny = min(bound[1] for bound in bounds) + maxx = max(bound[2] for bound in bounds) + maxy = max(bound[3] for bound in bounds) + return (minx, miny, maxx, maxy) + + +def _feature_bounds(feature: dict[str, Any]) -> tuple[float, float, float, float]: + coordinates = feature.get("geometry", {}).get("coordinates", []) + if not coordinates: + return (-180.0, -90.0, 180.0, 90.0) + ring = coordinates[0] + xs = [point[0] for point in ring] + ys = [point[1] for point in ring] + return (min(xs), min(ys), max(xs), max(ys)) + + +def _xclim_cache_dir(dataset_id: str, parameter: str) -> Path: + root = Path(os.getenv("EOAPI_XCLIM_CACHE_DIR", ".cache/xclim")) + path = root / dataset_id / parameter + path.mkdir(parents=True, exist_ok=True) + return path + + +def _time_dim(da: Any) -> str: + for dim in TIME_DIM_CANDIDATES: + if dim in da.dims: + return dim + raise RuntimeError(f"No time dimension found in data array dims={da.dims}") + + +def _spatial_dims(da: Any) -> tuple[str | None, str | None]: + x_dim = next((dim for dim in X_DIM_CANDIDATES if dim in da.dims), None) + y_dim = next((dim for dim in Y_DIM_CANDIDATES if dim in da.dims), None) + return x_dim, y_dim + + +def _slice_dim(da: Any, dim: str, lower: float, upper: float) -> Any: + coords = da[dim].values + if len(coords) == 0: + return da + if coords[0] <= coords[-1]: + return da.sel({dim: slice(lower, upper)}) + return da.sel({dim: slice(upper, lower)}) + + +def _sync_prefect_job(job: dict[str, Any]) -> dict[str, Any]: + execution = job.get("execution") or {} + if execution.get("source") != "prefect": + return job + + flow_run_id = execution.get("flowRunId") + if not flow_run_id or not prefect_enabled(): + return job + + try: + flow_run = get_flow_run(flow_run_id) + except RuntimeError: + return job + + state = flow_run.get("state") or {} + mapped_status = prefect_state_to_job_status(state.get("type")) + progress = 0 + if mapped_status == "running": + progress = 50 + elif mapped_status in {"succeeded", "failed"}: + progress = 100 + + updated = update_job( + job["jobId"], + { + "status": mapped_status, + "progress": progress, + "execution": { + **execution, + "state": state, + }, + }, + ) + return updated or job + + +def run_aggregate_import(inputs: AggregateImportInputs) -> dict[str, Any]: + parameters = _select_parameters(inputs.datasetId, inputs.parameters) + org_units = org_unit_items(level=inputs.orgUnitLevel) + if not org_units: + raise invalid_parameter(f"No org unit features found for level {inputs.orgUnitLevel}") + + dataset = load_datasets()[inputs.datasetId] + effective_time = inputs.datetime or inputs.start or dataset.temporal_interval[0] + + aggregate_start = inputs.start or inputs.datetime or dataset.temporal_interval[0] + aggregate_end = inputs.end or inputs.datetime or aggregate_start + + values_by_org: dict[str, dict[str, float]] = {} + value_source = "dhis2eo" + try: + values_by_org = _extract_aggregate_values_from_dhis2eo( + dataset_id=inputs.datasetId, + parameters=parameters, + start=_coerce_date(aggregate_start), + end=_coerce_date(aggregate_end), + aggregation=inputs.aggregation, + org_units=org_units, + ) + except Exception as exc: + logger.warning("Falling back to synthetic aggregate source after extraction failure: %s", exc) + value_source = "synthetic-fallback" + + result_features: list[dict[str, Any]] = [] + for index, feature in enumerate(org_units): + values = values_by_org.get(feature["id"]) + if values is None: + values = { + parameter: _value_for(index, parameter, inputs.aggregation) + for parameter in parameters + } + + result_features.append( + { + "type": "Feature", + "id": feature["id"], + "geometry": feature["geometry"], + "properties": { + "orgUnit": feature["id"], + "datasetId": inputs.datasetId, + "datetime": effective_time, + "aggregation": inputs.aggregation, + "source": value_source, + "values": values, + }, + } + ) + + period = iso_to_dhis2_period(effective_time) + data_values = _build_aggregate_data_values( + result_features, + default_data_element_id=inputs.dhis2.dataElementId, + data_element_map=inputs.dhis2.dataElementMap, + period=period, + ) + import_summary = import_data_values_to_dhis2(data_values, dry_run=inputs.dhis2.dryRun) + outputs = { + "importSummary": import_summary, + "features": result_features, + } + return create_job(AGGREGATE_PROCESS_ID, inputs.model_dump(), outputs) + + +def run_process(process_id: str, inputs: dict[str, Any]) -> dict[str, Any]: + parsed_inputs = _parse_process_inputs(process_id, ExecuteRequest(inputs=inputs)) + if isinstance(parsed_inputs, AggregateImportInputs): + return run_aggregate_import(parsed_inputs) + return _run_xclim(process_id, parsed_inputs) + + +@router.get("/processes") +def get_processes(request: Request) -> dict[str, Any]: + base = _base_url(request) + process_ids = [ + AGGREGATE_PROCESS_ID, + XCLIM_CDD_PROCESS_ID, + XCLIM_CWD_PROCESS_ID, + XCLIM_WARM_DAYS_PROCESS_ID, + ] + + return { + "processes": [ + { + "id": process_id, + "title": _process_definition(process_id, base)["title"], + "description": _process_definition(process_id, base)["description"], + "links": [ + { + "rel": "process", + "type": FORMAT_TYPES[F_JSON], + "href": url_join(base, "processes", process_id), + } + ], + } + for process_id in process_ids + ], + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "processes")}, + {"rel": "root", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "/")}, + ], + } + + +@router.get("/processes/{processId}") +def get_process(processId: str, request: Request) -> dict[str, Any]: + if processId not in { + AGGREGATE_PROCESS_ID, + XCLIM_CDD_PROCESS_ID, + XCLIM_CWD_PROCESS_ID, + XCLIM_WARM_DAYS_PROCESS_ID, + }: + raise not_found("Process", processId) + + base = _base_url(request) + return _process_definition(processId, base) + + +@router.post("/processes/{processId}/execution", status_code=202) +def execute_process(processId: str, payload: ExecuteRequest, request: Request) -> dict[str, Any]: + job = run_process(processId, payload.inputs) + + base = _base_url(request) + return { + "jobId": job["jobId"], + "processId": processId, + "status": "queued", + "links": [ + {"rel": "monitor", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "jobs", job["jobId"])}, + { + "rel": "results", + "type": FORMAT_TYPES[F_JSON], + "href": f"{url_join(base, 'features', 'aggregated-results', 'items')}?jobId={job['jobId']}", + }, + ], + } + + +@router.get("/jobs/{jobId}") +def get_job_status(jobId: str, request: Request) -> dict[str, Any]: + job = get_job(jobId) + if job is None: + raise not_found("Job", jobId) + + job = _sync_prefect_job(job) + import_summary = (job.get("outputs") or {}).get( + "importSummary", + { + "imported": 0, + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": True, + }, + ) + + base = _base_url(request) + return { + "jobId": job["jobId"], + "processId": job["processId"], + "status": job["status"], + "progress": job["progress"], + "created": job["created"], + "updated": job["updated"], + "importSummary": import_summary, + "execution": job.get("execution"), + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "jobs", jobId)}, + { + "rel": "results", + "type": FORMAT_TYPES[F_JSON], + "href": f"{url_join(base, 'features', 'aggregated-results', 'items')}?jobId={jobId}", + }, + ], + } diff --git a/eoapi/endpoints/root.py b/eoapi/endpoints/root.py new file mode 100644 index 0000000..54c25ee --- /dev/null +++ b/eoapi/endpoints/root.py @@ -0,0 +1,139 @@ +import os +from urllib.parse import urlparse + +from fastapi import APIRouter, Request + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.dhis2_integration import dhis2_configured +from eoapi.state_store import STATE_DIR_ENV, STATE_PERSIST_ENV + +router = APIRouter(tags=["Landing Page"]) + + +def _cors_origins() -> list[str]: + raw = os.getenv("EOAPI_CORS_ORIGINS", "*").strip() + if not raw: + return ["*"] + return [origin.strip() for origin in raw.split(",") if origin.strip()] + + +def _api_key_required() -> bool: + return bool(os.getenv("EOAPI_API_KEY", "").strip()) + + +def _internal_scheduler_enabled() -> bool: + raw = os.getenv("EOAPI_INTERNAL_SCHEDULER_ENABLED", "true").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _state_persistence_enabled() -> bool: + raw = os.getenv(STATE_PERSIST_ENV, "true").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _state_directory() -> str: + return os.getenv(STATE_DIR_ENV, ".cache/state").strip() or ".cache/state" + + +def _dhis2_auth_mode() -> str: + if os.getenv("EOAPI_DHIS2_TOKEN", "").strip(): + return "token" + + if os.getenv("EOAPI_DHIS2_USERNAME", "").strip() and os.getenv("EOAPI_DHIS2_PASSWORD", "").strip(): + return "basic" + + return "none" + + +def _dhis2_host() -> str: + base = os.getenv("EOAPI_DHIS2_BASE_URL", "").strip() + if not base: + return "unset" + + parsed = urlparse(base) + return parsed.netloc or parsed.path or "configured" + + +def _runtime_summary() -> dict: + cors = _cors_origins() + return { + "cors": { + "mode": "wildcard" if cors == ["*"] else "restricted", + "origins": len(cors), + }, + "apiKeyRequired": _api_key_required(), + "dhis2": { + "configured": dhis2_configured(), + "host": _dhis2_host(), + "authMode": _dhis2_auth_mode(), + }, + "state": { + "persistenceEnabled": _state_persistence_enabled(), + "directory": _state_directory(), + }, + "internalScheduler": { + "enabled": _internal_scheduler_enabled(), + }, + } + + +@router.get("/") +def read_index(request: Request) -> dict: + base = str(request.base_url).rstrip("/") + return { + "title": "DHIS2 EO API", + "description": "OGC-aligned Earth Observation API for DHIS2 and CHAP.", + "runtime": _runtime_summary(), + "links": [ + { + "rel": "self", + "type": FORMAT_TYPES[F_JSON], + "title": "This document", + "href": url_join(base, "/"), + }, + { + "rel": "conformance", + "type": FORMAT_TYPES[F_JSON], + "title": "Conformance", + "href": url_join(base, "conformance"), + }, + { + "rel": "data", + "type": FORMAT_TYPES[F_JSON], + "title": "Collections", + "href": url_join(base, "collections"), + }, + { + "rel": "data", + "type": FORMAT_TYPES[F_JSON], + "title": "Feature collections", + "href": url_join(base, "features"), + }, + { + "rel": "processes", + "type": FORMAT_TYPES[F_JSON], + "title": "Processes", + "href": url_join(base, "processes"), + }, + { + "rel": "processes", + "type": FORMAT_TYPES[F_JSON], + "title": "Workflows", + "href": url_join(base, "workflows"), + }, + { + "rel": "service-doc", + "type": "text/html", + "title": "OpenAPI docs", + "href": url_join(base, "docs"), + }, + { + "rel": "service", + "type": "text/html", + "title": "Example frontend app", + "href": url_join(base, "example-app"), + }, + ], + } diff --git a/eoapi/endpoints/schedules.py b/eoapi/endpoints/schedules.py new file mode 100644 index 0000000..a7bf60b --- /dev/null +++ b/eoapi/endpoints/schedules.py @@ -0,0 +1,310 @@ +import os +from secrets import compare_digest +from typing import Any + +from fastapi import APIRouter, Header, HTTPException, Request, Response +from pydantic import BaseModel, Field, model_validator + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.endpoints.errors import not_found +from eoapi.endpoints.processes import AGGREGATE_PROCESS_ID, AggregateImportInputs, run_aggregate_import +from eoapi.endpoints.workflows import run_workflow_by_id +from eoapi.jobs import create_pending_job, update_job +from eoapi.orchestration.prefect import prefect_enabled, submit_aggregate_import_run +from eoapi.schedules import ( + create_schedule, + delete_schedule, + get_schedule, + list_schedules, + mark_schedule_run, + update_schedule, +) + +router = APIRouter(tags=["Schedules"]) + + +class ScheduleCreateRequest(BaseModel): + name: str = Field(min_length=1) + cron: str = Field(min_length=1, description="Cron expression, e.g. 0 0 * * *") + timezone: str = Field(default="UTC", min_length=1) + enabled: bool = True + inputs: AggregateImportInputs | None = None + workflowId: str | None = Field(default=None, min_length=1) + + @model_validator(mode="after") + def validate_target(self) -> "ScheduleCreateRequest": + if self.inputs is None and self.workflowId is None: + raise ValueError("Either 'inputs' or 'workflowId' is required") + if self.inputs is not None and self.workflowId is not None: + raise ValueError("Provide only one of 'inputs' or 'workflowId'") + return self + + +class ScheduleUpdateRequest(BaseModel): + name: str | None = Field(default=None, min_length=1) + cron: str | None = Field(default=None, min_length=1) + timezone: str | None = Field(default=None, min_length=1) + enabled: bool | None = None + inputs: AggregateImportInputs | None = None + workflowId: str | None = Field(default=None, min_length=1) + + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _schedule_response(request: Request, schedule: dict[str, Any]) -> dict[str, Any]: + base = _base_url(request) + schedule_url = url_join(base, "schedules", schedule["scheduleId"]) + links = [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": schedule_url}, + {"rel": "run", "type": FORMAT_TYPES[F_JSON], "href": url_join(schedule_url, "run")}, + {"rel": "run", "type": FORMAT_TYPES[F_JSON], "href": url_join(schedule_url, "callback")}, + ] + + if schedule.get("workflowId"): + links.append( + { + "rel": "process", + "type": FORMAT_TYPES[F_JSON], + "href": url_join(base, "workflows", schedule["workflowId"]), + } + ) + else: + links.append( + { + "rel": "process", + "type": FORMAT_TYPES[F_JSON], + "href": url_join(base, "processes", AGGREGATE_PROCESS_ID), + } + ) + + return { + **schedule, + "links": links, + } + + +def execute_schedule_target(schedule_id: str, trigger: str) -> dict[str, Any]: + schedule = get_schedule(schedule_id) + if schedule is None: + raise not_found("Schedule", schedule_id) + + workflow_id = schedule.get("workflowId") + inputs_payload = schedule.get("inputs") + + execution_source = "local" + flow_run_id: str | None = None + job_ids: list[str] = [] + + if workflow_id: + workflow_result = run_workflow_by_id(workflow_id) + job_ids = workflow_result["jobIds"] + if not job_ids: + raise HTTPException( + status_code=400, + detail={ + "code": "InvalidParameterValue", + "description": "Workflow did not produce any job runs", + }, + ) + selected_job_id = job_ids[-1] + mark_schedule_run(schedule_id, selected_job_id) + return { + "scheduleId": schedule_id, + "workflowId": workflow_id, + "jobId": selected_job_id, + "jobIds": job_ids, + "status": "queued", + "trigger": trigger, + "execution": { + "source": "workflow", + "flowRunId": None, + }, + } + + if inputs_payload is None: + raise HTTPException( + status_code=400, + detail={ + "code": "InvalidParameterValue", + "description": "Schedule target is missing; provide inputs or workflowId", + }, + ) + + inputs = AggregateImportInputs.model_validate(inputs_payload) + + if prefect_enabled(): + pending_job = create_pending_job( + AGGREGATE_PROCESS_ID, + inputs.model_dump(), + source="prefect", + ) + try: + flow_run = submit_aggregate_import_run( + schedule_id=schedule_id, + payload_inputs=inputs.model_dump(mode="json"), + trigger=trigger, + eoapi_job_id=pending_job["jobId"], + ) + flow_run_id = flow_run.get("id") + update_job( + pending_job["jobId"], + { + "execution": { + "source": "prefect", + "flowRunId": flow_run_id, + }, + "status": "queued", + "progress": 0, + }, + ) + job = pending_job + execution_source = "prefect" + except RuntimeError: + # fall back to local execution to keep schedule runs operational in case Prefect is unavailable + job = run_aggregate_import(inputs) + update_job( + pending_job["jobId"], + { + "status": "failed", + "progress": 100, + }, + ) + else: + job = run_aggregate_import(inputs) + + mark_schedule_run(schedule_id, job["jobId"]) + return { + "scheduleId": schedule_id, + "jobId": job["jobId"], + "status": "queued", + "trigger": trigger, + "execution": { + "source": execution_source, + "flowRunId": flow_run_id, + }, + } + + +def _run_schedule_now(schedule_id: str, request: Request, trigger: str) -> dict[str, Any]: + payload = execute_schedule_target(schedule_id, trigger) + selected_job_id = payload["jobId"] + base = _base_url(request) + return { + **payload, + "links": [ + {"rel": "monitor", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "jobs", selected_job_id)}, + { + "rel": "results", + "type": FORMAT_TYPES[F_JSON], + "href": f"{url_join(base, 'features', 'aggregated-results', 'items')}?jobId={selected_job_id}", + }, + ], + } + + +def _assert_scheduler_callback_authorized(token: str | None) -> None: + expected = os.getenv("EOAPI_SCHEDULER_TOKEN") + if not expected: + raise HTTPException( + status_code=503, + detail={ + "code": "ServiceUnavailable", + "description": "Scheduler callback is not configured; set EOAPI_SCHEDULER_TOKEN", + }, + ) + + if token is None or not compare_digest(token, expected): + raise HTTPException( + status_code=403, + detail={ + "code": "Forbidden", + "description": "Invalid scheduler callback token", + }, + ) + + +@router.get("/schedules") +def get_schedules(request: Request) -> dict[str, Any]: + base = _base_url(request) + schedules = [_schedule_response(request, schedule) for schedule in list_schedules()] + return { + "schedules": schedules, + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "schedules")}, + {"rel": "root", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "/")}, + ], + } + + +@router.post("/schedules", status_code=201) +def post_schedule(payload: ScheduleCreateRequest, request: Request) -> dict[str, Any]: + process_id = "workflow" if payload.workflowId else AGGREGATE_PROCESS_ID + schedule = create_schedule( + { + "processId": process_id, + "workflowId": payload.workflowId, + "name": payload.name, + "cron": payload.cron, + "timezone": payload.timezone, + "enabled": payload.enabled, + "inputs": payload.inputs.model_dump() if payload.inputs else None, + } + ) + return _schedule_response(request, schedule) + + +@router.get("/schedules/{scheduleId}") +def get_schedule_by_id(scheduleId: str, request: Request) -> dict[str, Any]: + schedule = get_schedule(scheduleId) + if schedule is None: + raise not_found("Schedule", scheduleId) + return _schedule_response(request, schedule) + + +@router.patch("/schedules/{scheduleId}") +def patch_schedule(scheduleId: str, payload: ScheduleUpdateRequest, request: Request) -> dict[str, Any]: + updates = payload.model_dump(exclude_unset=True) + if "inputs" in updates and updates["inputs"] is not None: + updates["inputs"] = updates["inputs"].model_dump() + + if updates.get("workflowId") is not None: + updates["processId"] = "workflow" + if "inputs" not in updates: + updates["inputs"] = None + elif updates.get("inputs") is not None: + updates["processId"] = AGGREGATE_PROCESS_ID + if "workflowId" not in updates: + updates["workflowId"] = None + + schedule = update_schedule(scheduleId, updates) + if schedule is None: + raise not_found("Schedule", scheduleId) + + return _schedule_response(request, schedule) + + +@router.delete("/schedules/{scheduleId}", status_code=204) +def remove_schedule(scheduleId: str) -> Response: + deleted = delete_schedule(scheduleId) + if not deleted: + raise not_found("Schedule", scheduleId) + return Response(status_code=204) + + +@router.post("/schedules/{scheduleId}/run", status_code=202) +def run_schedule(scheduleId: str, request: Request) -> dict[str, Any]: + return _run_schedule_now(scheduleId, request, trigger="manual") + + +@router.post("/schedules/{scheduleId}/callback", status_code=202) +def callback_schedule( + scheduleId: str, + request: Request, + x_scheduler_token: str | None = Header(default=None, alias="X-Scheduler-Token"), +) -> dict[str, Any]: + _assert_scheduler_callback_authorized(x_scheduler_token) + return _run_schedule_now(scheduleId, request, trigger="scheduler-callback") diff --git a/eoapi/endpoints/workflows.py b/eoapi/endpoints/workflows.py new file mode 100644 index 0000000..472a1fa --- /dev/null +++ b/eoapi/endpoints/workflows.py @@ -0,0 +1,161 @@ +from typing import Any + +from fastapi import APIRouter, Request +from pydantic import BaseModel, Field + +from pygeoapi.api import FORMAT_TYPES, F_JSON +from pygeoapi.util import url_join + +from eoapi.endpoints.errors import invalid_parameter, not_found +from eoapi.endpoints.processes import run_process +from eoapi.workflows import ( + create_workflow, + delete_workflow, + get_workflow, + list_workflows, + mark_workflow_run, + update_workflow, +) + +router = APIRouter(tags=["Workflows"]) + + +class WorkflowStep(BaseModel): + name: str | None = Field(default=None, min_length=1) + processId: str = Field(min_length=1) + payload: dict[str, Any] + + +class WorkflowCreateRequest(BaseModel): + name: str = Field(min_length=1) + steps: list[WorkflowStep] = Field(min_length=1) + + +class WorkflowUpdateRequest(BaseModel): + name: str | None = Field(default=None, min_length=1) + steps: list[WorkflowStep] | None = None + + +def _base_url(request: Request) -> str: + return str(request.base_url).rstrip("/") + + +def _workflow_response(request: Request, workflow: dict[str, Any]) -> dict[str, Any]: + base = _base_url(request) + workflow_url = url_join(base, "workflows", workflow["workflowId"]) + return { + **workflow, + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": workflow_url}, + {"rel": "run", "type": FORMAT_TYPES[F_JSON], "href": url_join(workflow_url, "run")}, + ], + } + + +def run_workflow_by_id(workflow_id: str) -> dict[str, Any]: + workflow = get_workflow(workflow_id) + if workflow is None: + raise not_found("Workflow", workflow_id) + + steps = workflow.get("steps") or [] + if not steps: + raise invalid_parameter("Workflow has no steps") + + job_ids: list[str] = [] + step_results: list[dict[str, Any]] = [] + for index, step in enumerate(steps): + process_id = step.get("processId") + payload = step.get("payload") or {} + if process_id is None: + raise invalid_parameter(f"Workflow step {index + 1} is missing processId") + + inputs = payload.get("inputs") + if not isinstance(inputs, dict): + raise invalid_parameter(f"Workflow step {index + 1} payload must include object field 'inputs'") + + job = run_process(process_id, inputs) + job_ids.append(job["jobId"]) + step_results.append( + { + "step": step.get("name") or f"step-{index + 1}", + "processId": process_id, + "jobId": job["jobId"], + "status": job["status"], + } + ) + + mark_workflow_run(workflow_id, job_ids) + return { + "workflowId": workflow_id, + "status": "queued", + "jobIds": job_ids, + "steps": step_results, + } + + +@router.get("/workflows") +def get_workflows(request: Request) -> dict[str, Any]: + base = _base_url(request) + workflows = [_workflow_response(request, workflow) for workflow in list_workflows()] + return { + "workflows": workflows, + "links": [ + {"rel": "self", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "workflows")}, + {"rel": "root", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "/")}, + ], + } + + +@router.post("/workflows", status_code=201) +def post_workflow(payload: WorkflowCreateRequest, request: Request) -> dict[str, Any]: + workflow = create_workflow( + { + "name": payload.name, + "steps": [step.model_dump() for step in payload.steps], + } + ) + return _workflow_response(request, workflow) + + +@router.get("/workflows/{workflowId}") +def get_workflow_by_id(workflowId: str, request: Request) -> dict[str, Any]: + workflow = get_workflow(workflowId) + if workflow is None: + raise not_found("Workflow", workflowId) + return _workflow_response(request, workflow) + + +@router.patch("/workflows/{workflowId}") +def patch_workflow(workflowId: str, payload: WorkflowUpdateRequest, request: Request) -> dict[str, Any]: + updates = payload.model_dump(exclude_unset=True) + if "steps" in updates and updates["steps"] is not None: + updates["steps"] = [WorkflowStep.model_validate(step).model_dump() for step in updates["steps"]] + + workflow = update_workflow(workflowId, updates) + if workflow is None: + raise not_found("Workflow", workflowId) + + return _workflow_response(request, workflow) + + +@router.delete("/workflows/{workflowId}", status_code=204) +def remove_workflow(workflowId: str) -> None: + deleted = delete_workflow(workflowId) + if not deleted: + raise not_found("Workflow", workflowId) + + +@router.post("/workflows/{workflowId}/run", status_code=202) +def run_workflow(workflowId: str, request: Request) -> dict[str, Any]: + result = run_workflow_by_id(workflowId) + base = _base_url(request) + last_job_id = result["jobIds"][-1] if result["jobIds"] else None + + links = [] + if last_job_id: + links.append({"rel": "monitor", "type": FORMAT_TYPES[F_JSON], "href": url_join(base, "jobs", last_job_id)}) + + return { + **result, + "links": links, + } diff --git a/eoapi/external_ogc.py b/eoapi/external_ogc.py new file mode 100644 index 0000000..fd2c5f1 --- /dev/null +++ b/eoapi/external_ogc.py @@ -0,0 +1,264 @@ +import json +import os +from dataclasses import dataclass +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.parse import quote, urlencode +from urllib.request import Request, urlopen + + +FEDERATED_PREFIX = "ext" +SUPPORTED_PROXY_OPERATIONS = {"coverage", "position", "area"} + + +@dataclass(frozen=True) +class ExternalOGCProvider: + id: str + url: str + title: str | None = None + headers: dict[str, str] | None = None + api_key_env: str | None = None + auth_scheme: str = "Bearer" + timeout_seconds: float = 20.0 + retries: int = 0 + operations: tuple[str, ...] | None = None + + +def federated_collection_id(provider_id: str, collection_id: str) -> str: + return f"{FEDERATED_PREFIX}:{provider_id}:{collection_id}" + + +def parse_federated_collection_id(collection_id: str) -> tuple[str, str] | None: + parts = collection_id.split(":", 2) + if len(parts) != 3 or parts[0] != FEDERATED_PREFIX: + return None + return (parts[1], parts[2]) + + +def _provider_headers(provider: ExternalOGCProvider | None) -> dict[str, str]: + headers = {"Accept": "application/json"} + if provider is None: + return headers + + for key, value in (provider.headers or {}).items(): + if key and value: + headers[str(key)] = str(value) + + if provider.api_key_env: + api_key = os.getenv(provider.api_key_env, "").strip() + if api_key: + scheme = provider.auth_scheme.strip() if provider.auth_scheme else "Bearer" + if scheme.lower() == "none": + headers["Authorization"] = api_key + else: + headers["Authorization"] = f"{scheme} {api_key}" + + return headers + + +def _fetch_json(url: str, provider: ExternalOGCProvider | None = None) -> dict[str, Any]: + request = Request(url=url, method="GET") + for key, value in _provider_headers(provider).items(): + request.add_header(key, value) + + timeout = provider.timeout_seconds if provider is not None else 20.0 + retries = provider.retries if provider is not None else 0 + attempts = max(1, retries + 1) + + for attempt in range(attempts): + try: + with urlopen(request, timeout=timeout) as response: + return json.loads(response.read().decode("utf-8")) + except (HTTPError, URLError, json.JSONDecodeError): + if attempt + 1 >= attempts: + return {} + + return {} + + +def load_external_providers() -> list[ExternalOGCProvider]: + raw = os.getenv("EOAPI_EXTERNAL_OGC_SERVICES", "").strip() + if not raw: + return [] + + try: + payload = json.loads(raw) + except json.JSONDecodeError: + return [] + + if not isinstance(payload, list): + return [] + + providers: list[ExternalOGCProvider] = [] + for item in payload: + if not isinstance(item, dict): + continue + provider_id = str(item.get("id", "")).strip() + url = str(item.get("url", "")).strip().rstrip("/") + if not provider_id or not url: + continue + title = str(item.get("title", "")).strip() or None + raw_headers = item.get("headers") + headers = None + if isinstance(raw_headers, dict): + headers = {str(key): str(value) for key, value in raw_headers.items() if key and value is not None} + + api_key_env = str(item.get("apiKeyEnv", "")).strip() or None + auth_scheme = str(item.get("authScheme", "Bearer")).strip() or "Bearer" + + timeout_seconds = item.get("timeoutSeconds", 20) + try: + timeout_seconds = float(timeout_seconds) + except (TypeError, ValueError): + timeout_seconds = 20.0 + if timeout_seconds <= 0: + timeout_seconds = 20.0 + + retries = item.get("retries", 0) + try: + retries = int(retries) + except (TypeError, ValueError): + retries = 0 + if retries < 0: + retries = 0 + + operations = None + raw_operations = item.get("operations") + if isinstance(raw_operations, list): + normalized = sorted({str(value).strip().lower() for value in raw_operations if str(value).strip()}) + if any(value not in SUPPORTED_PROXY_OPERATIONS for value in normalized): + continue + operations = tuple(normalized) + + providers.append( + ExternalOGCProvider( + id=provider_id, + url=url, + title=title, + headers=headers, + api_key_env=api_key_env, + auth_scheme=auth_scheme, + timeout_seconds=timeout_seconds, + retries=retries, + operations=operations, + ) + ) + + return providers + + +def get_external_provider(provider_id: str) -> ExternalOGCProvider | None: + return next((item for item in load_external_providers() if item.id == provider_id), None) + + +def get_external_provider_for_collection_id(collection_id: str) -> tuple[ExternalOGCProvider, str] | None: + parsed = parse_federated_collection_id(collection_id) + if parsed is None: + return None + + provider_id, source_collection_id = parsed + provider = get_external_provider(provider_id) + if provider is None: + return None + + return (provider, source_collection_id) + + +def is_external_operation_enabled(collection_id: str, operation: str) -> bool | None: + resolved = get_external_provider_for_collection_id(collection_id) + if resolved is None: + return None + + provider, _ = resolved + if provider.operations is None: + return True + + return operation in provider.operations + + +def list_external_collections() -> list[dict[str, Any]]: + items: list[dict[str, Any]] = [] + for provider in load_external_providers(): + payload = _fetch_json(f"{provider.url}/collections", provider=provider) + collections = payload.get("collections", []) if isinstance(payload, dict) else [] + if not isinstance(collections, list): + continue + + for collection in collections: + if not isinstance(collection, dict): + continue + raw_id = str(collection.get("id", "")).strip() + if not raw_id: + continue + items.append( + { + **collection, + "id": federated_collection_id(provider.id, raw_id), + "federation": { + "providerId": provider.id, + "providerTitle": provider.title, + "providerUrl": provider.url, + "sourceCollectionId": raw_id, + }, + } + ) + + return items + + +def get_external_collection(collection_id: str) -> dict[str, Any] | None: + resolved = get_external_provider_for_collection_id(collection_id) + if resolved is None: + return None + + provider, source_collection_id = resolved + + encoded_id = quote(source_collection_id, safe="") + payload = _fetch_json(f"{provider.url}/collections/{encoded_id}", provider=provider) + if not isinstance(payload, dict) or not payload.get("id"): + return None + + return { + **payload, + "id": federated_collection_id(provider.id, source_collection_id), + "federation": { + "providerId": provider.id, + "providerTitle": provider.title, + "providerUrl": provider.url, + "sourceCollectionId": source_collection_id, + }, + } + + +def proxy_external_collection_request( + collection_id: str, + operation: str, + query_params: list[tuple[str, str]], +) -> dict[str, Any] | None: + resolved = get_external_provider_for_collection_id(collection_id) + if resolved is None: + return None + + provider, source_collection_id = resolved + + encoded_id = quote(source_collection_id, safe="") + filtered_query = [(key, value) for key, value in query_params if key] + query_string = urlencode(filtered_query, doseq=True) + url = f"{provider.url}/collections/{encoded_id}/{operation}" + if query_string: + url = f"{url}?{query_string}" + + payload = _fetch_json(url, provider=provider) + if not isinstance(payload, dict) or not payload: + return None + + return { + **payload, + "federation": { + "providerId": provider.id, + "providerTitle": provider.title, + "providerUrl": provider.url, + "sourceCollectionId": source_collection_id, + "operation": operation, + }, + } diff --git a/eoapi/jobs.py b/eoapi/jobs.py new file mode 100644 index 0000000..cd6a36c --- /dev/null +++ b/eoapi/jobs.py @@ -0,0 +1,95 @@ +from datetime import UTC, datetime +from threading import Lock +from typing import Any +from uuid import uuid4 + +from eoapi.state_store import load_state_map, save_state_map + + +_JOBS: dict[str, dict[str, Any]] = load_state_map("jobs") +_LOCK = Lock() + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +def create_job(process_id: str, inputs: dict[str, Any], outputs: dict[str, Any]) -> dict[str, Any]: + job_id = str(uuid4()) + timestamp = _now_iso() + job = { + "jobId": job_id, + "processId": process_id, + "status": "succeeded", + "progress": 100, + "created": timestamp, + "updated": timestamp, + "inputs": inputs, + "outputs": outputs, + } + + with _LOCK: + _JOBS[job_id] = job + save_state_map("jobs", _JOBS) + + return job + + +def create_pending_job( + process_id: str, + inputs: dict[str, Any], + *, + source: str, + flow_run_id: str | None = None, +) -> dict[str, Any]: + job_id = str(uuid4()) + timestamp = _now_iso() + job = { + "jobId": job_id, + "processId": process_id, + "status": "queued", + "progress": 0, + "created": timestamp, + "updated": timestamp, + "inputs": inputs, + "outputs": { + "importSummary": { + "imported": 0, + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": bool(inputs.get("dhis2", {}).get("dryRun", True)), + }, + "features": [], + }, + "execution": { + "source": source, + "flowRunId": flow_run_id, + }, + } + + with _LOCK: + _JOBS[job_id] = job + save_state_map("jobs", _JOBS) + + return job + + +def update_job(job_id: str, updates: dict[str, Any]) -> dict[str, Any] | None: + with _LOCK: + job = _JOBS.get(job_id) + if job is None: + return None + + for key, value in updates.items(): + if value is not None: + job[key] = value + + job["updated"] = _now_iso() + save_state_map("jobs", _JOBS) + return job + + +def get_job(job_id: str) -> dict[str, Any] | None: + with _LOCK: + return _JOBS.get(job_id) diff --git a/eoapi/orchestration/prefect.py b/eoapi/orchestration/prefect.py new file mode 100644 index 0000000..eac273e --- /dev/null +++ b/eoapi/orchestration/prefect.py @@ -0,0 +1,90 @@ +import os +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen +import json + + +def prefect_enabled() -> bool: + return os.getenv("EOAPI_PREFECT_ENABLED", "false").lower() in {"1", "true", "yes", "on"} + + +def _prefect_api_url() -> str: + value = os.getenv("EOAPI_PREFECT_API_URL", "").strip() + if not value: + raise RuntimeError("EOAPI_PREFECT_API_URL is required when EOAPI_PREFECT_ENABLED=true") + return value.rstrip("/") + + +def _prefect_headers() -> dict[str, str]: + headers = { + "Content-Type": "application/json", + } + api_key = os.getenv("EOAPI_PREFECT_API_KEY", "").strip() + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + return headers + + +def _json_request(method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: + url = f"{_prefect_api_url()}{path}" + data = None + if payload is not None: + data = json.dumps(payload).encode("utf-8") + + request = Request(url=url, data=data, method=method) + for key, value in _prefect_headers().items(): + request.add_header(key, value) + + try: + with urlopen(request, timeout=20) as response: + body = response.read().decode("utf-8") + return json.loads(body) if body else {} + except HTTPError as exc: + detail = exc.read().decode("utf-8") if exc.fp else str(exc) + raise RuntimeError(f"Prefect API error ({exc.code}): {detail}") from exc + except URLError as exc: + raise RuntimeError(f"Unable to reach Prefect API: {exc.reason}") from exc + + +def submit_aggregate_import_run( + schedule_id: str, + payload_inputs: dict[str, Any], + trigger: str, + eoapi_job_id: str, +) -> dict[str, Any]: + deployment_id = os.getenv("EOAPI_PREFECT_DEPLOYMENT_ID", "").strip() + if not deployment_id: + raise RuntimeError("EOAPI_PREFECT_DEPLOYMENT_ID is required when EOAPI_PREFECT_ENABLED=true") + + flow_run_payload = { + "name": f"eoapi-{schedule_id}-{eoapi_job_id}", + "parameters": { + "jobId": eoapi_job_id, + "scheduleId": schedule_id, + "trigger": trigger, + "inputs": payload_inputs, + }, + "tags": ["eoapi", "schedule", trigger], + } + return _json_request("POST", f"/api/deployments/{deployment_id}/create_flow_run", flow_run_payload) + + +def get_flow_run(flow_run_id: str) -> dict[str, Any]: + return _json_request("GET", f"/api/flow_runs/{flow_run_id}") + + +def prefect_state_to_job_status(state_type: str | None) -> str: + if not state_type: + return "queued" + + normalized = state_type.upper() + if normalized in {"PENDING", "SCHEDULED", "LATE", "PAUSED"}: + return "queued" + if normalized in {"RUNNING", "CANCELLING"}: + return "running" + if normalized in {"COMPLETED"}: + return "succeeded" + if normalized in {"FAILED", "CRASHED", "CANCELLED"}: + return "failed" + return "queued" diff --git a/eoapi/scheduler_runtime.py b/eoapi/scheduler_runtime.py new file mode 100644 index 0000000..bb3ab24 --- /dev/null +++ b/eoapi/scheduler_runtime.py @@ -0,0 +1,112 @@ +import logging +import os +from datetime import UTC, datetime +from threading import Event, Thread + +from eoapi.endpoints.schedules import execute_schedule_target +from eoapi.schedules import list_schedules + +logger = logging.getLogger(__name__) + +_THREAD: Thread | None = None +_STOP_EVENT = Event() + + +def _enabled() -> bool: + raw = os.getenv("EOAPI_INTERNAL_SCHEDULER_ENABLED", "true").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _poll_seconds() -> float: + raw = os.getenv("EOAPI_INTERNAL_SCHEDULER_POLL_SECONDS", "30").strip() + try: + value = float(raw) + except ValueError: + value = 30.0 + return value if value > 0 else 30.0 + + +def _parse_iso(value: str | None) -> datetime | None: + if not value: + return None + try: + normalized = value.replace("Z", "+00:00") + parsed = datetime.fromisoformat(normalized) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=UTC) + return parsed.astimezone(UTC) + except ValueError: + return None + + +def _schedule_due(schedule: dict, now_utc: datetime) -> bool: + if not schedule.get("enabled", True): + return False + + cron = str(schedule.get("cron", "")).strip() + if not cron: + return False + + try: + croniter_module = __import__("croniter", fromlist=["croniter"]) + croniter = getattr(croniter_module, "croniter") + from zoneinfo import ZoneInfo + except ImportError: + return False + + timezone_name = str(schedule.get("timezone", "UTC") or "UTC") + try: + timezone = ZoneInfo(timezone_name) + except Exception: + timezone = ZoneInfo("UTC") + + anchor = _parse_iso(schedule.get("lastRunAt")) or _parse_iso(schedule.get("created")) + if anchor is None: + anchor = now_utc + + anchor_tz = anchor.astimezone(timezone) + now_tz = now_utc.astimezone(timezone) + + try: + next_run = croniter(cron, anchor_tz).get_next(datetime) + except Exception: + return False + + return next_run <= now_tz + + +def poll_scheduler_once(now_utc: datetime | None = None) -> None: + if not _enabled(): + return + + current = now_utc or datetime.now(UTC) + for schedule in list_schedules(): + if not _schedule_due(schedule, current): + continue + schedule_id = schedule.get("scheduleId") + if not schedule_id: + continue + try: + execute_schedule_target(str(schedule_id), trigger="internal-cron") + except Exception as exc: + logger.warning("Internal scheduler failed for schedule %s: %s", schedule_id, exc) + + +def _worker() -> None: + while not _STOP_EVENT.is_set(): + poll_scheduler_once() + _STOP_EVENT.wait(timeout=_poll_seconds()) + + +def start_internal_scheduler() -> None: + global _THREAD + if _THREAD is not None and _THREAD.is_alive(): + return + + _STOP_EVENT.clear() + _THREAD = Thread(target=_worker, daemon=True, name="eoapi-internal-scheduler") + _THREAD.start() + + +def stop_internal_scheduler() -> None: + _STOP_EVENT.set() diff --git a/eoapi/schedules.py b/eoapi/schedules.py new file mode 100644 index 0000000..281d423 --- /dev/null +++ b/eoapi/schedules.py @@ -0,0 +1,87 @@ +from datetime import UTC, datetime +from threading import Lock +from typing import Any +from uuid import uuid4 + +from eoapi.state_store import load_state_map, save_state_map + + +_SCHEDULES: dict[str, dict[str, Any]] = load_state_map("schedules") +_LOCK = Lock() + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +def create_schedule(payload: dict[str, Any]) -> dict[str, Any]: + schedule_id = str(uuid4()) + timestamp = _now_iso() + schedule = { + "scheduleId": schedule_id, + "processId": payload["processId"], + "workflowId": payload.get("workflowId"), + "name": payload["name"], + "cron": payload["cron"], + "timezone": payload["timezone"], + "enabled": payload["enabled"], + "inputs": payload["inputs"], + "created": timestamp, + "updated": timestamp, + "lastRunAt": None, + "lastRunJobId": None, + } + + with _LOCK: + _SCHEDULES[schedule_id] = schedule + save_state_map("schedules", _SCHEDULES) + + return schedule + + +def list_schedules() -> list[dict[str, Any]]: + with _LOCK: + return list(_SCHEDULES.values()) + + +def get_schedule(schedule_id: str) -> dict[str, Any] | None: + with _LOCK: + return _SCHEDULES.get(schedule_id) + + +def update_schedule(schedule_id: str, updates: dict[str, Any]) -> dict[str, Any] | None: + with _LOCK: + schedule = _SCHEDULES.get(schedule_id) + if schedule is None: + return None + + for key, value in updates.items(): + if value is not None: + schedule[key] = value + + schedule["updated"] = _now_iso() + save_state_map("schedules", _SCHEDULES) + return schedule + + +def delete_schedule(schedule_id: str) -> bool: + with _LOCK: + if schedule_id not in _SCHEDULES: + return False + del _SCHEDULES[schedule_id] + save_state_map("schedules", _SCHEDULES) + return True + + +def mark_schedule_run(schedule_id: str, job_id: str) -> dict[str, Any] | None: + with _LOCK: + schedule = _SCHEDULES.get(schedule_id) + if schedule is None: + return None + + timestamp = _now_iso() + schedule["lastRunAt"] = timestamp + schedule["lastRunJobId"] = job_id + schedule["updated"] = timestamp + save_state_map("schedules", _SCHEDULES) + return schedule diff --git a/eoapi/state_store.py b/eoapi/state_store.py new file mode 100644 index 0000000..2c42aa5 --- /dev/null +++ b/eoapi/state_store.py @@ -0,0 +1,62 @@ +import json +import os +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Any + + +STATE_DIR_ENV = "EOAPI_STATE_DIR" +STATE_PERSIST_ENV = "EOAPI_STATE_PERSIST" + + +def _state_enabled() -> bool: + raw = os.getenv(STATE_PERSIST_ENV, "true").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _state_dir() -> Path: + return Path(os.getenv(STATE_DIR_ENV, ".cache/state")) + + +def _state_file(name: str) -> Path: + return _state_dir() / f"{name}.json" + + +def load_state_map(name: str) -> dict[str, dict[str, Any]]: + if not _state_enabled(): + return {} + + path = _state_file(name) + if not path.exists(): + return {} + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {} + + if not isinstance(payload, dict): + return {} + + return {str(key): value for key, value in payload.items() if isinstance(value, dict)} + + +def save_state_map(name: str, payload: dict[str, dict[str, Any]]) -> None: + if not _state_enabled(): + return + + state_dir = _state_dir() + try: + state_dir.mkdir(parents=True, exist_ok=True) + except OSError: + return + + path = _state_file(name) + + try: + with NamedTemporaryFile("w", delete=False, dir=state_dir, encoding="utf-8") as tmp: + json.dump(payload, tmp, ensure_ascii=False) + tmp_path = Path(tmp.name) + tmp_path.replace(path) + except OSError: + return diff --git a/eoapi/workflows.py b/eoapi/workflows.py new file mode 100644 index 0000000..baf2ea8 --- /dev/null +++ b/eoapi/workflows.py @@ -0,0 +1,82 @@ +from datetime import UTC, datetime +from threading import Lock +from typing import Any +from uuid import uuid4 + +from eoapi.state_store import load_state_map, save_state_map + + +_WORKFLOWS: dict[str, dict[str, Any]] = load_state_map("workflows") +_LOCK = Lock() + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") + + +def create_workflow(payload: dict[str, Any]) -> dict[str, Any]: + workflow_id = str(uuid4()) + timestamp = _now_iso() + workflow = { + "workflowId": workflow_id, + "name": payload["name"], + "steps": payload["steps"], + "created": timestamp, + "updated": timestamp, + "lastRunAt": None, + "lastRunJobIds": [], + } + + with _LOCK: + _WORKFLOWS[workflow_id] = workflow + save_state_map("workflows", _WORKFLOWS) + + return workflow + + +def list_workflows() -> list[dict[str, Any]]: + with _LOCK: + return list(_WORKFLOWS.values()) + + +def get_workflow(workflow_id: str) -> dict[str, Any] | None: + with _LOCK: + return _WORKFLOWS.get(workflow_id) + + +def update_workflow(workflow_id: str, updates: dict[str, Any]) -> dict[str, Any] | None: + with _LOCK: + workflow = _WORKFLOWS.get(workflow_id) + if workflow is None: + return None + + for key, value in updates.items(): + if value is not None: + workflow[key] = value + + workflow["updated"] = _now_iso() + save_state_map("workflows", _WORKFLOWS) + return workflow + + +def delete_workflow(workflow_id: str) -> bool: + with _LOCK: + if workflow_id not in _WORKFLOWS: + return False + del _WORKFLOWS[workflow_id] + save_state_map("workflows", _WORKFLOWS) + return True + + +def mark_workflow_run(workflow_id: str, job_ids: list[str]) -> dict[str, Any] | None: + with _LOCK: + workflow = _WORKFLOWS.get(workflow_id) + if workflow is None: + return None + + timestamp = _now_iso() + workflow["lastRunAt"] = timestamp + workflow["lastRunJobIds"] = job_ids + workflow["updated"] = timestamp + save_state_map("workflows", _WORKFLOWS) + return workflow diff --git a/main.py b/main.py index e82d5a0..ccd327a 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,107 @@ +import os +import logging +from urllib.parse import urlparse from fastapi import FastAPI -from titiler.core.factory import (TilerFactory, MultiBaseTilerFactory) +from fastapi import Request +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles +from pathlib import Path +from titiler.core.factory import (TilerFactory) from rio_tiler.io import STACReader +from eoapi.endpoints.collections import router as collections_router +from eoapi.endpoints.conformance import router as conformance_router +from eoapi.endpoints.features import router as features_router +from eoapi.endpoints.processes import router as processes_router +from eoapi.endpoints.root import router as root_router +from eoapi.endpoints.schedules import router as schedules_router +from eoapi.endpoints.workflows import router as workflows_router +from eoapi.dhis2_integration import dhis2_configured +from eoapi.scheduler_runtime import start_internal_scheduler, stop_internal_scheduler +from eoapi.state_store import STATE_DIR_ENV, STATE_PERSIST_ENV from starlette.middleware.cors import CORSMiddleware app = FastAPI() +logger = logging.getLogger(__name__) + + +def _cors_origins() -> list[str]: + raw = os.getenv("EOAPI_CORS_ORIGINS", "*").strip() + if not raw: + return ["*"] + return [origin.strip() for origin in raw.split(",") if origin.strip()] + + +def _api_key_required() -> str | None: + token = os.getenv("EOAPI_API_KEY", "").strip() + return token or None + + +def _internal_scheduler_enabled() -> bool: + raw = os.getenv("EOAPI_INTERNAL_SCHEDULER_ENABLED", "true").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _internal_scheduler_poll_seconds() -> float: + raw = os.getenv("EOAPI_INTERNAL_SCHEDULER_POLL_SECONDS", "30").strip() + try: + value = float(raw) + except ValueError: + value = 30.0 + return value if value > 0 else 30.0 + + +def _state_persistence_enabled() -> bool: + raw = os.getenv(STATE_PERSIST_ENV, "true").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _state_directory() -> str: + return os.getenv(STATE_DIR_ENV, ".cache/state").strip() or ".cache/state" + + +def _dhis2_auth_mode() -> str: + if os.getenv("EOAPI_DHIS2_TOKEN", "").strip(): + return "token" + + if os.getenv("EOAPI_DHIS2_USERNAME", "").strip() and os.getenv("EOAPI_DHIS2_PASSWORD", "").strip(): + return "basic" + + return "none" + + +def _dhis2_host() -> str: + base = os.getenv("EOAPI_DHIS2_BASE_URL", "").strip() + if not base: + return "unset" + + parsed = urlparse(base) + return parsed.netloc or parsed.path or "configured" + + +def _log_startup_configuration() -> None: + cors = _cors_origins() + logger.info( + "Startup config: cors=%s apiKeyRequired=%s", + "*" if cors == ["*"] else f"{len(cors)} origins", + _api_key_required() is not None, + ) + logger.info( + "Startup config: dhis2 configured=%s host=%s authMode=%s", + dhis2_configured(), + _dhis2_host(), + _dhis2_auth_mode(), + ) + logger.info( + "Startup config: statePersistence=%s stateDir=%s", + _state_persistence_enabled(), + _state_directory(), + ) + logger.info( + "Startup config: internalScheduler=%s pollSeconds=%s", + _internal_scheduler_enabled(), + _internal_scheduler_poll_seconds(), + ) # Bsed on: # https://developmentseed.org/titiler/user_guide/getting_started/#4-create-your-titiler-application @@ -13,34 +110,58 @@ # Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], # Allows all origins (for development - be more specific in production) + allow_origins=_cors_origins(), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + +@app.middleware("http") +async def _optional_api_key_guard(request: Request, call_next): + expected = _api_key_required() + if expected is None: + return await call_next(request) + + if request.method in {"POST", "PATCH", "DELETE", "PUT"}: + provided = request.headers.get("X-API-Key", "") + if provided != expected: + return JSONResponse( + status_code=403, + content={ + "detail": { + "code": "Forbidden", + "description": "Invalid or missing API key", + } + }, + ) + + return await call_next(request) + # Create a TilerFactory for Cloud-Optimized GeoTIFFs cog = TilerFactory() +app.include_router(root_router) +app.include_router(conformance_router) +app.include_router(collections_router) +app.include_router(features_router) +app.include_router(processes_router) +app.include_router(workflows_router) +app.include_router(schedules_router) + # Register all the COG endpoints automatically app.include_router(cog.router, prefix="/cog", tags=["Cloud Optimized GeoTIFF"]) -stac = MultiBaseTilerFactory( - reader=STACReader, - router_prefix="/stac", - add_ogc_maps=True, - # extensions=[stacViewerExtension(), stacRenderExtension(), wmtsExtension()], - # enable_telemetry=api_settings.telemetry_enabled, - # templates=titiler_templates, -) +webapp_dir = Path(__file__).resolve().parent / "webapp" +app.mount("/example-app", StaticFiles(directory=str(webapp_dir), html=True), name="example-app") + + +@app.on_event("startup") +def _on_startup() -> None: + _log_startup_configuration() + start_internal_scheduler() -app.include_router( - stac.router, - prefix="/stac", - tags=["SpatioTemporal Asset Catalog"], -) -# Optional: Add a welcome message for the root endpoint -@app.get("/") -def read_index(): - return {"message": "Welcome to DHIS2 EO API"} \ No newline at end of file +@app.on_event("shutdown") +def _on_shutdown() -> None: + stop_internal_scheduler() diff --git a/pyproject.toml b/pyproject.toml index a2f47d4..0ccbfa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + [project] name = "eo-api" version = "0.1.0" @@ -5,6 +9,13 @@ requires-python = ">=3.13" dependencies = [ "titiler-core>=1.2.0", "uvicorn>=0.41.0", + "pygeoapi>=0.22.0", + "croniter>=2.0.5", + "pyyaml>=6.0", + "xclim>=0.58.1", "dhis2eo @ git+https://github.com/dhis2/dhis2eo.git@v1.1.0", "dhis2-client @ git+https://github.com/dhis2/dhis2-python-client.git@V0.3.0", -] \ No newline at end of file +] + +[tool.setuptools.packages.find] +include = ["eoapi*"] \ No newline at end of file diff --git a/scripts/validate_datasets.py b/scripts/validate_datasets.py new file mode 100644 index 0000000..59c1b65 --- /dev/null +++ b/scripts/validate_datasets.py @@ -0,0 +1,24 @@ +from pathlib import Path +import sys + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from eoapi.datasets import load_datasets + + +def main() -> int: + datasets = load_datasets() + if not datasets: + print("No dataset YAML files found.") + return 1 + + print(f"Validated {len(datasets)} dataset definition(s):") + for dataset_id in sorted(datasets.keys()): + print(f"- {dataset_id}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_collections_federation.py b/tests/test_collections_federation.py new file mode 100644 index 0000000..e92f4db --- /dev/null +++ b/tests/test_collections_federation.py @@ -0,0 +1,75 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.collections import router as collections_router + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(collections_router) + return TestClient(app) + + +def test_collections_includes_external_federated_entries(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.collections.list_external_collections", + lambda: [ + { + "id": "ext:demo-provider:rainfall-collection", + "title": "External Rainfall", + "description": "Remote OGC collection", + "keywords": ["external"], + "extent": { + "spatial": {"bbox": [[-10.0, -10.0, 10.0, 10.0]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "itemType": "coverage", + "federation": { + "providerId": "demo-provider", + "providerUrl": "https://example-ogc.test", + "sourceCollectionId": "rainfall-collection", + }, + } + ], + ) + + response = client.get("/collections") + + assert response.status_code == 200 + payload = response.json() + ids = {collection["id"] for collection in payload["collections"]} + assert "chirps-daily" in ids + assert "ext:demo-provider:rainfall-collection" in ids + + +def test_get_external_collection_details(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.collections.get_external_collection", + lambda collection_id: { + "id": collection_id, + "title": "External Rainfall", + "description": "Remote OGC collection", + "extent": { + "spatial": {"bbox": [[-10.0, -10.0, 10.0, 10.0]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "itemType": "coverage", + "federation": { + "providerId": "demo-provider", + "providerUrl": "https://example-ogc.test", + "sourceCollectionId": "rainfall-collection", + }, + }, + ) + + response = client.get("/collections/ext:demo-provider:rainfall-collection") + + assert response.status_code == 200 + payload = response.json() + assert payload["id"] == "ext:demo-provider:rainfall-collection" + rels = {link["rel"] for link in payload["links"]} + assert "source" in rels diff --git a/tests/test_conformance_endpoint.py b/tests/test_conformance_endpoint.py new file mode 100644 index 0000000..c11f52a --- /dev/null +++ b/tests/test_conformance_endpoint.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.conformance import CONFORMANCE_CLASSES +from eoapi.endpoints.conformance import router as conformance_router + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(conformance_router) + return TestClient(app) + + +def test_conformance_endpoint_returns_classes() -> None: + client = create_client() + + response = client.get("/conformance") + + assert response.status_code == 200 + payload = response.json() + assert payload["conformsTo"] == CONFORMANCE_CLASSES + assert any(link["rel"] == "self" for link in payload["links"]) + assert any(link["rel"] == "root" for link in payload["links"]) diff --git a/tests/test_coverages_federation.py b/tests/test_coverages_federation.py new file mode 100644 index 0000000..59b4d3a --- /dev/null +++ b/tests/test_coverages_federation.py @@ -0,0 +1,64 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.collections import router as collections_router +from eoapi.endpoints.coverages import router as coverages_router + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(collections_router) + app.include_router(coverages_router) + return TestClient(app) + + +def test_external_coverage_proxy_success(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.coverages.proxy_external_collection_request", + lambda collection_id, operation, query_params: { + "type": "Coverage", + "title": "External coverage", + "federation": { + "providerId": "demo-provider", + "sourceCollectionId": "rainfall-collection", + "operation": operation, + }, + }, + ) + + response = client.get("/collections/ext:demo-provider:rainfall-collection/coverage", params={"f": "json"}) + + assert response.status_code == 200 + payload = response.json() + assert payload["type"] == "Coverage" + assert payload["federation"]["operation"] == "coverage" + + +def test_external_coverage_proxy_not_found(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.coverages.proxy_external_collection_request", + lambda collection_id, operation, query_params: None, + ) + + response = client.get("/collections/ext:demo-provider:missing-collection/coverage") + + assert response.status_code == 404 + assert response.json()["detail"]["code"] == "NotFound" + + +def test_external_coverage_proxy_disabled_operation(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.coverages.is_external_operation_enabled", + lambda collection_id, operation: False, + ) + + response = client.get("/collections/ext:demo-provider:rainfall-collection/coverage") + + assert response.status_code == 400 + assert response.json()["detail"]["code"] == "InvalidParameterValue" diff --git a/tests/test_edr_endpoint.py b/tests/test_edr_endpoint.py new file mode 100644 index 0000000..b04f6b6 --- /dev/null +++ b/tests/test_edr_endpoint.py @@ -0,0 +1,175 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.collections import router as collections_router +from eoapi.endpoints.coverages import router as coverages_router +from eoapi.endpoints.edr import router as edr_router + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(collections_router) + app.include_router(coverages_router) + app.include_router(edr_router) + return TestClient(app) + + +def test_edr_position_success() -> None: + client = create_client() + + response = client.get( + "/collections/chirps-daily/position", + params={ + "coords": "POINT(30 -1)", + "datetime": "2026-01-31T00:00:00Z", + "parameter-name": "precip", + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["type"] == "FeatureCollection" + assert payload["features"][0]["geometry"]["type"] == "Point" + assert payload["features"][0]["properties"]["parameters"] == ["precip"] + + +def test_edr_position_invalid_coords() -> None: + client = create_client() + + response = client.get( + "/collections/chirps-daily/position", + params={"coords": "30,-1"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"]["code"] == "InvalidParameterValue" + + +def test_edr_position_unknown_collection() -> None: + client = create_client() + + response = client.get( + "/collections/unknown/position", + params={"coords": "POINT(30 -1)"}, + ) + + assert response.status_code == 404 + assert response.json()["detail"]["code"] == "NotFound" + + +def test_edr_area_success() -> None: + client = create_client() + + response = client.get( + "/collections/era5-land-daily/area", + params={ + "bbox": "36,-2,38,0", + "parameter-name": "2m_temperature", + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["type"] == "FeatureCollection" + assert payload["features"][0]["geometry"]["type"] == "Polygon" + assert payload["features"][0]["properties"]["parameters"] == ["2m_temperature"] + + +def test_edr_area_invalid_bbox() -> None: + client = create_client() + + response = client.get( + "/collections/chirps-daily/area", + params={"bbox": "30,-5,30,2"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"]["code"] == "InvalidParameterValue" + + +def test_external_edr_position_proxy_success(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.edr.proxy_external_collection_request", + lambda collection_id, operation, query_params: { + "type": "FeatureCollection", + "features": [], + "federation": { + "providerId": "demo-provider", + "sourceCollectionId": "rainfall-collection", + "operation": operation, + }, + }, + ) + + response = client.get( + "/collections/ext:demo-provider:rainfall-collection/position", + params={"coords": "POINT(30 -1)", "f": "json"}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["type"] == "FeatureCollection" + assert payload["federation"]["operation"] == "position" + + +def test_external_edr_area_proxy_success(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.edr.proxy_external_collection_request", + lambda collection_id, operation, query_params: { + "type": "FeatureCollection", + "features": [], + "federation": { + "providerId": "demo-provider", + "sourceCollectionId": "rainfall-collection", + "operation": operation, + }, + }, + ) + + response = client.get( + "/collections/ext:demo-provider:rainfall-collection/area", + params={"bbox": "30,-5,35,2", "f": "json"}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["type"] == "FeatureCollection" + assert payload["federation"]["operation"] == "area" + + +def test_external_edr_position_proxy_disabled_operation(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.edr.is_external_operation_enabled", + lambda collection_id, operation: False, + ) + + response = client.get( + "/collections/ext:demo-provider:rainfall-collection/position", + params={"coords": "POINT(30 -1)", "f": "json"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"]["code"] == "InvalidParameterValue" + + +def test_external_edr_area_proxy_disabled_operation(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.edr.is_external_operation_enabled", + lambda collection_id, operation: False, + ) + + response = client.get( + "/collections/ext:demo-provider:rainfall-collection/area", + params={"bbox": "30,-5,35,2", "f": "json"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"]["code"] == "InvalidParameterValue" diff --git a/tests/test_endpoint_errors.py b/tests/test_endpoint_errors.py new file mode 100644 index 0000000..c3d8e3f --- /dev/null +++ b/tests/test_endpoint_errors.py @@ -0,0 +1,21 @@ +from eoapi.endpoints.errors import invalid_parameter, not_found + + +def test_not_found_error_payload() -> None: + error = not_found("Collection", "missing-id") + + assert error.status_code == 404 + assert error.detail == { + "code": "NotFound", + "description": "Collection 'missing-id' not found", + } + + +def test_invalid_parameter_error_payload() -> None: + error = invalid_parameter("bbox must contain 4 comma-separated numbers") + + assert error.status_code == 400 + assert error.detail == { + "code": "InvalidParameterValue", + "description": "bbox must contain 4 comma-separated numbers", + } diff --git a/tests/test_external_ogc_config.py b/tests/test_external_ogc_config.py new file mode 100644 index 0000000..92a0f93 --- /dev/null +++ b/tests/test_external_ogc_config.py @@ -0,0 +1,76 @@ +from urllib.error import URLError + +from eoapi.external_ogc import _fetch_json, load_external_providers + + +class _Response: + def __init__(self, payload: bytes): + self._payload = payload + + def read(self) -> bytes: + return self._payload + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + +def test_load_external_provider_auth_and_retry_settings(monkeypatch) -> None: + monkeypatch.setenv( + "EOAPI_EXTERNAL_OGC_SERVICES", + '[{"id":"demo","url":"https://example.test","headers":{"X-Custom":"abc"},"apiKeyEnv":"DEMO_API_KEY","authScheme":"Token","timeoutSeconds":7,"retries":2,"operations":["coverage","position"]}]', + ) + + providers = load_external_providers() + + assert len(providers) == 1 + provider = providers[0] + assert provider.id == "demo" + assert provider.headers == {"X-Custom": "abc"} + assert provider.api_key_env == "DEMO_API_KEY" + assert provider.auth_scheme == "Token" + assert provider.timeout_seconds == 7.0 + assert provider.retries == 2 + assert provider.operations == ("coverage", "position") + + +def test_fetch_json_applies_headers_timeout_and_retries(monkeypatch) -> None: + monkeypatch.setenv("DEMO_API_KEY", "secret-value") + + monkeypatch.setenv( + "EOAPI_EXTERNAL_OGC_SERVICES", + '[{"id":"demo","url":"https://example.test","headers":{"X-Custom":"abc"},"apiKeyEnv":"DEMO_API_KEY","authScheme":"Token","timeoutSeconds":3,"retries":1}]', + ) + provider = load_external_providers()[0] + + calls: list[tuple[object, float]] = [] + + def fake_urlopen(request, timeout): + calls.append((request, timeout)) + if len(calls) == 1: + raise URLError("temporary") + return _Response(b'{"collections": []}') + + monkeypatch.setattr("eoapi.external_ogc.urlopen", fake_urlopen) + + payload = _fetch_json("https://example.test/collections", provider=provider) + + assert payload == {"collections": []} + assert len(calls) == 2 + request, timeout = calls[0] + assert timeout == 3.0 + assert request.headers["X-custom"] == "abc" + assert request.headers["Authorization"] == "Token secret-value" + + +def test_load_external_provider_rejects_unknown_operations(monkeypatch) -> None: + monkeypatch.setenv( + "EOAPI_EXTERNAL_OGC_SERVICES", + '[{"id":"demo","url":"https://example.test","operations":["coverage","timeseries"]}]', + ) + + providers = load_external_providers() + + assert providers == [] diff --git a/tests/test_features_endpoint.py b/tests/test_features_endpoint.py new file mode 100644 index 0000000..7cc117b --- /dev/null +++ b/tests/test_features_endpoint.py @@ -0,0 +1,58 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.features import router as features_router + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(features_router) + return TestClient(app) + + +def test_features_collections_list() -> None: + client = create_client() + + response = client.get("/features") + + assert response.status_code == 200 + payload = response.json() + ids = {collection["id"] for collection in payload["collections"]} + assert {"dhis2-org-units", "aggregated-results"}.issubset(ids) + + +def test_features_org_units_items() -> None: + client = create_client() + + response = client.get("/features/dhis2-org-units/items", params={"level": 2}) + + assert response.status_code == 200 + payload = response.json() + assert payload["type"] == "FeatureCollection" + assert payload["numberReturned"] >= 1 + + +def test_features_org_units_items_from_dhis2(monkeypatch) -> None: + client = create_client() + + monkeypatch.setattr( + "eoapi.endpoints.features.fetch_org_units_from_dhis2", + lambda level: [ + { + "type": "Feature", + "id": "ou-demo", + "geometry": { + "type": "Polygon", + "coordinates": [[[10.0, 10.0], [11.0, 10.0], [11.0, 11.0], [10.0, 11.0], [10.0, 10.0]]], + }, + "properties": {"name": "Demo", "level": level}, + } + ], + ) + + response = client.get("/features/dhis2-org-units/items", params={"level": 2}) + + assert response.status_code == 200 + payload = response.json() + assert payload["numberReturned"] == 1 + assert payload["features"][0]["id"] == "ou-demo" diff --git a/tests/test_landing_page.py b/tests/test_landing_page.py new file mode 100644 index 0000000..3d840c1 --- /dev/null +++ b/tests/test_landing_page.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.root import router as root_router + + +app = FastAPI() + +app.include_router(root_router) + + +def test_landing_page_links() -> None: + client = TestClient(app) + + response = client.get("/") + + assert response.status_code == 200 + payload = response.json() + assert payload["title"] == "DHIS2 EO API" + runtime = payload["runtime"] + assert "dhis2" in runtime + assert "state" in runtime + assert "internalScheduler" in runtime + assert "apiKeyRequired" in runtime + rels = {link["rel"] for link in payload["links"]} + assert {"self", "conformance", "data", "service-doc"}.issubset(rels) diff --git a/tests/test_processes_endpoint.py b/tests/test_processes_endpoint.py new file mode 100644 index 0000000..deaee49 --- /dev/null +++ b/tests/test_processes_endpoint.py @@ -0,0 +1,255 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient +import pandas as pd +import xarray as xr + +from eoapi.endpoints.features import router as features_router +from eoapi.endpoints.processes import router as processes_router +from eoapi.jobs import create_job + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(features_router) + app.include_router(processes_router) + return TestClient(app) + + +def test_process_execution_and_job_status() -> None: + client = create_client() + + execute_response = client.post( + "/processes/eo-aggregate-import/execution", + json={ + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "abc123", + "dryRun": True, + }, + } + }, + ) + + assert execute_response.status_code == 202 + execute_payload = execute_response.json() + assert execute_payload["processId"] == "eo-aggregate-import" + + job_id = execute_payload["jobId"] + job_response = client.get(f"/jobs/{job_id}") + assert job_response.status_code == 200 + + job_payload = job_response.json() + assert job_payload["status"] == "succeeded" + assert job_payload["importSummary"]["dryRun"] is True + + features_response = client.get("/features/aggregated-results/items", params={"jobId": job_id}) + assert features_response.status_code == 200 + features_payload = features_response.json() + assert features_payload["type"] == "FeatureCollection" + assert features_payload["numberReturned"] >= 1 + + +def test_process_unknown_id() -> None: + client = create_client() + + response = client.get("/processes/unknown") + + assert response.status_code == 404 + assert response.json()["detail"]["code"] == "NotFound" + + +def test_process_list_includes_xclim_processes() -> None: + client = create_client() + + response = client.get("/processes") + + assert response.status_code == 200 + payload = response.json() + process_ids = {process["id"] for process in payload["processes"]} + assert "eo-aggregate-import" in process_ids + assert "xclim-cdd" in process_ids + assert "xclim-cwd" in process_ids + assert "xclim-warm-days" in process_ids + + +def test_xclim_execution_dispatch(monkeypatch) -> None: + client = create_client() + + def fake_run_xclim(process_id, inputs): + outputs = { + "importSummary": { + "imported": 0, + "updated": 0, + "ignored": 1, + "deleted": 0, + "dryRun": True, + }, + "features": [ + { + "type": "Feature", + "id": "org-demo", + "geometry": {"type": "Point", "coordinates": [0.0, 0.0]}, + "properties": {"orgUnit": "org-demo", "value": 4.0}, + } + ], + } + return create_job(process_id, inputs.model_dump(mode="json"), outputs) + + monkeypatch.setattr("eoapi.endpoints.processes._run_xclim", fake_run_xclim) + + execute_response = client.post( + "/processes/xclim-cdd/execution", + json={ + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": {"value": 1.0, "unit": "mm/day"}, + "dhis2": { + "dataElementId": "abc123", + "dryRun": True, + }, + } + }, + ) + + assert execute_response.status_code == 202 + payload = execute_response.json() + assert payload["processId"] == "xclim-cdd" + + job_response = client.get(f"/jobs/{payload['jobId']}") + assert job_response.status_code == 200 + assert job_response.json()["processId"] == "xclim-cdd" + + +def test_xclim_uses_dhis2eo_series_when_available(monkeypatch) -> None: + client = create_client() + + def fake_extract_org_unit_series(**kwargs): + times = pd.date_range("2026-01-01", "2026-01-31", freq="D") + ids = [feature["id"] for feature in kwargs["org_units"]] + return { + org_unit_id: xr.DataArray( + [0.4 + (index % 3) * 0.2 for index, _ in enumerate(times)], + coords={"time": times}, + dims=["time"], + attrs={"units": "mm/day"}, + ) + for org_unit_id in ids + } + + monkeypatch.setattr("eoapi.endpoints.processes._extract_org_unit_series", fake_extract_org_unit_series) + + execute_response = client.post( + "/processes/xclim-cdd/execution", + json={ + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": {"value": 1.0, "unit": "mm/day"}, + "dhis2": { + "dataElementId": "abc123", + "dryRun": True, + }, + } + }, + ) + + assert execute_response.status_code == 202 + job_id = execute_response.json()["jobId"] + features_response = client.get("/features/aggregated-results/items", params={"jobId": job_id}) + assert features_response.status_code == 200 + + payload = features_response.json() + assert payload["features"] + assert payload["features"][0]["properties"]["source"] == "dhis2eo" + + +def test_xclim_falls_back_to_synthetic_on_extractor_error(monkeypatch) -> None: + client = create_client() + + def fail_extract_org_unit_series(**kwargs): + raise RuntimeError("network unavailable") + + monkeypatch.setattr("eoapi.endpoints.processes._extract_org_unit_series", fail_extract_org_unit_series) + + execute_response = client.post( + "/processes/xclim-warm-days/execution", + json={ + "inputs": { + "datasetId": "era5-land-daily", + "parameter": "2m_temperature", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": {"value": 35.0, "unit": "degC"}, + "dhis2": { + "dataElementId": "abc123", + "dryRun": True, + }, + } + }, + ) + + assert execute_response.status_code == 202 + job_id = execute_response.json()["jobId"] + features_response = client.get("/features/aggregated-results/items", params={"jobId": job_id}) + assert features_response.status_code == 200 + + payload = features_response.json() + assert payload["features"] + assert payload["features"][0]["properties"]["source"] == "synthetic-fallback" + + +def test_aggregate_process_uses_dhis2_import_adapter(monkeypatch) -> None: + client = create_client() + + def fake_import(data_values, dry_run): + assert dry_run is False + assert len(data_values) >= 1 + return { + "imported": len(data_values), + "updated": 0, + "ignored": 0, + "deleted": 0, + "dryRun": False, + "source": "dhis2", + } + + monkeypatch.setattr("eoapi.endpoints.processes.import_data_values_to_dhis2", fake_import) + + execute_response = client.post( + "/processes/eo-aggregate-import/execution", + json={ + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "abc123", + "dryRun": False, + }, + } + }, + ) + + assert execute_response.status_code == 202 + job_id = execute_response.json()["jobId"] + + job_response = client.get(f"/jobs/{job_id}") + assert job_response.status_code == 200 + payload = job_response.json() + assert payload["importSummary"]["dryRun"] is False + assert payload["importSummary"]["source"] == "dhis2" diff --git a/tests/test_scheduler_runtime.py b/tests/test_scheduler_runtime.py new file mode 100644 index 0000000..5096699 --- /dev/null +++ b/tests/test_scheduler_runtime.py @@ -0,0 +1,45 @@ +from datetime import UTC, datetime + +from eoapi.scheduler_runtime import poll_scheduler_once + + +def test_poll_scheduler_once_triggers_due_schedule(monkeypatch) -> None: + monkeypatch.setenv("EOAPI_INTERNAL_SCHEDULER_ENABLED", "true") + + calls: list[tuple[str, str]] = [] + + monkeypatch.setattr( + "eoapi.scheduler_runtime.list_schedules", + lambda: [ + { + "scheduleId": "sch-1", + "enabled": True, + "cron": "* * * * *", + "timezone": "UTC", + "created": "2026-01-01T00:00:00Z", + "lastRunAt": "2026-01-01T00:00:00Z", + } + ], + ) + monkeypatch.setattr( + "eoapi.scheduler_runtime.execute_schedule_target", + lambda schedule_id, trigger: calls.append((schedule_id, trigger)), + ) + + poll_scheduler_once(now_utc=datetime(2026, 1, 1, 0, 2, tzinfo=UTC)) + + assert calls == [("sch-1", "internal-cron")] + + +def test_poll_scheduler_once_noop_when_disabled(monkeypatch) -> None: + monkeypatch.setenv("EOAPI_INTERNAL_SCHEDULER_ENABLED", "false") + + calls: list[tuple[str, str]] = [] + monkeypatch.setattr( + "eoapi.scheduler_runtime.execute_schedule_target", + lambda schedule_id, trigger: calls.append((schedule_id, trigger)), + ) + + poll_scheduler_once(now_utc=datetime(2026, 1, 1, 0, 2, tzinfo=UTC)) + + assert calls == [] diff --git a/tests/test_schedules_endpoint.py b/tests/test_schedules_endpoint.py new file mode 100644 index 0000000..2ca2084 --- /dev/null +++ b/tests/test_schedules_endpoint.py @@ -0,0 +1,227 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.features import router as features_router +from eoapi.endpoints.processes import router as processes_router +from eoapi.endpoints.schedules import router as schedules_router +from eoapi.endpoints.workflows import router as workflows_router +from eoapi.jobs import create_pending_job + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(features_router) + app.include_router(processes_router) + app.include_router(workflows_router) + app.include_router(schedules_router) + return TestClient(app) + + +def _create_schedule(client: TestClient) -> str: + response = client.post( + "/schedules", + json={ + "name": "nightly-precip-import", + "cron": "0 0 * * *", + "timezone": "UTC", + "enabled": True, + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": { + "dataElementId": "abc123", + "dryRun": True, + }, + }, + }, + ) + assert response.status_code == 201 + return response.json()["scheduleId"] + + +def test_schedule_crud_and_run() -> None: + client = create_client() + + schedule_id = _create_schedule(client) + + list_response = client.get("/schedules") + assert list_response.status_code == 200 + assert any(item["scheduleId"] == schedule_id for item in list_response.json()["schedules"]) + + get_response = client.get(f"/schedules/{schedule_id}") + assert get_response.status_code == 200 + assert get_response.json()["name"] == "nightly-precip-import" + + patch_response = client.patch( + f"/schedules/{schedule_id}", + json={"enabled": False}, + ) + assert patch_response.status_code == 200 + assert patch_response.json()["enabled"] is False + + run_response = client.post(f"/schedules/{schedule_id}/run") + assert run_response.status_code == 202 + job_id = run_response.json()["jobId"] + + job_response = client.get(f"/jobs/{job_id}") + assert job_response.status_code == 200 + assert job_response.json()["status"] == "succeeded" + + get_after_run = client.get(f"/schedules/{schedule_id}") + assert get_after_run.status_code == 200 + assert get_after_run.json()["lastRunJobId"] == job_id + + delete_response = client.delete(f"/schedules/{schedule_id}") + assert delete_response.status_code == 204 + + not_found_response = client.get(f"/schedules/{schedule_id}") + assert not_found_response.status_code == 404 + + +def test_schedule_callback_runs_job(monkeypatch) -> None: + client = create_client() + monkeypatch.setenv("EOAPI_SCHEDULER_TOKEN", "secret-token") + + schedule_id = _create_schedule(client) + + callback_response = client.post( + f"/schedules/{schedule_id}/callback", + headers={"X-Scheduler-Token": "secret-token"}, + ) + assert callback_response.status_code == 202 + callback_payload = callback_response.json() + assert callback_payload["trigger"] == "scheduler-callback" + + job_id = callback_payload["jobId"] + job_response = client.get(f"/jobs/{job_id}") + assert job_response.status_code == 200 + assert job_response.json()["status"] == "succeeded" + + +def test_schedule_callback_invalid_token(monkeypatch) -> None: + client = create_client() + monkeypatch.setenv("EOAPI_SCHEDULER_TOKEN", "secret-token") + + schedule_id = _create_schedule(client) + response = client.post( + f"/schedules/{schedule_id}/callback", + headers={"X-Scheduler-Token": "wrong-token"}, + ) + + assert response.status_code == 403 + assert response.json()["detail"]["code"] == "Forbidden" + + +def test_schedule_callback_missing_server_token(monkeypatch) -> None: + client = create_client() + monkeypatch.delenv("EOAPI_SCHEDULER_TOKEN", raising=False) + + schedule_id = _create_schedule(client) + response = client.post( + f"/schedules/{schedule_id}/callback", + headers={"X-Scheduler-Token": "any-value"}, + ) + + assert response.status_code == 503 + assert response.json()["detail"]["code"] == "ServiceUnavailable" + + +def test_schedule_run_uses_prefect_when_enabled(monkeypatch) -> None: + client = create_client() + monkeypatch.setenv("EOAPI_PREFECT_ENABLED", "true") + monkeypatch.setenv("EOAPI_PREFECT_API_URL", "http://prefect.local") + monkeypatch.setenv("EOAPI_PREFECT_DEPLOYMENT_ID", "dep-123") + + def fake_submit(schedule_id, payload_inputs, trigger, eoapi_job_id): + assert schedule_id + assert payload_inputs["datasetId"] == "chirps-daily" + assert trigger == "manual" + assert eoapi_job_id + return {"id": "flow-run-1"} + + monkeypatch.setattr("eoapi.endpoints.schedules.submit_aggregate_import_run", fake_submit) + + schedule_id = _create_schedule(client) + run_response = client.post(f"/schedules/{schedule_id}/run") + + assert run_response.status_code == 202 + run_payload = run_response.json() + assert run_payload["execution"]["source"] == "prefect" + assert run_payload["execution"]["flowRunId"] == "flow-run-1" + + +def test_job_status_syncs_prefect_state(monkeypatch) -> None: + client = create_client() + monkeypatch.setenv("EOAPI_PREFECT_ENABLED", "true") + + job = create_pending_job( + "eo-aggregate-import", + inputs={"dhis2": {"dryRun": True}}, + source="prefect", + flow_run_id="flow-run-2", + ) + + def fake_get_flow_run(flow_run_id): + assert flow_run_id == "flow-run-2" + return {"state": {"type": "COMPLETED", "name": "Completed"}} + + monkeypatch.setattr("eoapi.endpoints.processes.get_flow_run", fake_get_flow_run) + + job_response = client.get(f"/jobs/{job['jobId']}") + assert job_response.status_code == 200 + payload = job_response.json() + assert payload["status"] == "succeeded" + assert payload["progress"] == 100 + assert payload["execution"]["source"] == "prefect" + + +def test_schedule_run_for_workflow_target() -> None: + client = create_client() + + workflow_response = client.post( + "/workflows", + json={ + "name": "scheduled-workflow", + "steps": [ + { + "name": "aggregate", + "processId": "eo-aggregate-import", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": {"dataElementId": "abc123", "dryRun": True}, + } + }, + } + ], + }, + ) + assert workflow_response.status_code == 201 + workflow_id = workflow_response.json()["workflowId"] + + schedule_response = client.post( + "/schedules", + json={ + "name": "nightly-workflow", + "cron": "0 0 * * *", + "timezone": "UTC", + "enabled": True, + "workflowId": workflow_id, + }, + ) + assert schedule_response.status_code == 201 + schedule_id = schedule_response.json()["scheduleId"] + + run_response = client.post(f"/schedules/{schedule_id}/run") + assert run_response.status_code == 202 + run_payload = run_response.json() + assert run_payload["workflowId"] == workflow_id + assert run_payload["execution"]["source"] == "workflow" + assert len(run_payload["jobIds"]) == 1 diff --git a/tests/test_state_store.py b/tests/test_state_store.py new file mode 100644 index 0000000..61a8f95 --- /dev/null +++ b/tests/test_state_store.py @@ -0,0 +1,21 @@ +from eoapi.state_store import load_state_map, save_state_map + + +def test_state_store_roundtrip(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("EOAPI_STATE_DIR", str(tmp_path)) + monkeypatch.setenv("EOAPI_STATE_PERSIST", "true") + + save_state_map("jobs", {"a": {"jobId": "a"}}) + loaded = load_state_map("jobs") + + assert loaded == {"a": {"jobId": "a"}} + + +def test_state_store_disabled(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("EOAPI_STATE_DIR", str(tmp_path)) + monkeypatch.setenv("EOAPI_STATE_PERSIST", "false") + + save_state_map("jobs", {"a": {"jobId": "a"}}) + loaded = load_state_map("jobs") + + assert loaded == {} diff --git a/tests/test_workflows_endpoint.py b/tests/test_workflows_endpoint.py new file mode 100644 index 0000000..6806eba --- /dev/null +++ b/tests/test_workflows_endpoint.py @@ -0,0 +1,80 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from eoapi.endpoints.features import router as features_router +from eoapi.endpoints.processes import router as processes_router +from eoapi.endpoints.workflows import router as workflows_router + + +def create_client() -> TestClient: + app = FastAPI() + app.include_router(features_router) + app.include_router(processes_router) + app.include_router(workflows_router) + return TestClient(app) + + +def test_workflow_crud_and_run() -> None: + client = create_client() + + create_response = client.post( + "/workflows", + json={ + "name": "climate-workflow", + "steps": [ + { + "name": "aggregate", + "processId": "eo-aggregate-import", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameters": ["precip"], + "datetime": "2026-01-31T00:00:00Z", + "orgUnitLevel": 2, + "aggregation": "mean", + "dhis2": {"dataElementId": "abc123", "dryRun": True}, + } + }, + }, + { + "name": "cdd", + "processId": "xclim-cdd", + "payload": { + "inputs": { + "datasetId": "chirps-daily", + "parameter": "precip", + "start": "2026-01-01", + "end": "2026-01-31", + "orgUnitLevel": 2, + "threshold": {"value": 1.0, "unit": "mm/day"}, + "dhis2": {"dataElementId": "abc123", "dryRun": True}, + } + }, + }, + ], + }, + ) + + assert create_response.status_code == 201 + workflow_id = create_response.json()["workflowId"] + + list_response = client.get("/workflows") + assert list_response.status_code == 200 + assert any(item["workflowId"] == workflow_id for item in list_response.json()["workflows"]) + + run_response = client.post(f"/workflows/{workflow_id}/run") + assert run_response.status_code == 202 + run_payload = run_response.json() + assert run_payload["workflowId"] == workflow_id + assert len(run_payload["jobIds"]) == 2 + + get_response = client.get(f"/workflows/{workflow_id}") + assert get_response.status_code == 200 + get_payload = get_response.json() + assert get_payload["lastRunJobIds"] == run_payload["jobIds"] + + delete_response = client.delete(f"/workflows/{workflow_id}") + assert delete_response.status_code == 204 + + missing_response = client.get(f"/workflows/{workflow_id}") + assert missing_response.status_code == 404 diff --git a/uv.lock b/uv.lock index 13f52aa..658d722 100644 --- a/uv.lock +++ b/uv.lock @@ -63,6 +63,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boltons" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/54/71a94d8e02da9a865587fb3fff100cb0fc7aa9f4d5ed9ed3a591216ddcc7/boltons-25.0.0.tar.gz", hash = "sha256:e110fbdc30b7b9868cb604e3f71d4722dd8f4dcb4a5ddd06028ba8f1ab0b5ace", size = 246294, upload-time = "2025-02-03T05:57:59.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl", hash = "sha256:dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62", size = 194210, upload-time = "2025-02-03T05:57:56.705Z" }, +] + +[[package]] +name = "bottleneck" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/d8/6d641573e210768816023a64966d66463f2ce9fc9945fa03290c8a18f87c/bottleneck-1.6.0.tar.gz", hash = "sha256:028d46ee4b025ad9ab4d79924113816f825f62b17b87c9e1d0d8ce144a4a0e31", size = 104311, upload-time = "2025-09-08T16:30:38.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/1a/e117cd5ff7056126d3291deb29ac8066476e60b852555b95beb3fc9d62a0/bottleneck-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015de414ca016ebe56440bdf5d3d1204085080527a3c51f5b7b7a3e704fe6fd", size = 100521, upload-time = "2025-09-08T16:30:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/bd/22/05555a9752357e24caa1cd92324d1a7fdde6386aab162fcc451f8f8eedc2/bottleneck-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:456757c9525b0b12356f472e38020ed4b76b18375fd76e055f8d33fb62956f5e", size = 377719, upload-time = "2025-09-08T16:30:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/76593af47097d9633109bed04dbcf2170707dd84313ca29f436f9234bc51/bottleneck-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c65254d51b6063c55f6272f175e867e2078342ae75f74be29d6612e9627b2c0", size = 368577, upload-time = "2025-09-08T16:30:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/4dcacaf637d2b8d89ea746c74159adda43858d47358978880614c3fa4391/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a172322895fbb79c6127474f1b0db0866895f0b804a18d5c6b841fea093927fe", size = 361441, upload-time = "2025-09-08T16:30:07.613Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/21eb1eb1c42cb7be2872d0647c292fc75768d14e1f0db66bf907b24b2464/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5e81b642eb0d5a5bf00312598d7ed142d389728b694322a118c26813f3d1fa9", size = 373416, upload-time = "2025-09-08T16:30:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/48/cb/7957ff40367a151139b5f1854616bf92e578f10804d226fbcdecfd73aead/bottleneck-1.6.0-cp313-cp313-win32.whl", hash = "sha256:543d3a89d22880cd322e44caff859af6c0489657bf9897977d1f5d3d3f77299c", size = 108029, upload-time = "2025-09-08T16:30:09.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/a8/735df4156fa5595501d5d96a6ee102f49c13d2ce9e2a287ad51806bc3ba0/bottleneck-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:48a44307d604ceb81e256903e5d57d3adb96a461b1d3c6a69baa2c67e823bd36", size = 113497, upload-time = "2025-09-08T16:30:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/8c1260df8ade7cebc2a8af513a27082b5e36aa4a5fb762d56ea6d969d893/bottleneck-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:547e6715115867c4657c9ae8cc5ddac1fec8fdad66690be3a322a7488721b06b", size = 101606, upload-time = "2025-09-08T16:30:11.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/f03e2944e91ee962922c834ed21e5be6d067c8395681f5dc6c67a0a26853/bottleneck-1.6.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e4a4a6e05b6f014c307969129e10d1a0afd18f3a2c127b085532a4a76677aef", size = 391804, upload-time = "2025-09-08T16:30:13.13Z" }, + { url = "https://files.pythonhosted.org/packages/0b/58/2b356b8a81eb97637dccee6cf58237198dd828890e38be9afb4e5e58e38e/bottleneck-1.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2baae0d1589b4a520b2f9cf03528c0c8b20717b3f05675e212ec2200cf628f12", size = 383443, upload-time = "2025-09-08T16:30:14.318Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/cf7d09ed3736ad0d50c624787f9b580ae3206494d95cc0f4814b93eef728/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2e407139b322f01d8d5b6b2e8091b810f48a25c7fa5c678cfcdc420dfe8aea0a", size = 375458, upload-time = "2025-09-08T16:30:15.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/7c87a34a24e339860064f20fac49f6738e94f1717bc8726b9c47705601d8/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adefb89b92aba6de9c6ea871d99bcd29d519f4fb012cc5197917813b4fc2c7f", size = 386384, upload-time = "2025-09-08T16:30:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/59/57/db51855e18a47671801180be748939b4c9422a0544849af1919116346b5f/bottleneck-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:64b8690393494074923780f6abdf5f5577d844b9d9689725d1575a936e74e5f0", size = 109448, upload-time = "2025-09-08T16:30:18.076Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1e/683c090b624f13a5bf88a0be2241dc301e98b2fb72a45812a7ae6e456cc4/bottleneck-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:cb67247f65dcdf62af947c76c6c8b77d9f0ead442cac0edbaa17850d6da4e48d", size = 115190, upload-time = "2025-09-08T16:30:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/eb7c08964a3f3c4719f98795ccd21807ee9dd3071a0f9ad652a5f19196ff/bottleneck-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1d789042511a0f042b3bdcd2903e8567e956d3aa3be189cce3746daeb8550", size = 100544, upload-time = "2025-09-08T16:30:20.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/ec/c6f3be848f37689f481797ce7d9807d5f69a199d7fc0e46044f9b708c468/bottleneck-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1fad24c99e39ad7623fc2a76d37feb26bd32e4dd170885edf4dbf4bfce2199a3", size = 378315, upload-time = "2025-09-08T16:30:21.409Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/2d6600836e2ea8f14fcefac592dc83497e5b88d381470c958cb9cdf88706/bottleneck-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643e61e50a6f993debc399b495a1609a55b3bd76b057e433e4089505d9f605c7", size = 368978, upload-time = "2025-09-08T16:30:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/bf72b49f5040212873b985feef5050015645e0a02204b591e1d265fc522a/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa668efbe4c6b200524ea0ebd537212da9b9801287138016fdf64119d6fcf201", size = 362074, upload-time = "2025-09-08T16:30:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c8/c4891a0604eb680031390182c6e264247e3a9a8d067d654362245396fadf/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f7dd35262e89e28fedd79d45022394b1fa1aceb61d2e747c6d6842e50546daa", size = 374019, upload-time = "2025-09-08T16:30:26.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/ed096f8d1b9147e84914045dd89bc64e3c32eee49b862d1e20d573a9ab0d/bottleneck-1.6.0-cp314-cp314-win32.whl", hash = "sha256:bd90bec3c470b7fdfafc2fbdcd7a1c55a4e57b5cdad88d40eea5bc9bab759bf1", size = 110173, upload-time = "2025-09-08T16:30:27.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/70/1414acb6ae378a15063cfb19a0a39d69d1b6baae1120a64d2b069902549b/bottleneck-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:b43b6d36a62ffdedc6368cf9a708e4d0a30d98656c2b5f33d88894e1bcfd6857", size = 115899, upload-time = "2025-09-08T16:30:28.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ed/4570b5d8c1c85ce3c54963ebc37472231ed54f0b0d8dbb5dde14303f775f/bottleneck-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:53296707a8e195b5dcaa804b714bd222b5e446bd93cd496008122277eb43fa87", size = 101615, upload-time = "2025-09-08T16:30:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/93/c148faa07ae91f266be1f3fad1fde95aa2449e12937f3f3df2dd720b86e0/bottleneck-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6df19cc48a83efd70f6d6874332aa31c3f5ca06a98b782449064abbd564cf0e", size = 392411, upload-time = "2025-09-08T16:30:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1c/e6ad221d345a059e7efb2ad1d46a22d9fdae0486faef70555766e1123966/bottleneck-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96bb3a52cb3c0aadfedce3106f93ab940a49c9d35cd4ed612e031f6deb27e80f", size = 384022, upload-time = "2025-09-08T16:30:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/4f/40/5b15c01eb8c59d59bc84c94d01d3d30797c961f10ec190f53c27e05d62ab/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1db9e831b69d5595b12e79aeb04cb02873db35576467c8dd26cdc1ee6b74581", size = 376004, upload-time = "2025-09-08T16:30:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/cb228f5949553a5c01d1d5a3c933f0216d78540d9e0bf8dd4343bb449681/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4dd7ac619570865fcb7a0e8925df418005f076286ad2c702dd0f447231d7a055", size = 386909, upload-time = "2025-09-08T16:30:34.973Z" }, + { url = "https://files.pythonhosted.org/packages/09/9a/425065c37a67a9120bf53290371579b83d05bf46f3212cce65d8c01d470a/bottleneck-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:7fb694165df95d428fe00b98b9ea7d126ef786c4a4b7d43ae2530248396cadcb", size = 111636, upload-time = "2025-09-08T16:30:36.044Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/c41006e42909ec5114a8961818412310aa54646d1eae0495dbff3598a095/bottleneck-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:174b80930ce82bd8456c67f1abb28a5975c68db49d254783ce2cb6983b4fea40", size = 117611, upload-time = "2025-09-08T16:30:37.055Z" }, +] + [[package]] name = "cachetools" version = "7.0.1" @@ -115,6 +181,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] +[[package]] +name = "cf-xarray" +version = "0.10.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xarray" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/78/f4f38e7ea6221773ea48d85c00d529b1fdc7378a1a1b77c2b77661446a0b/cf_xarray-0.10.11.tar.gz", hash = "sha256:e10ee37b0ed3ba36f42346360f2bc070c690ddc73bb9dcdd9463b3a221453be3", size = 686693, upload-time = "2026-02-03T19:17:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ce/5c4f4660da5521d90bea62cdf8396d7e4ce4a00513e218d267b97f9ea453/cf_xarray-0.10.11-py3-none-any.whl", hash = "sha256:c47fff625766c69a66fedef368d9787acb0819b32d8bd022f8b045089b42109a", size = 78421, upload-time = "2026-02-03T19:17:40.431Z" }, +] + [[package]] name = "cffi" version = "2.0.0" @@ -373,6 +451,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/53/63d707d9de41aab3c51faa4f0e9234c36ee8c63f56c90c3ae23a01976e81/convertdate-2.4.1-py3-none-any.whl", hash = "sha256:0622acf8bc600956b937e8dd7cee36ba035669b62d2db224bf130af1fc84f754", size = 48440, upload-time = "2026-02-08T00:37:52.405Z" }, ] +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -400,6 +491,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl", hash = "sha256:46a0cf3b8d87f78a3d2e6b145aea4418a6d6d606fe6a16c79bd8ca2bb862bc91", size = 1482084, upload-time = "2026-01-30T21:04:18.363Z" }, ] +[package.optional-dependencies] +array = [ + { name = "numpy" }, +] + +[[package]] +name = "dateparser" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/668dfb8c073a5dde3efb80fa382de1502e3b14002fd386a8c1b0b49e92a9/dateparser-1.3.0.tar.gz", hash = "sha256:5bccf5d1ec6785e5be71cc7ec80f014575a09b4923e762f850e57443bddbf1a5", size = 337152, upload-time = "2026-02-04T16:00:06.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/c7/95349670e193b2891176e1b8e5f43e12b31bff6d9994f70e74ab385047f6/dateparser-1.3.0-py3-none-any.whl", hash = "sha256:8dc678b0a526e103379f02ae44337d424bd366aac727d3c6cf52ce1b01efbb5a", size = 318688, upload-time = "2026-02-04T16:00:04.652Z" }, +] + [[package]] name = "deprecation" version = "2.1.0" @@ -548,20 +659,28 @@ wheels = [ [[package]] name = "eo-api" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ + { name = "croniter" }, { name = "dhis2-client" }, { name = "dhis2eo" }, + { name = "pygeoapi" }, + { name = "pyyaml" }, { name = "titiler-core" }, { name = "uvicorn" }, + { name = "xclim" }, ] [package.metadata] requires-dist = [ + { name = "croniter", specifier = ">=2.0.5" }, { name = "dhis2-client", git = "https://github.com/dhis2/dhis2-python-client.git?rev=V0.3.0" }, { name = "dhis2eo", git = "https://github.com/dhis2/dhis2eo.git?rev=v1.1.0" }, + { name = "pygeoapi", specifier = ">=0.22.0" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "titiler-core", specifier = ">=1.2.0" }, { name = "uvicorn", specifier = ">=0.41.0" }, + { name = "xclim", specifier = ">=0.58.1" }, ] [[package]] @@ -598,6 +717,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/ff/76dd547e129206899e4e26446c3ca7aeaff948c31b05250e9b8690e76883/findlibs-0.1.2-py3-none-any.whl", hash = "sha256:5348bbc7055d2a505962576c2e285b6c0aae6d749f82ba71296e7d41336e66e8", size = 10707, upload-time = "2025-07-28T09:15:02.733Z" }, ] +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + [[package]] name = "flexcache" version = "0.3" @@ -693,6 +829,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/e4/fac19dc34cb686c96011388b813ff7b858a70681e5ce6ce7698e5021b0f4/geopandas-1.1.2-py3-none-any.whl", hash = "sha256:2bb0b1052cb47378addb4ba54c47f8d4642dcbda9b61375638274f49d9f0bb0d", size = 341734, upload-time = "2025-12-22T21:06:12.498Z" }, ] +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -739,6 +906,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -751,6 +927,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -837,6 +1022,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, ] +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" }, + { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -1060,6 +1270,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/2e/39d5e9179c543f2e6e149a65908f83afd9b6d64379a90789b323111761db/netcdf4-1.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:034220887d48da032cb2db5958f69759dbb04eb33e279ec6390571d4aea734fe", size = 2531682, upload-time = "2026-01-05T02:27:37.062Z" }, ] +[[package]] +name = "numba" +version = "0.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/c9/a0fb41787d01d621046138da30f6c2100d80857bf34b3390dd68040f27a3/numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1", size = 2765679, upload-time = "2026-02-18T18:41:20.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/80/2734de90f9300a6e2503b35ee50d9599926b90cbb7ac54f9e40074cd07f1/numba-0.64.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3bab2c872194dcd985f1153b70782ec0fbbe348fffef340264eacd3a76d59fd6", size = 2683392, upload-time = "2026-02-18T18:41:06.563Z" }, + { url = "https://files.pythonhosted.org/packages/42/e8/14b5853ebefd5b37723ef365c5318a30ce0702d39057eaa8d7d76392859d/numba-0.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703a246c60832cad231d2e73c1182f25bf3cc8b699759ec8fe58a2dbc689a70c", size = 3812245, upload-time = "2026-02-18T18:41:07.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f60dc6c96d19b7185144265a5fbf01c14993d37ff4cd324b09d0212aa7ce/numba-0.64.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2e49a7900ee971d32af7609adc0cfe6aa7477c6f6cccdf6d8138538cf7756f", size = 3511328, upload-time = "2026-02-18T18:41:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/fe7003ea7e7237ee7014f8eaeeb7b0d228a2db22572ca85bab2648cf52cb/numba-0.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:396f43c3f77e78d7ec84cdfc6b04969c78f8f169351b3c4db814b97e7acf4245", size = 2752668, upload-time = "2026-02-18T18:41:11.455Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8a/77d26afe0988c592dd97cb8d4e80bfb3dfc7dbdacfca7d74a7c5c81dd8c2/numba-0.64.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f565d55eaeff382cbc86c63c8c610347453af3d1e7afb2b6569aac1c9b5c93ce", size = 2683590, upload-time = "2026-02-18T18:41:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/8e/4b/600b8b7cdbc7f9cebee9ea3d13bb70052a79baf28944024ffcb59f0712e3/numba-0.64.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b55169b18892c783f85e9ad9e6f5297a6d12967e4414e6b71361086025ff0bb", size = 3781163, upload-time = "2026-02-18T18:41:15.377Z" }, + { url = "https://files.pythonhosted.org/packages/ff/73/53f2d32bfa45b7175e9944f6b816d8c32840178c3eee9325033db5bf838e/numba-0.64.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:196bcafa02c9dd1707e068434f6d5cedde0feb787e3432f7f1f0e993cc336c4c", size = 3481172, upload-time = "2026-02-18T18:41:17.281Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/aebd2f7f1e11e38814bb96e95a27580817a7b340608d3ac085fdbab83174/numba-0.64.0-cp314-cp314-win_amd64.whl", hash = "sha256:213e9acbe7f1c05090592e79020315c1749dd52517b90e94c517dca3f014d4a1", size = 2754700, upload-time = "2026-02-18T18:41:19.277Z" }, +] + [[package]] name = "numexpr" version = "2.14.1" @@ -1312,6 +1542,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1389,6 +1655,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] +[[package]] +name = "pygeoapi" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "click" }, + { name = "filelock" }, + { name = "flask" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pygeofilter" }, + { name = "pygeoif" }, + { name = "pyproj" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "rasterio" }, + { name = "requests" }, + { name = "shapely" }, + { name = "sqlalchemy" }, + { name = "tinydb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/46/3bcdd2915a8f2a9856cb0442f3f73cbba463bff4c5c059887dc3a20de33a/pygeoapi-0.22.0.tar.gz", hash = "sha256:43689d6c89e6bd7536c9384db4617fa499f82823394a656dd50c2ea126c92150", size = 324148, upload-time = "2025-11-07T20:22:43.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3d/a3dd54ac1870c99223fc2fc1981ac16f3a875d95c0d60fca0814c393ca8f/pygeoapi-0.22.0-py2.py3-none-any.whl", hash = "sha256:0975e9efc5e7c70466f05b085b8093311718c40ee8ecd9a15ac803945e8d5ab8", size = 518476, upload-time = "2025-11-07T20:22:41.982Z" }, +] + +[[package]] +name = "pygeofilter" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "dateparser" }, + { name = "lark" }, + { name = "pygeoif" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f0/30b916dc05ff1242eb9cc391e1bac367d34c9f403c0bd634923b87024c23/pygeofilter-0.3.3.tar.gz", hash = "sha256:8b9fec05ba144943a1e415b6ac3752ad6011f44aad7d1bb27e7ef48b073460bd", size = 63419, upload-time = "2025-12-20T08:47:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/e3/c777c08e9519c1d49fcfad726c84d7b0e7934e9f414430eaa3d1ab41ecf7/pygeofilter-0.3.3-py2.py3-none-any.whl", hash = "sha256:e719fcb929c6b60bca99de0cfde5f95bc3245cab50516c103dae1d4f12c4c7b6", size = 96568, upload-time = "2025-12-20T08:47:58.178Z" }, +] + +[[package]] +name = "pygeoif" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/c6660ceea2fc28feefdfb0389bf53b5d0e0ba92aaba72e813901cb0552ed/pygeoif-1.6.0.tar.gz", hash = "sha256:eb0efa59c6573ea2cadce69a7ea9d2d10394b895ed47831c00d44752219c01be", size = 40915, upload-time = "2025-10-01T10:02:13.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/7f/c803c39fa76fe055bc4154fb6e897185ad21946820a2227283e0a20eeb35/pygeoif-1.6.0-py3-none-any.whl", hash = "sha256:02f84807dadbaf1941c4bb2a9ef1ebac99b1b0404597d2602efdbb58910c69c9", size = 27976, upload-time = "2025-10-01T10:02:12.19Z" }, +] + [[package]] name = "pymeeus" version = "0.5.12" @@ -1620,6 +1942,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "regex" +version = "2026.2.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/c0/d8079d4f6342e4cec5c3e7d7415b5cd3e633d5f4124f7a4626908dbe84c7/regex-2026.2.19.tar.gz", hash = "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310", size = 414973, upload-time = "2026-02-19T19:03:47.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/2d/a849835e76ac88fcf9e8784e642d3ea635d183c4112150ca91499d6703af/regex-2026.2.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8df08decd339e8b3f6a2eb5c05c687fe9d963ae91f352bc57beb05f5b2ac6879", size = 489329, upload-time = "2026-02-19T19:01:23.841Z" }, + { url = "https://files.pythonhosted.org/packages/da/aa/78ff4666d3855490bae87845a5983485e765e1f970da20adffa2937b241d/regex-2026.2.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3aa0944f1dc6e92f91f3b306ba7f851e1009398c84bfd370633182ee4fc26a64", size = 291308, upload-time = "2026-02-19T19:01:25.605Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/714384efcc07ae6beba528a541f6e99188c5cc1bc0295337f4e8a868296d/regex-2026.2.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c13228fbecb03eadbfd8f521732c5fda09ef761af02e920a3148e18ad0e09968", size = 289033, upload-time = "2026-02-19T19:01:27.243Z" }, + { url = "https://files.pythonhosted.org/packages/75/ec/6438a9344d2869cf5265236a06af1ca6d885e5848b6561e10629bc8e5a11/regex-2026.2.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d0e72703c60d68b18b27cde7cdb65ed2570ae29fb37231aa3076bfb6b1d1c13", size = 798798, upload-time = "2026-02-19T19:01:28.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/be/b1ce2d395e3fd2ce5f2fde2522f76cade4297cfe84cd61990ff48308749c/regex-2026.2.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46e69a4bf552e30e74a8aa73f473c87efcb7f6e8c8ece60d9fd7bf13d5c86f02", size = 864444, upload-time = "2026-02-19T19:01:30.933Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/a3406460c504f7136f140d9461960c25f058b0240e4424d6fb73c7a067ab/regex-2026.2.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8edda06079bd770f7f0cf7f3bba1a0b447b96b4a543c91fe0c142d034c166161", size = 912633, upload-time = "2026-02-19T19:01:32.744Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d9/e5dbef95008d84e9af1dc0faabbc34a7fbc8daa05bc5807c5cf86c2bec49/regex-2026.2.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cbc69eae834afbf634f7c902fc72ff3e993f1c699156dd1af1adab5d06b7fe7", size = 803718, upload-time = "2026-02-19T19:01:34.61Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e5/61d80132690a1ef8dc48e0f44248036877aebf94235d43f63a20d1598888/regex-2026.2.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bcf57d30659996ee5c7937999874504c11b5a068edc9515e6a59221cc2744dd1", size = 775975, upload-time = "2026-02-19T19:01:36.525Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/ae828b3b312c972cf228b634447de27237d593d61505e6ad84723f8eabba/regex-2026.2.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8e6e77cd92216eb489e21e5652a11b186afe9bdefca8a2db739fd6b205a9e0a4", size = 788129, upload-time = "2026-02-19T19:01:38.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/25/d74f34676f22bec401eddf0e5e457296941e10cbb2a49a571ca7a2c16e5a/regex-2026.2.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b9ab8dec42afefa6314ea9b31b188259ffdd93f433d77cad454cd0b8d235ce1c", size = 858818, upload-time = "2026-02-19T19:01:40.409Z" }, + { url = "https://files.pythonhosted.org/packages/1e/eb/0bc2b01a6b0b264e1406e5ef11cae3f634c3bd1a6e61206fd3227ce8e89c/regex-2026.2.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:294c0fb2e87c6bcc5f577c8f609210f5700b993151913352ed6c6af42f30f95f", size = 764186, upload-time = "2026-02-19T19:01:43.009Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/5fe5a630d0d99ecf0c3570f8905dafbc160443a2d80181607770086c9812/regex-2026.2.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c0924c64b082d4512b923ac016d6e1dcf647a3560b8a4c7e55cbbd13656cb4ed", size = 850363, upload-time = "2026-02-19T19:01:45.015Z" }, + { url = "https://files.pythonhosted.org/packages/c3/45/ef68d805294b01ec030cfd388724ba76a5a21a67f32af05b17924520cb0b/regex-2026.2.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:790dbf87b0361606cb0d79b393c3e8f4436a14ee56568a7463014565d97da02a", size = 790026, upload-time = "2026-02-19T19:01:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/40d3b66923dfc5aeba182f194f0ca35d09afe8c031a193e6ae46971a0a0e/regex-2026.2.19-cp313-cp313-win32.whl", hash = "sha256:43cdde87006271be6963896ed816733b10967baaf0e271d529c82e93da66675b", size = 266372, upload-time = "2026-02-19T19:01:49.469Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f2/39082e8739bfd553497689e74f9d5e5bb531d6f8936d0b94f43e18f219c0/regex-2026.2.19-cp313-cp313-win_amd64.whl", hash = "sha256:127ea69273485348a126ebbf3d6052604d3c7da284f797bba781f364c0947d47", size = 277253, upload-time = "2026-02-19T19:01:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c2/852b9600d53fb47e47080c203e2cdc0ac7e84e37032a57e0eaa37446033a/regex-2026.2.19-cp313-cp313-win_arm64.whl", hash = "sha256:5e56c669535ac59cbf96ca1ece0ef26cb66809990cda4fa45e1e32c3b146599e", size = 270505, upload-time = "2026-02-19T19:01:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a2/e0b4575b93bc84db3b1fab24183e008691cd2db5c0ef14ed52681fbd94dd/regex-2026.2.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93d881cab5afdc41a005dba1524a40947d6f7a525057aa64aaf16065cf62faa9", size = 492202, upload-time = "2026-02-19T19:01:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/24/b5/b84fec8cbb5f92a7eed2b6b5353a6a9eed9670fee31817c2da9eb85dc797/regex-2026.2.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:80caaa1ddcc942ec7be18427354f9d58a79cee82dea2a6b3d4fd83302e1240d7", size = 292884, upload-time = "2026-02-19T19:01:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/70/0c/fe89966dfae43da46f475362401f03e4d7dc3a3c955b54f632abc52669e0/regex-2026.2.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d793c5b4d2b4c668524cd1651404cfc798d40694c759aec997e196fe9729ec60", size = 291236, upload-time = "2026-02-19T19:01:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f7/bda2695134f3e63eb5cccbbf608c2a12aab93d261ff4e2fe49b47fabc948/regex-2026.2.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5100acb20648d9efd3f4e7e91f51187f95f22a741dcd719548a6cf4e1b34b3f", size = 807660, upload-time = "2026-02-19T19:02:01.632Z" }, + { url = "https://files.pythonhosted.org/packages/11/56/6e3a4bf5e60d17326b7003d91bbde8938e439256dec211d835597a44972d/regex-2026.2.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e3a31e94d10e52a896adaa3adf3621bd526ad2b45b8c2d23d1bbe74c7423007", size = 873585, upload-time = "2026-02-19T19:02:03.522Z" }, + { url = "https://files.pythonhosted.org/packages/35/5e/c90c6aa4d1317cc11839359479cfdd2662608f339e84e81ba751c8a4e461/regex-2026.2.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8497421099b981f67c99eba4154cf0dfd8e47159431427a11cfb6487f7791d9e", size = 915243, upload-time = "2026-02-19T19:02:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/7c/981ea0694116793001496aaf9524e5c99e122ec3952d9e7f1878af3a6bf1/regex-2026.2.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e7a08622f7d51d7a068f7e4052a38739c412a3e74f55817073d2e2418149619", size = 812922, upload-time = "2026-02-19T19:02:08.115Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/9eda82afa425370ffdb3fa9f3ea42450b9ae4da3ff0a4ec20466f69e371b/regex-2026.2.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8abe671cf0f15c26b1ad389bf4043b068ce7d3b1c5d9313e12895f57d6738555", size = 781318, upload-time = "2026-02-19T19:02:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d5/50f0bbe56a8199f60a7b6c714e06e54b76b33d31806a69d0703b23ce2a9e/regex-2026.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5a8f28dd32a4ce9c41758d43b5b9115c1c497b4b1f50c457602c1d571fa98ce1", size = 795649, upload-time = "2026-02-19T19:02:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/d039f081e44a8b0134d0bb2dd805b0ddf390b69d0b58297ae098847c572f/regex-2026.2.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:654dc41a5ba9b8cc8432b3f1aa8906d8b45f3e9502442a07c2f27f6c63f85db5", size = 868844, upload-time = "2026-02-19T19:02:14.043Z" }, + { url = "https://files.pythonhosted.org/packages/ef/53/e2903b79a19ec8557fe7cd21cd093956ff2dbc2e0e33969e3adbe5b184dd/regex-2026.2.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4a02faea614e7fdd6ba8b3bec6c8e79529d356b100381cec76e638f45d12ca04", size = 770113, upload-time = "2026-02-19T19:02:16.161Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e2/784667767b55714ebb4e59bf106362327476b882c0b2f93c25e84cc99b1a/regex-2026.2.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d96162140bb819814428800934c7b71b7bffe81fb6da2d6abc1dcca31741eca3", size = 854922, upload-time = "2026-02-19T19:02:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/9ef4356bd4aed752775bd18071034979b85f035fec51f3a4f9dea497a254/regex-2026.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c227f2922153ee42bbeb355fd6d009f8c81d9d7bdd666e2276ce41f53ed9a743", size = 799636, upload-time = "2026-02-19T19:02:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/cf/54/fcfc9287f20c5c9bd8db755aafe3e8cf4d99a6a3f1c7162ee182e0ca9374/regex-2026.2.19-cp313-cp313t-win32.whl", hash = "sha256:a178df8ec03011153fbcd2c70cb961bc98cbbd9694b28f706c318bee8927c3db", size = 268968, upload-time = "2026-02-19T19:02:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a0/ff24c6cb1273e42472706d277147fc38e1f9074a280fb6034b0fc9b69415/regex-2026.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:2c1693ca6f444d554aa246b592355b5cec030ace5a2729eae1b04ab6e853e768", size = 280390, upload-time = "2026-02-19T19:02:25.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/a3f6ad89d780ffdeebb4d5e2e3e30bd2ef1f70f6a94d1760e03dd1e12c60/regex-2026.2.19-cp313-cp313t-win_arm64.whl", hash = "sha256:c0761d7ae8d65773e01515ebb0b304df1bf37a0a79546caad9cbe79a42c12af7", size = 271643, upload-time = "2026-02-19T19:02:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e2/7ad4e76a6dddefc0d64dbe12a4d3ca3947a19ddc501f864a5df2a8222ddd/regex-2026.2.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:03d191a9bcf94d31af56d2575210cb0d0c6a054dbcad2ea9e00aa4c42903b919", size = 489306, upload-time = "2026-02-19T19:02:29.058Z" }, + { url = "https://files.pythonhosted.org/packages/14/95/ee1736135733afbcf1846c58671046f99c4d5170102a150ebb3dd8d701d9/regex-2026.2.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:516ee067c6c721d0d0bfb80a2004edbd060fffd07e456d4e1669e38fe82f922e", size = 291218, upload-time = "2026-02-19T19:02:31.083Z" }, + { url = "https://files.pythonhosted.org/packages/ef/08/180d1826c3d7065200a5168c6b993a44947395c7bb6e04b2c2a219c34225/regex-2026.2.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:997862c619994c4a356cb7c3592502cbd50c2ab98da5f61c5c871f10f22de7e5", size = 289097, upload-time = "2026-02-19T19:02:33.485Z" }, + { url = "https://files.pythonhosted.org/packages/28/93/0651924c390c5740f5f896723f8ddd946a6c63083a7d8647231c343912ff/regex-2026.2.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b9e1b8a7ebe2807cd7bbdf662510c8e43053a23262b9f46ad4fc2dfc9d204e", size = 799147, upload-time = "2026-02-19T19:02:35.669Z" }, + { url = "https://files.pythonhosted.org/packages/a7/00/2078bd8bcd37d58a756989adbfd9f1d0151b7ca4085a9c2a07e917fbac61/regex-2026.2.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6c8fb3b19652e425ff24169dad3ee07f99afa7996caa9dfbb3a9106cd726f49a", size = 865239, upload-time = "2026-02-19T19:02:38.012Z" }, + { url = "https://files.pythonhosted.org/packages/2a/13/75195161ec16936b35a365fa8c1dd2ab29fd910dd2587765062b174d8cfc/regex-2026.2.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50f1ee9488dd7a9fda850ec7c68cad7a32fa49fd19733f5403a3f92b451dcf73", size = 911904, upload-time = "2026-02-19T19:02:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/ac42f6012179343d1c4bd0ffee8c948d841cb32ea188d37e96d80527fcc9/regex-2026.2.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab780092b1424d13200aa5a62996e95f65ee3db8509be366437439cdc0af1a9f", size = 803518, upload-time = "2026-02-19T19:02:42.923Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/75a08e2269b007b9783f0f86aa64488e023141219cb5f14dc1e69cda56c6/regex-2026.2.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17648e1a88e72d88641b12635e70e6c71c5136ba14edba29bf8fc6834005a265", size = 775866, upload-time = "2026-02-19T19:02:45.189Z" }, + { url = "https://files.pythonhosted.org/packages/92/41/70e7d05faf6994c2ca7a9fcaa536da8f8e4031d45b0ec04b57040ede201f/regex-2026.2.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f914ae8c804c8a8a562fe216100bc156bfb51338c1f8d55fe32cf407774359a", size = 788224, upload-time = "2026-02-19T19:02:47.804Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/34a2dd601f9deb13c20545c674a55f4a05c90869ab73d985b74d639bac43/regex-2026.2.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c7e121a918bbee3f12ac300ce0a0d2f2c979cf208fb071ed8df5a6323281915c", size = 859682, upload-time = "2026-02-19T19:02:50.583Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/136db9a09a7f222d6e48b806f3730e7af6499a8cad9c72ac0d49d52c746e/regex-2026.2.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2fedd459c791da24914ecc474feecd94cf7845efb262ac3134fe27cbd7eda799", size = 764223, upload-time = "2026-02-19T19:02:52.777Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/bb947743c78a16df481fa0635c50aa1a439bb80b0e6dc24cd4e49c716679/regex-2026.2.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ea8dfc99689240e61fb21b5fc2828f68b90abf7777d057b62d3166b7c1543c4c", size = 850101, upload-time = "2026-02-19T19:02:55.87Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/e3bfe6e97a99f7393665926be02fef772da7f8aa59e50bc3134e4262a032/regex-2026.2.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fff45852160960f29e184ec8a5be5ab4063cfd0b168d439d1fc4ac3744bf29e", size = 789904, upload-time = "2026-02-19T19:02:58.523Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/7e2be6f00cea59d08761b027ad237002e90cac74b1607200ebaa2ba3d586/regex-2026.2.19-cp314-cp314-win32.whl", hash = "sha256:5390b130cce14a7d1db226a3896273b7b35be10af35e69f1cca843b6e5d2bb2d", size = 271784, upload-time = "2026-02-19T19:03:00.418Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f6/639911530335773e7ec60bcaa519557b719586024c1d7eaad1daf87b646b/regex-2026.2.19-cp314-cp314-win_amd64.whl", hash = "sha256:e581f75d5c0b15669139ca1c2d3e23a65bb90e3c06ba9d9ea194c377c726a904", size = 280506, upload-time = "2026-02-19T19:03:02.302Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ec/2582b56b4e036d46bb9b5d74a18548439ffa16c11cf59076419174d80f48/regex-2026.2.19-cp314-cp314-win_arm64.whl", hash = "sha256:7187fdee1be0896c1499a991e9bf7c78e4b56b7863e7405d7bb687888ac10c4b", size = 273557, upload-time = "2026-02-19T19:03:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/49/0b/f901cfeb4efd83e4f5c3e9f91a6de77e8e5ceb18555698aca3a27e215ed3/regex-2026.2.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5ec1d7c080832fdd4e150c6f5621fe674c70c63b3ae5a4454cebd7796263b175", size = 492196, upload-time = "2026-02-19T19:03:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/349b959e3da874e15eda853755567b4cde7e5309dbb1e07bfe910cfde452/regex-2026.2.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8457c1bc10ee9b29cdfd897ccda41dce6bde0e9abd514bcfef7bcd05e254d411", size = 292878, upload-time = "2026-02-19T19:03:10.272Z" }, + { url = "https://files.pythonhosted.org/packages/98/b0/9d81b3c2c5ddff428f8c506713737278979a2c476f6e3675a9c51da0c389/regex-2026.2.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cce8027010d1ffa3eb89a0b19621cdc78ae548ea2b49fea1f7bfb3ea77064c2b", size = 291235, upload-time = "2026-02-19T19:03:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/be7818df8691dbe9508c381ea2cc4c1153e4fdb1c4b06388abeaa93bd712/regex-2026.2.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11c138febb40546ff9e026dbbc41dc9fb8b29e61013fa5848ccfe045f5b23b83", size = 807893, upload-time = "2026-02-19T19:03:15.064Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b6/b898a8b983190cfa0276031c17beb73cfd1db07c03c8c37f606d80b655e2/regex-2026.2.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74ff212aa61532246bb3036b3dfea62233414b0154b8bc3676975da78383cac3", size = 873696, upload-time = "2026-02-19T19:03:17.848Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/126ba671d54f19080ec87cad228fb4f3cc387fff8c4a01cb4e93f4ff9d94/regex-2026.2.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d00c95a2b6bfeb3ea1cb68d1751b1dfce2b05adc2a72c488d77a780db06ab867", size = 915493, upload-time = "2026-02-19T19:03:20.343Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/550c84a1a1a7371867fe8be2bea7df55e797cbca4709974811410e195c5d/regex-2026.2.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311fcccb76af31be4c588d5a17f8f1a059ae8f4b097192896ebffc95612f223a", size = 813094, upload-time = "2026-02-19T19:03:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/29/fb/ba221d2fc76a27b6b7d7a60f73a7a6a7bac21c6ba95616a08be2bcb434b0/regex-2026.2.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77cfd6b5e7c4e8bf7a39d243ea05882acf5e3c7002b0ef4756de6606893b0ecd", size = 781583, upload-time = "2026-02-19T19:03:26.872Z" }, + { url = "https://files.pythonhosted.org/packages/26/f1/af79231301297c9e962679efc04a31361b58dc62dec1fc0cb4b8dd95956a/regex-2026.2.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6380f29ff212ec922b6efb56100c089251940e0526a0d05aa7c2d9b571ddf2fe", size = 795875, upload-time = "2026-02-19T19:03:29.223Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/1e1d76cb0a2d0a4f38a039993e1c5cd971ae50435d751c5bae4f10e1c302/regex-2026.2.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:655f553a1fa3ab8a7fd570eca793408b8d26a80bfd89ed24d116baaf13a38969", size = 868916, upload-time = "2026-02-19T19:03:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/a1c01da76dbcfed690855a284c665cc0a370e7d02d1bd635cf9ff7dd74b8/regex-2026.2.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:015088b8558502f1f0bccd58754835aa154a7a5b0bd9d4c9b7b96ff4ae9ba876", size = 770386, upload-time = "2026-02-19T19:03:33.972Z" }, + { url = "https://files.pythonhosted.org/packages/49/6f/94842bf294f432ff3836bfd91032e2ecabea6d284227f12d1f935318c9c4/regex-2026.2.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9e6693b8567a59459b5dda19104c4a4dbbd4a1c78833eacc758796f2cfef1854", size = 855007, upload-time = "2026-02-19T19:03:36.238Z" }, + { url = "https://files.pythonhosted.org/packages/ff/93/393cd203ca0d1d368f05ce12d2c7e91a324bc93c240db2e6d5ada05835f4/regex-2026.2.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4071209fd4376ab5ceec72ad3507e9d3517c59e38a889079b98916477a871868", size = 799863, upload-time = "2026-02-19T19:03:38.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/35afda99bd92bf1a5831e55a4936d37ea4bed6e34c176a3c2238317faf4f/regex-2026.2.19-cp314-cp314t-win32.whl", hash = "sha256:2905ff4a97fad42f2d0834d8b1ea3c2f856ec209837e458d71a061a7d05f9f01", size = 274742, upload-time = "2026-02-19T19:03:40.804Z" }, + { url = "https://files.pythonhosted.org/packages/ae/42/7edc3344dcc87b698e9755f7f685d463852d481302539dae07135202d3ca/regex-2026.2.19-cp314-cp314t-win_amd64.whl", hash = "sha256:64128549b600987e0f335c2365879895f860a9161f283b14207c800a6ed623d3", size = 284443, upload-time = "2026-02-19T19:03:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1739,6 +2133,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + [[package]] name = "shapely" version = "2.1.2" @@ -1813,6 +2296,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -1825,6 +2343,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tinydb" +version = "4.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/79/4af51e2bb214b6ea58f857c51183d92beba85b23f7ba61c983ab3de56c33/tinydb-4.8.2.tar.gz", hash = "sha256:f7dfc39b8d7fda7a1ca62a8dbb449ffd340a117c1206b68c50b1a481fb95181d", size = 32566, upload-time = "2024-10-12T15:24:01.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/17/853354204e1ca022d6b7d011ca7f3206c4f8faa3cc743e92609b49c1d83f/tinydb-4.8.2-py3-none-any.whl", hash = "sha256:f97030ee5cbc91eeadd1d7af07ab0e48ceb04aa63d4a983adbaca4cba16e86c3", size = 24888, upload-time = "2024-10-12T15:23:59.833Z" }, +] + [[package]] name = "titiler-core" version = "1.2.0" @@ -1897,6 +2433,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -1919,6 +2467,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, +] + [[package]] name = "xarray" version = "2025.12.0" @@ -1932,3 +2492,44 @@ sdist = { url = "https://files.pythonhosted.org/packages/d3/af/7b945f331ba8911fd wheels = [ { url = "https://files.pythonhosted.org/packages/d5/e4/62a677feefde05b12a70a4fc9bdc8558010182a801fbcab68cb56c2b0986/xarray-2025.12.0-py3-none-any.whl", hash = "sha256:9e77e820474dbbe4c6c2954d0da6342aa484e33adaa96ab916b15a786181e970", size = 1381742, upload-time = "2025-12-05T21:51:20.841Z" }, ] + +[[package]] +name = "xclim" +version = "0.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, + { name = "bottleneck" }, + { name = "cf-xarray" }, + { name = "cftime" }, + { name = "click" }, + { name = "dask", extra = ["array"] }, + { name = "filelock" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pint" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "xarray" }, + { name = "yamale" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/68/eb89c903ff70b1c2d0b2d0b9e8948bd13f1148cbb132a8ea077370db2fb5/xclim-0.60.0.tar.gz", hash = "sha256:9ebefc07d8b4e2ec90edbc98df575c141ba8d0ee6133ef3ec2c2a79cf51178d2", size = 897292, upload-time = "2026-01-23T16:38:00.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/9f/b199b8741f5a481c5ed15b7fe3f2e55d7cfc737c87b2f770465d33541cff/xclim-0.60.0-py3-none-any.whl", hash = "sha256:3140f530bec0e765df222877f5631b6caee241a44d8fcb25bfea3a90d15328b0", size = 382551, upload-time = "2026-01-23T16:37:58.237Z" }, +] + +[[package]] +name = "yamale" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/64/9e5de0e829920b848dcf5fe3ff64936d83cc7471babd264588b08bca97e0/yamale-6.1.0.tar.gz", hash = "sha256:fd435aa7b830c73e89a9ef548c0ace2d3d8dc3e5e180e6b57ff70b31495672fd", size = 42402, upload-time = "2025-11-20T16:52:30.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/fc/cbad39af7e761525077690ddff1ae19ace7e2f54552e90fb848a43a270fa/yamale-6.1.0-py3-none-any.whl", hash = "sha256:7e109c9d83e3a7e42703516cb2b70b9c7aa5b7a738019c4a6c202b6b0b9096c5", size = 58215, upload-time = "2025-11-20T16:52:28.806Z" }, +] diff --git a/webapp/app.js b/webapp/app.js new file mode 100644 index 0000000..0445dde --- /dev/null +++ b/webapp/app.js @@ -0,0 +1,345 @@ +const output = document.getElementById("output"); +const jobOutput = document.getElementById("job-output"); +const schedulesBody = document.getElementById("schedules-body"); +const workflowsBody = document.getElementById("workflows-body"); +const datasetSelect = document.getElementById("datasetId"); +const scheduleWorkflowSelect = document.getElementById("schedule-workflow-id"); +const parameterHelp = document.getElementById("parameter-help"); + +function write(message, data) { + const line = data ? `${message}\n${JSON.stringify(data, null, 2)}` : message; + output.textContent = line; +} + +async function request(path, options = {}) { + const response = await fetch(path, { + headers: { "Content-Type": "application/json", ...(options.headers || {}) }, + ...options, + }); + + const data = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error( + data?.detail?.description || response.statusText || "Request failed", + ); + } + + return data; +} + +async function loadCollections() { + const data = await request("/collections"); + const collections = data.collections || []; + datasetSelect.innerHTML = ""; + collections.forEach((collection) => { + const option = document.createElement("option"); + option.value = collection.id; + option.textContent = `${collection.id} — ${collection.title}`; + datasetSelect.appendChild(option); + }); + + if (collections.length > 0) { + datasetSelect.value = collections[0].id; + await loadParameterHint(collections[0].id); + } +} + +async function loadParameterHint(datasetId) { + try { + const cov = await request(`/collections/${datasetId}/coverage`); + const keys = Object.keys(cov.parameters || {}); + if (keys.length) { + parameterHelp.textContent = `Available parameters for ${datasetId}: ${keys.join(", ")}`; + document.getElementById("parameters").value = keys.join(","); + } else { + parameterHelp.textContent = `No parameter metadata found for ${datasetId}.`; + } + } catch (error) { + parameterHelp.textContent = `Could not load parameters: ${error.message}`; + } +} + +function scheduleRow(schedule) { + const tr = document.createElement("tr"); + tr.innerHTML = ` + ${schedule.name} + ${schedule.cron} (${schedule.timezone}) + ${schedule.enabled ? "yes" : "no"} + ${schedule.lastRunJobId || "-"} + + + + + + + `; + return tr; +} + +function workflowRow(workflow) { + const tr = document.createElement("tr"); + tr.innerHTML = ` + ${workflow.name} + ${workflow.steps.length} + ${(workflow.lastRunJobIds || []).join(", ") || "-"} + + + + + `; + return tr; +} + +async function loadWorkflows() { + const data = await request("/workflows"); + const workflows = data.workflows || []; + workflowsBody.innerHTML = ""; + scheduleWorkflowSelect.innerHTML = + ''; + + workflows.forEach((workflow) => { + workflowsBody.appendChild(workflowRow(workflow)); + + const option = document.createElement("option"); + option.value = workflow.workflowId; + option.textContent = `${workflow.name} (${workflow.steps.length} steps)`; + scheduleWorkflowSelect.appendChild(option); + }); +} + +async function createWorkflow(event) { + event.preventDefault(); + + const name = document.getElementById("workflow-name").value.trim(); + const stepsRaw = document.getElementById("workflow-steps").value.trim(); + if (!name) { + write("Workflow name is required"); + return; + } + + let steps; + try { + steps = JSON.parse(stepsRaw); + } catch (error) { + write(`Invalid workflow JSON: ${error.message}`); + return; + } + + if (!Array.isArray(steps) || steps.length === 0) { + write("Workflow steps must be a non-empty array"); + return; + } + + const invalidStep = steps.find( + (step) => + !step || typeof step !== "object" || !step.processId || !step.payload, + ); + if (invalidStep) { + write( + "Each step must include processId and payload. Optional fields: name", + invalidStep, + ); + return; + } + + const workflow = { + name, + steps, + }; + + const created = await request("/workflows", { + method: "POST", + body: JSON.stringify(workflow), + }); + + await loadWorkflows(); + write("Workflow saved", created); +} + +async function runWorkflow(workflowId) { + const result = await request(`/workflows/${workflowId}/run`, { + method: "POST", + }); + + await loadWorkflows(); + write("Workflow run submitted", { + workflowId, + jobIds: result.jobIds, + }); + + if ((result.jobIds || []).length > 0) { + document.getElementById("job-id").value = + result.jobIds[result.jobIds.length - 1]; + } +} + +async function deleteWorkflow(workflowId) { + await request(`/workflows/${workflowId}`, { + method: "DELETE", + }); + + await loadWorkflows(); + write("Workflow deleted", { workflowId }); +} + +async function loadSchedules() { + const data = await request("/schedules"); + schedulesBody.innerHTML = ""; + (data.schedules || []).forEach((schedule) => { + schedulesBody.appendChild(scheduleRow(schedule)); + }); +} + +async function createSchedule(event) { + event.preventDefault(); + + const workflowId = scheduleWorkflowSelect.value || null; + const payload = { + name: document.getElementById("name").value, + cron: document.getElementById("cron").value, + timezone: document.getElementById("timezone").value, + enabled: true, + }; + + if (workflowId) { + payload.workflowId = workflowId; + } else { + payload.inputs = { + datasetId: datasetSelect.value, + parameters: document + .getElementById("parameters") + .value.split(",") + .map((x) => x.trim()) + .filter(Boolean), + datetime: document.getElementById("datetime").value, + orgUnitLevel: Number(document.getElementById("orgUnitLevel").value), + aggregation: document.getElementById("aggregation").value, + dhis2: { + dataElementId: document.getElementById("dataElementId").value, + dryRun: document.getElementById("dryRun").checked, + }, + }; + } + + try { + const data = await request("/schedules", { + method: "POST", + body: JSON.stringify(payload), + }); + write("Schedule created", data); + await loadSchedules(); + } catch (error) { + write(`Failed to create schedule: ${error.message}`); + } +} + +async function runSchedule(scheduleId) { + const data = await request(`/schedules/${scheduleId}/run`, { + method: "POST", + }); + write("Schedule started", data); + document.getElementById("job-id").value = data.jobId; + await loadSchedules(); +} + +async function callbackSchedule(scheduleId) { + const token = document.getElementById("scheduler-token").value; + const headers = token ? { "X-Scheduler-Token": token } : {}; + const data = await request(`/schedules/${scheduleId}/callback`, { + method: "POST", + headers, + }); + write("Scheduler callback started", data); + document.getElementById("job-id").value = data.jobId; + await loadSchedules(); +} + +async function toggleSchedule(scheduleId) { + const current = await request(`/schedules/${scheduleId}`); + const data = await request(`/schedules/${scheduleId}`, { + method: "PATCH", + body: JSON.stringify({ enabled: !current.enabled }), + }); + write("Schedule updated", data); + await loadSchedules(); +} + +async function deleteSchedule(scheduleId) { + await request(`/schedules/${scheduleId}`, { method: "DELETE" }); + write("Schedule deleted"); + await loadSchedules(); +} + +async function checkJob() { + const jobId = document.getElementById("job-id").value.trim(); + if (!jobId) { + jobOutput.textContent = "Provide a jobId"; + return; + } + + try { + const data = await request(`/jobs/${jobId}`); + jobOutput.textContent = JSON.stringify(data, null, 2); + } catch (error) { + jobOutput.textContent = `Failed to load job: ${error.message}`; + } +} + +document + .getElementById("schedule-form") + .addEventListener("submit", createSchedule); +document + .getElementById("workflow-form") + .addEventListener("submit", createWorkflow); +document + .getElementById("refresh-schedules") + .addEventListener("click", loadSchedules); +document + .getElementById("refresh-workflows") + .addEventListener("click", loadWorkflows); +document.getElementById("check-job").addEventListener("click", checkJob); +datasetSelect.addEventListener("change", (event) => + loadParameterHint(event.target.value), +); + +workflowsBody.addEventListener("click", async (event) => { + const button = event.target.closest("button"); + if (!button) return; + + const action = button.dataset.workflowAction; + const workflowId = button.dataset.workflowId; + + try { + if (action === "run") await runWorkflow(workflowId); + if (action === "delete") await deleteWorkflow(workflowId); + } catch (error) { + write(`Workflow action failed: ${error.message}`); + } +}); + +schedulesBody.addEventListener("click", async (event) => { + const button = event.target.closest("button"); + if (!button) return; + + const action = button.dataset.action; + const scheduleId = button.dataset.id; + + try { + if (action === "run") await runSchedule(scheduleId); + if (action === "callback") await callbackSchedule(scheduleId); + if (action === "toggle") await toggleSchedule(scheduleId); + if (action === "delete") await deleteSchedule(scheduleId); + } catch (error) { + write(`Action failed: ${error.message}`); + } +}); + +(async () => { + try { + await loadCollections(); + await loadWorkflows(); + await loadSchedules(); + } catch (error) { + write(`Initialization failed: ${error.message}`); + } +})(); diff --git a/webapp/index.html b/webapp/index.html new file mode 100644 index 0000000..dd2337d --- /dev/null +++ b/webapp/index.html @@ -0,0 +1,196 @@ + + + + + + EO API Example App + + + +
+

EO API Example App

+

+ Minimal example for scheduled daily temperature/precipitation imports + into DHIS2. +

+ +
+

1) Custom Workflows

+
+
+ +
+ +
+ + +
+
+

+ Workflows are saved in your browser and run steps sequentially via the + API. +

+
+ +
+

2) Workflows

+ + + + + + + + + + +
NameStepsLast JobsActions
+
+ +
+

3) Configure Schedule

+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+

+
+ +
+

4) Schedules

+ + + + + + + + + + + +
NameCronEnabledLast JobActions
+
+ +
+

5) Job Status

+
+ + +
+

+      
+ +
+

Scheduler Callback (Optional)

+

+ Use this only if callback token protection is enabled in the API + (`EOAPI_SCHEDULER_TOKEN`). +

+ +
+ +
+

Messages

+

+      
+
+ + + + diff --git a/webapp/styles.css b/webapp/styles.css new file mode 100644 index 0000000..a753a7e --- /dev/null +++ b/webapp/styles.css @@ -0,0 +1,114 @@ +:root { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +body { + margin: 0; + background: #f5f7fa; + color: #1f2937; +} + +.container { + max-width: 1100px; + margin: 24px auto; + padding: 0 16px 24px; +} + +.subtitle { + margin-top: -8px; + color: #4b5563; +} + +.panel { + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; + margin-top: 16px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} + +label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 14px; +} + +input, +textarea, +select, +button { + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 8px; + font-size: 14px; +} + +textarea { + width: 100%; + resize: vertical; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +button { + cursor: pointer; + background: #111827; + color: white; +} + +button.danger { + background: #b91c1c; +} + +.actions { + margin-top: 12px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.checkbox-row { + flex-direction: row; + align-items: center; + margin-top: 24px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + border-bottom: 1px solid #e5e7eb; + text-align: left; + padding: 8px; + font-size: 14px; + vertical-align: top; +} + +td button { + margin-right: 6px; + margin-bottom: 6px; +} + +pre { + margin: 0; + background: #f3f4f6; + border-radius: 6px; + padding: 10px; + white-space: pre-wrap; + word-break: break-word; +} + +.hint { + color: #6b7280; + font-size: 13px; +}