UPDATE
This commit is contained in:
+203
-16
@@ -1,4 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.paginator import EmptyPage, Paginator
|
||||
@@ -49,6 +50,21 @@ from .serializers import (
|
||||
from .tasks import run_remote_sensing_analysis_task
|
||||
|
||||
MAX_REMOTE_SENSING_PAGE_SIZE = 200
|
||||
REMOTE_SENSING_RUN_STAGE_ORDER = (
|
||||
"queued",
|
||||
"running",
|
||||
"preparing_analysis_grid",
|
||||
"analysis_grid_ready",
|
||||
"analysis_cells_selected",
|
||||
"using_cached_observations",
|
||||
"fetching_remote_metrics",
|
||||
"remote_metrics_fetched",
|
||||
"observations_persisted",
|
||||
"clustering_completed",
|
||||
"completed",
|
||||
"failed",
|
||||
"retrying",
|
||||
)
|
||||
|
||||
SoilLocationPayloadSerializer = inline_serializer(
|
||||
name="SoilLocationPayloadSerializer",
|
||||
@@ -636,13 +652,19 @@ class RemoteSensingRunStatusView(APIView):
|
||||
def _build_remote_sensing_run_status_payload(run: RemoteSensingRun, *, page: int, page_size: int) -> dict:
|
||||
run_data = RemoteSensingRunSerializer(run).data
|
||||
task_id = (run.metadata or {}).get("task_id")
|
||||
task_data = _build_remote_sensing_task_payload(run)
|
||||
effective_status = _apply_live_retry_state_override(run_data, task_data)
|
||||
status_payload = {
|
||||
"status": effective_status or run_data["status_label"],
|
||||
"source": "database",
|
||||
"run": run_data,
|
||||
"task_id": task_id,
|
||||
"task": task_data,
|
||||
}
|
||||
if run.status in {RemoteSensingRun.STATUS_PENDING, RemoteSensingRun.STATUS_RUNNING}:
|
||||
return {
|
||||
"status": run_data["status_label"],
|
||||
"source": "database",
|
||||
"run": run_data,
|
||||
"task_id": task_id,
|
||||
}
|
||||
return status_payload
|
||||
if run.status == RemoteSensingRun.STATUS_FAILURE:
|
||||
return status_payload
|
||||
|
||||
location = _get_location_by_lat_lon(run.soil_location.latitude, run.soil_location.longitude, prefetch=True)
|
||||
observations = _get_remote_sensing_observations(
|
||||
@@ -654,10 +676,7 @@ def _build_remote_sensing_run_status_payload(run: RemoteSensingRun, *, page: int
|
||||
subdivision_result = getattr(run, "subdivision_result", None)
|
||||
|
||||
response_payload = {
|
||||
"status": run_data["status_label"],
|
||||
"source": "database",
|
||||
"run": run_data,
|
||||
"task_id": task_id,
|
||||
**status_payload,
|
||||
"location": SoilLocationResponseSerializer(location).data,
|
||||
"block_code": run.block_code,
|
||||
"chunk_size_sqm": run.chunk_size_sqm,
|
||||
@@ -705,6 +724,180 @@ def _build_remote_sensing_run_status_payload(run: RemoteSensingRun, *, page: int
|
||||
return response_payload
|
||||
|
||||
|
||||
def _get_remote_sensing_async_result(task_id: str):
|
||||
try:
|
||||
from celery.result import AsyncResult
|
||||
except ImportError: # pragma: no cover - fallback when Celery is absent
|
||||
return None
|
||||
|
||||
try:
|
||||
return AsyncResult(task_id)
|
||||
except Exception: # pragma: no cover - depends on Celery backend configuration
|
||||
return None
|
||||
|
||||
|
||||
def _serialize_task_value(value):
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _serialize_task_value(item) for key, item in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_serialize_task_value(item) for item in value]
|
||||
return str(value)
|
||||
|
||||
|
||||
def _build_remote_sensing_task_payload(run: RemoteSensingRun) -> dict:
|
||||
metadata = dict(run.metadata or {})
|
||||
timestamps = dict(metadata.get("timestamps") or {})
|
||||
stage_details = dict(metadata.get("stage_details") or {})
|
||||
current_stage = metadata.get("stage")
|
||||
failed_stage = metadata.get("failed_stage")
|
||||
task_payload = {
|
||||
"current_stage": current_stage,
|
||||
"current_stage_details": stage_details.get(current_stage, {}),
|
||||
"timestamps": timestamps,
|
||||
"stages": _build_remote_sensing_stage_entries(
|
||||
current_stage=current_stage,
|
||||
stage_details=stage_details,
|
||||
timestamps=timestamps,
|
||||
run_status=run.status,
|
||||
),
|
||||
}
|
||||
if failed_stage:
|
||||
task_payload["failed_stage"] = failed_stage
|
||||
|
||||
metric_progress = (stage_details.get("fetching_remote_metrics") or {}).get("metric_progress")
|
||||
if metric_progress:
|
||||
task_payload["metric_progress"] = metric_progress
|
||||
|
||||
retry_context = stage_details.get("retrying")
|
||||
if retry_context:
|
||||
task_payload["retry"] = retry_context
|
||||
task_payload["last_error"] = retry_context.get("last_error")
|
||||
|
||||
failure_reason = None
|
||||
if metadata.get("stage") == "failed" or run.status == RemoteSensingRun.STATUS_FAILURE:
|
||||
failure_reason = metadata.get("failure_reason") or run.error_message
|
||||
if failure_reason:
|
||||
task_payload["failure_reason"] = failure_reason
|
||||
|
||||
task_id = metadata.get("task_id")
|
||||
celery_payload = _build_remote_sensing_celery_payload(str(task_id)) if task_id else None
|
||||
if celery_payload is not None:
|
||||
task_payload["celery"] = celery_payload
|
||||
|
||||
return task_payload
|
||||
|
||||
|
||||
def _apply_live_retry_state_override(run_data: dict[str, Any], task_data: dict[str, Any]) -> str | None:
|
||||
celery_payload = task_data.get("celery") or {}
|
||||
if celery_payload.get("state") != "RETRY":
|
||||
return None
|
||||
|
||||
retry_context = dict(task_data.get("retry") or {})
|
||||
if not retry_context:
|
||||
retry_context = {
|
||||
"retry_count": None,
|
||||
"retry_delay_seconds": None,
|
||||
"last_error": task_data.get("failure_reason") or celery_payload.get("info"),
|
||||
"failed_stage": task_data.get("failed_stage"),
|
||||
"failed_stage_details": (
|
||||
task_data.get("current_stage_details", {})
|
||||
if task_data.get("current_stage") == "failed"
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
||||
task_data["retry"] = retry_context
|
||||
task_data["last_error"] = retry_context.get("last_error") or celery_payload.get("info")
|
||||
task_data["current_stage"] = "retrying"
|
||||
task_data["current_stage_details"] = retry_context
|
||||
task_data.pop("failure_reason", None)
|
||||
_upsert_retrying_stage_entry(task_data, retry_context)
|
||||
|
||||
run_data["status_label"] = "retrying"
|
||||
run_data["pipeline_status"] = "retrying"
|
||||
run_data["stage"] = "retrying"
|
||||
return "retrying"
|
||||
|
||||
|
||||
def _upsert_retrying_stage_entry(task_data: dict[str, Any], retry_context: dict[str, Any]) -> None:
|
||||
stages = list(task_data.get("stages") or [])
|
||||
retrying_entry = {
|
||||
"name": "retrying",
|
||||
"status": "running",
|
||||
"entered_at": (task_data.get("timestamps") or {}).get("retrying_at"),
|
||||
"details": retry_context,
|
||||
}
|
||||
for index, entry in enumerate(stages):
|
||||
if entry.get("name") == "retrying":
|
||||
stages[index] = retrying_entry
|
||||
task_data["stages"] = stages
|
||||
return
|
||||
stages.append(retrying_entry)
|
||||
task_data["stages"] = stages
|
||||
|
||||
|
||||
def _build_remote_sensing_stage_entries(
|
||||
*,
|
||||
current_stage: str | None,
|
||||
stage_details: dict,
|
||||
timestamps: dict,
|
||||
run_status: str,
|
||||
) -> list[dict]:
|
||||
stage_names = []
|
||||
for stage_name in REMOTE_SENSING_RUN_STAGE_ORDER:
|
||||
if stage_name == current_stage or stage_name in stage_details or f"{stage_name}_at" in timestamps:
|
||||
stage_names.append(stage_name)
|
||||
if current_stage and current_stage not in stage_names:
|
||||
stage_names.append(current_stage)
|
||||
|
||||
entries = []
|
||||
for stage_name in stage_names:
|
||||
if run_status == RemoteSensingRun.STATUS_FAILURE and stage_name == current_stage:
|
||||
stage_status = "failed"
|
||||
elif stage_name == current_stage and run_status == RemoteSensingRun.STATUS_PENDING:
|
||||
stage_status = "pending"
|
||||
elif stage_name == current_stage and run_status == RemoteSensingRun.STATUS_RUNNING:
|
||||
stage_status = "running"
|
||||
else:
|
||||
stage_status = "completed"
|
||||
entries.append(
|
||||
{
|
||||
"name": stage_name,
|
||||
"status": stage_status,
|
||||
"entered_at": timestamps.get(f"{stage_name}_at"),
|
||||
"details": stage_details.get(stage_name, {}),
|
||||
}
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def _build_remote_sensing_celery_payload(task_id: str) -> dict | None:
|
||||
async_result = _get_remote_sensing_async_result(task_id)
|
||||
if async_result is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"state": str(async_result.state),
|
||||
"ready": bool(async_result.ready()),
|
||||
"successful": bool(async_result.successful()) if async_result.ready() else False,
|
||||
"failed": bool(async_result.failed()) if async_result.ready() else False,
|
||||
}
|
||||
except Exception: # pragma: no cover - depends on Celery backend configuration
|
||||
return None
|
||||
|
||||
info = getattr(async_result, "info", None)
|
||||
if info not in (None, {}):
|
||||
payload["info"] = _serialize_task_value(info)
|
||||
|
||||
if async_result.failed():
|
||||
payload["error"] = _serialize_task_value(async_result.result)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _get_location_by_lat_lon(lat, lon, *, prefetch: bool = False):
|
||||
lat_rounded = round(lat, 6)
|
||||
lon_rounded = round(lon, 6)
|
||||
@@ -894,8 +1087,6 @@ def _build_remote_sensing_summary(observations):
|
||||
ndwi_mean=Avg("ndwi"),
|
||||
lst_c_mean=Avg("lst_c"),
|
||||
soil_vv_db_mean=Avg("soil_vv_db"),
|
||||
dem_m_mean=Avg("dem_m"),
|
||||
slope_deg_mean=Avg("slope_deg"),
|
||||
)
|
||||
summary = {
|
||||
"cell_count": observations.count(),
|
||||
@@ -903,8 +1094,6 @@ def _build_remote_sensing_summary(observations):
|
||||
"ndwi_mean": _round_or_none(aggregates.get("ndwi_mean")),
|
||||
"lst_c_mean": _round_or_none(aggregates.get("lst_c_mean")),
|
||||
"soil_vv_db_mean": _round_or_none(aggregates.get("soil_vv_db_mean")),
|
||||
"dem_m_mean": _round_or_none(aggregates.get("dem_m_mean")),
|
||||
"slope_deg_mean": _round_or_none(aggregates.get("slope_deg_mean")),
|
||||
}
|
||||
return summary
|
||||
|
||||
@@ -916,8 +1105,6 @@ def _empty_remote_sensing_summary():
|
||||
"ndwi_mean": None,
|
||||
"lst_c_mean": None,
|
||||
"soil_vv_db_mean": None,
|
||||
"dem_m_mean": None,
|
||||
"slope_deg_mean": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user