CheddahBot/scripts/rebuild_customer_field.py

150 lines
5.1 KiB
Python

"""One-time script: rebuild the 'Customer' dropdown custom field in ClickUp.
Steps:
1. Fetch all folders from the PII-Agency-SEO space
2. Filter out non-client folders
3. Create a 'Customer' dropdown field with folder names as options
4. For each client folder, find the 'Overall' list and set Customer on all tasks
Usage:
DRY_RUN=1 uv run python scripts/rebuild_customer_field.py # preview only
uv run python scripts/rebuild_customer_field.py # live run
"""
from __future__ import annotations
import os
import sys
import time
from pathlib import Path
# Allow running from repo root
_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_root))
from dotenv import load_dotenv
load_dotenv(_root / ".env")
from cheddahbot.clickup import ClickUpClient
# ── Config ──────────────────────────────────────────────────────────────────
DRY_RUN = os.environ.get("DRY_RUN", "0") not in ("0", "false", "")
EXCLUDED_FOLDERS = {"SEO Audits", "SEO Projects", "Business Related"}
FIELD_NAME = "Customer"
API_TOKEN = os.environ.get("CLICKUP_API_TOKEN", "")
SPACE_ID = os.environ.get("CLICKUP_SPACE_ID", "")
if not API_TOKEN:
sys.exit("ERROR: CLICKUP_API_TOKEN env var is required")
if not SPACE_ID:
sys.exit("ERROR: CLICKUP_SPACE_ID env var is required")
def main() -> None:
client = ClickUpClient(api_token=API_TOKEN)
# 1. Get folders
print(f"\n{'=' * 60}")
print(f" Rebuild '{FIELD_NAME}' field -- Space {SPACE_ID}")
print(f" Mode: {'DRY RUN' if DRY_RUN else 'LIVE'}")
print(f"{'=' * 60}\n")
folders = client.get_folders(SPACE_ID)
print(f"Found {len(folders)} folders:\n")
client_folders = []
for f in folders:
excluded = f["name"] in EXCLUDED_FOLDERS
marker = " [SKIP]" if excluded else ""
list_names = [lst["name"] for lst in f["lists"]]
print(f" {f['name']}{marker} (lists: {', '.join(list_names) or 'none'})")
if not excluded:
client_folders.append(f)
if not client_folders:
sys.exit("\nNo client folders found -- nothing to do.")
option_names = sorted(f["name"] for f in client_folders)
print(f"\nDropdown options ({len(option_names)}): {', '.join(option_names)}")
# 2. Build a plan: folder → Overall list → tasks
plan: list[dict] = [] # {folder_name, list_id, tasks: [ClickUpTask]}
first_list_id = None
for f in client_folders:
overall = next((lst for lst in f["lists"] if lst["name"] == "Overall"), None)
if overall is None:
print(f"\n WARNING: '{f['name']}' has no 'Overall' list -- skipping task update")
continue
if first_list_id is None:
first_list_id = overall["id"]
tasks = client.get_tasks(overall["id"])
plan.append({"folder_name": f["name"], "list_id": overall["id"], "tasks": tasks})
# 3. Print summary
total_tasks = sum(len(p["tasks"]) for p in plan)
print("\n--- Update Plan ---")
for p in plan:
print(f" {p['folder_name']:30s} -> {len(p['tasks']):3d} tasks in list {p['list_id']}")
print(f" {'TOTAL':30s} -> {total_tasks:3d} tasks")
if DRY_RUN:
print("\n** DRY RUN -- no changes made. Unset DRY_RUN to execute. **\n")
return
if first_list_id is None:
sys.exit("\nNo 'Overall' list found in any client folder -- cannot create field.")
# 4. Create the dropdown field
print(f"\nCreating '{FIELD_NAME}' dropdown on list {first_list_id} ...")
type_config = {
"options": [{"name": name, "color": None} for name in option_names],
}
client.create_custom_field(first_list_id, FIELD_NAME, "drop_down", type_config)
print(" Field created.")
# Brief pause for ClickUp to propagate
time.sleep(2)
# 5. Discover the field UUID + option IDs
print("Discovering field UUID and option IDs ...")
field_info = client.discover_field_filter(first_list_id, FIELD_NAME)
if field_info is None:
sys.exit(f"\nERROR: Could not find '{FIELD_NAME}' field after creation!")
field_id = field_info["field_id"]
option_map = field_info["options"] # {name: uuid}
print(f" Field ID: {field_id}")
print(f" Options: {option_map}")
# 6. Set Customer field on each task
updated = 0
failed = 0
for p in plan:
folder_name = p["folder_name"]
opt_id = option_map.get(folder_name)
if not opt_id:
print(f"\n WARNING: No option ID for '{folder_name}' -- skipping")
continue
print(f"\nUpdating {len(p['tasks'])} tasks in '{folder_name}' ...")
for task in p["tasks"]:
ok = client.set_custom_field_value(task.id, field_id, opt_id)
if ok:
updated += 1
else:
failed += 1
print(f" FAILED: task {task.id} ({task.name})")
# Light rate-limit courtesy
time.sleep(0.15)
print(f"\n{'=' * 60}")
print(f" Done! Updated: {updated} | Failed: {failed}")
print(f"{'=' * 60}\n")
if __name__ == "__main__":
main()