CardFileFormatter: Tags and assignments WIP
Some checks failed
Test Python / Test (push) Failing after 26s
Some checks failed
Test Python / Test (push) Failing after 26s
This commit is contained in:
parent
9d2cd93ed3
commit
d4cfe4da22
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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']],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -13,60 +13,82 @@ 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 = []
|
||||||
|
|
||||||
# Frontmatter
|
# Choose frontmatter data
|
||||||
if OBSIDIAN_MODE:
|
frontmatter_data = {}
|
||||||
if card.name:
|
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
|
||||||
|
if frontmatter_data:
|
||||||
ls.append('---\n')
|
ls.append('---\n')
|
||||||
# TODO: Tags
|
for key, values in frontmatter_data.items():
|
||||||
ls.append('aliases:\n')
|
ls.append(key)
|
||||||
ls.append(' - ')
|
ls.append(':\n')
|
||||||
ls.append(card.name)
|
for v in values:
|
||||||
ls.append('\n')
|
ls.append(' - ')
|
||||||
|
ls.append(v)
|
||||||
|
ls.append('\n')
|
||||||
ls.append('---\n\n')
|
ls.append('---\n\n')
|
||||||
|
|
||||||
# Card name
|
# Card name
|
||||||
if card.name:
|
if card.name:
|
||||||
ls.append('# ')
|
ls.append('# ')
|
||||||
ls.append(card.name)
|
ls.append(card.name)
|
||||||
ls.append('\n\n')
|
ls.append('\n\n')
|
||||||
|
|
||||||
# Card contents
|
# Card contents
|
||||||
if card.description:
|
if card.description:
|
||||||
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
|
||||||
3. Finds content.
|
3. Finds content.
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
return CardContents(
|
description = self.renderer.render_children(document).strip()
|
||||||
name,
|
return CardContents(
|
||||||
renderer.render_children(document).strip(),
|
name,
|
||||||
)
|
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
12
favro_sync/secrets.py
Normal 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')
|
|
@ -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
1
test/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Testing package."""
|
22
test/test_client.py
Normal file
22
test/test_client.py
Normal 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
|
|
@ -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
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user