mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
First successful live run surfaced three issues: 1. Email looked rubbish (a giant raw presigned URL). Now sends a proper HTML email with a 'Download documents' button plus a plain-text fallback, and a summary (N documents across M properties, expiry). Email delivery is now best-effort: a transport failure no longer loses an already-built package (the link is still on sub_task.outputs), and the SMTP connect has a 30s timeout so an unreachable SES endpoint fails fast instead of hanging to the 900s Lambda timeout. 2. Every folder was 'address unavailable (...)': the resolver read property.address, but these are HubSpot deals with no property row. It now uses the deal's dealname from hubspot_deal_data. 3. No logs / no idea why a run took ~9 minutes: the worker's INFO logs were dropped (Lambda root logger defaults to WARNING). The handler now raises the level, and the orchestrator logs per-phase timing and volume (gather+plan, packaged N files / X MB, upload, email, total) so the slow phase is visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""bulk_document_download Lambda (ADR-0060).
|
|
|
|
Attach-mode (ADR-0055) handler. The FastAPI route created the app-owned Task +
|
|
SubTask and pinned the recipe onto the SubTask's ``inputs``; this reads that
|
|
recipe, assembles the Download Package through the orchestrator, and returns the
|
|
result (presigned URL + skipped-document summary), which `TaskOrchestrator`
|
|
records on ``sub_task.outputs``. A wholly-empty selection raises
|
|
``SubTaskFailure`` from the orchestrator — recorded, not retried.
|
|
|
|
PostgresConfig-only (POSTGRES_*), like the bulk_upload_finaliser Lambda. Source
|
|
documents are read from each row's own ``s3_file_bucket``; the ZIP is written to
|
|
the dedicated ``DOCUMENT_EXPORTS_BUCKET``. SES SMTP settings arrive as env vars
|
|
(terraform bakes the
|
|
Secrets Manager IAM-user credentials in).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any
|
|
|
|
import boto3
|
|
|
|
from applications.bulk_document_download.bulk_document_download_trigger_body import (
|
|
BulkDocumentDownloadTriggerBody,
|
|
)
|
|
from domain.tasks.tasks import Source
|
|
from infrastructure.email.ses_smtp_email_sender import SesSmtpEmailSender
|
|
from infrastructure.postgres.config import PostgresConfig
|
|
from infrastructure.postgres.engine import make_engine, make_session
|
|
from infrastructure.s3.s3_client import S3Client
|
|
from infrastructure.s3.s3_document_downloader import S3DocumentDownloader
|
|
from orchestration.bulk_document_download_orchestrator import (
|
|
BulkDocumentDownloadOrchestrator,
|
|
)
|
|
from repositories.property.landlord_address_resolver import LandlordAddressResolver
|
|
from repositories.tasks.subtask_postgres_repository import SubTaskPostgresRepository
|
|
from repositories.uploaded_file.uploaded_file_postgres_repository import (
|
|
UploadedFilePostgresRepository,
|
|
)
|
|
from utilities.aws_lambda.task_handler import task_handler
|
|
|
|
# Surface the worker's progress logs: AWS Lambda's root logger defaults to
|
|
# WARNING, which drops the orchestrator's INFO stage/timing lines. Raise it so a
|
|
# slow or failing run is observable in CloudWatch.
|
|
logging.getLogger().setLevel(logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# The Download Package link is valid for 60 minutes (ADR-0060).
|
|
_URL_TTL_SECONDS = 3600
|
|
|
|
|
|
def _email_sender() -> SesSmtpEmailSender:
|
|
return SesSmtpEmailSender(
|
|
host=os.environ["SES_SMTP_HOST"],
|
|
port=int(os.environ["SES_SMTP_PORT"]),
|
|
username=os.environ["SES_SMTP_USERNAME"],
|
|
password=os.environ["SES_SMTP_PASSWORD"],
|
|
from_address=os.environ["SES_SMTP_FROM_ADDRESS"],
|
|
)
|
|
|
|
|
|
@task_handler(task_source="bulk_document_download", source=Source.PORTFOLIO)
|
|
def handler(body: dict[str, Any], context: Any) -> dict[str, Any]:
|
|
trigger = BulkDocumentDownloadTriggerBody.model_validate(body)
|
|
engine = make_engine(PostgresConfig.from_env(os.environ))
|
|
session = make_session(engine)
|
|
try:
|
|
subtask = SubTaskPostgresRepository(session).get(trigger.subtask_id)
|
|
recipe: dict[str, Any] = subtask.inputs or {}
|
|
landlord_property_ids: list[str] = [
|
|
str(x) for x in recipe["landlord_property_ids"]
|
|
]
|
|
recipient_email: str = str(recipe["recipient_email"])
|
|
package_name: str = str(recipe["package_name"])
|
|
|
|
logger.info(
|
|
"bulk_document_download: starting run for %d properties (subtask=%s)",
|
|
len(landlord_property_ids),
|
|
trigger.subtask_id,
|
|
)
|
|
|
|
boto_s3: Any = boto3.client("s3") # pyright: ignore[reportUnknownMemberType]
|
|
orchestrator = BulkDocumentDownloadOrchestrator(
|
|
uploaded_files=UploadedFilePostgresRepository(session),
|
|
addresses=LandlordAddressResolver(session, landlord_property_ids),
|
|
documents=S3DocumentDownloader(boto_s3),
|
|
packages=S3Client(boto_s3, os.environ["DOCUMENT_EXPORTS_BUCKET"]),
|
|
email=_email_sender(),
|
|
url_ttl_seconds=_URL_TTL_SECONDS,
|
|
)
|
|
result = orchestrator.run(
|
|
landlord_property_ids=landlord_property_ids,
|
|
recipient_email=recipient_email,
|
|
package_name=package_name,
|
|
)
|
|
finally:
|
|
session.close()
|
|
|
|
return {
|
|
"presigned_url": result.presigned_url,
|
|
"package_s3_key": result.package_s3_key,
|
|
"included": result.included,
|
|
"skipped": [
|
|
{
|
|
"landlord_property_id": skip.landlord_property_id,
|
|
"s3_key": skip.s3_key,
|
|
"reason": skip.reason,
|
|
}
|
|
for skip in result.skipped
|
|
],
|
|
}
|