This commit is contained in:
parent
dde17e2887
commit
cb9593b744
|
@ -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`
|
||||
"""
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
47
favro_sync/favro_markdown.py
Normal file
47
favro_sync/favro_markdown.py
Normal 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))
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
19
test/test_markdown_parsing.py
Normal file
19
test/test_markdown_parsing.py
Normal 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
|
Loading…
Reference in New Issue
Block a user