diff --git a/favro_sync/__main__.py b/favro_sync/__main__.py index 7eda2a6..3c2311b 100644 --- a/favro_sync/__main__.py +++ b/favro_sync/__main__.py @@ -12,6 +12,8 @@ def main(): favro_username = secrets.load_or_fail('FAVRO_USERNAME') favro_password = secrets.load_or_fail('FAVRO_PASSWORD') + read_only = False + #with tempfile.TemporaryDirectory(prefix='favro_sync_') as tmpdirname: tmpdirname = './output' # TODO if True: @@ -19,7 +21,10 @@ def main(): client = FavroClient(favro_org_id=OrganizationId(favro_org_id), favro_username=favro_username, - favro_password=favro_password, session=session) + favro_password=favro_password, + session=session, + read_only=read_only, + ) client.check_logged_in() start_favro_fuse(client) diff --git a/favro_sync/favro_client.py b/favro_sync/favro_client.py index a1536d9..5f1e99f 100644 --- a/favro_sync/favro_client.py +++ b/favro_sync/favro_client.py @@ -4,8 +4,9 @@ import datetime from typing import Any import functools from collections.abc import Iterator +from logging import getLogger -PREFIX = 'PAR-' # TODO: Make configurable +logger = getLogger(__name__) # Types @@ -71,8 +72,8 @@ class Card: is_archived = json['archived'], organization_id = OrganizationId(json['organizationId']), name = json['name'], - todo_list_user_id = UserId(json['todoListUserId']), - todo_list_completed = json['todoListCompleted'], + todo_list_user_id = UserId(json['todoListUserId' ]) if 'todoListUserId' in json else None, + todo_list_completed = json.get('todoListCompleted'), dependencies = json['dependencies'], tags = json['tags'], creator_user_id = UserId(json['createdByUserId']), @@ -87,7 +88,7 @@ URL_UPDATE_CARD = URL_API_ROOT+'/cards/{card_id}' class FavroClient: def __init__(self, *, favro_org_id: OrganizationId, favro_username: str, favro_password: str, - session: requests.Session | None = None): + session: requests.Session | None = None, read_only=True): assert favro_org_id is not None assert favro_username is not None @@ -100,6 +101,10 @@ class FavroClient: 'organizationId': favro_org_id.raw_id, 'content-type': 'application/json', }) + self.read_only = read_only + + self.card_id_to_seq_id: dict[CardId, SeqId] = {} + self.seq_id_to_card_id: dict[SeqId, CardId] = {} def check_logged_in(self) -> None: next(self.get_todo_list_cards()) @@ -108,39 +113,68 @@ class FavroClient: yield from self.get_cards(todo_list=True) - def get_cards(self, *, seq_id: SeqId | None = None, todo_list=True) -> Iterator[Card]: + def get_cards(self, *, seq_id: SeqId | None = None, todo_list=False) -> Iterator[Card]: # Determine params for get_cards - params = {'descriptionFormat': 'markdown'} - if seq_id: - params['cardSequentialId']= str(seq_id.raw_id) - if todo_list: - params['todoList'] = 'true' + request = self._get_cards_request(seq_id, todo_list) # Run query - response = self.session.get(URL_GET_ALL_CARDS, params = params) + response = self.session.send(request) response.raise_for_status() json = response.json() # TODO: Pageination for entity_json in json['entities']: - yield Card.from_json(entity_json) + card = Card.from_json(entity_json) + self.card_id_to_seq_id[card.card_id] = card.seq_id + self.seq_id_to_card_id[card.seq_id] = card.card_id + yield card del entity_json + def _get_cards_request(self, seq_id: SeqId | None = None, todo_list=False) -> requests.PreparedRequest: + params = {'descriptionFormat': 'markdown'} + if seq_id is not None: + params['cardSequentialId']= str(seq_id.raw_id) + if todo_list is True: + params['todoList'] = 'true' + + request = requests.Request('GET', URL_GET_ALL_CARDS, params = params) + return self.session.prepare_request(request) + def get_card(self, seq_id: SeqId) -> Card: return next(self.get_cards(seq_id=seq_id)) def get_card_id(self, seq_id: SeqId) -> CardId: + if card_id := self.seq_id_to_card_id[seq_id]: + return card_id first_card = next(self.get_cards(seq_id = seq_id)) return first_card.card_id + def _invalidate_cache(self, card_id: CardId) -> None: + self.session.cache.delete(requests=[ + self._get_cards_request(seq_id=self.card_id_to_seq_id[card_id]) + ]) + + def update_card_description(self, card_id: CardId, description: str) -> Card: """Returns updated Card.""" + if self.read_only == 'silent': + logger.warning('FavroClient is silent read only: Discarding card description update of length %d', + len(description)) + return None # TODO + elif self.read_only is True: + raise Exception('FavroClient is read only') + json_body = { 'detailedDescription': description, 'descriptionFormat': 'markdown', } + logger.warning('Sending body: %s', json_body) response = self.session.put(URL_UPDATE_CARD.format(card_id=card_id.raw_id), json=json_body) response.raise_for_status() + logger.warning("Response: %s", response.json()) + self._invalidate_cache(card_id) return Card.from_json(response.json()) + + diff --git a/favro_sync/favro_fuse.py b/favro_sync/favro_fuse.py index dd43a97..c224f60 100644 --- a/favro_sync/favro_fuse.py +++ b/favro_sync/favro_fuse.py @@ -87,7 +87,6 @@ class FavroFuse(fuse.Fuse): for card in self.favro_client.get_todo_list_cards(): yield fuse.Direntry(CARD_FILENAME_FORMAT.format(seq_id=card.seq_id.raw_id)) - return # TODO def open(self, path: str, flags) -> int | None: thing = Thing.from_path(path) @@ -138,7 +137,6 @@ class FavroFuse(fuse.Fuse): return len(written_buffer) def truncate(self, path: str, new_size: int): - print('Trunca', path, new_size) # Check that this is a card thing. thing = Thing.from_path(path) if not isinstance(thing, CardThing): @@ -149,7 +147,9 @@ class FavroFuse(fuse.Fuse): # Splice contents contents_str = card_to_contents(card) contents = bytes(contents_str, 'utf8') - contents = contents[0:new_size] + old_size = len(contents) + contents = contents[0:new_size] + b' ' * (old_size - new_size) + assert len(contents) == old_size contents_str = contents.decode('utf8') # Write to favro