Working on writing functionality
This commit is contained in:
parent
58232b081a
commit
cd3d1e3f33
|
@ -6,4 +6,6 @@ Synchronize your local notes and your Favro.
|
||||||
|
|
||||||
Uses the [Favro API](https://favro.com/developer/). Rate limiting depends upon
|
Uses the [Favro API](https://favro.com/developer/). Rate limiting depends upon
|
||||||
your organization's payment plan.
|
your organization's payment plan.
|
||||||
|
|
||||||
|
Uses [`python-fuse`](https://github.com/libfuse/python-fuse) library.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -12,7 +12,9 @@ def main():
|
||||||
favro_username = secrets.load_or_fail('FAVRO_USERNAME')
|
favro_username = secrets.load_or_fail('FAVRO_USERNAME')
|
||||||
favro_password = secrets.load_or_fail('FAVRO_PASSWORD')
|
favro_password = secrets.load_or_fail('FAVRO_PASSWORD')
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix='favro_sync-') as tmpdirname:
|
#with tempfile.TemporaryDirectory(prefix='favro_sync_') as tmpdirname:
|
||||||
|
tmpdirname = './output' # TODO
|
||||||
|
if True:
|
||||||
session = requests_cache.CachedSession(tmpdirname + '/http-cache.sqlite', expire_after=360)
|
session = requests_cache.CachedSession(tmpdirname + '/http-cache.sqlite', expire_after=360)
|
||||||
|
|
||||||
client = FavroClient(favro_org_id=OrganizationId(favro_org_id),
|
client = FavroClient(favro_org_id=OrganizationId(favro_org_id),
|
||||||
|
|
|
@ -46,11 +46,6 @@ class Card:
|
||||||
|
|
||||||
detailed_description: str | None
|
detailed_description: str | None
|
||||||
|
|
||||||
# Derived
|
|
||||||
|
|
||||||
seq_id_with_prefix: str
|
|
||||||
|
|
||||||
|
|
||||||
''' TODO, fieds:
|
''' TODO, fieds:
|
||||||
'position': -399
|
'position': -399
|
||||||
'listPosition': -399
|
'listPosition': -399
|
||||||
|
@ -82,8 +77,6 @@ class Card:
|
||||||
tags = json['tags'],
|
tags = json['tags'],
|
||||||
creator_user_id = UserId(json['createdByUserId']),
|
creator_user_id = UserId(json['createdByUserId']),
|
||||||
creation_date = datetime.datetime.fromisoformat(json['createdAt']),
|
creation_date = datetime.datetime.fromisoformat(json['createdAt']),
|
||||||
|
|
||||||
seq_id_with_prefix = PREFIX + str(json['sequentialId']),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
|
@ -115,11 +108,11 @@ class FavroClient:
|
||||||
yield from self.get_cards(todo_list=True)
|
yield from self.get_cards(todo_list=True)
|
||||||
|
|
||||||
|
|
||||||
def get_cards(self, *, seqid: SeqId | None = None, todo_list=True) -> Iterator[Card]:
|
def get_cards(self, *, seq_id: SeqId | None = None, todo_list=True) -> Iterator[Card]:
|
||||||
# Determine params for get_cards
|
# Determine params for get_cards
|
||||||
params = {'descriptionFormat': 'markdown'}
|
params = {'descriptionFormat': 'markdown'}
|
||||||
if seqid:
|
if seq_id:
|
||||||
params['cardSequentialId']= str(seqid.raw_id)
|
params['cardSequentialId']= str(seq_id.raw_id)
|
||||||
if todo_list:
|
if todo_list:
|
||||||
params['todoList'] = 'true'
|
params['todoList'] = 'true'
|
||||||
|
|
||||||
|
@ -133,11 +126,11 @@ class FavroClient:
|
||||||
yield Card.from_json(entity_json)
|
yield Card.from_json(entity_json)
|
||||||
del entity_json
|
del entity_json
|
||||||
|
|
||||||
def get_card(self, seqid: SeqId) -> Card:
|
def get_card(self, seq_id: SeqId) -> Card:
|
||||||
return next(self.get_cards(seqid=seqid))
|
return next(self.get_cards(seq_id=seq_id))
|
||||||
|
|
||||||
def get_card_id(self, seqid: SeqId) -> CardId:
|
def get_card_id(self, seq_id: SeqId) -> CardId:
|
||||||
first_card = next(self.get_cards(seqid = seqid))
|
first_card = next(self.get_cards(seq_id = seq_id))
|
||||||
return first_card.card_id
|
return first_card.card_id
|
||||||
|
|
||||||
def update_card_description(self, card_id: CardId, description: str) -> Card:
|
def update_card_description(self, card_id: CardId, description: str) -> Card:
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import os, stat, errno, fuse
|
import os, stat, errno, fuse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import dataclasses
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
|
||||||
from .favro_client import FavroClient
|
from .favro_client import FavroClient, CardId, SeqId, Card
|
||||||
|
|
||||||
fuse.fuse_python_api = (0, 2)
|
fuse.fuse_python_api = (0, 2)
|
||||||
|
|
||||||
|
@ -22,21 +24,59 @@ class MyStat(fuse.Stat):
|
||||||
self.st_mtime = 0
|
self.st_mtime = 0
|
||||||
self.st_ctime = 0
|
self.st_ctime = 0
|
||||||
|
|
||||||
|
CARD_FILENAME_FORMAT = 'PAR-{seq_id}.md'
|
||||||
|
CARD_FILENAME_REGEX = r'^\/PAR\-(\d+)\.md$'
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Thing:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_path(path_str: str) -> 'Thing | None':
|
||||||
|
if path_str == '/':
|
||||||
|
return RootThing()
|
||||||
|
if m := re.match(CARD_FILENAME_REGEX, path_str):
|
||||||
|
return CardThing(SeqId(int(m.group(1))))
|
||||||
|
return None
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class RootThing(Thing):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
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):
|
class FavroFuse(fuse.Fuse):
|
||||||
|
'''Favro Filesystem in Userspace.
|
||||||
|
'''
|
||||||
|
|
||||||
def __init__(self, favro_client: FavroClient, **kwargs):
|
def __init__(self, favro_client: FavroClient, **kwargs):
|
||||||
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) -> MyStat | int:
|
||||||
|
thing = Thing.from_path(path)
|
||||||
|
|
||||||
st = MyStat()
|
st = MyStat()
|
||||||
if path == '/':
|
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
|
||||||
elif path == hello_path:
|
elif isinstance(thing, CardThing):
|
||||||
st.st_mode = stat.S_IFREG | 0o444
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
|
||||||
|
st.st_mode = stat.S_IFREG | 0o666
|
||||||
st.st_nlink = 1
|
st.st_nlink = 1
|
||||||
st.st_size = len(hello_str)
|
st.st_size = len(card_to_contents(card))
|
||||||
|
st.st_ctime = int(card.creation_date.timestamp())
|
||||||
|
st.st_mtime = st.st_ctime # TODO
|
||||||
else:
|
else:
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
return st
|
return st
|
||||||
|
@ -46,27 +86,81 @@ class FavroFuse(fuse.Fuse):
|
||||||
yield fuse.Direntry('..')
|
yield fuse.Direntry('..')
|
||||||
|
|
||||||
for card in self.favro_client.get_todo_list_cards():
|
for card in self.favro_client.get_todo_list_cards():
|
||||||
yield fuse.Direntry(card.seq_id_with_prefix)
|
yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id))
|
||||||
|
return # TODO
|
||||||
|
|
||||||
def open(self, path: str, flags) -> int:
|
def open(self, path: str, flags) -> int | None:
|
||||||
if path != hello_path:
|
thing = Thing.from_path(path)
|
||||||
|
if not isinstance(thing, CardThing):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
|
#accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
|
||||||
if (flags & accmode) != os.O_RDONLY:
|
#if (flags & accmode) != os.O_RDONLY: return -errno.EACCES
|
||||||
return -errno.EACCES
|
return None
|
||||||
|
|
||||||
def read(self, path: str, size: int, offset: int) -> bytes | int:
|
def read(self, path: str, size: int, offset: int) -> bytes | int:
|
||||||
if path != hello_path:
|
# Check that this is a card thing.
|
||||||
|
thing = Thing.from_path(path)
|
||||||
|
if not isinstance(thing, CardThing):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
slen = len(hello_str)
|
|
||||||
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
|
||||||
|
contents_str = card_to_contents(card)
|
||||||
|
contents = bytes(contents_str, 'utf8')
|
||||||
|
|
||||||
|
slen = len(contents)
|
||||||
if offset < slen:
|
if offset < slen:
|
||||||
if offset + size > slen:
|
if offset + size > slen:
|
||||||
size = slen - offset
|
size = slen - offset
|
||||||
buf = hello_str[offset:offset+size]
|
buf = contents[offset:offset+size]
|
||||||
else:
|
else:
|
||||||
buf = b''
|
buf = b''
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
def write(self, path: str, written_buffer: bytes, offset: int) -> int:
|
||||||
|
# Check that this is a card thing.
|
||||||
|
thing = Thing.from_path(path)
|
||||||
|
if not isinstance(thing, CardThing):
|
||||||
|
return -errno.ENOENT
|
||||||
|
|
||||||
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
|
||||||
|
# Splice contents
|
||||||
|
contents_str = card_to_contents(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)
|
||||||
|
|
||||||
|
# Return amount written
|
||||||
|
return len(written_buffer)
|
||||||
|
|
||||||
|
def truncate(self, path: str, new_size: int):
|
||||||
|
print('Trunca', path, new_size)
|
||||||
|
# Check that this is a card thing.
|
||||||
|
thing = Thing.from_path(path)
|
||||||
|
if not isinstance(thing, CardThing):
|
||||||
|
return -errno.ENOENT
|
||||||
|
|
||||||
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
|
||||||
|
# Splice contents
|
||||||
|
contents_str = card_to_contents(card)
|
||||||
|
contents = bytes(contents_str, 'utf8')
|
||||||
|
contents = contents[0:new_size]
|
||||||
|
contents_str = contents.decode('utf8')
|
||||||
|
|
||||||
|
# Write to favro
|
||||||
|
self.favro_client.update_card_description(card.card_id, contents_str)
|
||||||
|
|
||||||
|
# Return amount written
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def splice(original_buffer: bytes, input_buffer: bytes, offset: int) -> bytes:
|
||||||
|
return original_buffer[0:offset-1] + input_buffer + original_buffer[offset+len(input_buffer)+1:len(original_buffer)]
|
||||||
|
|
||||||
HELP="""
|
HELP="""
|
||||||
Userspace hello example
|
Userspace hello example
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user