"""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()