From cb9593b7448171a6628c9bb33584947226c894e9 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Fri, 27 Sep 2024 16:13:03 +0200 Subject: [PATCH] Parse and map markdown --- favro_sync/__init__.py | 8 +++++- favro_sync/__main__.py | 4 +-- favro_sync/favro_client.py | 32 +++++++++++++++--------- favro_sync/favro_fuse.py | 33 +++++++++--------------- favro_sync/favro_markdown.py | 47 +++++++++++++++++++++++++++++++++++ test/test_init.py | 1 + test/test_markdown_parsing.py | 19 ++++++++++++++ 7 files changed, 107 insertions(+), 37 deletions(-) create mode 100644 favro_sync/favro_markdown.py create mode 100644 test/test_markdown_parsing.py diff --git a/favro_sync/__init__.py b/favro_sync/__init__.py index 9025fca..0b81e2e 100644 --- a/favro_sync/__init__.py +++ b/favro_sync/__init__.py @@ -14,7 +14,6 @@ Limitations: - Only cards in todolist is fetched at the moment. - Doesn't include title anywhere. - Tasks cannot be updated or changed. -- Slow, due to inefficient use of caches. A more complete implementation will probably require a Markdown parser, to parse the saved input, and distribute it across the various Card fields (card @@ -28,4 +27,11 @@ name, description, tasks, etc...) overview of all supported flags (there is a lot, because [`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole bunch automatically.) + +## Architecture + +- `FavroFuse` +- Markdown Parser/Renderer +- `FavroClient` + - `CardCache` """ diff --git a/favro_sync/__main__.py b/favro_sync/__main__.py index da2ffc7..dce0556 100644 --- a/favro_sync/__main__.py +++ b/favro_sync/__main__.py @@ -19,9 +19,7 @@ def main(): read_only = False with tempfile.TemporaryDirectory(prefix='favro_sync_') as tmpdirname: - session = requests_cache.CachedSession( - tmpdirname + '/http-cache.sqlite', expire_after=360, - ) + session = requests_cache.CachedSession( tmpdirname + '/http-cache.sqlite', expire_after=10,) client = FavroClient( favro_org_id=OrganizationId(favro_org_id), diff --git a/favro_sync/favro_client.py b/favro_sync/favro_client.py index bdf178e..dcfdb90 100644 --- a/favro_sync/favro_client.py +++ b/favro_sync/favro_client.py @@ -2,8 +2,10 @@ from collections.abc import Iterator from logging import getLogger import requests +import dataclasses from .favro_data_model import Card, CardId, OrganizationId, SeqId +from .favro_markdown import CardContents logger = getLogger(__name__) @@ -22,20 +24,19 @@ class CardCache: self.cards.append(card) def get_card_by_card_id(self, card_id: CardId) -> Card | None: - for card in self.cards: + for card in reversed(self.cards): if card.card_id == card_id: return card def get_card_by_seq_id(self, seq_id: SeqId) -> Card | None: - for card in self.cards: + for card in reversed(self.cards): if card.seq_id == seq_id: return card def remove(self, card_id: CardId) -> Card | None: - card = self.get_card_by_card_id(card_id) - if card in self.cards: + while card := self.get_card_by_card_id(card_id): self.cards.remove(card) - return card + return card class FavroClient: def __init__( @@ -108,11 +109,18 @@ class FavroClient: def _invalidate_cache(self, card_id: CardId) -> None: card = self.cache.remove(card_id) - self.session.cache.delete( - requests=[self._get_cards_request(seq_id=card.seq_id)], - ) + if card: + self.session.cache.delete( + requests=[self._get_cards_request(seq_id=card.seq_id)], + ) - def update_card_description(self, card_id: CardId, description: str) -> Card: + def update_card_contents_locally(self, card_id: CardId, card_contents: CardContents) -> Card: + if card := self.cache.remove(card_id): + card = dataclasses.replace(card, detailed_description=card_contents.description,name=card_contents.name) + self.cache.add_card(card) + return card + + def update_card_contents(self, card_id: CardId, card_contents: CardContents) -> Card: """Returns updated Card.""" if self.read_only == 'silent': logger.warning( @@ -124,7 +132,8 @@ class FavroClient: raise Exception('FavroClient is read only') json_body = { - 'detailedDescription': description, + 'name': card_contents.name, + 'detailedDescription': card_contents.description, 'descriptionFormat': 'markdown', } @@ -135,5 +144,4 @@ class FavroClient: ) response.raise_for_status() self._invalidate_cache(card_id) - - return Card.from_json(response.json()) + return self.update_card_contents_locally(card_id, card_contents) diff --git a/favro_sync/favro_fuse.py b/favro_sync/favro_fuse.py index 5f886a5..a196643 100644 --- a/favro_sync/favro_fuse.py +++ b/favro_sync/favro_fuse.py @@ -9,12 +9,13 @@ import fuse from .favro_client import FavroClient from .favro_data_model import Card, SeqId +from .favro_markdown import format_card, CardContents, parse_card_contents logger = getLogger(__name__) fuse.fuse_python_api = (0, 2) -class MyStat(fuse.Stat): +class FavroStat(fuse.Stat): def __init__(self): self.st_mode = 0 self.st_ino = 0 @@ -52,16 +53,6 @@ class RootThing(Thing): class CardThing(Thing): seq_id: SeqId - -def card_to_contents(card: Card) -> str: - ls = [] - # ls.append('# ') - # ls.append(card.name) - # ls.append('\n\n') - ls.append(card.detailed_description or '') - return ''.join(ls) - - class FavroFuse(fuse.Fuse): """Favro Filesystem in Userspace.""" @@ -69,10 +60,10 @@ class FavroFuse(fuse.Fuse): self.favro_client = favro_client super().__init__(**kwargs) - def getattr(self, path: str) -> MyStat | int: + def getattr(self, path: str) -> FavroStat | int: thing = Thing.from_path(path) - st = MyStat() + st = FavroStat() if isinstance(thing, RootThing): st.st_mode = stat.S_IFDIR | 0o755 st.st_nlink = 2 @@ -81,7 +72,7 @@ class FavroFuse(fuse.Fuse): st.st_mode = stat.S_IFREG | 0o666 st.st_nlink = 1 - st.st_size = len(card_to_contents(card)) + st.st_size = len(format_card(card)) st.st_ctime = int(card.creation_date.timestamp()) st.st_mtime = st.st_ctime # TODO else: @@ -100,8 +91,6 @@ class FavroFuse(fuse.Fuse): thing = Thing.from_path(path) if not isinstance(thing, CardThing): return -errno.ENOENT - # accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR - # if (flags & accmode) != os.O_RDONLY: return -errno.EACCES return None def read(self, path: str, size: int, offset: int) -> bytes | int: @@ -112,7 +101,7 @@ class FavroFuse(fuse.Fuse): card = self.favro_client.get_card(thing.seq_id) - contents_str = card_to_contents(card) + contents_str = format_card(card) contents = bytes(contents_str, 'utf8') slen = len(contents) @@ -133,13 +122,14 @@ class FavroFuse(fuse.Fuse): card = self.favro_client.get_card(thing.seq_id) # Splice contents - contents_str = card_to_contents(card) + contents_str = format_card(card) contents = bytes(contents_str, 'utf8') contents = splice(contents, written_buffer, offset) contents_str = contents.decode('utf8') # Write to favro - self.favro_client.update_card_description(card.card_id, contents_str) + card_updated = parse_card_contents(contents_str) + self.favro_client.update_card_contents(card.card_id, card_updated) # Return amount written return len(written_buffer) @@ -153,7 +143,7 @@ class FavroFuse(fuse.Fuse): card = self.favro_client.get_card(thing.seq_id) # Splice contents - contents_str = card_to_contents(card) + contents_str = format_card(card) contents = bytes(contents_str, 'utf8') old_size = len(contents) contents = contents[0:new_size] + b' ' * (old_size - new_size) @@ -161,7 +151,8 @@ class FavroFuse(fuse.Fuse): contents_str = contents.decode('utf8') # Write to favro - self.favro_client.update_card_description(card.card_id, contents_str) + card_updated = parse_card_contents(contents_str) + self.favro_client.update_card_contents_locally(card.card_id, card_updated) # Return amount written return 0 diff --git a/favro_sync/favro_markdown.py b/favro_sync/favro_markdown.py new file mode 100644 index 0000000..744113d --- /dev/null +++ b/favro_sync/favro_markdown.py @@ -0,0 +1,47 @@ +import dataclasses +import errno +import re +import stat +from collections.abc import Iterator +from logging import getLogger + +import marko +import marko.md_renderer + +from .favro_data_model import Card, SeqId + +@dataclasses.dataclass(frozen=True) +class CardContents: + name: str + description: str + +markdown = marko.Markdown() +renderer = marko.md_renderer.MarkdownRenderer() + +def format_card_contents(card: CardContents) -> str: + ls = [] + if card.name: + ls.append('# ') + ls.append(card.name) + ls.append('\n\n') + ls.append(card.description or '') + return ''.join(ls) + +def parse_card_contents(contents: str) -> CardContents: + document = markdown.parse(contents.strip()) + name = None + for elem in document.children: + if isinstance(elem, marko.block.Heading): + name = renderer.render_children(elem) + document.children.remove(elem) + break + + return CardContents( + name, + renderer.render_children(document).strip(), + ) + +def format_card(card: Card) -> str: + return format_card_contents(CardContents(card.name, card.detailed_description)) + + diff --git a/test/test_init.py b/test/test_init.py index 37dc3f3..6321d27 100644 --- a/test/test_init.py +++ b/test/test_init.py @@ -2,5 +2,6 @@ def test_import(): import favro_sync.favro_data_model import favro_sync.favro_client import favro_sync.favro_fuse + import favro_sync.favro_markdown import favro_sync diff --git a/test/test_markdown_parsing.py b/test/test_markdown_parsing.py new file mode 100644 index 0000000..30b5356 --- /dev/null +++ b/test/test_markdown_parsing.py @@ -0,0 +1,19 @@ +from favro_sync.favro_markdown import parse_card_contents, format_card_contents + +EXAMPLE_TEXT_1 = ''' + +# Hello world + +Test description + +## Other header + +1. Derp +2. Derp +3. Derp +'''.strip() + +def test_parse_and_render(): + card_contents = parse_card_contents(EXAMPLE_TEXT_1) + print(card_contents) + assert format_card_contents(card_contents) == EXAMPLE_TEXT_1