mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
@task_handler never built or passed cloud_logs_url, so every app using it (incl. modelling_e2e) ran run_subtask with the None default and the CloudWatch deep-link was never saved onto the SubTask. @subtask_handler did this correctly. Extract the URL builder into a shared utilities/aws_lambda/cloud_logs.py (public cloudwatch_url()), use it from both handlers, and pass the URL into run_subtask from @task_handler. Add regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""@subtask_handler decorator for Lambdas that operate on existing SubTasks.
|
|
|
|
Translates an AWS Lambda invocation (SQS-shaped or direct) into
|
|
TaskOrchestrator.run_subtask(...) calls.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from contextlib import AbstractContextManager
|
|
from functools import wraps
|
|
from typing import Any, Callable, Optional, cast
|
|
|
|
from utilities.aws_lambda.cloud_logs import cloudwatch_url
|
|
from utilities.aws_lambda.default_orchestrator import default_orchestrator
|
|
from utilities.aws_lambda.subtask_trigger_body import SubtaskTriggerBody
|
|
from orchestration.task_orchestrator import TaskOrchestrator
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.INFO)
|
|
|
|
OrchestratorCM = Callable[[], AbstractContextManager[TaskOrchestrator]]
|
|
|
|
|
|
def subtask_handler(
|
|
*,
|
|
orchestrator_cm: Optional[OrchestratorCM] = None,
|
|
pass_task_orchestrator: bool = True,
|
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
"""Run the wrapped function as the body of an existing SubTask.
|
|
|
|
For each record, validates the body via SubtaskTriggerBody (must contain
|
|
task_id and sub_task_id), then runs the function inside
|
|
orchestrator.run_subtask(...). The orchestrator owns the start/complete/
|
|
fail lifecycle and cascades status into the parent Task. On failure the
|
|
underlying exception propagates after the SubTask is marked FAILED.
|
|
"""
|
|
factory = orchestrator_cm or default_orchestrator
|
|
|
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
_wants_orchestrator = pass_task_orchestrator
|
|
|
|
@wraps(func)
|
|
def wrapper(event: dict[str, Any], context: Any) -> None:
|
|
cloud_logs_url = cloudwatch_url()
|
|
with factory() as orchestrator:
|
|
for record in _records(event):
|
|
body = _parse_body(record)
|
|
trigger = SubtaskTriggerBody.model_validate(body)
|
|
logger.info("Running subtask %s", trigger.sub_task_id)
|
|
|
|
def _work_with(
|
|
_body: dict[str, Any] = body,
|
|
_o: TaskOrchestrator = orchestrator,
|
|
) -> Any:
|
|
return func(_body, context, _o)
|
|
|
|
def _work_without(_body: dict[str, Any] = body) -> Any:
|
|
return func(_body, context)
|
|
|
|
work: Callable[[], Any] = (
|
|
_work_with if _wants_orchestrator else _work_without
|
|
)
|
|
try:
|
|
orchestrator.run_subtask(
|
|
trigger.sub_task_id,
|
|
work=work,
|
|
cloud_logs_url=cloud_logs_url,
|
|
)
|
|
except Exception:
|
|
logger.exception("Subtask %s failed", trigger.sub_task_id)
|
|
raise
|
|
logger.info("Subtask %s completed", trigger.sub_task_id)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
def _parse_body(record: dict[str, Any]) -> dict[str, Any]:
|
|
raw = record.get("body", record)
|
|
if isinstance(raw, str):
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
return cast(dict[str, Any], parsed) if isinstance(parsed, dict) else {}
|
|
if isinstance(raw, dict):
|
|
return cast(dict[str, Any], raw)
|
|
return {}
|
|
|
|
|
|
def _records(event: dict[str, Any]) -> list[dict[str, Any]]:
|
|
raw_records = event.get("Records")
|
|
if isinstance(raw_records, list):
|
|
return [r for r in cast(list[Any], raw_records) if isinstance(r, dict)]
|
|
return [event]
|