Include description in tasks polling ETag (#698)
* Include description in tasks polling ETag * Align tasks endpoint headers with HTTP cache expectations
This commit is contained in:
parent
31cf56a685
commit
89fa9b4b49
@ -9,7 +9,8 @@ Handles:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
from email.utils import format_datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Header, HTTPException, Request, Response
|
from fastapi import APIRouter, Header, HTTPException, Request, Response
|
||||||
@ -578,20 +579,45 @@ async def list_project_tasks(
|
|||||||
|
|
||||||
tasks = result.get("tasks", [])
|
tasks = result.get("tasks", [])
|
||||||
|
|
||||||
# Generate ETag from task data (excluding timestamps for consistency)
|
# Generate ETag from task data (includes description and updated_at to drive polling invalidation)
|
||||||
etag_data = {
|
etag_tasks: list[dict[str, object]] = []
|
||||||
"tasks": [{
|
last_modified_dt: datetime | None = None
|
||||||
"id": task.get("id"),
|
|
||||||
"title": task.get("title"),
|
for task in tasks:
|
||||||
"status": task.get("status"),
|
raw_updated = task.get("updated_at")
|
||||||
"task_order": task.get("task_order"),
|
parsed_updated: datetime | None = None
|
||||||
"assignee": task.get("assignee"),
|
if isinstance(raw_updated, datetime):
|
||||||
"priority": task.get("priority"),
|
parsed_updated = raw_updated
|
||||||
"feature": task.get("feature")
|
elif isinstance(raw_updated, str):
|
||||||
} for task in tasks],
|
try:
|
||||||
"project_id": project_id,
|
parsed_updated = datetime.fromisoformat(raw_updated.replace("Z", "+00:00"))
|
||||||
"count": len(tasks)
|
except ValueError:
|
||||||
}
|
parsed_updated = None
|
||||||
|
|
||||||
|
if parsed_updated is not None:
|
||||||
|
parsed_updated = parsed_updated.astimezone(timezone.utc)
|
||||||
|
if last_modified_dt is None or parsed_updated > last_modified_dt:
|
||||||
|
last_modified_dt = parsed_updated
|
||||||
|
|
||||||
|
etag_tasks.append(
|
||||||
|
{
|
||||||
|
"id": task.get("id") or "",
|
||||||
|
"title": task.get("title") or "",
|
||||||
|
"status": task.get("status") or "",
|
||||||
|
"task_order": task.get("task_order") or 0,
|
||||||
|
"assignee": task.get("assignee") or "",
|
||||||
|
"priority": task.get("priority") or "",
|
||||||
|
"feature": task.get("feature") or "",
|
||||||
|
"description": task.get("description") or "",
|
||||||
|
"updated_at": (
|
||||||
|
parsed_updated.isoformat()
|
||||||
|
if parsed_updated is not None
|
||||||
|
else (str(raw_updated) if raw_updated else "")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
etag_data = {"tasks": etag_tasks, "project_id": project_id, "count": len(tasks)}
|
||||||
current_etag = generate_etag(etag_data)
|
current_etag = generate_etag(etag_data)
|
||||||
|
|
||||||
# Check if client's ETag matches (304 Not Modified)
|
# Check if client's ETag matches (304 Not Modified)
|
||||||
@ -599,14 +625,18 @@ async def list_project_tasks(
|
|||||||
response.status_code = 304
|
response.status_code = 304
|
||||||
response.headers["ETag"] = current_etag
|
response.headers["ETag"] = current_etag
|
||||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||||
response.headers["Last-Modified"] = datetime.utcnow().isoformat()
|
response.headers["Last-Modified"] = format_datetime(
|
||||||
|
last_modified_dt or datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
logfire.debug(f"Tasks unchanged, returning 304 | project_id={project_id} | etag={current_etag}")
|
logfire.debug(f"Tasks unchanged, returning 304 | project_id={project_id} | etag={current_etag}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Set ETag headers for successful response
|
# Set ETag headers for successful response
|
||||||
response.headers["ETag"] = current_etag
|
response.headers["ETag"] = current_etag
|
||||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||||
response.headers["Last-Modified"] = datetime.utcnow().isoformat()
|
response.headers["Last-Modified"] = format_datetime(
|
||||||
|
last_modified_dt or datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
logfire.debug(
|
logfire.debug(
|
||||||
f"Project tasks retrieved | project_id={project_id} | task_count={len(tasks)} | etag={current_etag}"
|
f"Project tasks retrieved | project_id={project_id} | task_count={len(tasks)} | etag={current_etag}"
|
||||||
@ -617,7 +647,7 @@ async def list_project_tasks(
|
|||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logfire.error(f"Failed to list project tasks | error={str(e)} | project_id={project_id}")
|
logfire.error(f"Failed to list project tasks | project_id={project_id}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail={"error": str(e)})
|
raise HTTPException(status_code=500, detail={"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user