Added the management of completed tasks
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ TUDUDI_API_KEY=your_tududi_api_key
|
|||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
CLEAN_FIRST=True
|
CLEAN_FIRST=True
|
||||||
USE_AREAS=False
|
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.
|
Make sure to replace `your_todoist_api_token`, `https://your_tududi_instance_url`, and `your_tududi_api_key` with your actual credentials.
|
||||||
|
|||||||
@@ -43,18 +43,48 @@ load_dotenv()
|
|||||||
# ----------------------------
|
# ----------------------------
|
||||||
def slug_tag(name: str) -> str:
|
def slug_tag(name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Convert label/tag to Tududi-safe ASCII slug:
|
Convert label/tag to Tududi-safe slug while preserving emojis:
|
||||||
- removes emoji/non-ascii
|
- emojis at the beginning are moved to the end
|
||||||
- removes accents
|
- text is ASCII-normalized
|
||||||
- keeps a-z0-9_- and collapses spaces to _
|
- result: <slug>_<emoji>
|
||||||
|
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 = "".join(ch for ch in s if not unicodedata.combining(ch))
|
||||||
s = s.encode("ascii", "ignore").decode("ascii")
|
s = s.encode("ascii", "ignore").decode("ascii")
|
||||||
s = s.strip().lower()
|
s = s.strip().lower()
|
||||||
s = re.sub(r"\s+", "_", s)
|
s = re.sub(r"\s+", "_", s)
|
||||||
s = re.sub(r"[^a-z0-9_-]", "", 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]]:
|
def normalize_list(resp: Any) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -661,18 +691,40 @@ def main() -> None:
|
|||||||
tududi_key = os.getenv("TUDUDI_API_KEY")
|
tududi_key = os.getenv("TUDUDI_API_KEY")
|
||||||
tududi_email = os.getenv("TUDUDI_EMAIL")
|
tududi_email = os.getenv("TUDUDI_EMAIL")
|
||||||
tududi_password = os.getenv("TUDUDI_PASSWORD")
|
tududi_password = os.getenv("TUDUDI_PASSWORD")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dry_run = getenv_bool("DRY_RUN", False)
|
dry_run = getenv_bool("DRY_RUN", False)
|
||||||
clean_first = getenv_bool("CLEAN_FIRST", False)
|
clean_first = getenv_bool("CLEAN_FIRST", False)
|
||||||
use_areas = getenv_bool("USE_AREAS", False)
|
use_areas = getenv_bool("USE_AREAS", False)
|
||||||
request_sleep = getenv_float("REQUEST_SLEEP", 0.25)
|
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] 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)...")
|
print("[INFO] Exporting from Todoist (sync full + completed best-effort)...")
|
||||||
|
|
||||||
tdc = TodoistClient(todoist_token)
|
tdc = TodoistClient(todoist_token)
|
||||||
data = tdc.export_everything()
|
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(
|
print(
|
||||||
f"[INFO] Todoist: projects={len(data.projects)} labels={len(data.labels)} "
|
f"[INFO] Todoist: projects={len(data.projects)} labels={len(data.labels)} "
|
||||||
f"sections={len(data.sections)} active_items={len(data.items)} "
|
f"sections={len(data.sections)} active_items={len(data.items)} "
|
||||||
@@ -754,6 +806,17 @@ def main() -> None:
|
|||||||
name = p.get("name")
|
name = p.get("name")
|
||||||
if not name:
|
if not name:
|
||||||
continue
|
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:
|
if name in proj_by_name:
|
||||||
# prendi ID numerico se esiste, fallback su uid se proprio manca (ma dovrebbe esserci)
|
# prendi ID numerico se esiste, fallback su uid se proprio manca (ma dovrebbe esserci)
|
||||||
@@ -794,12 +857,15 @@ def main() -> None:
|
|||||||
# Comments index
|
# Comments index
|
||||||
comments_idx = build_comments_index(data.notes)
|
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]] = []
|
all_tasks_src: List[Tuple[Dict[str, Any], bool]] = []
|
||||||
for i in data.items:
|
for i in data.items:
|
||||||
all_tasks_src.append((i, False))
|
all_tasks_src.append((i, False))
|
||||||
for c in data.completed_tasks:
|
if skip_completed_tasks:
|
||||||
all_tasks_src.append((c, True))
|
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_like = [x for (x, _done) in all_tasks_src if "id" in x]
|
||||||
items_sorted = topo_sort_items(items_like)
|
items_sorted = topo_sort_items(items_like)
|
||||||
@@ -878,6 +944,11 @@ def main() -> None:
|
|||||||
due = normalize_due(task)
|
due = normalize_due(task)
|
||||||
prio = map_priority(task.get("priority"))
|
prio = map_priority(task.get("priority"))
|
||||||
project_uid = get_project_uid_for_task(task)
|
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 note: Todoist link + Todoist description
|
||||||
tududi_description = build_tududi_description_from_todoist(task, name)
|
tududi_description = build_tududi_description_from_todoist(task, name)
|
||||||
|
|||||||
Reference in New Issue
Block a user