feat(12.1-02): centralize listing freshness gate — apply_listing_and_finalize

- Add ListingResult dataclass to cloud_items.py
- Add apply_listing_and_finalize() — single shared gate for complete/incomplete semantics
- complete=True: upserts, soft-deletes unseen children, marks fresh, advances last_refreshed_at
- complete=False: upserts seen items only, never deletes unseen, preserves prior timestamp, warns
- browse.py: replace unconditional update_folder_state('fresh') with apply_listing_and_finalize
- cloud_tasks.py: replace unconditional fresh transition with apply_listing_and_finalize
- Celery task returns structured warning status on incomplete (no credentials/provider item names)
- Fix timezone naive/aware comparison in test (SQLite strips tzinfo)
This commit is contained in:
curo1305
2026-06-22 08:29:41 +02:00
parent fe08afd740
commit bfa4dc502a
4 changed files with 118 additions and 22 deletions
+15 -11
View File
@@ -59,6 +59,7 @@ async def _run(user_id: str, connection_id: str, parent_ref: str | None) -> dict
from db.session import AsyncSessionLocal
from services.cloud_items import (
ConnectionNotFound,
apply_listing_and_finalize,
get_or_create_folder_state,
reconcile_cloud_listing,
resolve_owned_connection,
@@ -137,25 +138,28 @@ async def _run(user_id: str, connection_id: str, parent_ref: str | None) -> dict
# Transient failure — let Celery retry
raise _TransientProviderError("Provider error. Will retry.") from exc
# Reconcile listing into durable cloud_items rows
await reconcile_cloud_listing(
# apply_listing_and_finalize is the single shared gate — it handles
# complete vs. incomplete semantics; only a complete authoritative
# listing transitions the folder to fresh state.
listing_result = await apply_listing_and_finalize(
session,
user_id=user_id,
connection_id=connection_id,
parent_ref=parent_ref,
listing=listing,
)
await update_folder_state(
session,
user_id=user_id,
connection_id=connection_id,
parent_ref=parent_ref or "",
refresh_state="fresh",
)
await session.commit()
return {"status": "ok", "items_fetched": len(listing.items)}
if listing_result.is_fresh:
return {"status": "ok", "items_fetched": len(listing.items)}
else:
# Incomplete listing: task succeeded but folder is not fresh.
# Return a controlled status — no credentials, no provider item names.
return {
"status": "warning",
"warning_code": listing_result.warning_code,
"items_fetched": len(listing.items),
}
# ── Celery task ───────────────────────────────────────────────────────────────