from collections.abc import Iterator from logging import getLogger import requests import dataclasses from .favro_data_model import Card, CardId, OrganizationId, SeqId from .favro_markdown import CardContents logger = getLogger(__name__) # Endpoints 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}' class CardCache: def __init__(self): self.cards = [] def add_card(self, card: Card) -> None: self.remove(card.card_id) self.cards.append(card) def get_card_by_card_id(self, card_id: CardId) -> Card | None: for card in reversed(self.cards): if card.card_id == card_id: return card def get_card_by_seq_id(self, seq_id: SeqId) -> Card | None: for card in reversed(self.cards): if card.seq_id == seq_id: return card def remove(self, card_id: CardId) -> Card | None: while card := self.get_card_by_card_id(card_id): self.cards.remove(card) return card class FavroClient: def __init__( self, *, favro_org_id: OrganizationId, favro_username: str, favro_password: str, session: requests.Session | None = None, read_only=True, ): assert favro_org_id is not None assert favro_username is not None assert favro_password is not None # Setup session self.session = session or requests.Session() self.session.auth = (favro_username, favro_password) self.session.headers.update( { 'organizationId': favro_org_id.raw_id, 'content-type': 'application/json', }, ) self.read_only = read_only self.cache = CardCache() def check_logged_in(self) -> None: next(self.get_todo_list_cards()) def get_todo_list_cards(self) -> Iterator[Card]: yield from self.get_cards(todo_list=True) def get_cards( 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) # Run query logger.warning('Sending request: %s', request.url) response = self.session.send(request) response.raise_for_status() json = response.json() # TODO: Add support for pageination for entity_json in json['entities']: print(entity_json) card = Card.from_json(entity_json) self.cache.add_card(card) 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: if card := self.cache.get_card_by_seq_id(seq_id): return card return next(self.get_cards(seq_id=seq_id)) def _invalidate_cache(self, card_id: CardId) -> None: card = self.cache.remove(card_id) if card: self.session.cache.delete( requests=[self._get_cards_request(seq_id=card.seq_id)], ) 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) self.cache.add_card(card) return card def update_card_contents(self, card_id: CardId, card_contents: CardContents) -> 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 = { 'name': card_contents.name, 'detailedDescription': card_contents.description, 'descriptionFormat': 'markdown', } 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, ) response.raise_for_status() self._invalidate_cache(card_id) return self.update_card_contents_locally(card_id, card_contents)