Read-only support for tags and assignments
Some checks failed
Test Python / Test (push) Failing after 25s
Some checks failed
Test Python / Test (push) Failing after 25s
This commit is contained in:
parent
9fb19e39a5
commit
5b1502cdea
|
@ -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()),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']],
|
||||
)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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=[],
|
||||
)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user