From c901ce76531a75d127043602362d7d75d71eef04 Mon Sep 17 00:00:00 2001 From: Davide Di Ruscio Date: Wed, 31 Dec 2025 15:50:45 +0100 Subject: [PATCH] Added the management of completed tasks --- .env | 14 +++++++++ README | 2 ++ main.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..3ae6e73 --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +TODOIST_TOKEN=fbbe7596bcb32135a2d1d986fc729cc70211a8ff + +TUDUDI_BASE_URL=https://tududi.diruscio.org +TUDUDI_API_KEY=tt_78d7a84e8dcebebeaf300f1557ae7bd8390bddc9f257282d013909cfc0dfd6d2 +TUDUDI_SQLITE_PATH=./tududi_db/tudodi.sqlite3 + +DRY_RUN=false +CLEAN_FIRST=True +USE_AREAS=False +SKIP_COMPLETED_TASKS=True +SKIP_ARCHIVED_PROJECTS=True +SKIP_EMPTY_PROJECTS=True +REQUEST_SLEEP=0.10 + diff --git a/README b/README index dbb525a..0018e90 100644 --- a/README +++ b/README @@ -8,6 +8,8 @@ TUDUDI_API_KEY=your_tududi_api_key DRY_RUN=false CLEAN_FIRST=True USE_AREAS=False +SKIP_COMPLETED_TASKS=True +REQUEST_SLEEP=0.10 ``` Make sure to replace `your_todoist_api_token`, `https://your_tududi_instance_url`, and `your_tududi_api_key` with your actual credentials. diff --git a/main.py b/main.py index 4e2cd9a..45e8a98 100644 --- a/main.py +++ b/main.py @@ -43,18 +43,48 @@ load_dotenv() # ---------------------------- def slug_tag(name: str) -> str: """ - Convert label/tag to Tududi-safe ASCII slug: - - removes emoji/non-ascii - - removes accents - - keeps a-z0-9_- and collapses spaces to _ + Convert label/tag to Tududi-safe slug while preserving emojis: + - emojis at the beginning are moved to the end + - text is ASCII-normalized + - result: _ + Example: + "💡 IDEAS" -> "ideas_💡" + "📚 Reading list" -> "reading_list_📚" """ - s = unicodedata.normalize("NFKD", name) + + if not name: + return "tag" + + # 1) Extract emojis (very permissive, Unicode-aware) + emojis = re.findall( + r"[\U0001F300-\U0001FAFF\U00002600-\U000027BF]", + name + ) + + # 2) Remove emojis from text + text = re.sub( + r"[\U0001F300-\U0001FAFF\U00002600-\U000027BF]", + "", + name + ) + + # 3) Normalize text → ASCII slug + s = unicodedata.normalize("NFKD", text) s = "".join(ch for ch in s if not unicodedata.combining(ch)) s = s.encode("ascii", "ignore").decode("ascii") s = s.strip().lower() s = re.sub(r"\s+", "_", s) s = re.sub(r"[^a-z0-9_-]", "", s) - return s or "tag" + + if not s: + s = "tag" + + # 4) Append emojis at the end (if any) + if emojis: + s = f"{s}_{''.join(emojis)}" + + return s + def normalize_list(resp: Any) -> List[Dict[str, Any]]: """ @@ -661,18 +691,40 @@ def main() -> None: tududi_key = os.getenv("TUDUDI_API_KEY") tududi_email = os.getenv("TUDUDI_EMAIL") tududi_password = os.getenv("TUDUDI_PASSWORD") + + dry_run = getenv_bool("DRY_RUN", False) clean_first = getenv_bool("CLEAN_FIRST", False) use_areas = getenv_bool("USE_AREAS", False) request_sleep = getenv_float("REQUEST_SLEEP", 0.25) + skip_completed_tasks = getenv_bool("SKIP_COMPLETED_TASKS", False) + skip_archived_projects = getenv_bool("SKIP_ARCHIVED_PROJECTS", False) + skip_empty_projects = getenv_bool("SKIP_EMPTY_PROJECTS", False) print(f"[INFO] Starting migration @ {iso_now()} (dry_run={dry_run})") + print(f"[INFO] SKIP_COMPLETED_TASKS={skip_completed_tasks}") + print(f"[INFO] Options: " + f"SKIP_ARCHIVED_PROJECTS={skip_archived_projects}, " + f"SKIP_EMPTY_PROJECTS={skip_empty_projects}") print("[INFO] Exporting from Todoist (sync full + completed best-effort)...") tdc = TodoistClient(todoist_token) data = tdc.export_everything() + # Build project -> task count map (active + completed) + project_task_count: Dict[str, int] = {} + + for t in data.items: + pid = t.get("project_id") or t.get("projectId") + if pid is not None: + project_task_count[str(pid)] = project_task_count.get(str(pid), 0) + 1 + + for t in data.completed_tasks: + pid = t.get("project_id") or t.get("projectId") + if pid is not None: + project_task_count[str(pid)] = project_task_count.get(str(pid), 0) + 1 + print( f"[INFO] Todoist: projects={len(data.projects)} labels={len(data.labels)} " f"sections={len(data.sections)} active_items={len(data.items)} " @@ -754,6 +806,17 @@ def main() -> None: name = p.get("name") if not name: continue + + # --- SKIP ARCHIVED PROJECTS --- + if skip_archived_projects and p.get("is_archived"): + print(f"[SKIP] Archived project: {name}") + continue + + # --- SKIP EMPTY PROJECTS --- + if skip_empty_projects and project_task_count.get(tid, 0) == 0: + print(f"[SKIP] Empty project: {name}") + continue + if name in proj_by_name: # prendi ID numerico se esiste, fallback su uid se proprio manca (ma dovrebbe esserci) @@ -794,12 +857,15 @@ def main() -> None: # Comments index comments_idx = build_comments_index(data.notes) - # Merge active + completed tasks + # Merge active + (if requested) completed tasks all_tasks_src: List[Tuple[Dict[str, Any], bool]] = [] for i in data.items: all_tasks_src.append((i, False)) - for c in data.completed_tasks: - all_tasks_src.append((c, True)) + if skip_completed_tasks: + print(f"[INFO] Skipping completed tasks from Todoist export: {len(data.completed_tasks)}") + else: + for c in data.completed_tasks: + all_tasks_src.append((c, True)) items_like = [x for (x, _done) in all_tasks_src if "id" in x] items_sorted = topo_sort_items(items_like) @@ -878,6 +944,11 @@ def main() -> None: due = normalize_due(task) prio = map_priority(task.get("priority")) project_uid = get_project_uid_for_task(task) + + # Skip task if project was skipped + if project_uid is None and (task.get("project_id") or task.get("projectId")): + print(f"[SKIP] Task {tid} belongs to skipped project") + continue # Tududi note: Todoist link + Todoist description tududi_description = build_tududi_description_from_todoist(task, name)