parent
e594dff149
commit
cbb0cba076
|
@ -4,21 +4,21 @@ Implements methods for interacting with the [Favro API](https://favro.com/devel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import datetime
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import datetime
|
|
||||||
|
|
||||||
import requests_util
|
|
||||||
import requests
|
import requests
|
||||||
|
import requests_util
|
||||||
|
|
||||||
from .favro_data_model import (
|
from .favro_data_model import (
|
||||||
Card,
|
Card,
|
||||||
CardId,
|
CardId,
|
||||||
Collection,
|
Collection,
|
||||||
|
CollectionId,
|
||||||
CustomFieldId,
|
CustomFieldId,
|
||||||
CustomFieldInfo,
|
CustomFieldInfo,
|
||||||
OrganizationId,
|
OrganizationId,
|
||||||
CollectionId,
|
|
||||||
SeqId,
|
SeqId,
|
||||||
TagId,
|
TagId,
|
||||||
TagInfo,
|
TagInfo,
|
||||||
|
@ -107,9 +107,15 @@ class FavroClient:
|
||||||
self.card_cache = CardCache()
|
self.card_cache = CardCache()
|
||||||
|
|
||||||
# Setup caching
|
# Setup caching
|
||||||
requests_util.setup_limiter(self.session, URL_API_ROOT, datetime.timedelta(days=7))
|
requests_util.setup_limiter(
|
||||||
requests_util.setup_limiter(self.session, URL_GET_CARDS, datetime.timedelta(minutes=10))
|
self.session, URL_API_ROOT, datetime.timedelta(days=7),
|
||||||
requests_util.setup_limiter(self.session, URL_GET_TASKS, datetime.timedelta(minutes=10))
|
)
|
||||||
|
requests_util.setup_limiter(
|
||||||
|
self.session, URL_GET_CARDS, datetime.timedelta(minutes=10),
|
||||||
|
)
|
||||||
|
requests_util.setup_limiter(
|
||||||
|
self.session, URL_GET_TASKS, datetime.timedelta(minutes=10),
|
||||||
|
)
|
||||||
|
|
||||||
def check_logged_in(self) -> None:
|
def check_logged_in(self) -> None:
|
||||||
next(self.get_todo_list_cards())
|
next(self.get_todo_list_cards())
|
||||||
|
@ -124,7 +130,9 @@ class FavroClient:
|
||||||
collection_id: CollectionId | None = None,
|
collection_id: CollectionId | None = None,
|
||||||
todo_list=False,
|
todo_list=False,
|
||||||
) -> Iterator[Card]:
|
) -> Iterator[Card]:
|
||||||
request = self._get_cards_request(seq_id=seq_id,todo_list=todo_list,collection_id=collection_id)
|
request = self._get_cards_request(
|
||||||
|
seq_id=seq_id, todo_list=todo_list, collection_id=collection_id,
|
||||||
|
)
|
||||||
for card in self._get_paginated(request, Card.from_json):
|
for card in self._get_paginated(request, Card.from_json):
|
||||||
self.card_cache.add_card(card)
|
self.card_cache.add_card(card)
|
||||||
yield card
|
yield card
|
||||||
|
@ -134,9 +142,10 @@ class FavroClient:
|
||||||
yield from self._get_paginated(request, Collection.from_json)
|
yield from self._get_paginated(request, Collection.from_json)
|
||||||
|
|
||||||
def _get_cards_prepared_request(
|
def _get_cards_prepared_request(
|
||||||
self, **kwargs,
|
self,
|
||||||
|
**kwargs,
|
||||||
) -> requests.PreparedRequest:
|
) -> requests.PreparedRequest:
|
||||||
request = self._get_cards_request(**kwargs)
|
request = self._get_cards_request(**kwargs)
|
||||||
return self.session.prepare_request(request)
|
return self.session.prepare_request(request)
|
||||||
|
|
||||||
def _get_cards_request(
|
def _get_cards_request(
|
||||||
|
@ -234,7 +243,9 @@ class FavroClient:
|
||||||
self._invalidate_cache(card_id)
|
self._invalidate_cache(card_id)
|
||||||
return self.update_card_contents_locally(card_id, card_contents)
|
return self.update_card_contents_locally(card_id, card_contents)
|
||||||
|
|
||||||
def _get_paginated(self, base_request: requests.Request, entity_from_json) -> Iterator:
|
def _get_paginated(
|
||||||
|
self, base_request: requests.Request, entity_from_json,
|
||||||
|
) -> Iterator:
|
||||||
page = 0
|
page = 0
|
||||||
request_id = None
|
request_id = None
|
||||||
num_pages = 1
|
num_pages = 1
|
||||||
|
|
|
@ -136,8 +136,8 @@ class CustomFieldItem:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(json: dict[str, Any]) -> 'CustomFieldItem':
|
def from_json(json: dict[str, Any]) -> 'CustomFieldItem':
|
||||||
return CustomFieldItem(
|
return CustomFieldItem(
|
||||||
custom_field_item_id=CustomFieldItemId(json['customFieldItemId']),
|
custom_field_item_id=CustomFieldItemId(json['customFieldItemId']),
|
||||||
name=json['name'],
|
name=json['name'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,7 +163,9 @@ class CustomFieldInfo:
|
||||||
enabled: bool
|
enabled: bool
|
||||||
custom_field_items: list[CustomFieldItem]
|
custom_field_items: list[CustomFieldItem]
|
||||||
|
|
||||||
def get_field_item(self, field_item_id: CustomFieldItemId) -> CustomFieldItem | None:
|
def get_field_item(
|
||||||
|
self, field_item_id: CustomFieldItemId,
|
||||||
|
) -> CustomFieldItem | None:
|
||||||
for item in self.custom_field_items:
|
for item in self.custom_field_items:
|
||||||
if item.custom_field_item_id == field_item_id:
|
if item.custom_field_item_id == field_item_id:
|
||||||
return item
|
return item
|
||||||
|
@ -174,11 +176,13 @@ class CustomFieldInfo:
|
||||||
return CustomFieldInfo(
|
return CustomFieldInfo(
|
||||||
organization_id=OrganizationId(json['organizationId']),
|
organization_id=OrganizationId(json['organizationId']),
|
||||||
custom_field_id=CustomFieldId(json['customFieldId']),
|
custom_field_id=CustomFieldId(json['customFieldId']),
|
||||||
widget_common_id=map_opt(WidgetCommonId,json.get('widgetCommonId')),
|
widget_common_id=map_opt(WidgetCommonId, json.get('widgetCommonId')),
|
||||||
type=json['type'],
|
type=json['type'],
|
||||||
name=json['name'],
|
name=json['name'],
|
||||||
enabled=json['enabled'],
|
enabled=json['enabled'],
|
||||||
custom_field_items=[CustomFieldItem.from_json(f) for f in json.get('customFieldItems', [])],
|
custom_field_items=[
|
||||||
|
CustomFieldItem.from_json(f) for f in json.get('customFieldItems', [])
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,9 +202,9 @@ class CustomField:
|
||||||
else:
|
else:
|
||||||
typed_value = [CustomFieldItemId(v) for v in value]
|
typed_value = [CustomFieldItemId(v) for v in value]
|
||||||
return CustomField(
|
return CustomField(
|
||||||
custom_field_id = CustomFieldId(json['customFieldId']),
|
custom_field_id=CustomFieldId(json['customFieldId']),
|
||||||
value = typed_value,
|
value=typed_value,
|
||||||
color = json.get('color'),
|
color=json.get('color'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -243,12 +247,13 @@ class Task:
|
||||||
position=json['position'],
|
position=json['position'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Collection:
|
class Collection:
|
||||||
collection_id: CollectionId
|
collection_id: CollectionId
|
||||||
organization_id: OrganizationId
|
organization_id: OrganizationId
|
||||||
name: str
|
name: str
|
||||||
shared_to_users: list[dict[str,str]] # TODO
|
shared_to_users: list[dict[str, str]] # TODO
|
||||||
public_sharing: str
|
public_sharing: str
|
||||||
background: str
|
background: str
|
||||||
is_archived: bool
|
is_archived: bool
|
||||||
|
@ -257,16 +262,17 @@ class Collection:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(json: dict[str, Any]) -> 'Collection':
|
def from_json(json: dict[str, Any]) -> 'Collection':
|
||||||
return Collection(
|
return Collection(
|
||||||
collection_id=CollectionId(json['collectionId']),
|
collection_id=CollectionId(json['collectionId']),
|
||||||
organization_id=OrganizationId(json['collectionId']),
|
organization_id=OrganizationId(json['collectionId']),
|
||||||
name=json['name'],
|
name=json['name'],
|
||||||
shared_to_users=json['sharedToUsers'],
|
shared_to_users=json['sharedToUsers'],
|
||||||
public_sharing=json['publicSharing'],
|
public_sharing=json['publicSharing'],
|
||||||
background=json['background'],
|
background=json['background'],
|
||||||
is_archived=json['archived'],
|
is_archived=json['archived'],
|
||||||
widget_common_id=map_opt(WidgetCommonId,json.get('widgetCommonId')),
|
widget_common_id=map_opt(WidgetCommonId, json.get('widgetCommonId')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Card:
|
class Card:
|
||||||
card_id: CardId
|
card_id: CardId
|
||||||
|
|
|
@ -8,7 +8,13 @@ from logging import getLogger
|
||||||
import fuse
|
import fuse
|
||||||
|
|
||||||
from .favro_client import FavroClient
|
from .favro_client import FavroClient
|
||||||
from .favro_data_model import Card, SeqId, CustomFieldInfo, CustomFieldItemId, CustomField
|
from .favro_data_model import (
|
||||||
|
Card,
|
||||||
|
CustomField,
|
||||||
|
CustomFieldInfo,
|
||||||
|
CustomFieldItemId,
|
||||||
|
SeqId,
|
||||||
|
)
|
||||||
from .favro_markdown import CardContents, CardFileFormatter
|
from .favro_markdown import CardContents, CardFileFormatter
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
|
@ -26,9 +32,12 @@ CARD_FILENAME_REGEX = r'^PAR\-(\d+)\.md$'
|
||||||
################################################################################
|
################################################################################
|
||||||
# Formatting
|
# Formatting
|
||||||
|
|
||||||
def to_custom_field_value(custom_field: CustomField, field_def: CustomFieldInfo) -> str | None:
|
|
||||||
|
def to_custom_field_value(
|
||||||
|
custom_field: CustomField, field_def: CustomFieldInfo,
|
||||||
|
) -> str | None:
|
||||||
value: CustomFieldItemId | list[CustomFieldItemId] = custom_field.value
|
value: CustomFieldItemId | list[CustomFieldItemId] = custom_field.value
|
||||||
if field_def.type in {'Single select','Multiple select'}:
|
if field_def.type in {'Single select', 'Multiple select'}:
|
||||||
items = [field_def.get_field_item(item_id) for item_id in value]
|
items = [field_def.get_field_item(item_id) for item_id in value]
|
||||||
items = [i for i in items if i]
|
items = [i for i in items if i]
|
||||||
return items[0].name
|
return items[0].name
|
||||||
|
@ -36,7 +45,8 @@ def to_custom_field_value(custom_field: CustomField, field_def: CustomFieldInfo)
|
||||||
return custom_field.color
|
return custom_field.color
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def to_custom_fields(card: Card, favro_client: FavroClient) -> dict[str,str]:
|
|
||||||
|
def to_custom_fields(card: Card, favro_client: FavroClient) -> dict[str, str]:
|
||||||
custom_fields = {}
|
custom_fields = {}
|
||||||
for field_assignment in card.custom_fields:
|
for field_assignment in card.custom_fields:
|
||||||
field_def = favro_client.get_custom_field(field_assignment.custom_field_id)
|
field_def = favro_client.get_custom_field(field_assignment.custom_field_id)
|
||||||
|
@ -46,6 +56,7 @@ def to_custom_fields(card: Card, favro_client: FavroClient) -> dict[str,str]:
|
||||||
del field_assignment, str_value
|
del field_assignment, str_value
|
||||||
return custom_fields
|
return custom_fields
|
||||||
|
|
||||||
|
|
||||||
def to_card_contents(card: Card, favro_client: FavroClient) -> CardContents:
|
def to_card_contents(card: Card, favro_client: FavroClient) -> CardContents:
|
||||||
tags = [favro_client.get_tag(tag_id).name for tag_id in card.tags]
|
tags = [favro_client.get_tag(tag_id).name for tag_id in card.tags]
|
||||||
assignments = [
|
assignments = [
|
||||||
|
@ -80,6 +91,7 @@ def to_card_contents(card: Card, favro_client: FavroClient) -> CardContents:
|
||||||
################################################################################
|
################################################################################
|
||||||
# FUSE
|
# FUSE
|
||||||
|
|
||||||
|
|
||||||
class FavroStat(fuse.Stat):
|
class FavroStat(fuse.Stat):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.st_mode = 0
|
self.st_mode = 0
|
||||||
|
@ -101,7 +113,6 @@ class FileSystemItem:
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class RootFileSystemItem(FileSystemItem):
|
class RootFileSystemItem(FileSystemItem):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_path_segment(_segment: str) -> 'RootFileSystemItem':
|
def from_path_segment(_segment: str) -> 'RootFileSystemItem':
|
||||||
return RootFileSystemItem()
|
return RootFileSystemItem()
|
||||||
|
@ -109,6 +120,7 @@ class RootFileSystemItem(FileSystemItem):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '/'
|
return '/'
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CollectionFileSystemItem(FileSystemItem):
|
class CollectionFileSystemItem(FileSystemItem):
|
||||||
collection_name: str
|
collection_name: str
|
||||||
|
@ -123,6 +135,7 @@ class CollectionFileSystemItem(FileSystemItem):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.collection_name
|
return self.collection_name
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CardFileSystemItem(FileSystemItem):
|
class CardFileSystemItem(FileSystemItem):
|
||||||
seq_id: SeqId
|
seq_id: SeqId
|
||||||
|
@ -137,7 +150,9 @@ class CardFileSystemItem(FileSystemItem):
|
||||||
return CARD_FILENAME_FORMAT.format(seq_id=self.seq_id.raw_id)
|
return CARD_FILENAME_FORMAT.format(seq_id=self.seq_id.raw_id)
|
||||||
|
|
||||||
|
|
||||||
def path_to_file_system_item(path_str: str, path_components: list[type[FileSystemItem]]) -> FileSystemItem | None:
|
def path_to_file_system_item(
|
||||||
|
path_str: str, path_components: list[type[FileSystemItem]],
|
||||||
|
) -> FileSystemItem | None:
|
||||||
path = re.findall(r'[^/]+', path_str)
|
path = re.findall(r'[^/]+', path_str)
|
||||||
component = path_components[len(path)]
|
component = path_components[len(path)]
|
||||||
return component.from_path_segment(path[-1] if path else None)
|
return component.from_path_segment(path[-1] if path else None)
|
||||||
|
@ -155,7 +170,11 @@ class FavroFuse(fuse.Fuse):
|
||||||
self.favro_client = favro_client
|
self.favro_client = favro_client
|
||||||
self.formatter = formatter
|
self.formatter = formatter
|
||||||
self.wiped_cards = set()
|
self.wiped_cards = set()
|
||||||
self.path_components = [RootFileSystemItem, CollectionFileSystemItem, CardFileSystemItem]
|
self.path_components = [
|
||||||
|
RootFileSystemItem,
|
||||||
|
CollectionFileSystemItem,
|
||||||
|
CardFileSystemItem,
|
||||||
|
]
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def getattr(self, path: str) -> FavroStat | int:
|
def getattr(self, path: str) -> FavroStat | int:
|
||||||
|
@ -190,7 +209,6 @@ class FavroFuse(fuse.Fuse):
|
||||||
del collection
|
del collection
|
||||||
|
|
||||||
elif isinstance(file_system_item, CollectionFileSystemItem):
|
elif isinstance(file_system_item, CollectionFileSystemItem):
|
||||||
|
|
||||||
# TODO: move into own function
|
# TODO: move into own function
|
||||||
for collection in self.favro_client.get_collections():
|
for collection in self.favro_client.get_collections():
|
||||||
if collection.name.replace('/', '') == file_system_item.collection_name:
|
if collection.name.replace('/', '') == file_system_item.collection_name:
|
||||||
|
|
|
@ -35,7 +35,7 @@ class CardContents:
|
||||||
is_archived: bool
|
is_archived: bool
|
||||||
start_date: datetime.date | None
|
start_date: datetime.date | None
|
||||||
due_date: datetime.date | None
|
due_date: datetime.date | None
|
||||||
custom_fields: dict[str,str]
|
custom_fields: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
def format_obsidian_link(text: str) -> str:
|
def format_obsidian_link(text: str) -> str:
|
||||||
|
@ -173,5 +173,5 @@ class CardFileFormatter:
|
||||||
is_archived=is_archived,
|
is_archived=is_archived,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
due_date=due_date,
|
due_date=due_date,
|
||||||
custom_fields={}, # TODO
|
custom_fields={}, # TODO
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import requests_cache
|
import requests_cache
|
||||||
|
|
||||||
from favro_sync import secrets
|
from favro_sync import secrets
|
||||||
from favro_sync.favro_client import FavroClient, OrganizationId, SeqId
|
from favro_sync.favro_client import FavroClient, OrganizationId, SeqId
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ def test_get_cards():
|
||||||
for card in client.get_cards(todo_list=True):
|
for card in client.get_cards(todo_list=True):
|
||||||
assert_valid_card(card)
|
assert_valid_card(card)
|
||||||
|
|
||||||
|
|
||||||
@needs_secrets
|
@needs_secrets
|
||||||
def test_get_collections():
|
def test_get_collections():
|
||||||
client = create_client()
|
client = create_client()
|
||||||
|
@ -62,6 +63,7 @@ def test_get_collections():
|
||||||
assert collection.is_archived is not None
|
assert collection.is_archived is not None
|
||||||
assert collection.widget_common_id is None
|
assert collection.widget_common_id is None
|
||||||
|
|
||||||
|
|
||||||
def create_client():
|
def create_client():
|
||||||
session = requests_cache.CachedSession('output/test-http-cache.sqlite')
|
session = requests_cache.CachedSession('output/test-http-cache.sqlite')
|
||||||
return FavroClient(
|
return FavroClient(
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from favro_sync.favro_client import SeqId
|
|
||||||
from favro_sync.favro_fuse import to_card_contents
|
from favro_sync.favro_fuse import to_card_contents
|
||||||
from favro_sync.favro_markdown import CardFileFormatter
|
|
||||||
|
|
||||||
from .test_client import create_client, needs_secrets
|
from .test_client import create_client, needs_secrets
|
||||||
|
|
||||||
|
|
||||||
@needs_secrets
|
@needs_secrets
|
||||||
def test_format_all_cards():
|
def test_format_all_cards():
|
||||||
client = create_client()
|
client = create_client()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user