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:
1 parent
bfd72825b4
commit
3e7cc4afe9
2 files changed
+66
-45
No files matched your search
+35
-28
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user