From d13d8708b856db2145f33f9fa11fd27794231d0e Mon Sep 17 00:00:00 2001 From: C'tri Goudie <43475390+Ctri-The-Third@users.noreply.github.com> Date: Sun, 18 Jun 2023 11:29:19 +0100 Subject: [PATCH] add "request_and_validate" and "get_actions" methods (#26) Refactored to solve linting suggestions set to version 3.1 --- serviceHelpers/trello.py | 580 ++++++++++++++++++++----------- setup.py | 2 +- tests/test_trello_functions.py | 151 +++----- tests/test_trello_integration.py | 94 +++++ tests/test_zendesk.py | 2 +- 5 files changed, 529 insertions(+), 300 deletions(-) create mode 100644 tests/test_trello_integration.py diff --git a/serviceHelpers/trello.py b/serviceHelpers/trello.py index 8ba2188..44bb24c 100644 --- a/serviceHelpers/trello.py +++ b/serviceHelpers/trello.py @@ -1,206 +1,280 @@ +import json + from asyncio.log import logger import logging import re import requests -import json -import urllib +HOST = "https://api.trello.com/" +API_VERSION = "1" +BASE_URL = f"{HOST}{API_VERSION}/" +_LO = logging.getLogger("TrelloHelper") +_LO.setLevel(logging.WARN) + +class trello: + """represents a trello board, and provides methods to interact with it -lo = logging.getLogger("TrelloHelper") -lo.setLevel(logging.WARN) + Args: + `board_id` (str): the id of the board to interact with + `key` (str): the trello api key + `token` (str): the trello api token for the authenticated user + """ -class trello(): 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._cached_cards = {} # listID into list of cards, sorted by their IDs self.dirty_cache = True - pass def find_trello_card(self, regex) -> dict: - "uses regexes to search name and description of cached / fetched cards, or exactly matching IDs. Returns the first it finds. " + "uses regexes to search name and description of cached / fetched cards, or exactly matching IDs. Returns the first it finds." cards = ( self.fetch_trello_cards() if self.dirty_cache else list(self._cached_cards.values()) ) for card in cards: - card:dict + card: dict try: - - if card.get("id","") == regex or re.search(regex, card.get("name","")) or re.search(regex, card.get("desc","")): + if ( + card.get("id", "") == regex + or re.search(regex, card.get("name", "")) + or re.search(regex, card.get("desc", "")) + ): return card except (Exception,) as err: - lo.error( + _LO.error( "Failed to parse trello card's title & description when looking for matches, %s ", err, ) return - def find_trello_cards(self,regex): + + def find_trello_cards(self, regex): "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 list(self._cached_cards.values()) + cards = ( + self.fetch_trello_cards() + if self.dirty_cache + else list(self._cached_cards.values()) + ) foundCards = [] - for card in cards: - card:dict - if card.get("id","") == regex or re.search(regex, card.get("name","")) or re.search(regex, card.get("desc","")): + for card in cards: + card: dict + if ( + card.get("id", "") == regex + or re.search(regex, card.get("name", "")) + or re.search(regex, card.get("desc", "")) + ): foundCards.append(card) return foundCards - def search_trello_cards(self,search_criteria,board_id = None) -> list: + def search_trello_cards(self, search_criteria, board_id=None) -> list: "uses the trello search criteria, can return archived cards" - url = "https://api.trello.com/1/search" - params = self._get_trello_params() - params['card_fields'] = 'desc, name' - params['modelTypes'] = 'cards' + url = f"{BASE_URL}search" + params = {} + params["card_fields"] = "desc, name" + params["modelTypes"] = "cards" if board_id: params["idBoards"] = board_id params["query"] = search_criteria - r = requests.get(url, params = params) - matching_summaries = json.loads(r.content)["cards"] - return matching_summaries - - - def purge_trello_cards(self,titlePattern = "", descPattern = "", targetLists = [] - , customFieldIDs = []): - """ - * titlePattern and descPattern are both used for regex searches through title and card descriptions + + r = self._request_and_validate(url, params=params) + return r + + def purge_trello_cards( + self, title_pattern="", descPattern="", target_lists=None, custom_field_ids=None + ): + """ + * titlePattern and descPattern are both used for regex searches through title and card descriptions * 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 """ + target_lists = [] if target_lists is None else target_lists + custom_field_ids = [] if custom_field_ids is None else custom_field_ids 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 - if targetLists[0] != "*" and card["idList"] not in targetLists: - continueSearch = False - if continueSearch and customFieldIDs != []: - #if we're clear to continue, and there is an ID, scruitinse ID - continueSearch = False + # search through all cards, attempt to DQ from list. If all checks pass and not DQd, delete + continue_search = True + if target_lists[0] != "*" and card["idList"] not in target_lists: + continue_search = False + if continue_search and custom_field_ids != []: + # if we're clear to continue, and there is an ID, scruitinse ID + continue_search = False for field in card["customFieldItems"]: - if field["idCustomField"] in customFieldIDs: - #card has the right ID - continueSearch = True - if continueSearch and titlePattern != "" and re.search(titlePattern, card["name"]) == None: - #no objections found so far, there is a regex for the title, and it missed - continueSearch = False - if continueSearch and descPattern != "" and re.search(descPattern, card["desc"]) == None: - #no objections found so far, there is a regex for the description - continueSearch = False - - if continueSearch == True: - #we found no disqualifiers. - self.deleteTrelloCard(card["id"]) + if field["idCustomField"] in custom_field_ids: + # card has the right ID + continue_search = True + if ( + continue_search + and title_pattern != "" + and re.search(title_pattern, card["name"]) is None + ): + # no objections found so far, there is a regex for the title, and it missed + continue_search = False + if ( + continue_search + and descPattern != "" + and re.search(descPattern, card["desc"]) is None + ): + # no objections found so far, there is a regex for the description + continue_search = False + + if continue_search: + # we found no disqualifiers. + self.delete_trello_card(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() + self.fetch_trello_cards() cards = self._cached_cards - filtered_cards = {k:v for k,v in cards.items() if v["idList"] == list_id} + 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 + # creates Key:value FOR - (for each Key, value in cards) BUT ONLY IF the list_id matches def fetch_trello_cards(self) -> list: - """returns all visible cards from the board""" - url = "https://api.trello.com/1/boards/%s/cards" % (self.board_id) + """returns all visible cards from the board""" + url = f"{BASE_URL}boards/%s/cards" % (self.board_id) params = self._get_trello_params() params["filter"] = "visible" - - #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) + + # params["customFieldItems"] = "true" + + cards = self._request_and_validate(url, params=params) + self._populate_cache(cards) self.dirty_cache = False return cards - def fetch_trello_card(self,id): + def fetch_trello_card(self, card_id: str) -> dict: "returns a single trello card" - url = "https://api.trello.com/1/cards/%s" % (id) + + url = f"{BASE_URL}cards/%s" % card_id params = self._get_trello_params() - r = requests.get(url, params = params) - card = json.loads(r.content) - - self._populate_cache([card]) - return card + card = self._request_and_validate(url, params=params) + self._populate_cache([card]) + return card - def get_trello_card(self,id): + def get_trello_card(self, card_id: str) -> dict: "gets a card from the cache, or fetches it" - if id in self._cached_cards: - return self._cached_cards[id] + if card_id in self._cached_cards: + return self._cached_cards[card_id] else: - return self.fetch_trello_card(id) + return self.fetch_trello_card(card_id) - def convert_index_to_pos(self, list_id,position) -> float: + 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 + return cards[0][1].get("pos", 0) + 1 if len(cards) == 0: - return 0 + return 0 if position == 0: - return (cards[0][1].get("pos",0))/2 + return (cards[0][1].get("pos", 0)) / 2 if position == -1: - return (cards[len(cards)-1][1].get("pos",0)+0.0000001) - + 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 + 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 - - + _LO.error( + "got a horrible value when trying to convert index to pos value - index=[%s], len=[%s]" + ) + return 0 + + def fetch_actions_for_board( + self, + since: str = None, + before: str = None, + limit: int = None, + actions_filter: str = None, + ) -> list: + """hits the `/1/boards/{{board_id}}/actions` endpoint - def create_card(self,title, list_id, description = None, labelID = None, dueTimestamp = None, position:int = None): + Filter should be a comma seperated list of action types: + https://developer.atlassian.com/cloud/trello/guides/rest-api/action-types/ + + returns a list of lists of actions, each list being a page of actions + """ + + url = f"{BASE_URL}boards/{self.board_id}/actions" + + params = { + "memberCreator": "false", + "member": "false", + } + if limit is not None: + params["limit"] = limit + if since is not None: + params["since"] = since + if before is not None: + params["before"] = before + if actions_filter is not None: + params["filter"] = actions_filter + + actions = self._request_and_validate_paginated( + url, + params=params, + page_limit=10, + page_size_limit=50, + since=since, + ) + return actions + + 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 - + params["name"] = title + if description is not None: params["desc"] = description if labelID is not None: - params["idLabels"] = (labelID) + 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) + # 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/" - r = requests.post(url,params=params) + url = f"{BASE_URL}cards/" + r = requests.post(url, params=params, timeout=10) if r.status_code != 200: - print("Unexpected response code [%s], whilst creating card [%s]" % (r.status_code, title)) + print( + "Unexpected response code [%s], whilst creating card [%s]" + % (r.status_code, title) + ) print(r.content) - return + return card = json.loads(r.content) if not self._try_update_cache(r.content): self.dirty_cache = True - return card - + return card def update_card( self, @@ -223,178 +297,282 @@ def update_card( params["idList"] = new_list_id if new_due_timestamp is not None: params["due"] = new_due_timestamp - url = "https://api.trello.com/1/cards/%s" % (card_id) - r = requests.put(url, params=params) + url = f"{BASE_URL}cards/%s" % (card_id) + r = requests.put(url, params=params, timeout=10) if r.status_code != 200: - print( - "ERROR: %s couldn't update the Gmail Trello card's name" - % (r.status_code) + _LO.error( + "ERROR: %s couldn't update the Gmail Trello card's name", r.status_code ) + return if not self._try_update_cache(r.content): self.dirty_cache = True return - - def create_checklist_on_card(self, cardID, checklistName = "Todo items") -> str: + def create_checklist_on_card(self, cardID, checklistName="Todo items") -> str: "create a checklist on an existing card, returns the ID for use." - url = "https://api.trello.com/1/checklists" + url = f"{BASE_URL}checklists" params = self._get_trello_params() params["idCard"] = cardID params["name"] = checklistName - - r = requests.post(url, params=params) - + + r = requests.post(url, params=params, timeout=10) + if r.status_code != 200: - print("ERROR: %s when attempting create a trello checklist (HabiticaMapper) \n%s" % (r.status_code,r.content)) - return + 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 + self.dirty_cache = True return checklist["id"] - - def add_item_to_checklist(self, checklistID, text): - url = "https://api.trello.com/1/checklists/%s/checkItems" % (checklistID) + def add_item_to_checklist(self, checklistID: str, text: str): + "add a single item to an existing checklist" + url = f"{BASE_URL}checklists/%s/checkItems" % (checklistID) params = self._get_trello_params() params["pos"] = "bottom" params["name"] = text - r = requests.post(url,params=params) + r = requests.post(url, params=params, timeout=10) if r.status_code != 200: - print("ERROR: %s when attempting add to a trello checklist (HabiticaMapper) \n%s" % (r.status_code,r.content)) + print( + "ERROR: %s when attempting add to a trello checklist (HabiticaMapper) \n%s", + r.status_code, + r.content, + ) + return - self.dirty_cache = True - return + self.dirty_cache = True + return - def delete_checklist_item(self,checklist_id, checklist_item_id): - url = "https://api.trello.com/1/checklists/%s/checkItems/%s" % (checklist_id,checklist_item_id) + def delete_checklist_item(self, checklist_id, checklist_item_id): + "delete a single item from an existing checklist" + url = f"{BASE_URL}checklists/%s/checkItems/%s" % ( + checklist_id, + checklist_item_id, + ) params = self._get_trello_params() - r = requests.delete(url,params=params) + r = requests.delete(url, params=params, timeout=10) 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 + _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) - data = json.dumps({"value" : {"text" : "%s"% (value)}}) + def _setCustomFieldValue(self, cardID, value, fieldID): + url = f"{BASE_URL}card/%s/customField/%s/item" % (cardID, fieldID) + data = json.dumps({"value": {"text": "%s" % (value)}}) params = self._get_trello_params() headers = {"Content-type": "application/json"} - r = requests.put(url = url, data = data,params = params, headers=headers) + r = requests.put(url=url, data=data, params=params, headers=headers, timeout=10) if r.status_code != 200: - print("Unexpected response code [%s], whilst updating card %s's field ID %s" % (r.status_code,cardID,fieldID)) + _LO.error( + "Unexpected response code [%s], whilst updating card %s's field ID %s", + r.status_code, + cardID, + fieldID, + ) + print(r.content) - - def archiveTrelloCard(self,trelloCardID): - url = "https://api.trello.com/1/card/%s" % (trelloCardID) + def archive_trello_card(self, trelloCardID): + "archives an exisiting trello card" + url = f"{BASE_URL}card/%s" % (trelloCardID) params = self._get_trello_params() params["closed"] = True - r = requests.put(url,params=params) + r = requests.put(url, params=params, timeout=10) if r.status_code != 200: print(r) print(r.reason) - self.dirty_cache = True - + self.dirty_cache = True - def create_custom_field(self,field_name) -> str: + def create_custom_field(self, field_name) -> str: + "creates a custom field on the board, returns the ID for use." board_id = self.board_id - url = "https://api.trello.com/1/customFields/" + url = f"{BASE_URL}customFields/" params = self._get_trello_params() data = { - "idModel":board_id, - "modelType":"board", - "name":field_name, - "type":"text", - "pos":"bottom" + "idModel": board_id, + "modelType": "board", + "name": field_name, + "type": "text", + "pos": "bottom", } - result = requests.post(url,data=data,params=params) + result = requests.post(url, data=data, params=params, timeout=10) if result.status_code != 200: - logger.warn("Creation of a custom field failed!") + _LO.warning("Creation of a custom field failed!") return "" 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 "" - def _get_trello_params(self): - params = { - 'key' : self.key, - 'token' : self.token - } - return params + except json.JSONDecodeError as e: + _LO.warning( + "Couldn't parse JSON of new field? this shouldn't happen - %s", e + ) + return "" - def deleteTrelloCard(self,trelloCardID): + def _get_trello_params(self): + params = {"key": self.key, "token": self.token} + return params - url = "https://api.trello.com/1/card/%s" % (trelloCardID) - r = requests.delete(url, params=self._get_trello_params() ) + def delete_trello_card(self, trelloCardID): + "deletes an exisiting trello card" + url = f"{BASE_URL}card/%s" % (trelloCardID) + r = requests.delete(url, params=self._get_trello_params(), timeout=10) if r.status_code != 200: print(r) print(r.reason) - return + return self.dirty_cache = True - - - - def fetch_checklist_content(self, checklist_id) ->list: - url = "https://api.trello.com/1/checklists/%s" % (checklist_id) - 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 - %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 + def fetch_checklist_content(self, checklist_id) -> list: + "fetches the content of a checklist, returns a list of dicts" + url = f"{BASE_URL}checklists/%s" % (checklist_id) + r = self._request_and_validate(url) + return r - def _sort_trello_cards_list(self, array_of_cards:list) -> list: + 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)) - + 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. + 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: + 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 + card: dict if "id" not in card: - lo.warn("skipped adding a card to the cache, no id present") + _LO.warning("skipped adding a card to the cache, no id present") continue - self._cached_cards[card.get("id")] = card + self._cached_cards[card.get("id")] = card - def _try_update_cache(self,response_content) -> bool: + def _try_update_cache(self, response_content) -> bool: "attempts to parse the raw response from update/create and turn it into a cached card. Returns True on success" - + try: response_content = response_content.decode() except (UnicodeDecodeError, AttributeError): pass - if not isinstance(response_content,str): + if not isinstance(response_content, str): logger.error("error in _try_update_cache, supplied object not a string.") return False try: card = json.loads(response_content) - card:dict + card: dict except Exception as err: - logger.error("error in _try_update_cache, couldn't parse supplied object becase '%s'",err) + logger.error( + "error in _try_update_cache, couldn't parse supplied object becase '%s'", + err, + ) logger.debug("supplied object %s", response_content) return False - - self._cached_cards[card.get("id")] = card - return True - - \ No newline at end of file + + self._cached_cards[card.get("id")] = card + return True + + def _request_and_validate(self, url, headers=None, params=None, body=None) -> dict: + """internal method to make a GET request and return the parsed json response (either a dict of cards, or a dict of actions, or whatever is returned) + + arguments: + `url` - the url to request, including the base url + `headers` - any additional headers to send + `params` - any additional params to send (the key and token are added automatically) + `body` - any additional body to send + + returns: + a dict of the parsed json response, or an empty dict on failure + """ + if params is None: + params = self._get_trello_params() + else: + params = {**params, **self._get_trello_params()} + + try: + result = requests.get( + url=url, headers=headers, data=body, params=params, timeout=10 + ) + except ConnectionError as e: + _LO.error("Couldn't connect to %s - %s", url, e) + return {} + if result.status_code == 400: + _LO.error("Invalid request to %s - %s", url, result.content) + return {} + if result.status_code == 401: + _LO.error("Unrecognised token/key at %s - %s", url, result.content) + return {} + elif result.status_code == 403: + _LO.error("insufficient permissions %s - %s", url, result.content) + return {} + if result.status_code != 200: + _LO.error( + "Got an invalid response: %s - %s ", result.status_code, result.content + ) + return {} + try: + parsed_content = json.loads(result.content) + if "cards" in parsed_content: + parsed_content = parsed_content["cards"] + elif "actions" in parsed_content: + parsed_content = parsed_content["actions"] + except json.JSONDecodeError as e: + _LO.error("Couldn't parse JSON from %s - %s", url, e) + return {} + return parsed_content + + # we haven't handled pagination yet - this was copied from the zendesk thing. + def _request_and_validate_paginated( + self, + url, + params=None, + headers=None, + body=None, + page_limit=None, + page_size_limit=None, + since=None, + ) -> list: + params = {} if params is None else params + params["limit"] = page_size_limit + + page_limit = 10 if page_limit is None else page_limit + page_size_limit = 100 if page_size_limit is None else page_size_limit + next_page = 1 + pages = [] + if since is not None: + params["since"] = since + while next_page is not None: + r_url = f"{url}" + try: + resp = self._request_and_validate(r_url, headers, params, body) + except Exception as e: + _LO.error("Couldn't get page %s from %s- %s", next_page, url, e) + break + if resp in pages or len(resp) == 0: + break + pages.append(resp) + + # this is probably going to break at some point + params["since"] = resp[0]["id"] + # check if `resp` is already in `pages`` + + flattened_list = [] + for page in pages: + flattened_list.extend(page) + return flattened_list diff --git a/setup.py b/setup.py index f127bb9..10629ff 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="hex-helpers", - version="3.0.1", + version="3.1.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 index eb4c67e..21661e1 100644 --- a/tests/test_trello_functions.py +++ b/tests/test_trello_functions.py @@ -1,148 +1,105 @@ -import sys +import sys + sys.path.append("") -import os +import os from serviceHelpers.trello import trello TEST_BOARD_ID = "5f0dee6c5026590ce300472c" TEST_LIST_ID = "5f0dee6c5026590ce3004732" TEST_LIST_ID_TWO = "61d8367af7a2942f50afd468" +TRELLO_KEY = os.environ.get("TRELLO_KEY") +TRELLO_TOKEN = os.environ.get("TRELLO_TOKEN") + def test_trello_sort(): - helper = trello("none","none","none") + helper = trello("none", "none", "none") list_to_sort = [ - {"pos":300}, - {"pos":0.304}, - {"pos":20.4}, - {"pos":44.4}, - {"pos":209.30955} + {"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 + 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") + 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}, - + "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(): + 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 = 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}, - + "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) + 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 = 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"}, - + "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 ) + assert len(cards) == 3 def test_find_cards(): - helper = trello("none","none","none") + helper = trello("none", "none", "none") helper._cached_cards = { - "id_1":{"id":"id_1","name":"match","desc":""}, - "id_2":{"id":"id_2","name":"main","desc":"match"}, - "id_3":{"id":"id_3","name":"skip","desc":""}, - "id_4":{"id":"id_4","name":"main","desc":"different"}, - "id_5":{"id":"id_5","name":"fancy"}, - "id_6":{"id":"id_6","name":"jam","desc":"hello"}, - "id_7":{"id":"id_7","name":"train","desc":"ignore me"}, - + "id_1": {"id": "id_1", "name": "match", "desc": ""}, + "id_2": {"id": "id_2", "name": "main", "desc": "match"}, + "id_3": {"id": "id_3", "name": "skip", "desc": ""}, + "id_4": {"id": "id_4", "name": "main", "desc": "different"}, + "id_5": {"id": "id_5", "name": "fancy"}, + "id_6": {"id": "id_6", "name": "jam", "desc": "hello"}, + "id_7": {"id": "id_7", "name": "train", "desc": "ignore me"}, } helper.dirty_cache = False found = helper.find_trello_cards("match") - assert isinstance(found,list) + assert isinstance(found, list) assert len(found) == 2 - - -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"]) - -def test_move_card_from_list_to_list(): - helper = trello(TEST_BOARD_ID,os.environ["TRELLO_KEY"],os.environ["TRELLO_TOKEN"]) - card = helper.create_card("TEST CARD, PLEASE IGNORE",TEST_LIST_ID) - helper.update_card(card["id"], card["name"],new_list_id=TEST_LIST_ID_TWO) - new_card = helper.fetch_trello_card(card["id"]) - - helper.deleteTrelloCard(new_card["id"]) - assert card["id"] == new_card["id"] - assert new_card.get("idList","") == TEST_LIST_ID_TWO - - - -def test_update_card(): - old_ts = "2022-10-01 23:59:00" - new_ts = "2011-11-11 11:11:00" - helper = trello(TEST_BOARD_ID,os.environ["TRELLO_KEY"],os.environ["TRELLO_TOKEN"]) - card = helper.create_card("TEST CARD, PLEASE IGNORE",TEST_LIST_ID,dueTimestamp=old_ts) - helper.update_card(card["id"], card["name"], new_due_timestamp=new_ts) - new_card = helper.find_trello_card(card["id"]) - - helper.deleteTrelloCard(new_card["id"]) - assert new_card["due"] == "2011-11-11T11:11:00.000Z" - diff --git a/tests/test_trello_integration.py b/tests/test_trello_integration.py new file mode 100644 index 0000000..32c47ce --- /dev/null +++ b/tests/test_trello_integration.py @@ -0,0 +1,94 @@ +import sys + +sys.path.append("") +import os + +from serviceHelpers.trello import trello + +TEST_BOARD_ID = "5f0dee6c5026590ce300472c" +TEST_LIST_ID = "5f0dee6c5026590ce3004732" +TEST_LIST_ID_TWO = "61d8367af7a2942f50afd468" +TRELLO_KEY = os.environ.get("TRELLO_KEY") +TRELLO_TOKEN = os.environ.get("TRELLO_TOKEN") + + +def test_trello_get_cards(): + helper = trello(TEST_BOARD_ID, TRELLO_KEY, TRELLO_TOKEN) + cards = helper.fetch_trello_cards() + for card in cards: + assert "id" in card + + assert len(cards) == len(helper._cached_cards) + + +def test_create_card_at_correct_position(): + helper = trello(TEST_BOARD_ID, TRELLO_KEY, TRELLO_TOKEN) + card = helper.create_card( + "TEST CARD, PLEASE IGNORE", + TEST_LIST_ID, + "A temporary card that should get deleted", + position=0, + ) + helper.delete_trello_card(card["id"]) + + card = helper.create_card( + "TEST CARD, PLEASE IGNORE", + TEST_LIST_ID, + "A temporary card that should get deleted", + position=3, + ) + helper.delete_trello_card(card["id"]) + + card = helper.create_card( + "TEST CARD, PLEASE IGNORE", + TEST_LIST_ID, + "A temporary card that should get deleted", + position=-1, + ) + helper.delete_trello_card(card["id"]) + + +def test_move_card_from_list_to_list(): + helper = trello(TEST_BOARD_ID, TRELLO_KEY, TRELLO_TOKEN) + card = helper.create_card("TEST CARD, PLEASE IGNORE", TEST_LIST_ID) + helper.update_card(card["id"], card["name"], new_list_id=TEST_LIST_ID_TWO) + new_card = helper.fetch_trello_card(card["id"]) + + helper.delete_trello_card(new_card["id"]) + assert card["id"] == new_card["id"] + assert new_card.get("idList", "") == TEST_LIST_ID_TWO + + +def test_update_card(): + old_ts = "2022-10-01 23:59:00" + new_ts = "2011-11-11 11:11:00" + helper = trello(TEST_BOARD_ID, TRELLO_KEY, TRELLO_TOKEN) + card = helper.create_card( + "TEST CARD, PLEASE IGNORE", TEST_LIST_ID, dueTimestamp=old_ts + ) + helper.update_card(card["id"], card["name"], new_due_timestamp=new_ts) + new_card = helper.find_trello_card(card["id"]) + + helper.delete_trello_card(new_card["id"]) + assert new_card["due"] == "2011-11-11T11:11:00.000Z" + + +def test_fetch_all_actions(): + helper = trello(TEST_BOARD_ID, TRELLO_KEY, TRELLO_TOKEN) + actions = helper.fetch_actions_for_board() + + assert isinstance(actions, list) + assert len(actions) > 0 + assert "id" in actions[0] + return actions + + +def test_actions_since(): + helper = trello(TEST_BOARD_ID, TRELLO_KEY, TRELLO_TOKEN) + + actions = test_fetch_all_actions() + since = actions[2]["id"] + new_actions = helper.fetch_actions_for_board(since=since) + + assert isinstance(new_actions, list) + assert len(new_actions) < len(actions) diff --git a/tests/test_zendesk.py b/tests/test_zendesk.py index 53508a7..282382e 100644 --- a/tests/test_zendesk.py +++ b/tests/test_zendesk.py @@ -229,7 +229,7 @@ def test_get_form_id_in_ticket(): ticket = tickets[1239674] ticket: ZendeskTicket - assert ticket.ticket_form_id is not 0 + assert ticket.ticket_form_id != 0 def test_get_form_details(): "fetches a test form and checks that the returned name matches the expected value"