Download assigned tasks; skip only "Facesheets Needed (GG)"

Tasks assigned to someone (BT, SM, ...) were skipped, so their facesheets
were never downloaded. Only GG tasks must be skipped now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-28 10:54:10 +05:30
1 parent bfd72825b4
commit 3e7cc4afe9
2 files changed
+66 -45

No files matched your search

+35 -28
View File
@@ -6,8 +6,8 @@ Architecture:
authenticated request to graph.microsoft.com, then closes immediately. authenticated request to graph.microsoft.com, then closes immediately.
2. All subsequent work uses httpx against the Microsoft Graph API: 2. All subsequent work uses httpx against the Microsoft Graph API:
- Discover plans via user's group memberships - Discover plans via user's group memberships
- List Not-Started tasks per facility plan, skipping any task that is - List Not-Started tasks per facility plan, skipping only
already assigned to someone (their work — not ours to download) "Facesheets Needed (GG)" tasks (assigned or not, all others download)
- Resolve SharePoint file references via Graph shares endpoint - Resolve SharePoint file references via Graph shares endpoint
- Stream-download each PDF - Stream-download each PDF
3. Identical output structure to the Selenium bot: 3. Identical output structure to the Selenium bot:
@@ -893,6 +893,12 @@ def _task_assignee_ids(task: dict) -> list[str]:
return [uid for uid in (task.get("assignments") or {}) if uid] return [uid for uid in (task.get("assignments") or {}) if uid]
def _is_gg_task(task: dict) -> bool:
"""'Facesheets Needed (GG)' tasks are the only ones the bot must not download."""
# ponytail: title match only; also check task details notes if GG ever appears there
return "facesheets needed (gg)" in " ".join((task.get("title") or "").lower().split())
def _resolve_user_names( def _resolve_user_names(
client: httpx.Client, client: httpx.Client,
user_ids: list[str], user_ids: list[str],
@@ -1030,7 +1036,7 @@ def _build_workable_files(
output_root: Path, output_root: Path,
today_iso: str, today_iso: str,
) -> tuple[list[Path], Path]: ) -> tuple[list[Path], Path]:
ddmmyyyy = datetime.strptime(today_iso, "%Y-%m-%d").strftime("%d-%m-%Y") mmddyyyy = datetime.strptime(today_iso, "%Y-%m-%d").strftime("%m-%d-%Y")
fac_safe = _sanitize_folder_name(facility_name) fac_safe = _sanitize_folder_name(facility_name)
workable_root = output_root / today_iso / "Workable" workable_root = output_root / today_iso / "Workable"
medicare_root = output_root / today_iso / "Workable_Medicare" medicare_root = output_root / today_iso / "Workable_Medicare"
@@ -1045,9 +1051,9 @@ def _build_workable_files(
for i, src in enumerate(downloaded, start=1): for i, src in enumerate(downloaded, start=1):
ext = src.suffix or ".pdf" ext = src.suffix or ".pdf"
base = ( base = (
f"{ddmmyyyy}_{fac_safe}" f"{mmddyyyy}_{fac_safe}"
if len(downloaded) == 1 if len(downloaded) == 1
else f"{ddmmyyyy}_{fac_safe}_{i}" else f"{mmddyyyy}_{fac_safe}_{i}"
) )
# Check if this workable file already exists (previous interrupted run) # Check if this workable file already exists (previous interrupted run)
dst_first = workable_root / f"{base}{ext}" dst_first = workable_root / f"{base}{ext}"
@@ -1094,14 +1100,14 @@ def _write_summary(
tasks_att = int(payload.get("tasks_with_attachments") or 0) tasks_att = int(payload.get("tasks_with_attachments") or 0)
tasks_dl = int(payload.get("tasks_with_downloads") or 0) tasks_dl = int(payload.get("tasks_with_downloads") or 0)
tasks_skip = int(payload.get("tasks_skipped_no_attachment") or 0) tasks_skip = int(payload.get("tasks_skipped_no_attachment") or 0)
tasks_assigned = int(payload.get("tasks_skipped_assigned") or 0) tasks_gg = int(payload.get("tasks_skipped_gg") or 0)
status_changed = int(payload.get("status_changed") or 0) status_changed = int(payload.get("status_changed") or 0)
task_rows.extend(payload.get("task_rows") or []) task_rows.extend(payload.get("task_rows") or [])
if tasks_total <= 0: if tasks_total <= 0:
status = "No tasks" status = "No tasks"
elif tasks_att <= 0 and tasks_assigned > 0: elif tasks_att <= 0 and tasks_gg > 0:
status = "All assigned — skipped" status = "All GG — skipped"
elif tasks_att <= 0: elif tasks_att <= 0:
status = "No attachments" status = "No attachments"
elif tasks_dl <= 0 or file_count <= 0: elif tasks_dl <= 0 or file_count <= 0:
@@ -1126,7 +1132,7 @@ def _write_summary(
"Files Downloaded": file_count, "Files Downloaded": file_count,
"Tasks Not Started": tasks_total, "Tasks Not Started": tasks_total,
"Tasks With Attachments": tasks_att, "Tasks With Attachments": tasks_att,
"Tasks Skipped (Assigned)": tasks_assigned, "Tasks Skipped (GG)": tasks_gg,
"Tasks Skipped (No File)": tasks_skip, "Tasks Skipped (No File)": tasks_skip,
"Status": status, "Status": status,
"Status Changed": status_changed_label, "Status Changed": status_changed_label,
@@ -1139,7 +1145,7 @@ def _write_summary(
"Files Downloaded": 0, "Files Downloaded": 0,
"Tasks Not Started": 0, "Tasks Not Started": 0,
"Tasks With Attachments": 0, "Tasks With Attachments": 0,
"Tasks Skipped (Assigned)": 0, "Tasks Skipped (GG)": 0,
"Tasks Skipped (No File)": 0, "Tasks Skipped (No File)": 0,
"Status": status, "Status": status,
"Status Changed": "N/A", "Status Changed": "N/A",
@@ -1191,14 +1197,14 @@ def _process_facility(
tasks_with_att = 0 tasks_with_att = 0
tasks_with_dl = 0 tasks_with_dl = 0
tasks_no_att = 0 # Not Started but no file attachment → skip tasks_no_att = 0 # Not Started but no file attachment → skip
tasks_assigned = 0 # Not Started but already assigned to someone → skip tasks_gg = 0 # "Facesheets Needed (GG)" → skip
status_changed = 0 status_changed = 0
downloaded: list[Path] = [] downloaded: list[Path] = []
task_rows: list[dict] = [] task_rows: list[dict] = []
name_cache: dict[str, str] = {} name_cache: dict[str, str] = {}
_log.info( _log.info(
" Processing ONLY unassigned Not Started tasks with attachments " " Processing Not Started tasks with attachments, assigned or not "
"(%d Not Started found)", "(%d Not Started found)",
tasks_total, tasks_total,
) )
@@ -1207,22 +1213,23 @@ def _process_facility(
tid = task["id"] tid = task["id"]
title = task.get("title", "") title = task.get("title", "")
assignee_ids = _task_assignee_ids(task) assignee_ids = _task_assignee_ids(task)
assigned_to = (
", ".join(_resolve_user_names(client, assignee_ids, cfg, name_cache))
if assignee_ids else ""
)
# A task someone has already picked up is theirs to work — never # GG tasks are never downloaded and their status is never touched.
# download it and never touch its status. Checked before the # Checked before the attachment lookup so they cost no extra Graph call.
# attachment lookup so an assigned task costs no extra Graph call. if _is_gg_task(task):
if assignee_ids: _log.info(" SKIP '%s': Facesheets Needed (GG)", title[:60])
names = _resolve_user_names(client, assignee_ids, cfg, name_cache) tasks_gg += 1
assigned_to = ", ".join(names)
_log.info(" SKIP '%s': assigned to %s", title[:60], assigned_to)
tasks_assigned += 1
task_rows.append({ task_rows.append({
"Facility Name": facility_name, "Facility Name": facility_name,
"Task Name": title, "Task Name": title,
"Assigned To": assigned_to, "Assigned To": assigned_to,
"Attachments": "", "Attachments": "",
"Downloaded": "No", "Downloaded": "No",
"Reason": f"Skipped — assigned to {assigned_to}", "Reason": "Skipped — Facesheets Needed (GG)",
}) })
continue continue
@@ -1235,14 +1242,14 @@ def _process_facility(
task_rows.append({ task_rows.append({
"Facility Name": facility_name, "Facility Name": facility_name,
"Task Name": title, "Task Name": title,
"Assigned To": "", "Assigned To": assigned_to,
"Attachments": 0, "Attachments": 0,
"Downloaded": "No", "Downloaded": "No",
"Reason": "Skipped — no attachment", "Reason": "Skipped — no attachment",
}) })
continue continue
# Unassigned, Not Started, WITH attachment(s) — download ALL of them # Not Started, WITH attachment(s) — download ALL of them
tasks_with_att += 1 tasks_with_att += 1
total_urls = len(file_urls) total_urls = len(file_urls)
task_dl = 0 task_dl = 0
@@ -1271,7 +1278,7 @@ def _process_facility(
task_rows.append({ task_rows.append({
"Facility Name": facility_name, "Facility Name": facility_name,
"Task Name": title, "Task Name": title,
"Assigned To": "", "Assigned To": assigned_to,
"Attachments": total_urls, "Attachments": total_urls,
"Downloaded": "Yes" if task_dl else "No", "Downloaded": "Yes" if task_dl else "No",
"Reason": reason, "Reason": reason,
@@ -1305,10 +1312,10 @@ def _process_facility(
) )
_log.info( _log.info(
" Summary: %d Not Started | %d assigned (skipped) | " " Summary: %d Not Started | %d GG (skipped) | "
"%d with attachments (processed) | %d no attachment (skipped) | " "%d with attachments (processed) | %d no attachment (skipped) | "
"%d file(s) downloaded", "%d file(s) downloaded",
tasks_total, tasks_assigned, tasks_with_att, tasks_no_att, len(downloaded), tasks_total, tasks_gg, tasks_with_att, tasks_no_att, len(downloaded),
) )
return { return {
@@ -1317,7 +1324,7 @@ def _process_facility(
"tasks_with_attachments": tasks_with_att, "tasks_with_attachments": tasks_with_att,
"tasks_with_downloads": tasks_with_dl, "tasks_with_downloads": tasks_with_dl,
"tasks_skipped_no_attachment": tasks_no_att, "tasks_skipped_no_attachment": tasks_no_att,
"tasks_skipped_assigned": tasks_assigned, "tasks_skipped_gg": tasks_gg,
"task_rows": task_rows, "task_rows": task_rows,
"status_changed": status_changed, "status_changed": status_changed,
"status_via_task_details": 0, "status_via_task_details": 0,
@@ -1460,7 +1467,7 @@ def run_planner_http_workflow(
"original": [], "workable": [], "comment": err_msg[:500], "original": [], "workable": [], "comment": err_msg[:500],
"tasks_total": 0, "tasks_with_attachments": 0, "tasks_total": 0, "tasks_with_attachments": 0,
"tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0, "tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0,
"tasks_skipped_assigned": 0, "task_rows": [], "tasks_skipped_gg": 0, "task_rows": [],
"status_changed": 0, "status_via_task_details": 0, "status_changed": 0, "status_via_task_details": 0,
} }
save_checkpoint( save_checkpoint(
+31 -17
View File
@@ -1,7 +1,7 @@
"""Only unassigned Not-Started tasks may be downloaded. """Assigned tasks download like any other; only "Facesheets Needed (GG)" is skipped.
A task carrying an 'Assigned to' avatar in the Planner grid has been picked up A task assigned to someone (BT, SM, ...) with a facesheet attached must still be
by a person; the bot must leave it — and its status — alone. downloaded. GG tasks are never downloaded and their status is never touched.
""" """
from __future__ import annotations from __future__ import annotations
@@ -16,7 +16,7 @@ for _p in (str(ROOT), str(SRC)):
if _p not in sys.path: if _p not in sys.path:
sys.path.insert(0, _p) sys.path.insert(0, _p)
from onedrive_planner_http import _task_assignee_ids, _write_summary # noqa: E402 from onedrive_planner_http import _is_gg_task, _task_assignee_ids, _write_summary # noqa: E402
class TestTaskAssigneeIds: class TestTaskAssigneeIds:
@@ -42,6 +42,20 @@ class TestTaskAssigneeIds:
assert _task_assignee_ids({"assignments": {"": {}}}) == [] assert _task_assignee_ids({"assignments": {"": {}}}) == []
class TestGgSkip:
def test_gg_title_is_skipped(self):
assert _is_gg_task({"title": "Facesheets Needed (GG)"})
assert _is_gg_task({"title": " facesheets needed (gg) "})
def test_assigned_non_gg_task_is_downloaded(self):
# Reo Vista / Balboa misses: assigned to BT / SM, must NOT be skipped.
assert not _is_gg_task({"title": "10.26.26", "assignments": {"u1": {}}})
assert not _is_gg_task(
{"title": "Melzer, Gilbert 8/23/63-Need Medi-Cal ID", "assignments": {"u2": {}}}
)
assert not _is_gg_task({"title": None})
class TestSummaryWorkbook: class TestSummaryWorkbook:
def _write(self, tmp_path: Path, payload: dict) -> Path: def _write(self, tmp_path: Path, payload: dict) -> Path:
out = tmp_path / "Download_Summary_HTTP_test.xlsx" out = tmp_path / "Download_Summary_HTTP_test.xlsx"
@@ -49,18 +63,18 @@ class TestSummaryWorkbook:
return out return out
def _balboa_payload(self) -> dict: def _balboa_payload(self) -> dict:
"""The real Balboa plan: 2 unassigned with files, 2 assigned.""" """The real Balboa plan: 2 downloaded (one assigned), 2 GG skipped."""
return { return {
"original": [Path("a.pdf"), Path("b.pdf")], "original": [Path("a.pdf"), Path("b.pdf")],
"tasks_total": 4, "tasks_total": 4,
"tasks_with_attachments": 2, "tasks_with_attachments": 2,
"tasks_with_downloads": 2, "tasks_with_downloads": 2,
"tasks_skipped_no_attachment": 0, "tasks_skipped_no_attachment": 0,
"tasks_skipped_assigned": 2, "tasks_skipped_gg": 2,
"status_changed": 2, "status_changed": 2,
"task_rows": [ "task_rows": [
{"Facility Name": "Balboa Nursing and Rehab", "Task Name": "8.24.26", {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "10.26.26",
"Assigned To": "", "Attachments": 1, "Downloaded": "Yes", "Assigned To": "B T", "Attachments": 1, "Downloaded": "Yes",
"Reason": "Downloaded 1/1 attachment(s)"}, "Reason": "Downloaded 1/1 attachment(s)"},
{"Facility Name": "Balboa Nursing and Rehab", "Task Name": "8.21.26", {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "8.21.26",
"Assigned To": "", "Attachments": 1, "Downloaded": "Yes", "Assigned To": "", "Attachments": 1, "Downloaded": "Yes",
@@ -68,18 +82,18 @@ class TestSummaryWorkbook:
{"Facility Name": "Balboa Nursing and Rehab", {"Facility Name": "Balboa Nursing and Rehab",
"Task Name": "Facesheets Needed (GG)", "Assigned To": "A S", "Task Name": "Facesheets Needed (GG)", "Assigned To": "A S",
"Attachments": "", "Downloaded": "No", "Attachments": "", "Downloaded": "No",
"Reason": "Skipped — assigned to A S"}, "Reason": "Skipped — Facesheets Needed (GG)"},
{"Facility Name": "Balboa Nursing and Rehab", {"Facility Name": "Balboa Nursing and Rehab",
"Task Name": "Facesheets Needed (GG)", "Assigned To": "D M", "Task Name": "Facesheets Needed (GG)", "Assigned To": "D M",
"Attachments": "", "Downloaded": "No", "Attachments": "", "Downloaded": "No",
"Reason": "Skipped — assigned to D M"}, "Reason": "Skipped — Facesheets Needed (GG)"},
], ],
} }
def test_download_sheet_reports_assigned_skips(self, tmp_path): def test_download_sheet_reports_gg_skips(self, tmp_path):
out = self._write(tmp_path, self._balboa_payload()) out = self._write(tmp_path, self._balboa_payload())
df = pd.read_excel(out, sheet_name="Download") df = pd.read_excel(out, sheet_name="Download")
assert df.loc[0, "Tasks Skipped (Assigned)"] == 2 assert df.loc[0, "Tasks Skipped (GG)"] == 2
assert df.loc[0, "Files Downloaded"] == 2 assert df.loc[0, "Files Downloaded"] == 2
assert df.loc[0, "Status"] == "Success" assert df.loc[0, "Status"] == "Success"
@@ -94,27 +108,27 @@ class TestSummaryWorkbook:
facesheets = df[df["Task Name"] == "Facesheets Needed (GG)"] facesheets = df[df["Task Name"] == "Facesheets Needed (GG)"]
assert len(facesheets) == 2 assert len(facesheets) == 2
assert all(facesheets["Downloaded"] == "No") assert all(facesheets["Downloaded"] == "No")
assert all("assigned to" in r for r in facesheets["Reason"]) assert all("GG" in r for r in facesheets["Reason"])
def test_facility_whose_tasks_are_all_assigned(self, tmp_path): def test_facility_whose_tasks_are_all_gg(self, tmp_path):
out = self._write(tmp_path, { out = self._write(tmp_path, {
"original": [], "original": [],
"tasks_total": 3, "tasks_total": 3,
"tasks_with_attachments": 0, "tasks_with_attachments": 0,
"tasks_with_downloads": 0, "tasks_with_downloads": 0,
"tasks_skipped_no_attachment": 0, "tasks_skipped_no_attachment": 0,
"tasks_skipped_assigned": 3, "tasks_skipped_gg": 3,
"status_changed": 0, "status_changed": 0,
"task_rows": [], "task_rows": [],
}) })
df = pd.read_excel(out, sheet_name="Download") df = pd.read_excel(out, sheet_name="Download")
assert df.loc[0, "Status"] == "All assigned — skipped" assert df.loc[0, "Status"] == "All GG — skipped"
def test_tasks_sheet_exists_even_with_no_task_rows(self, tmp_path): def test_tasks_sheet_exists_even_with_no_task_rows(self, tmp_path):
out = self._write(tmp_path, { out = self._write(tmp_path, {
"original": [], "tasks_total": 0, "tasks_with_attachments": 0, "original": [], "tasks_total": 0, "tasks_with_attachments": 0,
"tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0, "tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0,
"tasks_skipped_assigned": 0, "status_changed": 0, "task_rows": [], "tasks_skipped_gg": 0, "status_changed": 0, "task_rows": [],
}) })
df = pd.read_excel(out, sheet_name="Tasks") df = pd.read_excel(out, sheet_name="Tasks")
assert df.empty assert df.empty