2024-09-26 14:43:30 +00:00
|
|
|
import requests
|
|
|
|
import dataclasses
|
2024-09-26 17:16:53 +00:00
|
|
|
import datetime
|
2024-09-26 14:43:30 +00:00
|
|
|
from typing import Any
|
|
|
|
import functools
|
2024-09-26 16:49:28 +00:00
|
|
|
from collections.abc import Iterator
|
2024-09-26 21:04:26 +00:00
|
|
|
from logging import getLogger
|
2024-09-26 16:49:28 +00:00
|
|
|
|
2024-09-26 21:04:26 +00:00
|
|
|
logger = getLogger(__name__)
|
2024-09-26 14:43:30 +00:00
|
|
|
|
|
|
|
# Types
|
|
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class SeqId:
|
|
|
|
raw_id: int
|
|
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CardId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-09-26 17:16:53 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class CommonId:
|
|
|
|
raw_id: str
|
|
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class UserId:
|
|
|
|
raw_id: str
|
|
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class OrganizationId:
|
|
|
|
raw_id: str
|
|
|
|
|
2024-09-26 16:49:28 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class Card:
|
|
|
|
card_id: CardId
|
|
|
|
seq_id: SeqId
|
2024-09-26 17:16:53 +00:00
|
|
|
common_id: CommonId
|
|
|
|
organization_id: OrganizationId
|
|
|
|
is_archived: bool
|
|
|
|
name: str
|
|
|
|
dependencies: list[None] # TODO
|
|
|
|
tags: list[None] # TODO
|
|
|
|
todo_list_user_id: UserId | None
|
|
|
|
todo_list_completed: bool | None
|
|
|
|
creator_user_id: UserId
|
|
|
|
creation_date: datetime.datetime
|
|
|
|
|
|
|
|
detailed_description: str | None
|
|
|
|
|
|
|
|
''' TODO, fieds:
|
|
|
|
'position': -399
|
|
|
|
'listPosition': -399
|
|
|
|
|
|
|
|
'isLane': False
|
|
|
|
'assignments': [{'userId': 'Faieomp8fuS8DrnyP' 'completed': True}]
|
|
|
|
'tasksTotal': 0
|
|
|
|
'tasksDone': 0
|
|
|
|
'attachments': []
|
|
|
|
'customFields':
|
|
|
|
'timeOnBoard': None
|
|
|
|
'timeOnColumns': None
|
|
|
|
'favroAttachments': []
|
|
|
|
'''
|
2024-09-26 16:49:28 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_json(json: dict[str, Any]) -> 'Card':
|
|
|
|
return Card(
|
|
|
|
card_id = CardId(json['cardId']),
|
2024-09-26 17:16:53 +00:00
|
|
|
seq_id = SeqId(json['sequentialId']),
|
|
|
|
common_id = CommonId(json['cardCommonId']),
|
|
|
|
detailed_description = json.get('detailedDescription'),
|
|
|
|
is_archived = json['archived'],
|
|
|
|
organization_id = OrganizationId(json['organizationId']),
|
|
|
|
name = json['name'],
|
2024-09-26 21:04:26 +00:00
|
|
|
todo_list_user_id = UserId(json['todoListUserId' ]) if 'todoListUserId' in json else None,
|
|
|
|
todo_list_completed = json.get('todoListCompleted'),
|
2024-09-26 17:16:53 +00:00
|
|
|
dependencies = json['dependencies'],
|
|
|
|
tags = json['tags'],
|
|
|
|
creator_user_id = UserId(json['createdByUserId']),
|
|
|
|
creation_date = datetime.datetime.fromisoformat(json['createdAt']),
|
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'
|
|
|
|
URL_GET_ALL_CARDS = URL_API_ROOT+'/cards'
|
|
|
|
URL_UPDATE_CARD = URL_API_ROOT+'/cards/{card_id}'
|
|
|
|
|
|
|
|
class FavroClient:
|
|
|
|
|
2024-09-26 17:16:53 +00:00
|
|
|
def __init__(self, *, favro_org_id: OrganizationId, favro_username: str, favro_password: str,
|
2024-09-26 21:04:26 +00:00
|
|
|
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 14:43:30 +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:04:26 +00:00
|
|
|
self.read_only = read_only
|
|
|
|
|
|
|
|
self.card_id_to_seq_id: dict[CardId, SeqId] = {}
|
|
|
|
self.seq_id_to_card_id: dict[SeqId, CardId] = {}
|
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 14:43:30 +00:00
|
|
|
|
2024-09-26 21:04:26 +00:00
|
|
|
def get_cards(self, *, seq_id: SeqId | None = None, todo_list=False) -> Iterator[Card]:
|
2024-09-26 16:49:28 +00:00
|
|
|
# Determine params for get_cards
|
2024-09-26 21:04:26 +00:00
|
|
|
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-26 21:04:26 +00:00
|
|
|
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: Pageination
|
|
|
|
for entity_json in json['entities']:
|
2024-09-26 21:04:26 +00:00
|
|
|
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
|
2024-09-26 16:49:28 +00:00
|
|
|
del entity_json
|
|
|
|
|
2024-09-26 21:04:26 +00:00
|
|
|
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)
|
|
|
|
|
2024-09-26 19:51:53 +00:00
|
|
|
def get_card(self, seq_id: SeqId) -> Card:
|
|
|
|
return next(self.get_cards(seq_id=seq_id))
|
2024-09-26 14:43:30 +00:00
|
|
|
|
2024-09-26 19:51:53 +00:00
|
|
|
def get_card_id(self, seq_id: SeqId) -> CardId:
|
2024-09-26 21:04:26 +00:00
|
|
|
if card_id := self.seq_id_to_card_id[seq_id]:
|
|
|
|
return card_id
|
2024-09-26 19:51:53 +00:00
|
|
|
first_card = next(self.get_cards(seq_id = seq_id))
|
2024-09-26 17:16:53 +00:00
|
|
|
return first_card.card_id
|
2024-09-26 14:43:30 +00:00
|
|
|
|
2024-09-26 21:04:26 +00:00
|
|
|
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])
|
|
|
|
])
|
|
|
|
|
|
|
|
|
2024-09-26 16:49:28 +00:00
|
|
|
def update_card_description(self, card_id: CardId, description: str) -> Card:
|
|
|
|
"""Returns updated Card."""
|
2024-09-26 21:04:26 +00:00
|
|
|
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')
|
|
|
|
|
2024-09-26 16:49:28 +00:00
|
|
|
json_body = {
|
|
|
|
'detailedDescription': description,
|
|
|
|
'descriptionFormat': 'markdown',
|
|
|
|
}
|
|
|
|
|
2024-09-26 21:04:26 +00:00
|
|
|
logger.warning('Sending body: %s', json_body)
|
2024-09-26 16:49:28 +00:00
|
|
|
response = self.session.put(URL_UPDATE_CARD.format(card_id=card_id.raw_id), json=json_body)
|
|
|
|
response.raise_for_status()
|
2024-09-26 21:04:26 +00:00
|
|
|
logger.warning("Response: %s", response.json())
|
|
|
|
self._invalidate_cache(card_id)
|
2024-09-26 14:43:30 +00:00
|
|
|
|
2024-09-26 16:49:28 +00:00
|
|
|
return Card.from_json(response.json())
|
2024-09-26 21:04:26 +00:00
|
|
|
|
|
|
|
|