137 lines
3.5 KiB
Python
137 lines
3.5 KiB
Python
"""
|
|
[[Roam][https://roamresearch.com]] data
|
|
"""
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from itertools import chain
|
|
import re
|
|
from typing import NamedTuple, Iterator, List, Optional
|
|
|
|
import pytz
|
|
|
|
from .common import get_files, LazyLogger, Json
|
|
|
|
from my.config import roamresearch as config
|
|
|
|
logger = LazyLogger(__name__)
|
|
|
|
|
|
def last() -> Path:
|
|
return max(get_files(config.export_path, '*.json'))
|
|
|
|
|
|
class Keys:
|
|
CREATED = 'create-time'
|
|
EDITED = 'edit-time'
|
|
EDIT_EMAIL = 'edit-email'
|
|
STRING = 'string'
|
|
CHILDREN = 'children'
|
|
TITLE = 'title'
|
|
UID = 'uid'
|
|
|
|
|
|
class Node(NamedTuple):
|
|
raw: Json
|
|
|
|
# TODO not sure if UTC
|
|
@property
|
|
def created(self) -> datetime:
|
|
ct = self.raw.get(Keys.CREATED)
|
|
if ct is not None:
|
|
return datetime.fromtimestamp(ct / 1000, tz=pytz.utc)
|
|
# ugh. daily notes don't have create time for some reason???
|
|
|
|
title = self.title
|
|
assert title is not None
|
|
# the format is 'February 8th, 2020'. Fucking hell.
|
|
m = re.fullmatch(r'(\w+) (\d+)\w+, (\d+)', title)
|
|
assert m is not None
|
|
# strip off 'th'/'rd' crap
|
|
dts = m.group(1) + ' ' + m.group(2) + ' ' + m.group(3)
|
|
dt = datetime.strptime(dts, '%B %d %Y')
|
|
return pytz.utc.localize(dt)
|
|
|
|
@property
|
|
def edited(self) -> datetime:
|
|
rt = self.raw[Keys.EDITED]
|
|
return datetime.fromtimestamp(rt / 1000, tz=pytz.utc)
|
|
|
|
@property
|
|
def title(self) -> Optional[str]:
|
|
return self.raw.get(Keys.TITLE)
|
|
|
|
@property
|
|
def body(self) -> Optional[str]:
|
|
return self.raw.get(Keys.STRING)
|
|
|
|
@property
|
|
def children(self) -> List['Node']:
|
|
ch = self.raw.get(Keys.CHILDREN, [])
|
|
return list(map(Node, ch))
|
|
|
|
@property
|
|
def permalink(self) -> str:
|
|
username = config.username # sadly, Roam research doesn't provide 3
|
|
return f'https://roamresearch.com/#/app/{username}/page/{self.uid}'
|
|
|
|
@property
|
|
def uid(self) -> str:
|
|
u = self.raw.get(Keys.UID)
|
|
if u is not None:
|
|
return u
|
|
# ugh. so None apparently means "Daily note"
|
|
|
|
# yes, it is using US date format...
|
|
return self.created.strftime('%m-%d-%Y')
|
|
|
|
|
|
def _render(self) -> Iterator[str]:
|
|
ss = f'[{self.created:%Y-%m-%d %H:%M}] {self.title or " "}'
|
|
body = self.body
|
|
sc = chain.from_iterable(c._render() for c in self.children)
|
|
|
|
yield ss
|
|
if body is not None:
|
|
yield body
|
|
yield self.permalink
|
|
for c in sc:
|
|
yield '| ' + c
|
|
|
|
def render(self) -> str:
|
|
return '\n'.join(self._render())
|
|
|
|
def __repr__(self):
|
|
return f'Node(created={self.created}, title={self.title}, body={self.body})'
|
|
|
|
@staticmethod
|
|
def make(raw: Json) -> Iterator['Node']:
|
|
is_empty = set(raw.keys()) == {Keys.EDITED, Keys.EDIT_EMAIL, Keys.TITLE}
|
|
# not sure about that... but daily notes end up like that
|
|
if is_empty:
|
|
# todo log?
|
|
return
|
|
yield Node(raw)
|
|
|
|
|
|
class Roam:
|
|
def __init__(self, json: List[Json]) -> None:
|
|
self.nodes: List[Node] = []
|
|
# TODO make it lazy?
|
|
for j in json:
|
|
self.nodes.extend(Node.make(j))
|
|
|
|
|
|
def roam() -> Roam:
|
|
import json
|
|
raw = json.loads(last().read_text())
|
|
roam = Roam(raw)
|
|
return roam
|
|
|
|
|
|
def print_all_notes():
|
|
# just a demo method
|
|
# TODO demonstrate dumping as org-mode??
|
|
for n in roam().nodes:
|
|
print(n.render())
|
|
|
|
# TODO could generate org-mode mirror in a single file for a demo?
|