1
0

CardFileFormatter: Tags and assignments WIP
Some checks failed
Test Python / Test (push) Failing after 26s

This commit is contained in:
Jon Michael Aanes 2024-09-28 12:37:19 +02:00
parent 9d2cd93ed3
commit d4cfe4da22
Signed by: Jmaa
SSH Key Fingerprint: SHA256:Ab0GfHGCblESJx7JRE4fj4bFy/KRpeLhi41y4pF3sNA
10 changed files with 164 additions and 89 deletions

View File

@ -3,18 +3,14 @@ import tempfile
import logging import logging
import requests_cache import requests_cache
import secret_loader
from .favro_client import FavroClient, OrganizationId from .favro_client import FavroClient, OrganizationId
from .favro_fuse import start_favro_fuse from .favro_fuse import start_favro_fuse
import secrets
def main(): def main():
logging.basicConfig() logging.basicConfig()
secrets = secret_loader.SecretLoader()
favro_org_id = secrets.load_or_fail('FAVRO_ORGANIZATION_ID')
favro_username = secrets.load_or_fail('FAVRO_USERNAME')
favro_password = secrets.load_or_fail('FAVRO_PASSWORD')
read_only = False read_only = False
@ -22,9 +18,9 @@ def main():
session = requests_cache.CachedSession( tmpdirname + '/http-cache.sqlite', expire_after=10,) session = requests_cache.CachedSession( tmpdirname + '/http-cache.sqlite', expire_after=10,)
client = FavroClient( client = FavroClient(
favro_org_id=OrganizationId(favro_org_id), favro_org_id=OrganizationId(secrets.favro_org_id()),
favro_username=favro_username, favro_username=secrets.favro_username(),
favro_password=favro_password, favro_password=secrets.favro_password(),
session=session, session=session,
read_only=read_only, read_only=read_only,
) )

View File

@ -83,8 +83,9 @@ class FavroClient:
response.raise_for_status() response.raise_for_status()
json = response.json() json = response.json()
# TODO: Pageination # TODO: Add support for pageination
for entity_json in json['entities']: for entity_json in json['entities']:
print(entity_json)
card = Card.from_json(entity_json) card = Card.from_json(entity_json)
self.cache.add_card(card) self.cache.add_card(card)
yield card yield card

View File

@ -28,6 +28,38 @@ class OrganizationId:
raw_id: str raw_id: str
@dataclasses.dataclass(frozen=True)
class CardAssignment:
user: UserId
completed: bool
@staticmethod
def from_json(json: dict[str, Any]) -> 'CardAssignment':
return CardAssignment(
UserId(json['userId']),
json['completed'],
)
@dataclasses.dataclass(frozen=True)
class TagId:
raw_id: str
@dataclasses.dataclass(frozen=True)
class CardDependency:
card_id: CardId
card_common_id: CommonId
is_before: bool
reverse_card_id: CardId
@staticmethod
def from_json(json: dict[str, Any]) -> 'CardDependency':
return CardDependency(
CardId(json['cardId']),
CommonId(json['cardCommonId']),
json['isBefore'],
CardId(json['reverseCardId']),
)
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Card: class Card:
card_id: CardId card_id: CardId
@ -36,8 +68,9 @@ class Card:
organization_id: OrganizationId organization_id: OrganizationId
is_archived: bool is_archived: bool
name: str name: str
dependencies: list[None] # TODO dependencies: list[CardDependency]
tags: list[None] # TODO assignments: list[CardAssignment]
tags: list[TagId]
todo_list_user_id: UserId | None todo_list_user_id: UserId | None
todo_list_completed: bool | None todo_list_completed: bool | None
creator_user_id: UserId creator_user_id: UserId
@ -45,23 +78,9 @@ class Card:
detailed_description: str | None detailed_description: str | None
""" TODO, fieds:
'position': -399
'listPosition': -399
'isLane': False
'assignments': [{'userId': 'Faieomp8fuS8DrnyP' 'completed': True}]
'tasksTotal': 0
'tasksDone': 0
'attachments': []
'customFields':
'timeOnBoard': None
'timeOnColumns': None
'favroAttachments': []
"""
@staticmethod @staticmethod
def from_json(json: dict[str, Any]) -> 'Card': def from_json(json: dict[str, Any]) -> 'Card':
todo_list_user_id = UserId(json['todoListUserId']) if 'todoListUserId' in json else None
return Card( return Card(
card_id=CardId(json['cardId']), card_id=CardId(json['cardId']),
seq_id=SeqId(json['sequentialId']), seq_id=SeqId(json['sequentialId']),
@ -70,12 +89,11 @@ class Card:
is_archived=json['archived'], is_archived=json['archived'],
organization_id=OrganizationId(json['organizationId']), organization_id=OrganizationId(json['organizationId']),
name=json['name'], name=json['name'],
todo_list_user_id=UserId(json['todoListUserId']) todo_list_user_id=todo_list_user_id,
if 'todoListUserId' in json
else None,
todo_list_completed=json.get('todoListCompleted'), todo_list_completed=json.get('todoListCompleted'),
dependencies=json['dependencies'], dependencies=json['dependencies'],
tags=json['tags'], tags=[TagId(tag) for tag in 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']),
assignments = [CardAssignment.from_json(ass) for ass in json['assignments']],
) )

View File

@ -9,7 +9,7 @@ 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 from .favro_markdown import CardContents, CardFileFormatter
logger = getLogger(__name__) logger = getLogger(__name__)
@ -56,8 +56,9 @@ class CardThing(Thing):
class FavroFuse(fuse.Fuse): class FavroFuse(fuse.Fuse):
"""Favro Filesystem in Userspace.""" """Favro Filesystem in Userspace."""
def __init__(self, favro_client: FavroClient, **kwargs): def __init__(self, favro_client: FavroClient, formatter: CardFileFormatter, **kwargs):
self.favro_client = favro_client self.favro_client = favro_client
self.formatter = formatter
super().__init__(**kwargs) super().__init__(**kwargs)
def getattr(self, path: str) -> FavroStat | int: def getattr(self, path: str) -> FavroStat | int:
@ -72,7 +73,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(format_card(card)) st.st_size = len(self.formatter.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:
@ -101,7 +102,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 = format_card(card) contents_str = self.formatter.format_card(card)
contents = bytes(contents_str, 'utf8') contents = bytes(contents_str, 'utf8')
slen = len(contents) slen = len(contents)
@ -122,13 +123,13 @@ 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 = format_card(card) contents_str = self.formatter.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
card_updated = 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)
# Return amount written # Return amount written
@ -143,7 +144,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 = format_card(card) contents_str = self.formatter.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)
@ -151,7 +152,7 @@ class FavroFuse(fuse.Fuse):
contents_str = contents.decode('utf8') contents_str = contents.decode('utf8')
# Write to favro # Write to favro
card_updated = 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)
# Return amount written # Return amount written
@ -179,6 +180,7 @@ def start_favro_fuse(favro_client: FavroClient):
# TODO: # TODO:
server = FavroFuse( server = FavroFuse(
favro_client=favro_client, favro_client=favro_client,
formatter=CardFileFormatter(),
version='%prog ' + fuse.__version__, version='%prog ' + fuse.__version__,
usage=HELP, usage=HELP,
dash_s_do='setsingle', dash_s_do='setsingle',

View File

@ -13,25 +13,44 @@ from .favro_data_model import Card, SeqId
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class CardContents: class CardContents:
name: str name: str | None
description: str description: str | None
tags: list[str]
assignments: list[str]
markdown = marko.Markdown() class CardFileFormatter:
renderer = marko.md_renderer.MarkdownRenderer() """Component for formatting and parsing card files """
OBSIDIAN_MODE = True def __init__(self, obsidian_mode = True):
self.obsidian_mode = obsidian_mode
self.markdown = marko.Markdown()
self.renderer = marko.md_renderer.MarkdownRenderer()
def format_card_contents(card: CardContents) -> str: def format_card_contents(self, card: CardContents) -> str:
ls = [] ls = []
# Choose frontmatter data
frontmatter_data = {}
if card.name and self.obsidian_mode:
frontmatter_data['aliases'] = [card.name]
if card.tags:
frontmatter_data['tags'] = card.tags
if self.obsidian_mode:
frontmatter_data['tags'] = ['#'+t for t in frontmatter_data['tags']]
if card.assignments:
frontmatter_data['assignments'] = card.assignments
if self.obsidian_mode:
frontmatter_data['assignments'] = [f'[[{name}]]' for name in frontmatter_data['assignments']]
# Frontmatter # Frontmatter
if OBSIDIAN_MODE: if frontmatter_data:
if card.name:
ls.append('---\n') ls.append('---\n')
# TODO: Tags for key, values in frontmatter_data.items():
ls.append('aliases:\n') ls.append(key)
ls.append(':\n')
for v in values:
ls.append(' - ') ls.append(' - ')
ls.append(card.name) ls.append(v)
ls.append('\n') ls.append('\n')
ls.append('---\n\n') ls.append('---\n\n')
@ -46,7 +65,7 @@ def format_card_contents(card: CardContents) -> str:
ls.append(card.description) ls.append(card.description)
return ''.join(ls) return ''.join(ls)
def parse_card_contents(contents: str) -> CardContents: def parse_card_contents(self, contents: str) -> CardContents:
""" """
1. Strips frontmatter 1. Strips frontmatter
2. Parses header 2. Parses header
@ -55,18 +74,21 @@ def parse_card_contents(contents: str) -> CardContents:
fm = frontmatter.loads(contents) fm = frontmatter.loads(contents)
del contents del contents
document = markdown.parse(fm.content.strip()) document = self.markdown.parse(fm.content.strip())
name = None name = None
for elem in document.children: for elem in document.children:
if isinstance(elem, marko.block.Heading): if isinstance(elem, marko.block.Heading):
name = renderer.render_children(elem) name = self.renderer.render_children(elem)
document.children.remove(elem) document.children.remove(elem)
break break
description = self.renderer.render_children(document).strip()
return CardContents( return CardContents(
name, name,
renderer.render_children(document).strip(), description,
tags = [],
assignments = [],
) )
def format_card(card: Card) -> str: def format_card(self, card: Card) -> str:
return format_card_contents(CardContents(card.name, card.detailed_description)) return self.format_card_contents(CardContents(card.name, card.detailed_description))

12
favro_sync/secrets.py Normal file
View File

@ -0,0 +1,12 @@
import secret_loader
secrets = secret_loader.SecretLoader()
def favro_org_id():
return secrets.load_or_fail('FAVRO_ORGANIZATION_ID')
def favro_username():
return secrets.load_or_fail('FAVRO_USERNAME')
def favro_password():
return secrets.load_or_fail('FAVRO_PASSWORD')

View File

@ -2,3 +2,5 @@ requests
requests-cache requests-cache
fuse-python fuse-python
secret_loader @ git+https://gitfub.space/Jmaa/secret_loader secret_loader @ git+https://gitfub.space/Jmaa/secret_loader
marko
python-frontmatter

1
test/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Testing package."""

22
test/test_client.py Normal file
View File

@ -0,0 +1,22 @@
from favro_sync import secrets
from favro_sync .favro_client import FavroClient, OrganizationId
from favro_sync .favro_fuse import start_favro_fuse
# TODO: Skip if no secrets
def test_create_client():
client = FavroClient(
favro_org_id=OrganizationId(secrets.favro_org_id()),
favro_username=secrets.favro_username(),
favro_password=secrets.favro_password(),
read_only=True,
)
assert client is not None
client.check_logged_in()
return client
def test_get_card():
client = test_create_client()
card = next(client.get_todo_list_cards())
assert card is not None

View File

@ -1,4 +1,4 @@
from favro_sync.favro_markdown import parse_card_contents, format_card_contents from favro_sync.favro_markdown import CardFileFormatter
EXAMPLE_TEXT_1 = ''' EXAMPLE_TEXT_1 = '''
--- ---
@ -17,12 +17,11 @@ Test description
3. Derp 3. Derp
'''.strip() '''.strip()
FORMATTER = CardFileFormatter()
def test_parse_and_render(): def test_parse_and_render():
card_contents = parse_card_contents(EXAMPLE_TEXT_1) card_contents = FORMATTER.parse_card_contents(EXAMPLE_TEXT_1)
assert card_contents.name == 'Hello World' assert card_contents.name == 'Hello World'
assert '---' not in card_contents.description assert '---' not in card_contents.description
print(card_contents) assert FORMATTER.format_card_contents(card_contents) == EXAMPLE_TEXT_1
assert False
assert format_card_contents(card_contents) == EXAMPLE_TEXT_1