1
0

Read-only support for tags and assignments
Some checks failed
Test Python / Test (push) Failing after 25s

This commit is contained in:
Jon Michael Aanes 2024-09-28 12:54:34 +02:00
parent 9fb19e39a5
commit 5b1502cdea
Signed by: Jmaa
SSH Key Fingerprint: SHA256:Ab0GfHGCblESJx7JRE4fj4bFy/KRpeLhi41y4pF3sNA
8 changed files with 94 additions and 54 deletions

View File

@ -1,12 +1,11 @@
import tempfile
import logging import logging
import tempfile
import requests_cache import requests_cache
from . import secrets
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
from . import secrets
def main(): def main():
@ -15,7 +14,10 @@ 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( 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(secrets.favro_org_id()), favro_org_id=OrganizationId(secrets.favro_org_id()),

View File

@ -1,11 +1,19 @@
import dataclasses
from collections.abc import Iterator 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, UserInfo, from .favro_data_model import (
UserId, UserInfo, TagId, TagInfo) Card,
CardId,
OrganizationId,
SeqId,
TagId,
TagInfo,
UserId,
UserInfo,
)
from .favro_markdown import CardContents from .favro_markdown import CardContents
logger = getLogger(__name__) logger = getLogger(__name__)
@ -17,8 +25,8 @@ URL_UPDATE_CARD = URL_API_ROOT + '/cards/{card_id}'
URL_GET_USER = URL_API_ROOT + '/users/{user_id}' URL_GET_USER = URL_API_ROOT + '/users/{user_id}'
URL_GET_TAG = URL_API_ROOT + '/tags/{tag_id}' URL_GET_TAG = URL_API_ROOT + '/tags/{tag_id}'
class CardCache:
class CardCache:
def __init__(self): def __init__(self):
self.cards = [] self.cards = []
@ -44,6 +52,7 @@ class CardCache:
return card return card
return None return None
class FavroClient: class FavroClient:
def __init__( def __init__(
self, self,
@ -78,7 +87,10 @@ class FavroClient:
yield from self.get_cards(todo_list=True) yield from self.get_cards(todo_list=True)
def get_cards( def get_cards(
self, *, seq_id: SeqId | None = None, todo_list=False, self,
*,
seq_id: SeqId | None = None,
todo_list=False,
) -> Iterator[Card]: ) -> Iterator[Card]:
# Determine params for get_cards # Determine params for get_cards
request = self._get_cards_request(seq_id, todo_list) request = self._get_cards_request(seq_id, todo_list)
@ -97,7 +109,9 @@ class FavroClient:
del entity_json del entity_json
def _get_cards_request( def _get_cards_request(
self, seq_id: SeqId | None = None, todo_list=False, self,
seq_id: SeqId | None = None,
todo_list=False,
) -> requests.PreparedRequest: ) -> requests.PreparedRequest:
params = {'descriptionFormat': 'markdown'} params = {'descriptionFormat': 'markdown'}
if seq_id is not None: if seq_id is not None:
@ -128,13 +142,21 @@ class FavroClient:
requests=[self._get_cards_request(seq_id=card.seq_id)], requests=[self._get_cards_request(seq_id=card.seq_id)],
) )
def update_card_contents_locally(self, card_id: CardId, card_contents: CardContents) -> Card: def update_card_contents_locally(
self, card_id: CardId, card_contents: CardContents,
) -> Card:
if card := self.cache.remove(card_id): if card := self.cache.remove(card_id):
card = dataclasses.replace(card, detailed_description=card_contents.description,name=card_contents.name) card = dataclasses.replace(
card,
detailed_description=card_contents.description,
name=card_contents.name,
)
self.cache.add_card(card) self.cache.add_card(card)
return card return card
def update_card_contents(self, card_id: CardId, card_contents: CardContents) -> 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(
@ -154,7 +176,8 @@ class FavroClient:
url = URL_UPDATE_CARD.format(card_id=card_id.raw_id) url = URL_UPDATE_CARD.format(card_id=card_id.raw_id)
logger.warning('Sending request: %s', url) logger.warning('Sending request: %s', url)
response = self.session.put( response = self.session.put(
url, json=json_body, url,
json=json_body,
) )
response.raise_for_status() response.raise_for_status()
self._invalidate_cache(card_id) self._invalidate_cache(card_id)

View File

@ -40,6 +40,7 @@ class CardAssignment:
json['completed'], json['completed'],
) )
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class UserInfo: class UserInfo:
user_id: UserId user_id: UserId
@ -56,10 +57,12 @@ class UserInfo:
json['organizationRole'], json['organizationRole'],
) )
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class TagId: class TagId:
raw_id: str raw_id: str
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class TagInfo: class TagInfo:
tag_id: TagId tag_id: TagId
@ -76,6 +79,7 @@ class TagInfo:
json.get('color'), json.get('color'),
) )
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class CardDependency: class CardDependency:
card_id: CardId card_id: CardId
@ -92,6 +96,7 @@ class CardDependency:
CardId(json['reverseCardId']), CardId(json['reverseCardId']),
) )
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Card: class Card:
card_id: CardId card_id: CardId
@ -112,7 +117,9 @@ class Card:
@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 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']),
@ -127,5 +134,5 @@ class Card:
tags=[TagId(tag) for tag in 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']], assignments=[CardAssignment.from_json(ass) for ass in json['assignments']],
) )

View File

@ -15,6 +15,7 @@ logger = getLogger(__name__)
fuse.fuse_python_api = (0, 2) fuse.fuse_python_api = (0, 2)
class FavroStat(fuse.Stat): class FavroStat(fuse.Stat):
def __init__(self): def __init__(self):
self.st_mode = 0 self.st_mode = 0
@ -53,10 +54,13 @@ class RootThing(Thing):
class CardThing(Thing): class CardThing(Thing):
seq_id: SeqId seq_id: SeqId
class FavroFuse(fuse.Fuse): class FavroFuse(fuse.Fuse):
"""Favro Filesystem in Userspace.""" """Favro Filesystem in Userspace."""
def __init__(self, favro_client: FavroClient, formatter: CardFileFormatter, **kwargs): def __init__(
self, favro_client: FavroClient, formatter: CardFileFormatter, **kwargs,
):
self.favro_client = favro_client self.favro_client = favro_client
self.formatter = formatter self.formatter = formatter
super().__init__(**kwargs) super().__init__(**kwargs)
@ -81,7 +85,7 @@ class FavroFuse(fuse.Fuse):
return st return st
def readdir(self, path: str, offset: int) -> Iterator[fuse.Direntry]: def readdir(self, path: str, offset: int) -> Iterator[fuse.Direntry]:
logger.warning('readdir(path=%s, offset=%s)',path, offset) logger.warning('readdir(path=%s, offset=%s)', path, offset)
yield fuse.Direntry('.') yield fuse.Direntry('.')
yield fuse.Direntry('..') yield fuse.Direntry('..')
@ -160,7 +164,10 @@ class FavroFuse(fuse.Fuse):
def _format_card_file(self, card: Card) -> str: def _format_card_file(self, card: Card) -> str:
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 = [self.favro_client.get_user(assignment.user).name for assignment in card.assignments] assignments = [
self.favro_client.get_user(assignment.user).name
for assignment in card.assignments
]
card_contents = CardContents( card_contents = CardContents(
card.name, card.name,
card.detailed_description, card.detailed_description,
@ -169,6 +176,7 @@ class FavroFuse(fuse.Fuse):
) )
return self.formatter.format_card_contents(card_contents) return self.formatter.format_card_contents(card_contents)
def splice(original_buffer: bytes, input_buffer: bytes, offset: int) -> bytes: def splice(original_buffer: bytes, input_buffer: bytes, offset: int) -> bytes:
return ( return (
original_buffer[0 : offset - 1] original_buffer[0 : offset - 1]

View File

@ -1,15 +1,9 @@
import dataclasses import dataclasses
import errno
import re
import stat
from collections.abc import Iterator
from logging import getLogger
import frontmatter import frontmatter
import marko import marko
import marko.md_renderer import marko.md_renderer
from .favro_data_model import Card, SeqId
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class CardContents: class CardContents:
@ -18,10 +12,11 @@ class CardContents:
tags: list[str] tags: list[str]
assignments: list[str] assignments: list[str]
class CardFileFormatter: class CardFileFormatter:
"""Component for formatting and parsing card files.""" """Component for formatting and parsing card files."""
def __init__(self, obsidian_mode = True): def __init__(self, obsidian_mode=True):
self.obsidian_mode = obsidian_mode self.obsidian_mode = obsidian_mode
self.markdown = marko.Markdown() self.markdown = marko.Markdown()
self.renderer = marko.md_renderer.MarkdownRenderer() self.renderer = marko.md_renderer.MarkdownRenderer()
@ -36,11 +31,13 @@ class CardFileFormatter:
if card.tags: if card.tags:
frontmatter_data['tags'] = card.tags frontmatter_data['tags'] = card.tags
if self.obsidian_mode: if self.obsidian_mode:
frontmatter_data['tags'] = ['#'+t for t in frontmatter_data['tags']] frontmatter_data['tags'] = ['#' + t for t in frontmatter_data['tags']]
if card.assignments: if card.assignments:
frontmatter_data['assignments'] = card.assignments frontmatter_data['assignments'] = card.assignments
if self.obsidian_mode: if self.obsidian_mode:
frontmatter_data['assignments'] = [f'[[{name}]]' for name in frontmatter_data['assignments']] frontmatter_data['assignments'] = [
f'[[{name}]]' for name in frontmatter_data['assignments']
]
# Frontmatter # Frontmatter
if frontmatter_data: if frontmatter_data:
@ -86,7 +83,6 @@ class CardFileFormatter:
return CardContents( return CardContents(
name, name,
description, description,
tags = [], tags=[],
assignments = [], assignments=[],
) )

View File

@ -2,11 +2,14 @@ import secret_loader
secrets = secret_loader.SecretLoader() secrets = secret_loader.SecretLoader()
def favro_org_id(): def favro_org_id():
return secrets.load_or_fail('FAVRO_ORGANIZATION_ID') return secrets.load_or_fail('FAVRO_ORGANIZATION_ID')
def favro_username(): def favro_username():
return secrets.load_or_fail('FAVRO_USERNAME') return secrets.load_or_fail('FAVRO_USERNAME')
def favro_password(): def favro_password():
return secrets.load_or_fail('FAVRO_PASSWORD') return secrets.load_or_fail('FAVRO_PASSWORD')

View File

@ -1,10 +1,9 @@
from favro_sync import secrets from favro_sync import secrets
from favro_sync .favro_client import FavroClient, OrganizationId from favro_sync.favro_client import FavroClient, OrganizationId
from favro_sync .favro_fuse import start_favro_fuse
# TODO: Skip if no secrets # TODO: Skip if no secrets
def test_create_client(): def test_create_client():
client = FavroClient( client = FavroClient(
favro_org_id=OrganizationId(secrets.favro_org_id()), favro_org_id=OrganizationId(secrets.favro_org_id()),
@ -16,6 +15,7 @@ def test_create_client():
client.check_logged_in() client.check_logged_in()
return client return client
def test_get_card(): def test_get_card():
client = test_create_client() client = test_create_client()
card = next(client.get_todo_list_cards()) card = next(client.get_todo_list_cards())

View File

@ -1,6 +1,6 @@
from favro_sync.favro_markdown import CardFileFormatter from favro_sync.favro_markdown import CardFileFormatter
EXAMPLE_TEXT_1 = ''' EXAMPLE_TEXT_1 = """
--- ---
aliases: aliases:
- Hello World - Hello World
@ -15,10 +15,11 @@ Test description
1. Derp 1. Derp
2. Derp 2. Derp
3. Derp 3. Derp
'''.strip() """.strip()
FORMATTER = CardFileFormatter() FORMATTER = CardFileFormatter()
def test_parse_and_render(): def test_parse_and_render():
card_contents = FORMATTER.parse_card_contents(EXAMPLE_TEXT_1) card_contents = FORMATTER.parse_card_contents(EXAMPLE_TEXT_1)