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 tempfile
import requests_cache
from . import secrets
from .favro_client import FavroClient, OrganizationId
from .favro_fuse import start_favro_fuse
from . import secrets
def main():
@ -15,7 +14,10 @@ def main():
read_only = False
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(
favro_org_id=OrganizationId(secrets.favro_org_id()),

View File

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

View File

@ -36,10 +36,11 @@ class CardAssignment:
@staticmethod
def from_json(json: dict[str, Any]) -> 'CardAssignment':
return CardAssignment(
UserId(json['userId']),
json['completed'],
UserId(json['userId']),
json['completed'],
)
@dataclasses.dataclass(frozen=True)
class UserInfo:
user_id: UserId
@ -50,16 +51,18 @@ class UserInfo:
@staticmethod
def from_json(json: dict[str, Any]) -> 'UserInfo':
return UserInfo(
UserId(json['userId']),
json['name'],
json['email'],
json['organizationRole'],
UserId(json['userId']),
json['name'],
json['email'],
json['organizationRole'],
)
@dataclasses.dataclass(frozen=True)
class TagId:
raw_id: str
@dataclasses.dataclass(frozen=True)
class TagInfo:
tag_id: TagId
@ -70,12 +73,13 @@ class TagInfo:
@staticmethod
def from_json(json: dict[str, Any]) -> 'TagInfo':
return TagInfo(
TagId(json['tagId']),
OrganizationId(json['organizationId']),
json['name'],
json.get('color'),
TagId(json['tagId']),
OrganizationId(json['organizationId']),
json['name'],
json.get('color'),
)
@dataclasses.dataclass(frozen=True)
class CardDependency:
card_id: CardId
@ -86,12 +90,13 @@ class CardDependency:
@staticmethod
def from_json(json: dict[str, Any]) -> 'CardDependency':
return CardDependency(
CardId(json['cardId']),
CommonId(json['cardCommonId']),
json['isBefore'],
CardId(json['reverseCardId']),
CardId(json['cardId']),
CommonId(json['cardCommonId']),
json['isBefore'],
CardId(json['reverseCardId']),
)
@dataclasses.dataclass(frozen=True)
class Card:
card_id: CardId
@ -112,7 +117,9 @@ class Card:
@staticmethod
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(
card_id=CardId(json['cardId']),
seq_id=SeqId(json['sequentialId']),
@ -127,5 +134,5 @@ class Card:
tags=[TagId(tag) for tag in json['tags']],
creator_user_id=UserId(json['createdByUserId']),
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)
class FavroStat(fuse.Stat):
def __init__(self):
self.st_mode = 0
@ -53,10 +54,13 @@ class RootThing(Thing):
class CardThing(Thing):
seq_id: SeqId
class FavroFuse(fuse.Fuse):
"""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.formatter = formatter
super().__init__(**kwargs)
@ -81,7 +85,7 @@ class FavroFuse(fuse.Fuse):
return st
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('..')
@ -160,7 +164,10 @@ class FavroFuse(fuse.Fuse):
def _format_card_file(self, card: Card) -> str:
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.name,
card.detailed_description,
@ -169,6 +176,7 @@ class FavroFuse(fuse.Fuse):
)
return self.formatter.format_card_contents(card_contents)
def splice(original_buffer: bytes, input_buffer: bytes, offset: int) -> bytes:
return (
original_buffer[0 : offset - 1]

View File

@ -1,15 +1,9 @@
import dataclasses
import errno
import re
import stat
from collections.abc import Iterator
from logging import getLogger
import frontmatter
import marko
import marko.md_renderer
from .favro_data_model import Card, SeqId
@dataclasses.dataclass(frozen=True)
class CardContents:
@ -18,10 +12,11 @@ class CardContents:
tags: list[str]
assignments: list[str]
class CardFileFormatter:
"""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.markdown = marko.Markdown()
self.renderer = marko.md_renderer.MarkdownRenderer()
@ -36,11 +31,13 @@ class CardFileFormatter:
if card.tags:
frontmatter_data['tags'] = card.tags
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:
frontmatter_data['assignments'] = card.assignments
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
if frontmatter_data:
@ -84,9 +81,8 @@ class CardFileFormatter:
description = self.renderer.render_children(document).strip()
return CardContents(
name,
description,
tags = [],
assignments = [],
name,
description,
tags=[],
assignments=[],
)

View File

@ -2,11 +2,14 @@ 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

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

View File

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