1
0
favro-sync/favro_sync/favro_client.py

196 lines
5.7 KiB
Python
Raw Normal View History

2024-09-28 10:57:27 +00:00
"""Favro API Client.
Implements methods for interacting with the [Favro API](https://favro.com/developer/).
"""
import dataclasses
2024-09-26 16:49:28 +00:00
from collections.abc import Iterator
from logging import getLogger
2024-09-26 16:49:28 +00:00
2024-09-26 21:08:37 +00:00
import requests
from .favro_data_model import (
Card,
CardId,
OrganizationId,
SeqId,
TagId,
TagInfo,
UserId,
UserInfo,
)
2024-09-27 14:13:03 +00:00
from .favro_markdown import CardContents
2024-09-26 17:16:53 +00:00
2024-09-26 21:07:46 +00:00
logger = getLogger(__name__)
2024-09-26 16:49:28 +00:00
2024-09-26 14:43:30 +00:00
# Endpoints
URL_API_ROOT = 'https://favro.com/api/v1'
2024-09-26 21:08:37 +00:00
URL_GET_ALL_CARDS = URL_API_ROOT + '/cards'
URL_UPDATE_CARD = URL_API_ROOT + '/cards/{card_id}'
URL_GET_USER = URL_API_ROOT + '/users/{user_id}'
URL_GET_TAG = URL_API_ROOT + '/tags/{tag_id}'
2024-09-26 14:43:30 +00:00
2024-09-27 09:06:06 +00:00
class CardCache:
2024-09-27 09:06:06 +00:00
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:
2024-09-27 14:13:03 +00:00
for card in reversed(self.cards):
2024-09-27 09:06:06 +00:00
if card.card_id == card_id:
return card
return None
2024-09-27 09:06:06 +00:00
def get_card_by_seq_id(self, seq_id: SeqId) -> Card | None:
2024-09-27 14:13:03 +00:00
for card in reversed(self.cards):
2024-09-27 09:06:06 +00:00
if card.seq_id == seq_id:
return card
return None
2024-09-27 09:06:06 +00:00
def remove(self, card_id: CardId) -> Card | None:
2024-09-27 14:13:03 +00:00
while card := self.get_card_by_card_id(card_id):
2024-09-27 09:06:06 +00:00
self.cards.remove(card)
2024-09-27 14:13:03 +00:00
return card
return None
2024-09-26 17:16:53 +00:00
2024-09-26 21:08:37 +00:00
class FavroClient:
2024-09-28 10:57:27 +00:00
"""Favro API Client.
Implements methods for interacting with the [Favro API](https://favro.com/developer/).
"""
2024-09-26 21:08:37 +00:00
def __init__(
self,
*,
favro_org_id: OrganizationId,
favro_username: str,
favro_password: str,
session: requests.Session | None = None,
read_only=True,
):
2024-09-26 17:16:53 +00:00
assert favro_org_id is not None
assert favro_username is not None
assert favro_password is not None
2024-09-26 14:43:30 +00:00
# Setup session
self.session = session or requests.Session()
2024-09-26 17:16:53 +00:00
self.session.auth = (favro_username, favro_password)
2024-09-26 21:08:37 +00:00
self.session.headers.update(
{
2024-09-26 17:16:53 +00:00
'organizationId': favro_org_id.raw_id,
2024-09-26 14:43:30 +00:00
'content-type': 'application/json',
2024-09-26 21:08:37 +00:00
},
)
self.read_only = read_only
2024-09-27 09:06:06 +00:00
self.cache = CardCache()
2024-09-26 14:43:30 +00:00
2024-09-26 17:16:53 +00:00
def check_logged_in(self) -> None:
next(self.get_todo_list_cards())
2024-09-26 16:49:28 +00:00
def get_todo_list_cards(self) -> Iterator[Card]:
yield from self.get_cards(todo_list=True)
2024-09-26 21:08:37 +00:00
def get_cards(
self,
*,
seq_id: SeqId | None = None,
todo_list=False,
2024-09-26 21:08:37 +00:00
) -> Iterator[Card]:
2024-09-26 16:49:28 +00:00
# Determine params for get_cards
request = self._get_cards_request(seq_id, todo_list)
2024-09-26 14:43:30 +00:00
2024-09-26 16:49:28 +00:00
# Run query
2024-09-27 09:12:14 +00:00
logger.warning('Sending request: %s', request.url)
response = self.session.send(request)
2024-09-26 14:43:30 +00:00
response.raise_for_status()
json = response.json()
2024-09-26 16:49:28 +00:00
# TODO: Add support for pageination
2024-09-26 16:49:28 +00:00
for entity_json in json['entities']:
card = Card.from_json(entity_json)
2024-09-27 09:06:06 +00:00
self.cache.add_card(card)
yield card
2024-09-26 16:49:28 +00:00
del entity_json
2024-09-26 21:08:37 +00:00
def _get_cards_request(
self,
seq_id: SeqId | None = None,
todo_list=False,
2024-09-26 21:08:37 +00:00
) -> requests.PreparedRequest:
params = {'descriptionFormat': 'markdown'}
if seq_id is not None:
2024-09-26 21:08:37 +00:00
params['cardSequentialId'] = str(seq_id.raw_id)
if todo_list is True:
params['todoList'] = 'true'
2024-09-26 21:08:37 +00:00
request = requests.Request('GET', URL_GET_ALL_CARDS, params=params)
return self.session.prepare_request(request)
2024-09-26 19:51:53 +00:00
def get_card(self, seq_id: SeqId) -> Card:
2024-09-27 09:06:06 +00:00
if card := self.cache.get_card_by_seq_id(seq_id):
return card
2024-09-26 19:51:53 +00:00
return next(self.get_cards(seq_id=seq_id))
2024-09-26 14:43:30 +00:00
def get_user(self, user_id: UserId) -> UserInfo:
response = self.session.get(URL_GET_USER.format(user_id=user_id.raw_id))
return UserInfo.from_json(response.json())
def get_tag(self, tag_id: TagId) -> TagInfo:
response = self.session.get(URL_GET_TAG.format(tag_id=tag_id.raw_id))
return TagInfo.from_json(response.json())
def _invalidate_cache(self, card_id: CardId) -> None:
2024-09-27 09:06:06 +00:00
card = self.cache.remove(card_id)
2024-09-27 14:13:03 +00:00
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:
2024-09-27 14:13:03 +00:00
if card := self.cache.remove(card_id):
card = dataclasses.replace(
card,
detailed_description=card_contents.description,
name=card_contents.name,
2024-09-28 11:21:11 +00:00
tags=[], # TODO?
assignments=[],
)
2024-09-27 14:13:03 +00:00
self.cache.add_card(card)
return card
def update_card_contents(
self, card_id: CardId, card_contents: CardContents,
) -> Card:
2024-09-26 16:49:28 +00:00
"""Returns updated Card."""
if self.read_only == 'silent':
2024-09-26 21:08:37 +00:00
logger.warning(
'FavroClient is silent read only: Discarding card description update',
2024-09-26 21:08:37 +00:00
)
return None # TODO
if self.read_only is True:
msg = 'FavroClient is read only'
raise Exception(msg)
2024-09-26 16:49:28 +00:00
json_body = {
2024-09-27 14:13:03 +00:00
'name': card_contents.name,
'detailedDescription': card_contents.description,
2024-09-26 21:08:37 +00:00
'descriptionFormat': 'markdown',
2024-09-26 16:49:28 +00:00
}
2024-09-27 09:12:14 +00:00
url = URL_UPDATE_CARD.format(card_id=card_id.raw_id)
logger.warning('Sending request: %s', url)
2024-09-26 21:08:37 +00:00
response = self.session.put(
url,
json=json_body,
2024-09-26 21:08:37 +00:00
)
2024-09-26 16:49:28 +00:00
response.raise_for_status()
self._invalidate_cache(card_id)
2024-09-27 14:13:03 +00:00
return self.update_card_contents_locally(card_id, card_contents)