diff --git a/serviceHelpers/trello.py b/serviceHelpers/trello.py index 288d040..ca81adf 100644 --- a/serviceHelpers/trello.py +++ b/serviceHelpers/trello.py @@ -17,17 +17,23 @@ def __init__(self, board_id, key, token) -> None: self.board_id = board_id self.key = key self.token = token + self._cached_cards = {} #listID into list of cards, sorted by their IDs + self.dirty_cache = True pass def find_trello_card(self, regex): - cards = self.fetch_trello_cards() + "uses regexes to search name and description of cached / fetched cards. Returns the first it finds." + cards = self.fetch_trello_cards() if self.dirty_cache else self._cached_cards + for card in cards: if re.search(regex,card["name"]) or re.search(regex,card["desc"]): return card return def find_trello_cards(self,regex): - cards = self.fetch_trello_cards() + "uses regexes to search name and description of cached / fetched cards. Returns all cards found." + + cards = self.fetch_trello_cards() if self.dirty_cache else self._cached_cards foundCards = [] for card in cards: if re.search(regex,card["name"]) or re.search(regex,card["desc"]): @@ -55,8 +61,8 @@ def purge_trello_cards(self,titlePattern = "", descPattern = "", targetLists = [ * customFieldIDs and targetLists are an array of IDs. Cards without those fields populated, or outside of those fields will be deleted. * targetLists can have the value `[*]` which will result in all lists being considered valid """ - cards = self.fetch_trello_cards() - + cards = self.fetch_trello_cards() if self.dirty_cache else self._cached_cards + for card in cards: #search through all cards, attempt to DQ from list. If all checks pass and not DQd, delete continueSearch = True @@ -79,32 +85,84 @@ def purge_trello_cards(self,titlePattern = "", descPattern = "", targetLists = [ if continueSearch == True: #we found no disqualifiers. self.deleteTrelloCard(card["id"]) + self.dirty_cache = True return + def get_all_cards_on_list(self, list_id): + """Returns all the visible cards on a given list""" + if self.dirty_cache: + self.fetch_trello_cards() + cards = self._cached_cards + filtered_cards = {k:v for k,v in cards.items() if v["idList"] == list_id} + return filtered_cards + #creates Key:value FOR - (for each Key, value in cards) BUT ONLY IF the list_id matches def fetch_trello_cards(self): """returns all visible cards from the board""" url = "https://api.trello.com/1/boards/%s/cards" % (self.board_id) params = self._get_trello_params() params["filter"] = "visible" - params["customFieldItems"] = "true" + + #params["customFieldItems"] = "true" r = requests.get(url,params=params) if r.status_code != 200: print("Unexpected response code [%s], whilst getting ticket list" % r.status_code) print(r.content) return - cards = json.loads(r.content) + cards = json.loads(r.content) + self._populate_cache(cards) + self.dirty_cache = False return cards - def get_trello_card(self,id): + + def fetch_trello_card(self,id): + "returns a single trello card" url = "https://api.trello.com/1/cards/%s" % (id) params = self._get_trello_params() r = requests.get(url, params = params) card = json.loads(r.content) + + self._populate_cache([card]) return card - def create_card(self,title, list_id, description = None, labelID = None, dueTimestamp = None) : - + + def get_trello_card(self,id): + "gets a card from the cache, or fetches it" + if id in self._cached_cards: + return self._cached_cards[id] + else: + return self.fetch_trello_card(id) + + def convert_index_to_pos(self, list_id,position) -> float: + "takes the index of a card, and finds a suitable position float value for it" + cards = self.get_all_cards_on_list(list_id) + cards = self._sort_trello_cards_dict(cards) + + + cards = list(cards.items()) + if len(cards) == 1: + return cards[0][1].get("pos",0)+1 + if len(cards) == 0: + return 0 + if position == 0: + return (cards[0][1].get("pos",0))/2 + if position == -1: + return (cards[len(cards)-1][1].get("pos",0)+0.0000001) + + try: + card = cards[position] + prev_card = cards[position-1] + pos = (prev_card[1]["pos"] + card[1]["pos"])/2 + return pos + except IndexError: + lo.error("got a horrible value when trying to convert index to pos value - index=[%s], len=[%s]") + return 0 + + + + def create_card(self,title, list_id, description = None, labelID = None, dueTimestamp = None, position:int = None): + "`position` is the numerical index of the card on the list it's appearing on. 0 = top, 1 = second, -1 = last" + params = self._get_trello_params() params["name"] =title @@ -114,6 +172,9 @@ def create_card(self,title, list_id, description = None, labelID = None, dueTim params["idLabels"] = (labelID) if dueTimestamp is not None: params["due"] = dueTimestamp + if position is not None and position >= 0: + #if position is -1 (bottom) then use default behaviour + params["pos"] = self.convert_index_to_pos(list_id,position) params["idList"] = list_id url = "https://api.trello.com/1/cards/" @@ -123,6 +184,7 @@ def create_card(self,title, list_id, description = None, labelID = None, dueTim print(r.content) return card = json.loads(r.content) + self.dirty_cache = True return card @@ -139,6 +201,7 @@ def update_card( self, card_id, title, description = None, pos = None): r = requests.put(url,params=params) if r.status_code != 200: print("ERROR: %s couldn't update the Gmail Trello card's name" % (r.status_code)) + self.dirty_cache = True return @@ -155,6 +218,7 @@ def create_checklist_on_card(self, cardID, checklistName = "Todo items") -> str: print("ERROR: %s when attempting create a trello checklist (HabiticaMapper) \n%s" % (r.status_code,r.content)) return checklist = json.loads(r.content) + self.dirty_cache = True return checklist["id"] @@ -166,7 +230,8 @@ def add_item_to_checklist(self, checklistID, text): r = requests.post(url,params=params) if r.status_code != 200: print("ERROR: %s when attempting add to a trello checklist (HabiticaMapper) \n%s" % (r.status_code,r.content)) - return + return + self.dirty_cache = True return def delete_checklist_item(self,checklist_id, checklist_item_id): @@ -175,7 +240,7 @@ def delete_checklist_item(self,checklist_id, checklist_item_id): r = requests.delete(url,params=params) if r.status_code != 200: lo.error("ERROR: %s, Couldn't delete trello checklist item %s " % (r.status_code,checklist_item_id)) - + self.dirty_cache = True def _setCustomFieldValue(self, cardID,value, fieldID): url = "https://api.trello.com/1/card/%s/customField/%s/item" % (cardID,fieldID) @@ -196,7 +261,8 @@ def archiveTrelloCard(self,trelloCardID): if r.status_code != 200: print(r) print(r.reason) - + self.dirty_cache = True + def create_custom_field(self,field_name) -> str: board_id = self.board_id @@ -216,10 +282,11 @@ def create_custom_field(self,field_name) -> str: try: new_field = json.loads(result.content)["id"] return new_field + except Exception as e: logger.warn("Couldn't parse JSON of new field? this shouldn't happen") - return "" - + + return "" def _get_trello_params(self): params = { @@ -236,6 +303,8 @@ def deleteTrelloCard(self,trelloCardID): if r.status_code != 200: print(r) print(r.reason) + return + self.dirty_cache = True @@ -244,9 +313,34 @@ def fetch_checklist_content(self, checklist_id) ->list: params = self._get_trello_params() r = requests.get(url=url,params=params) if r.status_code != 200: - lo.error("Couldn't fetch checklist content %s" % (r.status_code, r.content)) + lo.error("Couldn't fetch checklist content %s - %s", r.status_code, r.content) try: content = json.loads(r.content)["checkItems"] except Exception as e: lo.error(f"Couldn't parse the json from the fetch_checklist_content method for {checklist_id}") - return content \ No newline at end of file + return content + + + def _sort_trello_cards_list(self, array_of_cards:list) -> list: + "Takes an arbitrary list of cards and sorts them by their position value if present" + array_of_cards = sorted(array_of_cards,key=lambda card:card.get("pos",0)) + + return array_of_cards + + def _sort_trello_cards_dict(self,dict_of_cards:dict) -> dict: + sorted_dict = {k:v for k,v in sorted(dict_of_cards.items(), key=lambda x:x[1].get("pos"))} + #note for future C'tri reading this - the part that does the sorting is this: + #key=lambda x:x[1].get("pos") + #in this case, x is a tuple comprised of K and V + #x[1] is the card - a dict, and x[0] is the id, a string. + return sorted_dict + + def _populate_cache(self,array_of_cards:list) -> None: + "takes a list of cards and puts them into the _cached_cards dict" + for card in array_of_cards: + card:dict + if "id" not in card: + lo.warn("skipped adding a card to the cache, no id present") + continue + + self._cached_cards[card.get("id")] = card \ No newline at end of file diff --git a/setup.py b/setup.py index c37ad47..111173a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="hex-helpers", - version="2.6.0", + version="2.7.0", description="A series of light helpers for `freshdesk`,`gmail`,`habitica`,`hue lights`,`jira`,`slack`,`trello`", long_description=README, long_description_content_type="text/markdown", diff --git a/tests/test_trello_functions.py b/tests/test_trello_functions.py new file mode 100644 index 0000000..c1c44c8 --- /dev/null +++ b/tests/test_trello_functions.py @@ -0,0 +1,105 @@ +import sys +sys.path.append("") +import os + +from serviceHelpers.trello import trello + +TEST_BOARD_ID = "5f0dee6c5026590ce300472c" +TEST_LIST_ID = "5f0dee6c5026590ce3004732" + +def test_trello_sort(): + helper = trello("none","none","none") + + list_to_sort = [ + {"pos":300}, + {"pos":0.304}, + {"pos":20.4}, + {"pos":44.4}, + {"pos":209.30955} + ] + list_to_sort = helper._sort_trello_cards_list(list_to_sort) + last_pos = 0 + for card in list_to_sort: + assert card["pos"] > last_pos + last_pos = card["pos"] + +def test_trello_sort_array(): + helper = trello("none","none","none") + cards = { + "id_1":{"id":"id_1","pos":300}, + "id_2":{"id":"id_2","pos":0.304}, + "id_5":{"id":"id_5","pos":20.4}, + "id_3":{"id":"id_3","pos":44.4}, + "id_7":{"id":"id_7","pos":209.30955}, + "id_12":{"id":"id_12","pos":4444}, + "id_33":{"id":"id_44","pos":3.5}, + "id_32":{"id":"id_32","pos":2.1}, + + } + sorted_cards = helper._sort_trello_cards_dict(cards) + last_pos = 0 + for _,card in sorted_cards.items(): + assert card["pos"] >= last_pos + last_pos = card["pos"] + + +def test_convert_index_to_pos(): + helper = trello("none","none","none") + helper._cached_cards = { + "id_1":{"id":"id_1","idList":"main","pos":300}, + "id_2":{"id":"id_2","idList":"main","pos":0.304}, + "id_5":{"id":"id_5","idList":"main","pos":20.4}, + "id_3":{"id":"id_3","idList":"main","pos":44.4}, + "id_7":{"id":"id_7","idList":"main","pos":209.30955}, + "id_12":{"id":"id_12","idList":"main","pos":4444}, + "id_33":{"id":"id_44","idList":"main","pos":3.5}, + "id_32":{"id":"id_32","idList":"main","pos":2.1}, + + } + helper.dirty_cache = False + pos = helper.convert_index_to_pos("main",5) + assert pos > 44.4 + assert pos < 209.30955 + + +def test_keys_present(): + assert "TRELLO_KEY" in os.environ + assert "TRELLO_TOKEN" in os.environ + +def test_trello_get_cards(): + helper = trello(TEST_BOARD_ID,os.environ["TRELLO_KEY"],os.environ["TRELLO_TOKEN"]) + cards = helper.fetch_trello_cards() + for card in cards: + assert "id" in card + + assert len(cards) == len(helper._cached_cards) + +def test_get_list(): + helper = trello("none","none","none") + helper._cached_cards = { + "id_1":{"id":"id_1","idList":"not_on_thelist"}, + "id_2":{"id":"id_2","idList":"yes"}, + "id_5":{"id":"id_5","idList":"DEFINITELY_not"}, + "id_3":{"id":"id_3","idList":"not_on_thelist"}, + "id_7":{"id":"id_7","idList":"yes"}, + "id_12":{"id":"id_12","idList":"yes_but_not_really"}, + "id_33":{"id":"id_44","idList":"yes"}, + "id_32":{"id":"id_32","idList":"not_on_thelist"}, + + } + helper.dirty_cache = False + cards = helper.get_all_cards_on_list("yes") + assert(len(cards) == 3 ) + + +def test_create_card_at_correct_position(): + helper = trello(TEST_BOARD_ID,os.environ["TRELLO_KEY"],os.environ["TRELLO_TOKEN"]) + card = helper.create_card("TEST CARD, PLEASE IGNORE",TEST_LIST_ID,"A temporary card that should get deleted",position=0) + helper.deleteTrelloCard(card["id"]) + + card = helper.create_card("TEST CARD, PLEASE IGNORE",TEST_LIST_ID,"A temporary card that should get deleted",position=3) + helper.deleteTrelloCard(card["id"]) + + + card = helper.create_card("TEST CARD, PLEASE IGNORE",TEST_LIST_ID,"A temporary card that should get deleted",position=-1) + helper.deleteTrelloCard(card["id"])