wunderlist-to-org/convert.py

289 lines
7.2 KiB
Python
Executable file

#!/usr/bin/env python
import sys
import datetime
import json
import io
from collections import namedtuple
from contextlib import contextmanager
import re
def _format_org_date(date):
return date.strftime("%Y-%m-%d %a %H:%M")
def sanitize_text(text):
star_re = re.compile(r"^\*+", flags=re.MULTILINE)
return star_re.sub(" +", text)
class OrgWriter:
def __init__(self, level=0):
self._level = level
self._org_string = ""
def emit(self, message="", newline=True):
"""Emit the message keep it simple for now."""
if newline:
message += "\n"
self._org_string += message
return self
def get_org_string(self):
return self._org_string
@contextmanager
def new_level(self, level=None):
if not level:
level = self._level + 1
old_level = self._level
self.set_level(level)
yield self._level
self.set_level(old_level)
@contextmanager
def drawer(self, name):
self.emit(f":{name.upper()}:")
yield self
self.emit(f":END:")
###########################################################################
# Fluent Interface #
###########################################################################
def raise_level(self):
self._level += 1
return self
def lower_level(self):
if self._level > 0:
self._level -= 1
return self
def set_level(self, level):
if level < 0:
raise RuntimeError("Heading levels have to be positive.")
self._level = level
return self
def emit_node_title(self, title, todo_state=None, tags=None):
out_title = "*" * (self._level + 1)
if todo_state:
out_title += f" {todo_state.upper()}"
out_title += f" {title}"
if tags:
for i, tag in enumerate(tags):
tags[i] = tag.upper()
out_title += f" :{':'.join(tags)}:"
return self.emit(out_title)
def emit_timestamp(self, date, timestamp_type=None, active=True, newline=True):
if not date:
return self
stamp = (
((self._level + 2) * " " + f"{timestamp_type.upper()}: ")
if timestamp_type
else ""
)
formated_date = _format_org_date(date)
stamp += f"<{formated_date}>" if active else f"[{formated_date}]"
return self.emit(stamp, newline)
def emit_node(
self,
title,
content=None,
tags=None,
timestamp=None,
timestamp_type=None,
todo_state=None,
):
self.emit_node_title(title, todo_state, tags)
self.emit_timestamp(timestamp, timestamp_type)
if content:
self.emit(content)
return self
def emit_content(self, content):
self.emit(sanitize_text(content))
return self.emit()
def emit_list_item(self, text):
return self.emit(f" - {text}")
def emit_property(self, name, value=None):
self.emit(f":{name.upper()}: ", newline=False)
return self.emit(value) if value else self
def convert_wunderlist(filename):
with io.open(filename, "r", encoding="utf-8-sig") as wunder_file:
wunder_data = json.loads(wunder_file.read())
if not wunder_data:
raise RuntimeError("Could not read wunderlist data.")
writer = OrgWriter()
for todo_list in wunder_data:
convert_wunderlist_list(writer, todo_list)
return writer.get_org_string()
def convert_wunderlist_list(writer, todo_list):
tags = []
if todo_list["folder"]:
tags.append(todo_list["folder"]["title"])
writer.emit_node_title(
todo_list["title"], tags=tags,
)
with writer.new_level():
for task in todo_list["tasks"]:
convert_wunderlist_task(writer, task)
def parse_wunderlist_date(date_str):
if not date_str:
return None
try:
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
except ValueError:
pass
try:
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
pass
try:
return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S")
except ValueError:
pass
return None
def convert_wunderlist_person(person):
return f"[[mailto:{person['email']}][{person['name']}]]"
def convert_wunderlist_title(title):
tag_re = re.compile(r"#(\S+?)(?:\s+|$)", re.MULTILINE)
tags = [tag for tag in tag_re.findall(title)]
return title, tags
def convert_wunderlist_task(writer, task):
title, tags = convert_wunderlist_title(task["title"])
writer.emit_node(
title,
todo_state="next"
if task["starred"]
else ("done" if task["completed"] else "todo"),
timestamp=parse_wunderlist_date(task["dueDate"]),
timestamp_type="deadline",
tags=tags,
)
with writer.drawer("properties"):
writer.emit_property("created-by", convert_wunderlist_person(task["createdBy"]))
if task["createdAt"]:
writer.emit_property("created")
writer.emit_timestamp(
parse_wunderlist_date(task["createdAt"]), active=False
)
if task["completedBy"]:
writer.emit_property(
"COMPLETED-BY", convert_wunderlist_person(task["completedBy"])
)
if task["completedAt"]:
writer.emit_property("completed")
writer.emit_timestamp(
parse_wunderlist_date(task["completedAt"]), active=False
)
if task["reminders"]:
writer.emit_property("reminders")
first = True
for reminder in task["reminders"]:
if first:
first = False
else:
writer.emit(" ", newline=False)
writer.emit_timestamp(
parse_wunderlist_date(reminder["remindAt"]), newline=False
)
writer.emit()
if task["assignee"]:
writer.emit_property(
"assignee", convert_wunderlist_person(task["assignee"])
)
for note in task["notes"]:
writer.emit_content(note["content"])
if task["comments"]:
with writer.new_level():
writer.emit_node_title("Comments")
for comment in task["comments"]:
convert_wunderlist_comment(writer, comment)
writer.emit()
if task["files"]:
with writer.new_level():
writer.emit_node_title("Files")
for w_file in task["files"]:
convert_wunderlist_file(writer, w_file)
writer.emit()
def convert_wunderlist_comment(writer, comment):
comment_text = convert_wunderlist_person(comment["author"])
comment_text += f": {comment['text']}"
writer.emit_list_item(comment_text)
def convert_wunderlist_file(writer, w_file):
writer.emit_list_item(f"[[file:{w_file['filePath']}][{w_file['fileName']}]]")
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} [input-file]")
sys.exit(1)
print(convert_wunderlist(sys.argv[1]))