Compare commits
No commits in common. "6201fc9c8c4fc115ed18b6a795dd86966c7885b2" and "e61e0ef34d70e9c768adde7bbf5e0e4d1fc7b0b1" have entirely different histories.
6201fc9c8c
...
e61e0ef34d
|
@ -92,7 +92,6 @@ Full list of requirements:
|
||||||
- [requests-cache](https://pypi.org/project/requests-cache/)
|
- [requests-cache](https://pypi.org/project/requests-cache/)
|
||||||
- [fuse-python](https://pypi.org/project/fuse-python/)
|
- [fuse-python](https://pypi.org/project/fuse-python/)
|
||||||
- [secret_loader](https://gitfub.space/Jmaa/secret_loader)
|
- [secret_loader](https://gitfub.space/Jmaa/secret_loader)
|
||||||
- [requests_util](https://gitfub.space/Jmaa/requests_util)
|
|
||||||
- [marko](https://pypi.org/project/marko/)
|
- [marko](https://pypi.org/project/marko/)
|
||||||
- [python-frontmatter](https://pypi.org/project/python-frontmatter/)
|
- [python-frontmatter](https://pypi.org/project/python-frontmatter/)
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,6 @@ Limitations:
|
||||||
[`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole
|
[`python-fuse`](https://github.com/libfuse/python-fuse) implements a whole
|
||||||
bunch automatically.)
|
bunch automatically.)
|
||||||
|
|
||||||
Use `umount` to unmount the cards again.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- `FavroFuse`
|
- `FavroFuse`
|
||||||
|
|
|
@ -10,7 +10,6 @@ from .favro_fuse import start_favro_fuse
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
logging.getLogger().setLevel('INFO')
|
|
||||||
|
|
||||||
read_only = False
|
read_only = False
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ def main():
|
||||||
)
|
)
|
||||||
|
|
||||||
client.check_logged_in()
|
client.check_logged_in()
|
||||||
start_favro_fuse(client, secrets.favro_collection_filter())
|
start_favro_fuse(client)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -4,20 +4,15 @@ Implements methods for interacting with the [Favro API](https://favro.com/devel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from typing import Any
|
|
||||||
import requests_cache
|
|
||||||
import datetime
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
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,
|
||||||
|
@ -35,7 +30,6 @@ logger = getLogger(__name__)
|
||||||
URL_API_ROOT = 'https://favro.com/api/v1'
|
URL_API_ROOT = 'https://favro.com/api/v1'
|
||||||
URL_GET_CARDS = URL_API_ROOT + '/cards'
|
URL_GET_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_USERS = URL_API_ROOT + '/users/'
|
|
||||||
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}'
|
||||||
URL_GET_CUSTOM_FIELD = URL_API_ROOT + '/customfields/{custom_field_id}'
|
URL_GET_CUSTOM_FIELD = URL_API_ROOT + '/customfields/{custom_field_id}'
|
||||||
|
@ -107,43 +101,24 @@ class FavroClient:
|
||||||
)
|
)
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
|
|
||||||
self.card_cache = CardCache()
|
self.cache = CardCache()
|
||||||
self.user_cache = None
|
|
||||||
|
|
||||||
# Setup caching
|
|
||||||
requests_util.setup_limiter(
|
|
||||||
self.session, URL_API_ROOT, datetime.timedelta(days=7),
|
|
||||||
)
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
requests_util.setup_limiter(
|
|
||||||
self.session, URL_GET_CUSTOM_FIELD, datetime.timedelta(days=30),
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_logged_in(self) -> None:
|
def check_logged_in(self) -> None:
|
||||||
next(self.get_todo_list_cards())
|
next(self.get_todo_list_cards())
|
||||||
|
|
||||||
def get_todo_list_cards(self) -> Iterator[Card]:
|
def get_todo_list_cards(self) -> Iterator[Card]:
|
||||||
yield from self.get_cards(todo_list=True)
|
for card in self.get_cards(todo_list=True):
|
||||||
|
self.cache.add_card(card)
|
||||||
|
yield card
|
||||||
|
|
||||||
def get_cards(
|
def get_cards(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
seq_id: SeqId | None = None,
|
seq_id: SeqId | None = None,
|
||||||
collection_id: CollectionId | None = None,
|
|
||||||
todo_list=False,
|
todo_list=False,
|
||||||
) -> Iterator[Card]:
|
) -> Iterator[Card]:
|
||||||
logger.info('Getting cards: seq_id=%s, collection_id=%s, todo_list=%s', seq_id, collection_id, todo_list)
|
request = self._get_cards_request(seq_id=seq_id, todo_list=todo_list)
|
||||||
request = self._get_cards_request(
|
yield from self._get_paginated(request, Card.from_json)
|
||||||
seq_id=seq_id, todo_list=todo_list, collection_id=collection_id,
|
|
||||||
)
|
|
||||||
for card in self._get_paginated(request, Card.from_json):
|
|
||||||
self.card_cache.add_card(card)
|
|
||||||
yield card
|
|
||||||
|
|
||||||
def get_collections(self) -> Iterator[Collection]:
|
def get_collections(self) -> Iterator[Collection]:
|
||||||
request = requests.Request('GET', URL_GET_COLLECTIONS)
|
request = requests.Request('GET', URL_GET_COLLECTIONS)
|
||||||
|
@ -160,60 +135,46 @@ class FavroClient:
|
||||||
self,
|
self,
|
||||||
seq_id: SeqId | None = None,
|
seq_id: SeqId | None = None,
|
||||||
todo_list: bool = False,
|
todo_list: bool = False,
|
||||||
collection_id: CollectionId | None = None,
|
request_id: None | str = None,
|
||||||
|
page: None | int = None,
|
||||||
) -> requests.Request:
|
) -> requests.Request:
|
||||||
params = {'descriptionFormat': 'markdown'}
|
params = {'descriptionFormat': 'markdown'}
|
||||||
if seq_id is not None:
|
if seq_id is not None:
|
||||||
params['cardSequentialId'] = str(seq_id.raw_id)
|
params['cardSequentialId'] = str(seq_id.raw_id)
|
||||||
if todo_list is True:
|
if todo_list is True:
|
||||||
params['todoList'] = 'true'
|
params['todoList'] = 'true'
|
||||||
if collection_id is not None:
|
if request_id:
|
||||||
params['collectionId'] = str(collection_id.raw_id)
|
params['requestId'] = request_id
|
||||||
|
if page:
|
||||||
|
params['page'] = page
|
||||||
|
|
||||||
return requests.Request('GET', URL_GET_CARDS, params=params)
|
return requests.Request('GET', URL_GET_CARDS, params=params)
|
||||||
|
|
||||||
def get_card(self, seq_id: SeqId) -> Card:
|
def get_card(self, seq_id: SeqId) -> Card:
|
||||||
if card := self.get_card_if_cached(seq_id):
|
if card := self.cache.get_card_by_seq_id(seq_id):
|
||||||
return card
|
return card
|
||||||
return next(self.get_cards(seq_id=seq_id))
|
return next(self.get_cards(seq_id=seq_id))
|
||||||
|
|
||||||
def get_card_if_cached(self, seq_id: SeqId) -> Card | None:
|
|
||||||
if card := self.card_cache.get_card_by_seq_id(seq_id):
|
|
||||||
return card
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_card_by_card_id(self, card_id: CardId) -> Card:
|
def get_card_by_card_id(self, card_id: CardId) -> Card:
|
||||||
json_data = self._get_json(URL_UPDATE_CARD.format(card_id=card_id.raw_id))
|
response = self.session.get(URL_UPDATE_CARD.format(card_id=card_id.raw_id))
|
||||||
return Card.from_json(json_data)
|
return Card.from_json(response.json())
|
||||||
|
|
||||||
def get_user(self, user_id: UserId) -> UserInfo:
|
def get_user(self, user_id: UserId) -> UserInfo:
|
||||||
self._load_users()
|
response = self.session.get(URL_GET_USER.format(user_id=user_id.raw_id))
|
||||||
return self.user_cache[user_id]
|
return UserInfo.from_json(response.json())
|
||||||
|
|
||||||
def _load_users(self) -> None:
|
|
||||||
if self.user_cache is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
request = requests.Request('GET', URL_GET_USERS)
|
|
||||||
|
|
||||||
user_cache = {}
|
|
||||||
for user_info in self._get_paginated(request, UserInfo.from_json):
|
|
||||||
user_cache[user_info.user_id] = user_info
|
|
||||||
self.user_cache = user_cache
|
|
||||||
logger.info('Loaded %s users', len(user_cache))
|
|
||||||
|
|
||||||
def get_tag(self, tag_id: TagId) -> TagInfo:
|
def get_tag(self, tag_id: TagId) -> TagInfo:
|
||||||
json_data = self._get_json(URL_GET_TAG.format(tag_id=tag_id.raw_id))
|
response = self.session.get(URL_GET_TAG.format(tag_id=tag_id.raw_id))
|
||||||
return TagInfo.from_json(json_data)
|
return TagInfo.from_json(response.json())
|
||||||
|
|
||||||
def get_custom_field(self, custom_field_id: CustomFieldId) -> CustomFieldInfo:
|
def get_custom_field(self, custom_field_id: CustomFieldId) -> CustomFieldInfo:
|
||||||
json_data = self._get_json(
|
response = self.session.get(
|
||||||
URL_GET_CUSTOM_FIELD.format(custom_field_id=custom_field_id.raw_id),
|
URL_GET_CUSTOM_FIELD.format(custom_field_id=custom_field_id.raw_id),
|
||||||
)
|
)
|
||||||
return CustomFieldInfo.from_json(json_data)
|
return CustomFieldInfo.from_json(response.json())
|
||||||
|
|
||||||
def _invalidate_cache(self, card_id: CardId) -> None:
|
def _invalidate_cache(self, card_id: CardId) -> None:
|
||||||
card = self.card_cache.remove(card_id)
|
card = self.cache.remove(card_id)
|
||||||
if card:
|
if card:
|
||||||
self.session.cache.delete(
|
self.session.cache.delete(
|
||||||
requests=[self._get_cards_prepared_request(seq_id=card.seq_id)],
|
requests=[self._get_cards_prepared_request(seq_id=card.seq_id)],
|
||||||
|
@ -224,7 +185,7 @@ class FavroClient:
|
||||||
card_id: CardId,
|
card_id: CardId,
|
||||||
card_contents: CardContents,
|
card_contents: CardContents,
|
||||||
) -> Card | None:
|
) -> Card | None:
|
||||||
if card := self.card_cache.remove(card_id):
|
if card := self.cache.remove(card_id):
|
||||||
card = dataclasses.replace(
|
card = dataclasses.replace(
|
||||||
card,
|
card,
|
||||||
detailed_description=card_contents.description,
|
detailed_description=card_contents.description,
|
||||||
|
@ -233,7 +194,7 @@ class FavroClient:
|
||||||
assignments=[], # TODO?
|
assignments=[], # TODO?
|
||||||
dependencies=[], # TODO
|
dependencies=[], # TODO
|
||||||
)
|
)
|
||||||
self.card_cache.add_card(card)
|
self.cache.add_card(card)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def update_card_contents(
|
def update_card_contents(
|
||||||
|
@ -259,7 +220,7 @@ 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.info('Sending PUT: %s', url)
|
logger.warning('Sending request: %s', url)
|
||||||
response = self.session.put(
|
response = self.session.put(
|
||||||
url,
|
url,
|
||||||
json=json_body,
|
json=json_body,
|
||||||
|
@ -282,11 +243,9 @@ class FavroClient:
|
||||||
request = self.session.prepare_request(base_request)
|
request = self.session.prepare_request(base_request)
|
||||||
|
|
||||||
# Run query
|
# Run query
|
||||||
logger.debug('Sending GET: %s', request.url)
|
logger.warning('Sending request: %s', request.url)
|
||||||
response = self.session.send(request)
|
response = self.session.send(request)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
if not response.from_cache:
|
|
||||||
logger.warning('Got new result: %s', request.url)
|
|
||||||
json = response.json()
|
json = response.json()
|
||||||
|
|
||||||
for entity_json in json['entities']:
|
for entity_json in json['entities']:
|
||||||
|
@ -298,9 +257,3 @@ class FavroClient:
|
||||||
page = json['page'] + 1
|
page = json['page'] + 1
|
||||||
request_id = json['requestId']
|
request_id = json['requestId']
|
||||||
num_pages = json['pages']
|
num_pages = json['pages']
|
||||||
|
|
||||||
def _get_json(self, url: str) -> dict[str, Any]:
|
|
||||||
logger.debug('Sending GET: %s', url)
|
|
||||||
response = self.session.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
|
@ -33,9 +33,7 @@ CARD_FILENAME_REGEX = r'^PAR\-(\d+)\.md$'
|
||||||
# Formatting
|
# Formatting
|
||||||
|
|
||||||
|
|
||||||
def to_custom_field_value(
|
def to_custom_field_value(custom_field: CustomField, field_def: CustomFieldInfo) -> str:
|
||||||
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]
|
||||||
|
@ -43,22 +41,21 @@ def to_custom_field_value(
|
||||||
return items[0].name
|
return items[0].name
|
||||||
if field_def.type in {'Color'}:
|
if field_def.type in {'Color'}:
|
||||||
return custom_field.color
|
return custom_field.color
|
||||||
return None
|
assert False, 'Unknown type: ' + field_def.type
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
str_value = to_custom_field_value(field_assignment, field_def)
|
custom_fields[field_def.name] = to_custom_field_value(
|
||||||
if str_value is not None:
|
field_assignment, field_def,
|
||||||
custom_fields[field_def.name] = str_value
|
)
|
||||||
del field_assignment, str_value
|
del field_assignment
|
||||||
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) -> str:
|
||||||
logger.info('Getting card contents for card: PAR-%s', card.seq_id)
|
|
||||||
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 = [
|
||||||
favro_client.get_user(assignment.user).name for assignment in card.assignments
|
favro_client.get_user(assignment.user).name for assignment in card.assignments
|
||||||
|
@ -70,8 +67,6 @@ def to_card_contents(card: Card, favro_client: FavroClient) -> CardContents:
|
||||||
for dep in card.dependencies
|
for dep in card.dependencies
|
||||||
if dep.is_before
|
if dep.is_before
|
||||||
]
|
]
|
||||||
custom_fields = to_custom_fields(card, favro_client)
|
|
||||||
|
|
||||||
return CardContents(
|
return CardContents(
|
||||||
CARD_IDENTIFIER_FORMAT.format(seq_id=card.seq_id.raw_id),
|
CARD_IDENTIFIER_FORMAT.format(seq_id=card.seq_id.raw_id),
|
||||||
card.name,
|
card.name,
|
||||||
|
@ -87,7 +82,7 @@ def to_card_contents(card: Card, favro_client: FavroClient) -> CardContents:
|
||||||
is_archived=card.is_archived,
|
is_archived=card.is_archived,
|
||||||
start_date=card.start_date,
|
start_date=card.start_date,
|
||||||
due_date=card.due_date,
|
due_date=card.due_date,
|
||||||
custom_fields=custom_fields,
|
custom_fields=to_custom_fields(card, favro_client),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,26 +112,17 @@ 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()
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '/'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class CollectionFileSystemItem(FileSystemItem):
|
class OrganizationFileSystemItem(FileSystemItem):
|
||||||
collection_name: str
|
name: str
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
assert '/' not in self.collection_name
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_path_segment(segment: str) -> 'CollectionFileSystemItem':
|
def from_path_segment(segment: str) -> 'OrganizationFileSystemItem':
|
||||||
return CollectionFileSystemItem(segment)
|
return OrganizationFileSystemItem(segment)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.collection_name
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
@ -149,9 +135,6 @@ class CardFileSystemItem(FileSystemItem):
|
||||||
return CardFileSystemItem(SeqId(int(m.group(1))))
|
return CardFileSystemItem(SeqId(int(m.group(1))))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return CARD_FILENAME_FORMAT.format(seq_id=self.seq_id.raw_id)
|
|
||||||
|
|
||||||
|
|
||||||
def path_to_file_system_item(
|
def path_to_file_system_item(
|
||||||
path_str: str, path_components: list[type[FileSystemItem]],
|
path_str: str, path_components: list[type[FileSystemItem]],
|
||||||
|
@ -160,102 +143,57 @@ def path_to_file_system_item(
|
||||||
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)
|
||||||
|
|
||||||
FAST_GETATTR = True
|
|
||||||
|
|
||||||
|
|
||||||
class FavroFuse(fuse.Fuse):
|
class FavroFuse(fuse.Fuse):
|
||||||
"""Favro FileSystem in Userspace."""
|
"""Favro FileSystem in Userspace."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
|
||||||
favro_client: FavroClient,
|
favro_client: FavroClient,
|
||||||
formatter: CardFileFormatter,
|
formatter: CardFileFormatter,
|
||||||
collection_filter: frozenset[str],
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
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.collection_filter = collection_filter
|
# self.path_components = [RootFileSystemItem, OrganizationFileSystemItem, CardFileSystemItem]
|
||||||
self.path_components = [
|
self.path_components = [RootFileSystemItem, CardFileSystemItem]
|
||||||
RootFileSystemItem,
|
|
||||||
CollectionFileSystemItem,
|
|
||||||
CardFileSystemItem,
|
|
||||||
]
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def getattr(self, path: str) -> FavroStat | int:
|
def getattr(self, path: str) -> FavroStat | int:
|
||||||
logger.info('getattr: %s', path)
|
|
||||||
file_system_item = path_to_file_system_item(path, self.path_components)
|
file_system_item = path_to_file_system_item(path, self.path_components)
|
||||||
|
|
||||||
st = FavroStat()
|
st = FavroStat()
|
||||||
if isinstance(file_system_item, RootFileSystemItem | CollectionFileSystemItem):
|
if isinstance(file_system_item, RootFileSystemItem):
|
||||||
st.st_mode = stat.S_IFDIR | 0o755
|
st.st_mode = stat.S_IFDIR | 0o755
|
||||||
st.st_nlink = 2
|
st.st_nlink = 2
|
||||||
elif isinstance(file_system_item, CardFileSystemItem):
|
elif isinstance(file_system_item, CardFileSystemItem):
|
||||||
|
card = self.favro_client.get_card(file_system_item.seq_id)
|
||||||
|
|
||||||
st.st_mode = stat.S_IFREG | 0o666
|
st.st_mode = stat.S_IFREG | 0o666
|
||||||
st.st_nlink = 1
|
st.st_nlink = 1
|
||||||
|
st.st_size = len(self._format_card_file(card))
|
||||||
card = self.favro_client.get_card_if_cached(file_system_item.seq_id)
|
st.st_ctime = int(card.creation_date.timestamp())
|
||||||
if not FAST_GETATTR and card is None:
|
|
||||||
card = self.favro_client.get_card(file_system_item.seq_id)
|
|
||||||
|
|
||||||
if card is not None:
|
|
||||||
st.st_size = len(self._format_card_file(card))
|
|
||||||
st.st_ctime = int(card.creation_date.timestamp())
|
|
||||||
else:
|
|
||||||
st.st_size = len(path)
|
|
||||||
|
|
||||||
st.st_mtime = st.st_ctime # TODO
|
st.st_mtime = st.st_ctime # TODO
|
||||||
else:
|
else:
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
return st
|
return st
|
||||||
|
|
||||||
def _is_allowed_collection(self, item: CollectionFileSystemItem) -> bool:
|
|
||||||
if self.collection_filter is None:
|
|
||||||
return True
|
|
||||||
return item.collection_name in self.collection_filter
|
|
||||||
|
|
||||||
def readdir(self, path: str, offset: int) -> Iterator[fuse.Direntry]:
|
def readdir(self, path: str, offset: int) -> Iterator[fuse.Direntry]:
|
||||||
logger.info('Reading directory: %s', path)
|
logger.warning('readdir(path=%s, offset=%s)', path, offset)
|
||||||
file_system_item = path_to_file_system_item(path, self.path_components)
|
|
||||||
|
|
||||||
yield fuse.Direntry('.')
|
yield fuse.Direntry('.')
|
||||||
yield fuse.Direntry('..')
|
yield fuse.Direntry('..')
|
||||||
|
|
||||||
if isinstance(file_system_item, RootFileSystemItem):
|
for card in self.favro_client.get_todo_list_cards():
|
||||||
for collection in self.favro_client.get_collections():
|
yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id))
|
||||||
item = CollectionFileSystemItem(collection.name.replace('/', ''))
|
|
||||||
if self._is_allowed_collection(item):
|
|
||||||
yield fuse.Direntry(str(item))
|
|
||||||
del collection, item
|
|
||||||
|
|
||||||
elif isinstance(file_system_item, CollectionFileSystemItem):
|
|
||||||
if not self._is_allowed_collection(file_system_item):
|
|
||||||
return # Yield nothing
|
|
||||||
|
|
||||||
# TODO: move into own function
|
|
||||||
for collection in self.favro_client.get_collections():
|
|
||||||
if collection.name.replace('/', '') == file_system_item.collection_name:
|
|
||||||
collection_id = collection.collection_id
|
|
||||||
del collection
|
|
||||||
|
|
||||||
for card in self.favro_client.get_cards(collection_id=collection_id):
|
|
||||||
yield fuse.Direntry(str(CardFileSystemItem(card.seq_id)))
|
|
||||||
del card
|
|
||||||
|
|
||||||
logger.info('Finished reading directory: %s', path)
|
|
||||||
|
|
||||||
def open(self, path: str, flags: int) -> int | None:
|
def open(self, path: str, flags: int) -> int | None:
|
||||||
logger.info('Opening: %s', path)
|
|
||||||
file_system_item = path_to_file_system_item(path, self.path_components)
|
file_system_item = path_to_file_system_item(path, self.path_components)
|
||||||
if not isinstance(file_system_item, CardFileSystemItem):
|
if not isinstance(file_system_item, CardFileSystemItem):
|
||||||
return -errno.ENOENT
|
return -errno.ENOENT
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def read(self, path: str, size: int, offset: int) -> bytes | int:
|
def read(self, path: str, size: int, offset: int) -> bytes | int:
|
||||||
logger.info('Reading: %s', path)
|
|
||||||
# Check that this is a card file_system_item.
|
# Check that this is a card file_system_item.
|
||||||
file_system_item = path_to_file_system_item(path, self.path_components)
|
file_system_item = path_to_file_system_item(path, self.path_components)
|
||||||
if not isinstance(file_system_item, CardFileSystemItem):
|
if not isinstance(file_system_item, CardFileSystemItem):
|
||||||
|
@ -276,7 +214,6 @@ class FavroFuse(fuse.Fuse):
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
def write(self, path: str, written_buffer: bytes, offset: int) -> int:
|
def write(self, path: str, written_buffer: bytes, offset: int) -> int:
|
||||||
logger.info('Writing: %s', path)
|
|
||||||
# Check that this is a card file_system_item.
|
# Check that this is a card file_system_item.
|
||||||
file_system_item = path_to_file_system_item(path, self.path_components)
|
file_system_item = path_to_file_system_item(path, self.path_components)
|
||||||
if not isinstance(file_system_item, CardFileSystemItem):
|
if not isinstance(file_system_item, CardFileSystemItem):
|
||||||
|
@ -300,7 +237,6 @@ class FavroFuse(fuse.Fuse):
|
||||||
return len(written_buffer)
|
return len(written_buffer)
|
||||||
|
|
||||||
def truncate(self, path: str, new_size: int):
|
def truncate(self, path: str, new_size: int):
|
||||||
logger.info('Truncating: %s', path)
|
|
||||||
# Check that this is a card file_system_item.
|
# Check that this is a card file_system_item.
|
||||||
file_system_item = path_to_file_system_item(path, self.path_components)
|
file_system_item = path_to_file_system_item(path, self.path_components)
|
||||||
if not isinstance(file_system_item, CardFileSystemItem):
|
if not isinstance(file_system_item, CardFileSystemItem):
|
||||||
|
@ -349,12 +285,11 @@ Userspace hello example
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_favro_fuse(favro_client: FavroClient, collection_filter: frozenset[str]):
|
def start_favro_fuse(favro_client: FavroClient):
|
||||||
logger.info('Starting favro FUSE with collection filter: %s', collection_filter)
|
# TODO:
|
||||||
server = FavroFuse(
|
server = FavroFuse(
|
||||||
favro_client=favro_client,
|
favro_client=favro_client,
|
||||||
formatter=CardFileFormatter(),
|
formatter=CardFileFormatter(),
|
||||||
collection_filter=collection_filter,
|
|
||||||
version='%prog ' + fuse.__version__,
|
version='%prog ' + fuse.__version__,
|
||||||
usage=HELP,
|
usage=HELP,
|
||||||
dash_s_do='setsingle',
|
dash_s_do='setsingle',
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import logging
|
|
||||||
|
|
||||||
import frontmatter
|
import frontmatter
|
||||||
import marko
|
import marko
|
||||||
import marko.md_renderer
|
import marko.md_renderer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# FrontMatter keys
|
# FrontMatter keys
|
||||||
|
|
||||||
|
@ -68,7 +65,6 @@ class CardFileFormatter:
|
||||||
|
|
||||||
def format_card_contents(self, card: CardContents) -> str:
|
def format_card_contents(self, card: CardContents) -> str:
|
||||||
"""Formats card contents. Mostly the inverse of [`parse_card_contents`]."""
|
"""Formats card contents. Mostly the inverse of [`parse_card_contents`]."""
|
||||||
logger.info('Formatting card: %s', card.identifier)
|
|
||||||
# Choose frontmatter data
|
# Choose frontmatter data
|
||||||
frontmatter_data = {}
|
frontmatter_data = {}
|
||||||
if self.obsidian_mode:
|
if self.obsidian_mode:
|
||||||
|
@ -138,8 +134,6 @@ class CardFileFormatter:
|
||||||
2. Parses header
|
2. Parses header
|
||||||
3. Finds content.
|
3. Finds content.
|
||||||
"""
|
"""
|
||||||
logger.info('Parsing card contents (len %d)', len(contents))
|
|
||||||
|
|
||||||
fm = frontmatter.loads(contents)
|
fm = frontmatter.loads(contents)
|
||||||
del contents
|
del contents
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,3 @@ def favro_username():
|
||||||
|
|
||||||
def favro_password():
|
def favro_password():
|
||||||
return secrets.load_or_fail('FAVRO_PASSWORD')
|
return secrets.load_or_fail('FAVRO_PASSWORD')
|
||||||
|
|
||||||
|
|
||||||
def favro_collection_filter() -> frozenset[str]:
|
|
||||||
loaded = secrets.load('FAVRO_COLLECTION_FILTER')
|
|
||||||
if loaded is None:
|
|
||||||
return None
|
|
||||||
values = loaded.strip().split('\n')
|
|
||||||
values = [v.strip() for v in values]
|
|
||||||
values = [v for v in values if v]
|
|
||||||
return frozenset(values)
|
|
||||||
|
|
|
@ -2,6 +2,5 @@ requests
|
||||||
requests-cache
|
requests-cache
|
||||||
fuse-python
|
fuse-python
|
||||||
secret_loader @ git+https://gitfub.space/Jmaa/secret_loader
|
secret_loader @ git+https://gitfub.space/Jmaa/secret_loader
|
||||||
requests_util @ git+https://gitfub.space/Jmaa/requests_util
|
|
||||||
marko
|
marko
|
||||||
python-frontmatter
|
python-frontmatter
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -106,7 +106,6 @@ REQUIREMENTS_MAIN = [
|
||||||
'requests-cache',
|
'requests-cache',
|
||||||
'fuse-python',
|
'fuse-python',
|
||||||
'secret_loader @ git+https://gitfub.space/Jmaa/secret_loader',
|
'secret_loader @ git+https://gitfub.space/Jmaa/secret_loader',
|
||||||
'requests_util @ git+https://gitfub.space/Jmaa/requests_util',
|
|
||||||
'marko',
|
'marko',
|
||||||
'python-frontmatter',
|
'python-frontmatter',
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
from favro_sync.favro_fuse import to_card_contents
|
|
||||||
|
|
||||||
from .test_client import create_client, needs_secrets
|
|
||||||
|
|
||||||
|
|
||||||
@needs_secrets
|
|
||||||
def test_format_all_cards():
|
|
||||||
client = create_client()
|
|
||||||
for card in client.get_cards(todo_list=True):
|
|
||||||
contents = to_card_contents(card, client)
|
|
||||||
assert contents is not None
|
|
Loading…
Reference in New Issue
Block a user