1
0

Parse and map markdown
Some checks failed
Test Python / Test (push) Failing after 24s

This commit is contained in:
Jon Michael Aanes 2024-09-27 16:13:03 +02:00
parent dde17e2887
commit cb9593b744
7 changed files with 107 additions and 37 deletions

View File

@ -14,7 +14,6 @@ Limitations:
- Only cards in todolist is fetched at the moment. - Only cards in todolist is fetched at the moment.
- Doesn't include title anywhere. - Doesn't include title anywhere.
- Tasks cannot be updated or changed. - Tasks cannot be updated or changed.
- Slow, due to inefficient use of caches.
A more complete implementation will probably require a Markdown parser, to A more complete implementation will probably require a Markdown parser, to
parse the saved input, and distribute it across the various Card fields (card 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 overview of all supported flags (there is a lot, because
[`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole [`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole
bunch automatically.) bunch automatically.)
## Architecture
- `FavroFuse`
- Markdown Parser/Renderer
- `FavroClient`
- `CardCache`
""" """

View File

@ -19,9 +19,7 @@ def main():
read_only = False read_only = False
with tempfile.TemporaryDirectory(prefix='favro_sync_') as tmpdirname: with tempfile.TemporaryDirectory(prefix='favro_sync_') as tmpdirname:
session = requests_cache.CachedSession( session = requests_cache.CachedSession( tmpdirname + '/http-cache.sqlite', expire_after=10,)
tmpdirname + '/http-cache.sqlite', expire_after=360,
)
client = FavroClient( client = FavroClient(
favro_org_id=OrganizationId(favro_org_id), favro_org_id=OrganizationId(favro_org_id),

View File

@ -2,8 +2,10 @@ from collections.abc import Iterator
from logging import getLogger from logging import getLogger
import requests import requests
import dataclasses
from .favro_data_model import Card, CardId, OrganizationId, SeqId from .favro_data_model import Card, CardId, OrganizationId, SeqId
from .favro_markdown import CardContents
logger = getLogger(__name__) logger = getLogger(__name__)
@ -22,20 +24,19 @@ class CardCache:
self.cards.append(card) self.cards.append(card)
def get_card_by_card_id(self, card_id: CardId) -> Card | None: 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: if card.card_id == card_id:
return card return card
def get_card_by_seq_id(self, seq_id: SeqId) -> Card | None: 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: if card.seq_id == seq_id:
return card return card
def remove(self, card_id: CardId) -> Card | None: def remove(self, card_id: CardId) -> Card | None:
card = self.get_card_by_card_id(card_id) while card := self.get_card_by_card_id(card_id):
if card in self.cards:
self.cards.remove(card) self.cards.remove(card)
return card return card
class FavroClient: class FavroClient:
def __init__( def __init__(
@ -108,11 +109,18 @@ class FavroClient:
def _invalidate_cache(self, card_id: CardId) -> None: def _invalidate_cache(self, card_id: CardId) -> None:
card = self.cache.remove(card_id) card = self.cache.remove(card_id)
self.session.cache.delete( if card:
requests=[self._get_cards_request(seq_id=card.seq_id)], 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.""" """Returns updated Card."""
if self.read_only == 'silent': if self.read_only == 'silent':
logger.warning( logger.warning(
@ -124,7 +132,8 @@ class FavroClient:
raise Exception('FavroClient is read only') raise Exception('FavroClient is read only')
json_body = { json_body = {
'detailedDescription': description, 'name': card_contents.name,
'detailedDescription': card_contents.description,
'descriptionFormat': 'markdown', 'descriptionFormat': 'markdown',
} }
@ -135,5 +144,4 @@ class FavroClient:
) )
response.raise_for_status() response.raise_for_status()
self._invalidate_cache(card_id) self._invalidate_cache(card_id)
return self.update_card_contents_locally(card_id, card_contents)
return Card.from_json(response.json())

View File

@ -9,12 +9,13 @@ import fuse
from .favro_client import FavroClient from .favro_client import FavroClient
from .favro_data_model import Card, SeqId from .favro_data_model import Card, SeqId
from .favro_markdown import format_card, CardContents, parse_card_contents
logger = getLogger(__name__) logger = getLogger(__name__)
fuse.fuse_python_api = (0, 2) fuse.fuse_python_api = (0, 2)
class MyStat(fuse.Stat): class FavroStat(fuse.Stat):
def __init__(self): def __init__(self):
self.st_mode = 0 self.st_mode = 0
self.st_ino = 0 self.st_ino = 0
@ -52,16 +53,6 @@ class RootThing(Thing):
class CardThing(Thing): class CardThing(Thing):
seq_id: SeqId 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): class FavroFuse(fuse.Fuse):
"""Favro Filesystem in Userspace.""" """Favro Filesystem in Userspace."""
@ -69,10 +60,10 @@ class FavroFuse(fuse.Fuse):
self.favro_client = favro_client self.favro_client = favro_client
super().__init__(**kwargs) super().__init__(**kwargs)
def getattr(self, path: str) -> MyStat | int: def getattr(self, path: str) -> FavroStat | int:
thing = Thing.from_path(path) thing = Thing.from_path(path)
st = MyStat() st = FavroStat()
if isinstance(thing, RootThing): if isinstance(thing, RootThing):
st.st_mode = stat.S_IFDIR | 0o755 st.st_mode = stat.S_IFDIR | 0o755
st.st_nlink = 2 st.st_nlink = 2
@ -81,7 +72,7 @@ class FavroFuse(fuse.Fuse):
st.st_mode = stat.S_IFREG | 0o666 st.st_mode = stat.S_IFREG | 0o666
st.st_nlink = 1 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_ctime = int(card.creation_date.timestamp())
st.st_mtime = st.st_ctime # TODO st.st_mtime = st.st_ctime # TODO
else: else:
@ -100,8 +91,6 @@ class FavroFuse(fuse.Fuse):
thing = Thing.from_path(path) thing = Thing.from_path(path)
if not isinstance(thing, CardThing): if not isinstance(thing, CardThing):
return -errno.ENOENT return -errno.ENOENT
# accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
# if (flags & accmode) != os.O_RDONLY: return -errno.EACCES
return None return None
def read(self, path: str, size: int, offset: int) -> bytes | int: 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) 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') contents = bytes(contents_str, 'utf8')
slen = len(contents) slen = len(contents)
@ -133,13 +122,14 @@ class FavroFuse(fuse.Fuse):
card = self.favro_client.get_card(thing.seq_id) card = self.favro_client.get_card(thing.seq_id)
# Splice contents # Splice contents
contents_str = card_to_contents(card) contents_str = format_card(card)
contents = bytes(contents_str, 'utf8') contents = bytes(contents_str, 'utf8')
contents = splice(contents, written_buffer, offset) contents = splice(contents, written_buffer, offset)
contents_str = contents.decode('utf8') contents_str = contents.decode('utf8')
# Write to favro # 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 amount written
return len(written_buffer) return len(written_buffer)
@ -153,7 +143,7 @@ class FavroFuse(fuse.Fuse):
card = self.favro_client.get_card(thing.seq_id) card = self.favro_client.get_card(thing.seq_id)
# Splice contents # Splice contents
contents_str = card_to_contents(card) contents_str = format_card(card)
contents = bytes(contents_str, 'utf8') contents = bytes(contents_str, 'utf8')
old_size = len(contents) old_size = len(contents)
contents = contents[0:new_size] + b' ' * (old_size - new_size) contents = contents[0:new_size] + b' ' * (old_size - new_size)
@ -161,7 +151,8 @@ class FavroFuse(fuse.Fuse):
contents_str = contents.decode('utf8') contents_str = contents.decode('utf8')
# Write to favro # 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 amount written
return 0 return 0

View File

@ -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))

View File

@ -2,5 +2,6 @@ def test_import():
import favro_sync.favro_data_model import favro_sync.favro_data_model
import favro_sync.favro_client import favro_sync.favro_client
import favro_sync.favro_fuse import favro_sync.favro_fuse
import favro_sync.favro_markdown
import favro_sync import favro_sync

View File

@ -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