This commit is contained in:
parent
412a20b006
commit
c09245bd1c
53
README.md
53
README.md
|
@ -15,16 +15,29 @@ your organization's payment plan.
|
||||||
|
|
||||||
Uses [`python-fuse`](https://github.com/libfuse/python-fuse) library.
|
Uses [`python-fuse`](https://github.com/libfuse/python-fuse) library.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Local access to cards in todolist.
|
||||||
|
- Read card features:
|
||||||
|
- Title
|
||||||
|
- Description
|
||||||
|
- Tags
|
||||||
|
- Assignees
|
||||||
|
- Dependencies
|
||||||
|
- Change card features:
|
||||||
|
- Title
|
||||||
|
- Description
|
||||||
|
- [Obsidian](https://obsidian.md/) compatibility:
|
||||||
|
- Mountable within your vault.
|
||||||
|
- Link to cards by either card number or card title.
|
||||||
|
- Tags and dependencies are integrated.
|
||||||
|
|
||||||
Limitations:
|
Limitations:
|
||||||
|
|
||||||
- Only cards in todolist is fetched at the moment.
|
- Only cards in todolist is fetched at the moment.
|
||||||
- Doesn't include title anywhere.
|
- Tasks (checklists on cards) cannot be updated or changed.
|
||||||
- Tasks cannot be updated or changed.
|
- Images cannot be updated or changed.
|
||||||
- Slow, due to inefficient use of caches.
|
- You cannot create new cards, nor any other files.
|
||||||
|
|
||||||
A more complete implementation will probably require a Markdown parser, to
|
|
||||||
parse the saved input, and distribute it across the various Card fields (card
|
|
||||||
name, description, tasks, etc...)
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -35,23 +48,27 @@ name, description, tasks, etc...)
|
||||||
[`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
|
||||||
|
|
||||||
## Dependencies
|
- `FavroFuse`
|
||||||
|
- Markdown Parser/Renderer
|
||||||
|
- `FavroClient`
|
||||||
|
- `CardCache`
|
||||||
|
|
||||||
All requirements can be installed easily using:
|
## Work in Progress
|
||||||
|
|
||||||
```bash
|
Following features are work in progress:
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Full list of requirements:
|
- [ ] Frontmatter: Update Tags
|
||||||
- [requests](https://pypi.org/project/requests/)
|
- [ ] Frontmatter: Updated assigned members
|
||||||
- [requests-cache](https://pypi.org/project/requests-cache/)
|
- [ ] Frontmatter: Arbitrary structured data? Read-only.
|
||||||
- [fuse-python](https://pypi.org/project/fuse-python/)
|
- [ ] Frontmatter: Dependencies. As vault links in Obsidian mode.
|
||||||
- [secret_loader](https://gitfub.space/Jmaa/secret_loader)
|
- [ ] Allow users to toggle Obsidian mode, instead of being default.
|
||||||
|
- [ ] Get the correct last-modified date.
|
||||||
|
- [ ] Improve cache behaviour. User and tags can have much longer cache times.
|
||||||
|
|
||||||
|
|
||||||
## License
|
# License
|
||||||
|
|
||||||
```
|
```
|
||||||
MIT License
|
MIT License
|
||||||
|
|
|
@ -36,28 +36,28 @@ CARD_FILENAME_REGEX = r'^\/PAR\-(\d+)\.md$'
|
||||||
OFFICIAL_URL='https://favro.com/organization/{org_id}?card=par-{seq_id}'
|
OFFICIAL_URL='https://favro.com/organization/{org_id}?card=par-{seq_id}'
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Thing:
|
class FileSystemItem:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_path(path_str: str) -> 'Thing | None':
|
def from_path(path_str: str) -> 'FileSystemItem | None':
|
||||||
if path_str == '/':
|
if path_str == '/':
|
||||||
return RootThing()
|
return RootFileSystemItem()
|
||||||
if m := re.match(CARD_FILENAME_REGEX, path_str):
|
if m := re.match(CARD_FILENAME_REGEX, path_str):
|
||||||
return CardThing(SeqId(int(m.group(1))))
|
return CardFileSystemItem(SeqId(int(m.group(1))))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class RootThing(Thing):
|
class RootFileSystemItem(FileSystemItem):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CardThing(Thing):
|
class CardFileSystemItem(FileSystemItem):
|
||||||
seq_id: SeqId
|
seq_id: SeqId
|
||||||
|
|
||||||
|
|
||||||
class FavroFuse(fuse.Fuse):
|
class FavroFuse(fuse.Fuse):
|
||||||
"""Favro Filesystem in Userspace."""
|
"""Favro FileSystem in Userspace."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -67,16 +67,17 @@ class FavroFuse(fuse.Fuse):
|
||||||
):
|
):
|
||||||
self.favro_client = favro_client
|
self.favro_client = favro_client
|
||||||
self.formatter = formatter
|
self.formatter = formatter
|
||||||
|
self.wiped_cards = set()
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def getattr(self, path: str) -> FavroStat | int:
|
def getattr(self, path: str) -> FavroStat | int:
|
||||||
thing = Thing.from_path(path)
|
thing = FileSystemItem.from_path(path)
|
||||||
|
|
||||||
st = FavroStat()
|
st = FavroStat()
|
||||||
if isinstance(thing, RootThing):
|
if isinstance(thing, RootFileSystemItem):
|
||||||
st.st_mode = stat.S_IFDIR | 0o755
|
st.st_mode = stat.S_IFDIR | 0o755
|
||||||
st.st_nlink = 2
|
st.st_nlink = 2
|
||||||
elif isinstance(thing, CardThing):
|
elif isinstance(thing, CardFileSystemItem):
|
||||||
card = self.favro_client.get_card(thing.seq_id)
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
|
||||||
st.st_mode = stat.S_IFREG | 0o666
|
st.st_mode = stat.S_IFREG | 0o666
|
||||||
|
@ -97,19 +98,20 @@ class FavroFuse(fuse.Fuse):
|
||||||
yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id))
|
yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id))
|
||||||
|
|
||||||
def open(self, path: str, flags: int) -> int | None:
|
def open(self, path: str, flags: int) -> int | None:
|
||||||
thing = Thing.from_path(path)
|
thing = FileSystemItem.from_path(path)
|
||||||
if not isinstance(thing, CardThing):
|
if not isinstance(thing, CardFileSystemItem):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
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:
|
||||||
# Check that this is a card thing.
|
# Check that this is a card thing.
|
||||||
thing = Thing.from_path(path)
|
thing = FileSystemItem.from_path(path)
|
||||||
if not isinstance(thing, CardThing):
|
if not isinstance(thing, CardFileSystemItem):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
|
|
||||||
card = self.favro_client.get_card(thing.seq_id)
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
|
||||||
|
|
||||||
contents_str = self._format_card_file(card)
|
contents_str = self._format_card_file(card)
|
||||||
contents = bytes(contents_str, 'utf8')
|
contents = bytes(contents_str, 'utf8')
|
||||||
|
|
||||||
|
@ -124,8 +126,8 @@ class FavroFuse(fuse.Fuse):
|
||||||
|
|
||||||
def write(self, path: str, written_buffer: bytes, offset: int) -> int:
|
def write(self, path: str, written_buffer: bytes, offset: int) -> int:
|
||||||
# Check that this is a card thing.
|
# Check that this is a card thing.
|
||||||
thing = Thing.from_path(path)
|
thing = FileSystemItem.from_path(path)
|
||||||
if not isinstance(thing, CardThing):
|
if not isinstance(thing, CardFileSystemItem):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
|
|
||||||
card = self.favro_client.get_card(thing.seq_id)
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
@ -140,13 +142,15 @@ class FavroFuse(fuse.Fuse):
|
||||||
card_updated = self.formatter.parse_card_contents(contents_str)
|
card_updated = self.formatter.parse_card_contents(contents_str)
|
||||||
self.favro_client.update_card_contents(card.card_id, card_updated)
|
self.favro_client.update_card_contents(card.card_id, card_updated)
|
||||||
|
|
||||||
|
self.wiped_cards.remove(thing.seq_id)
|
||||||
|
|
||||||
# Return amount written
|
# Return amount written
|
||||||
return len(written_buffer)
|
return len(written_buffer)
|
||||||
|
|
||||||
def truncate(self, path: str, new_size: int):
|
def truncate(self, path: str, new_size: int):
|
||||||
# Check that this is a card thing.
|
# Check that this is a card thing.
|
||||||
thing = Thing.from_path(path)
|
thing = FileSystemItem.from_path(path)
|
||||||
if not isinstance(thing, CardThing):
|
if not isinstance(thing, CardFileSystemItem):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
|
|
||||||
card = self.favro_client.get_card(thing.seq_id)
|
card = self.favro_client.get_card(thing.seq_id)
|
||||||
|
@ -163,10 +167,15 @@ class FavroFuse(fuse.Fuse):
|
||||||
card_updated = self.formatter.parse_card_contents(contents_str)
|
card_updated = self.formatter.parse_card_contents(contents_str)
|
||||||
self.favro_client.update_card_contents_locally(card.card_id, card_updated)
|
self.favro_client.update_card_contents_locally(card.card_id, card_updated)
|
||||||
|
|
||||||
|
self.wiped_cards.add(thing.seq_id)
|
||||||
|
|
||||||
# Return amount written
|
# Return amount written
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _format_card_file(self, card: Card) -> str:
|
def _format_card_file(self, card: Card) -> str:
|
||||||
|
if card.seq_id in self.wiped_cards:
|
||||||
|
return ''
|
||||||
|
|
||||||
tags = [self.favro_client.get_tag(tag_id).name for tag_id in card.tags]
|
tags = [self.favro_client.get_tag(tag_id).name for tag_id in card.tags]
|
||||||
assignments = [
|
assignments = [
|
||||||
self.favro_client.get_user(assignment.user).name
|
self.favro_client.get_user(assignment.user).name
|
||||||
|
|
|
@ -39,7 +39,9 @@ class CardFileFormatter:
|
||||||
frontmatter_data = {}
|
frontmatter_data = {}
|
||||||
if card.name and self.obsidian_mode:
|
if card.name and self.obsidian_mode:
|
||||||
frontmatter_data['aliases'] = [card.name]
|
frontmatter_data['aliases'] = [card.name]
|
||||||
|
if card.tags:
|
||||||
frontmatter_data['tags'] = card.tags
|
frontmatter_data['tags'] = card.tags
|
||||||
|
if card.url and self.obsidian_mode:
|
||||||
frontmatter_data['url'] = card.url
|
frontmatter_data['url'] = card.url
|
||||||
if card.assignments:
|
if card.assignments:
|
||||||
frontmatter_data['assignments'] = card.assignments
|
frontmatter_data['assignments'] = card.assignments
|
||||||
|
@ -94,6 +96,8 @@ class CardFileFormatter:
|
||||||
card_dependencies: list[str] = fm.metadata.get('dependencies', [])
|
card_dependencies: list[str] = fm.metadata.get('dependencies', [])
|
||||||
card_dependencies = [parse_obsidian_link(text) for text in card_dependencies]
|
card_dependencies = [parse_obsidian_link(text) for text in card_dependencies]
|
||||||
|
|
||||||
|
url: list[str] = fm.metadata.get('url')
|
||||||
|
|
||||||
description = self.renderer.render_children(document).strip()
|
description = self.renderer.render_children(document).strip()
|
||||||
return CardContents(
|
return CardContents(
|
||||||
name,
|
name,
|
||||||
|
@ -101,5 +105,5 @@ class CardFileFormatter:
|
||||||
tags=tags,
|
tags=tags,
|
||||||
assignments=assignments,
|
assignments=assignments,
|
||||||
card_dependencies=card_dependencies,
|
card_dependencies=card_dependencies,
|
||||||
url='', # TODO
|
url=url,
|
||||||
)
|
)
|
||||||
|
|
|
@ -40,6 +40,7 @@ EXAMPLE_TEXT_3 = """
|
||||||
---
|
---
|
||||||
aliases:
|
aliases:
|
||||||
- 'Card: The Adventure of Card'
|
- 'Card: The Adventure of Card'
|
||||||
|
url: https://example.org
|
||||||
---
|
---
|
||||||
|
|
||||||
# Card: The Adventure of Card
|
# Card: The Adventure of Card
|
||||||
|
|
Loading…
Reference in New Issue
Block a user