1
0

[Client]: Support collections
All checks were successful
Test Python / Test (push) Successful in 26s

This commit is contained in:
Jon Michael Aanes 2024-10-02 16:23:15 +02:00
parent d4a56d8d91
commit 471704d9fe
3 changed files with 85 additions and 33 deletions

View File

@ -12,6 +12,7 @@ import requests
from .favro_data_model import ( from .favro_data_model import (
Card, Card,
CardId, CardId,
Collection,
CustomFieldId, CustomFieldId,
CustomFieldInfo, CustomFieldInfo,
OrganizationId, OrganizationId,
@ -27,12 +28,13 @@ logger = getLogger(__name__)
# Endpoints # Endpoints
URL_API_ROOT = 'https://favro.com/api/v1' URL_API_ROOT = 'https://favro.com/api/v1'
URL_GET_ALL_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_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}'
URL_GET_TASKS = URL_API_ROOT + '/tasks' URL_GET_TASKS = URL_API_ROOT + '/tasks'
URL_GET_COLLECTIONS = URL_API_ROOT + '/collections'
class CardCache: class CardCache:
@ -105,7 +107,9 @@ class FavroClient:
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,
@ -113,35 +117,18 @@ class FavroClient:
seq_id: SeqId | None = None, seq_id: SeqId | None = None,
todo_list=False, todo_list=False,
) -> Iterator[Card]: ) -> Iterator[Card]:
page = 0 request = self._get_cards_request(seq_id=seq_id,todo_list=todo_list)
request_id = None yield from self._get_paginated(request, Card.from_json)
num_pages = 1
while page < num_pages: def get_collections(self) -> Iterator[Collection]:
# Determine params for get_cards request = requests.Request('GET', URL_GET_COLLECTIONS)
request = self._get_cards_request( yield from self._get_paginated(request, Collection.from_json)
seq_id,
todo_list,
page=page,
request_id=request_id,
)
# Run query def _get_cards_prepared_request(
logger.warning('Sending request: %s', request.url) self, **kwargs,
response = self.session.send(request) ) -> requests.PreparedRequest:
response.raise_for_status() request = self._get_cards_request(**kwargs)
json = response.json() return self.session.prepare_request(request)
for entity_json in json['entities']:
card = Card.from_json(entity_json)
self.cache.add_card(card)
yield card
del entity_json
# Pagination bookkeeping
page = json['page'] + 1
request_id = json['requestId']
num_pages = json['pages']
def _get_cards_request( def _get_cards_request(
self, self,
@ -149,7 +136,7 @@ class FavroClient:
todo_list: bool = False, todo_list: bool = False,
request_id: None | str = None, request_id: None | str = None,
page: None | int = None, page: None | int = None,
) -> requests.PreparedRequest: ) -> 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)
@ -160,8 +147,7 @@ class FavroClient:
if page: if page:
params['page'] = page params['page'] = page
request = requests.Request('GET', URL_GET_ALL_CARDS, params=params) return requests.Request('GET', URL_GET_CARDS, params=params)
return self.session.prepare_request(request)
def get_card(self, seq_id: SeqId) -> Card: def get_card(self, seq_id: SeqId) -> Card:
if card := self.cache.get_card_by_seq_id(seq_id): if card := self.cache.get_card_by_seq_id(seq_id):
@ -190,7 +176,7 @@ class FavroClient:
card = self.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_request(seq_id=card.seq_id)], requests=[self._get_cards_prepared_request(seq_id=card.seq_id)],
) )
def update_card_contents_locally( def update_card_contents_locally(
@ -241,3 +227,30 @@ class FavroClient:
response.raise_for_status() response.raise_for_status()
self._invalidate_cache(card_id) self._invalidate_cache(card_id)
return self.update_card_contents_locally(card_id, card_contents) return self.update_card_contents_locally(card_id, card_contents)
def _get_paginated(self, base_request: requests.Request, entity_from_json) -> Iterator:
page = 0
request_id = None
num_pages = 1
while page < num_pages:
# Determine params for get_cards
base_request.params['page'] = page
base_request.params['request_id'] = request_id
request = self.session.prepare_request(base_request)
# Run query
logger.warning('Sending request: %s', request.url)
response = self.session.send(request)
response.raise_for_status()
json = response.json()
for entity_json in json['entities']:
entity = entity_from_json(entity_json)
yield entity
del entity_json, entity
# Pagination bookkeeping
page = json['page'] + 1
request_id = json['requestId']
num_pages = json['pages']

View File

@ -15,6 +15,11 @@ def map_opt(mapper, value):
return None return None
@dataclasses.dataclass(frozen=True)
class CollectionId:
raw_id: int
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class SeqId: class SeqId:
raw_id: int raw_id: int
@ -238,6 +243,29 @@ class Task:
position=json['position'], position=json['position'],
) )
@dataclasses.dataclass(frozen=True)
class Collection:
collection_id: CollectionId
organization_id: OrganizationId
name: str
shared_to_users: list[dict[str,str]] # TODO
public_sharing: str
background: str
is_archived: bool
widget_common_id: WidgetCommonId
@staticmethod
def from_json(json: dict[str, Any]) -> 'Collection':
return Collection(
collection_id=CollectionId(json['collectionId']),
organization_id=OrganizationId(json['collectionId']),
name=json['name'],
shared_to_users=json['sharedToUsers'],
public_sharing=json['publicSharing'],
background=json['background'],
is_archived=json['archived'],
widget_common_id=map_opt(WidgetCommonId,json.get('widgetCommonId')),
)
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Card: class Card:

View File

@ -50,6 +50,17 @@ def test_get_cards():
for card in client.get_cards(todo_list=True): for card in client.get_cards(todo_list=True):
assert_valid_card(card) assert_valid_card(card)
@needs_secrets
def test_get_collections():
client = create_client()
for collection in client.get_collections():
print(collection)
assert collection.collection_id is not None
assert collection.organization_id is not None
assert collection.name is not None
assert collection.background is not None
assert collection.is_archived is not None
assert collection.widget_common_id is None
def create_client(): def create_client():
session = requests_cache.CachedSession('output/test-http-cache.sqlite') session = requests_cache.CachedSession('output/test-http-cache.sqlite')