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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/climate_api/data/datasets/chirps3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
sync_execution: append
sync_availability:
latest_available_function: climate_api.providers.availability.chirps3_daily_latest_available
ingestion:
ingestion:
function: dhis2eo.data.chc.chirps3.daily.download
units: mm
resolution: 5 km x 5 km
Expand All @@ -16,4 +16,4 @@
display:
colormap: blues
range: [0.0, 20.0]
nodata: 0.0
nodata: -9999.0
2 changes: 1 addition & 1 deletion src/climate_api/data/datasets/era5_land.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
source_url: https://earthdatahub.destine.eu/collections/era5/datasets/reanalysis-era5-land
display:
colormap: rdbu_r
range: [15.0, 40.0]
range: [233.0, 313.0]

- id: era5land_precipitation_hourly
name: Total precipitation (ERA5-Land)
Expand Down
4 changes: 2 additions & 2 deletions src/climate_api/data/datasets/worldpop.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
source: WorldPop Global2
source_url: https://hub.worldpop.org/project/categories?id=3
display:
colormap: viridis
range: [0.0, 1000.0]
colormap: reds
range: [0.0, 25.0]
nodata: 0.0
87 changes: 86 additions & 1 deletion src/climate_api/system/routes.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Root API endpoints."""

import sys
import urllib.parse
from importlib.metadata import version as _pkg_version

from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, JSONResponse, Response
from starlette.responses import RedirectResponse

from .schemas import AppInfo, HealthStatus, Status
from .templates import ROOT_RESPONSES, app_version, render_landing, render_maps, root_json, wants_json
from .templates import ROOT_RESPONSES, app_version, render_landing, render_manage, render_maps, root_json, wants_json

router = APIRouter()

Expand All @@ -28,6 +30,89 @@ def maps(request: Request) -> HTMLResponse:
return HTMLResponse(render_maps(base))


@router.get("/manage", response_class=HTMLResponse, include_in_schema=False)
def manage(
request: Request,
message: str | None = None,
error: str | None = None,
) -> HTMLResponse:
"""Return the management interface for ingestion and sync operations."""
base = str(request.base_url).rstrip("/")
return HTMLResponse(render_manage(app_version, base, message=message, error=error))


@router.post("/manage/ingest", include_in_schema=False)
async def manage_ingest(request: Request) -> RedirectResponse:
"""Handle ingest form submission and redirect to the management page."""
from fastapi import HTTPException

from climate_api.data_registry.services.datasets import get_dataset
from climate_api.extents.services import get_extent
from climate_api.ingestions.services import create_artifact

base = str(request.base_url).rstrip("/")
try:
form = await request.form()
dataset_id = str(form.get("dataset_id", ""))
start = str(form.get("start", ""))
end = str(form.get("end", "")) or None
publish = "publish" in form
overwrite = "overwrite" in form

template = get_dataset(dataset_id)
if template is None:
msg = urllib.parse.quote(f"Dataset template '{dataset_id}' not found")
return RedirectResponse(f"{base}/manage?error={msg}", status_code=303)

extent = get_extent()
resolved_bbox = list(extent["bbox"]) if extent else None
extent_id = extent["id"] if extent else None
country_code = extent.get("country_code") if extent else None

create_artifact(
dataset=template,
start=start,
end=end,
extent_id=extent_id,
bbox=resolved_bbox,
country_code=country_code,
overwrite=overwrite,
prefer_zarr=True,
publish=publish,
)
name = urllib.parse.quote(template.get("name", dataset_id))
return RedirectResponse(f"{base}/manage?message=Ingested+{name}", status_code=303)
except HTTPException as exc:
msg = urllib.parse.quote(str(exc.detail))
return RedirectResponse(f"{base}/manage?error={msg}", status_code=303)
except Exception as exc:
msg = urllib.parse.quote(str(exc))
return RedirectResponse(f"{base}/manage?error={msg}", status_code=303)


@router.post("/manage/sync", include_in_schema=False)
async def manage_sync(request: Request) -> RedirectResponse:
"""Handle sync form submission and redirect to the management page."""
from fastapi import HTTPException

from climate_api.ingestions.services import sync_dataset

base = str(request.base_url).rstrip("/")
try:
form = await request.form()
dataset_id = str(form.get("dataset_id", ""))
publish = "publish" in form

sync_dataset(dataset_id=dataset_id, end=None, prefer_zarr=True, publish=publish)
return RedirectResponse(f"{base}/manage?message=Sync+completed", status_code=303)
except HTTPException as exc:
msg = urllib.parse.quote(str(exc.detail))
return RedirectResponse(f"{base}/manage?error={msg}", status_code=303)
except Exception as exc:
msg = urllib.parse.quote(str(exc))
return RedirectResponse(f"{base}/manage?error={msg}", status_code=303)


@router.get("/health")
def health() -> HealthStatus:
"""Return health status for container health checks."""
Expand Down
58 changes: 46 additions & 12 deletions src/climate_api/system/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import importlib.resources
import logging
from datetime import date
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _pkg_version
from typing import Any

import jinja2
from fastapi import Request

from climate_api.data_registry.services import datasets as registry_datasets
from climate_api.extents.services import get_extent
from climate_api.ingestions.services import list_datasets

Expand Down Expand Up @@ -96,26 +98,58 @@ def wants_json(request: Request) -> bool:

def render_maps(base: str) -> str:
"""Render the map viewer page."""
return get_template("maps.html").render(base=base)
return get_template("map-viewer.html").render(base=base)


def render_landing(version: str, base: str) -> str:
"""Render the root landing page with live instance status."""
def _load_extent() -> dict[str, Any] | None:
try:
extent: dict[str, Any] | None = get_extent()
return get_extent()
except ValueError:
extent = None
return None
except Exception:
_log.exception("Unexpected error loading extent for landing page")
extent = None
_log.exception("Unexpected error loading extent")
return None


def _load_templates() -> list[dict[str, Any]]:
try:
datasets = list_datasets().items
return registry_datasets.list_datasets()
except Exception:
_log.exception("Unexpected error loading datasets for landing page")
datasets = []
_log.exception("Unexpected error loading dataset templates")
return []


def _load_datasets() -> list[Any]:
try:
return list_datasets().items
except Exception:
_log.exception("Unexpected error loading datasets")
return []


def render_landing(version: str, base: str) -> str:
"""Render the root landing page with live instance status."""
return get_template("landing_page.html").render(
version=version,
base=base,
extent=extent,
datasets=datasets,
extent=_load_extent(),
datasets=_load_datasets(),
templates=_load_templates(),
)


def render_manage(version: str, base: str, message: str | None = None, error: str | None = None) -> str:
"""Render the management page."""
today = date.today().isoformat()
year_ago = date.today().replace(year=date.today().year - 1).isoformat()
return get_template("manage.html").render(
version=version,
base=base,
extent=_load_extent(),
templates=_load_templates(),
datasets=_load_datasets(),
today=today,
year_ago=year_ago,
message=message,
error=error,
)
43 changes: 43 additions & 0 deletions src/climate_api/templates/landing_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,53 @@ <h2>Datasets <span class="badge">{{ datasets | length }}</span></h2>
{% endif %}
</div>

<!-- Available templates -->
<div class="card">
<h2>Available dataset templates <span class="badge">{{ templates | length }}</span></h2>
{% if templates %}
<table>
<thead>
<tr>
<th>Name</th>
<th>Variable</th>
<th>Period</th>
<th>Source</th>
</tr>
</thead>
<tbody>
{% for t in templates %}
<tr>
<td>{{ t.name }}</td>
<td>{{ t.variable }}</td>
<td>{{ t.period_type }}</td>
<td>
{% if t.source_url %}
<a href="{{ t.source_url }}">{{ t.source or t.id }}</a>
{% else %}
{{ t.source or '—' }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="note">No dataset templates found.</p>
{% endif %}
</div>

<!-- Links -->
<div class="card">
<h2>Explore</h2>
<ul class="links-list">
<li>
<span class="link-label"
><a href="{{ base }}/manage">Manage</a></span
>
<span class="link-desc"
>Ingest and sync datasets without using the API directly</span
>
</li>
<li>
<span class="link-label"
><a href="{{ base }}/docs">API documentation</a></span
Expand Down
Loading