This commit is contained in:
parent
dde17e2887
commit
cb9593b744
|
@ -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`
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,18 +24,17 @@ 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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
if card:
|
||||||
self.session.cache.delete(
|
self.session.cache.delete(
|
||||||
requests=[self._get_cards_request(seq_id=card.seq_id)],
|
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())
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
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_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
|
||||||
|
|
||||||
|
|
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