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 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()),
|
||||||
|
|
|
@ -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__)
|
||||||
|
@ -14,11 +22,11 @@ logger = getLogger(__name__)
|
||||||
URL_API_ROOT = 'https://favro.com/api/v1'
|
URL_API_ROOT = 'https://favro.com/api/v1'
|
||||||
URL_GET_ALL_CARDS = URL_API_ROOT + '/cards'
|
URL_GET_ALL_CARDS = URL_API_ROOT + '/cards'
|
||||||
URL_UPDATE_CARD = URL_API_ROOT + '/cards/{card_id}'
|
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)
|
||||||
|
|
|
@ -36,10 +36,11 @@ class CardAssignment:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(json: dict[str, Any]) -> 'CardAssignment':
|
def from_json(json: dict[str, Any]) -> 'CardAssignment':
|
||||||
return CardAssignment(
|
return CardAssignment(
|
||||||
UserId(json['userId']),
|
UserId(json['userId']),
|
||||||
json['completed'],
|
json['completed'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class UserInfo:
|
class UserInfo:
|
||||||
user_id: UserId
|
user_id: UserId
|
||||||
|
@ -50,16 +51,18 @@ class UserInfo:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(json: dict[str, Any]) -> 'UserInfo':
|
def from_json(json: dict[str, Any]) -> 'UserInfo':
|
||||||
return UserInfo(
|
return UserInfo(
|
||||||
UserId(json['userId']),
|
UserId(json['userId']),
|
||||||
json['name'],
|
json['name'],
|
||||||
json['email'],
|
json['email'],
|
||||||
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
|
||||||
|
@ -70,12 +73,13 @@ class TagInfo:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(json: dict[str, Any]) -> 'TagInfo':
|
def from_json(json: dict[str, Any]) -> 'TagInfo':
|
||||||
return TagInfo(
|
return TagInfo(
|
||||||
TagId(json['tagId']),
|
TagId(json['tagId']),
|
||||||
OrganizationId(json['organizationId']),
|
OrganizationId(json['organizationId']),
|
||||||
json['name'],
|
json['name'],
|
||||||
json.get('color'),
|
json.get('color'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CardDependency:
|
class CardDependency:
|
||||||
card_id: CardId
|
card_id: CardId
|
||||||
|
@ -86,12 +90,13 @@ class CardDependency:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(json: dict[str, Any]) -> 'CardDependency':
|
def from_json(json: dict[str, Any]) -> 'CardDependency':
|
||||||
return CardDependency(
|
return CardDependency(
|
||||||
CardId(json['cardId']),
|
CardId(json['cardId']),
|
||||||
CommonId(json['cardCommonId']),
|
CommonId(json['cardCommonId']),
|
||||||
json['isBefore'],
|
json['isBefore'],
|
||||||
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']],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -84,9 +81,8 @@ class CardFileFormatter:
|
||||||
|
|
||||||
description = self.renderer.render_children(document).strip()
|
description = self.renderer.render_children(document).strip()
|
||||||
return CardContents(
|
return CardContents(
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
tags = [],
|
tags=[],
|
||||||
assignments = [],
|
assignments=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user