Model/applications/bulk_document_download/handler.py
Khalim Conn-Kowlessar 8c0b015923 bulk_document_download Lambda: attach-mode handler, trigger body, packaging 🟩
Reads the recipe from sub_task.inputs, assembles the Download Package via the
orchestrator (source docs from each row's bucket -> ZIP -> the dedicated
DOCUMENT_EXPORTS_BUCKET), returns the presigned URL + skip summary for
sub_task.outputs. Dockerfile/requirements mirror bulk_upload_finaliser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:51:51 +00:00

100 lines
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 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_bytes_reader import S3DocumentBytesReader
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
# 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"])
boto_s3: Any = boto3.client("s3") # pyright: ignore[reportUnknownMemberType]
orchestrator = BulkDocumentDownloadOrchestrator(
uploaded_files=UploadedFilePostgresRepository(session),
addresses=LandlordAddressResolver(session, landlord_property_ids),
documents=S3DocumentBytesReader(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
],
}