mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Resolves the authenticated requester's email (ADR-0059), resolves + caps the property selection, pins the recipe (landlord_property_ids, recipient_email, package_name) onto one pre-created sub_task (raw SQL, dodging the mirror double-registration), and enqueues one message to the worker. Refuses double-submits (409) and oversized selections (400). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""The bulk-download route's view of the app-owned task's sub_task (ADR-0060).
|
|
|
|
Parameterised SQL rather than the SQLModel mirrors: importing the
|
|
``infrastructure.postgres`` mirror of ``sub_task`` into the FastAPI process
|
|
double-registers the table and crashes at import (the same constraint the
|
|
Modelling Run distributor works around — see ``modelling/run_tasks.py``).
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Optional
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import text
|
|
from sqlmodel import Session
|
|
|
|
|
|
class DocumentDownloadTasks:
|
|
def __init__(self, session: Session) -> None:
|
|
self._session = session
|
|
|
|
def already_distributed(self, task_id: UUID) -> bool:
|
|
"""Whether the task already has a sub_task — the 409 guard against a
|
|
double-submit re-triggering the same download."""
|
|
row = (
|
|
self._session.connection()
|
|
.execute(
|
|
text("SELECT 1 FROM sub_task WHERE task_id = :task_id LIMIT 1"),
|
|
{"task_id": task_id},
|
|
)
|
|
.first()
|
|
)
|
|
return row is not None
|
|
|
|
def resolve_selection(
|
|
self,
|
|
portfolio_id: int,
|
|
property_ids: Optional[list[int]],
|
|
select_all: bool,
|
|
) -> tuple[int, list[str]]:
|
|
"""Resolve the selection to (selected property count, the distinct
|
|
``landlord_property_id`` values that carry a downloadable identifier).
|
|
|
|
The count is the cap basis (property count, ADR-0060); files are matched
|
|
by ``landlord_property_id`` (the missing ``property_id`` on
|
|
``uploaded_files`` is a known future gap), so a selected property with a
|
|
null ``landlord_property_id`` contributes no files.
|
|
"""
|
|
if select_all:
|
|
rows = self._session.connection().execute(
|
|
text(
|
|
"SELECT id, landlord_property_id FROM property"
|
|
" WHERE portfolio_id = :portfolio_id"
|
|
),
|
|
{"portfolio_id": portfolio_id},
|
|
)
|
|
else:
|
|
rows = self._session.connection().execute(
|
|
text(
|
|
"SELECT id, landlord_property_id FROM property"
|
|
" WHERE portfolio_id = :portfolio_id AND id = ANY(:ids)"
|
|
),
|
|
{"portfolio_id": portfolio_id, "ids": property_ids or []},
|
|
)
|
|
resolved = rows.all()
|
|
landlord_property_ids = sorted(
|
|
{row[1] for row in resolved if row[1] is not None}
|
|
)
|
|
return len(resolved), landlord_property_ids
|
|
|
|
def create_download_subtask(
|
|
self, task_id: UUID, subtask_id: UUID, recipe: dict[str, Any]
|
|
) -> None:
|
|
"""Pre-create the single ``waiting`` sub_task under the app-owned task,
|
|
pinning the recipe (landlord_property_ids, recipient_email,
|
|
package_name) onto its inputs — the worker's reproducible instructions
|
|
(ADR-0055/0060)."""
|
|
now = datetime.now(timezone.utc)
|
|
self._session.connection().execute(
|
|
text(
|
|
"INSERT INTO sub_task (id, task_id, status, inputs, updated_at)"
|
|
" VALUES (:id, :task_id, 'waiting', :inputs, :updated_at)"
|
|
),
|
|
{
|
|
"id": subtask_id,
|
|
"task_id": task_id,
|
|
"inputs": json.dumps(recipe),
|
|
"updated_at": now,
|
|
},
|
|
)
|
|
self._session.commit()
|