From cee3fe6a8cedd0661e61c5d818af0791954e7f60 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 14 Nov 2023 09:32:01 -0500 Subject: [PATCH 01/24] Fix syntax error by reference items on self We want to delete all checklist items on the current instance. --- trello/checklist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trello/checklist.py b/trello/checklist.py index 881a1f6..3d9d8a6 100644 --- a/trello/checklist.py +++ b/trello/checklist.py @@ -59,7 +59,7 @@ def clear(self): """Clear checklist by removing all checklist items""" # copy item list to prevent modifying while iterating, which would break # for-loops behaviour - old_items = items[:] + old_items = self.items[:] for item in old_items: self.delete_checklist_item(item) From c040fe7b852e3df1061925ca44da2fbb0f753abe Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 14 Nov 2023 09:36:16 -0500 Subject: [PATCH 02/24] Always fetch checklists regardless of count There are two problems with trying to do this check to prevent an actual fetch from happening: 1. `self.countChecklists` is only written to in `from_json` when the card is created 2. If the backing card is modified externally while the card object lives in memory, it will have no way of knowing a list was added. Point 1 could be fixed, but 2 could not since there's no way of knowing if the card has been updated externally. --- trello/card.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/trello/card.py b/trello/card.py index c69a77d..d07ad6c 100644 --- a/trello/card.py +++ b/trello/card.py @@ -242,9 +242,6 @@ def get_comments(self): def fetch_checklists(self): - if self.countCheckLists == 0: - return [] - checklists = [] json_obj = self.client.fetch_json( '/cards/' + self.id + '/checklists', ) From 694768a0c35b66b40b82941cc9be425ae7a40fc4 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 14 Nov 2023 10:51:20 -0500 Subject: [PATCH 03/24] Create board for checklist tests instead of using existing one This simplifies setup for the test and ensures it manages it's own test resources. --- test/test_checklist.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/test_checklist.py b/test/test_checklist.py index b37d8bb..433ecb5 100644 --- a/test/test_checklist.py +++ b/test/test_checklist.py @@ -13,18 +13,20 @@ class TrelloChecklistTestCase(unittest.TestCase): independently. """ + _trello = None + _board = None + @classmethod def setUpClass(cls): cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], token=os.environ['TRELLO_TOKEN']) - for b in cls._trello.list_boards(): - if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: - cls._board = b - break - if not cls._board: - cls.fail("Couldn't find test board") + cls._board = cls._trello.add_board("TEST BOARD") cls._list = cls._board.add_list(str(datetime.now())) + @classmethod + def tearDownClass(cls): + cls._board.delete() + def _add_card(self, name, description=None): try: card = self._list.add_card(name, description) From fbe8f3ed7deaff01b565872d43dd66e3267a01a3 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 14 Nov 2023 11:21:15 -0500 Subject: [PATCH 04/24] Create test board for board and card tests instead of using existing one This simplifies setup for the tests and ensures they manage their own test resources. --- test/test_board.py | 14 ++++++++------ test/test_card.py | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/test/test_board.py b/test/test_board.py index 1582c53..690f98f 100644 --- a/test/test_board.py +++ b/test/test_board.py @@ -13,18 +13,20 @@ class TrelloBoardTestCase(unittest.TestCase): independently. """ + _trello = None + _board = None + @classmethod def setUpClass(cls): cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], token=os.environ['TRELLO_TOKEN']) - for b in cls._trello.list_boards(): - if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: - cls._board = b - break - if not cls._board: - cls.fail("Couldn't find test board") + cls._board = cls._trello.add_board("TEST BOARD") cls._list = cls._board.add_list(str(datetime.now())) + @classmethod + def tearDownClass(cls): + cls._board.delete() + def _add_card(self, name, description=None): try: card = self._list.add_card(name, description) diff --git a/test/test_card.py b/test/test_card.py index 575e48f..8f799f3 100644 --- a/test/test_card.py +++ b/test/test_card.py @@ -13,18 +13,20 @@ class TrelloCardTestCase(unittest.TestCase): independently. """ + _trello = None + _board = None + @classmethod def setUpClass(cls): cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], token=os.environ['TRELLO_TOKEN']) - for b in cls._trello.list_boards(): - if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: - cls._board = b - break - if not cls._board: - cls.fail("Couldn't find test board") + cls._board = cls._trello.add_board("TEST BOARD") cls._list = cls._board.add_list(str(datetime.now())) + @classmethod + def tearDownClass(cls): + cls._board.delete() + def _add_card(self, name, description=None): try: card = self._list.add_card(name, description) From 4bb6fc54f6c62bf60611d8c42ae674087e8c92a6 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 14 Nov 2023 15:49:31 -0500 Subject: [PATCH 05/24] Create test board in trello client tests This simplifies setup for the tests since they don't rely on the external dependency of a test board being created by the user. They now manage their own dependencies. - Remove test_list_stars because this functionality is already tested in another test. --- test/test_trello_client.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/test_trello_client.py b/test/test_trello_client.py index 904e770..77920ba 100644 --- a/test/test_trello_client.py +++ b/test/test_trello_client.py @@ -20,9 +20,11 @@ def setUp(self): token=os.environ['TRELLO_TOKEN']) def test01_list_boards(self): - self.assertEqual( - len(self._trello.list_boards(board_filter="open")), - int(os.environ['TRELLO_TEST_BOARD_COUNT'])) + board = self._trello.add_board("TEST BOARD") + boards = self._trello.list_boards(board_filter="open") + self.assertGreater(len(boards), 0) + self.assertIn(board, boards) + board.delete() def test10_board_attrs(self): boards = self._trello.list_boards() @@ -111,23 +113,18 @@ def test54_resource_unavailable(self): self.assertRaises(ResourceUnavailable, self._trello.get_card, '0') - def test_list_stars(self): - """ - Test trello client star list - """ - self.assertEqual(len(self._trello.list_stars()), int(os.environ["TRELLO_TEST_STAR_COUNT"]), "Number of stars does not match TRELLO_TEST_STAR_COUNT") - def test_add_delete_star(self): """ Test add and delete star to/from test board """ - test_board_id = self._trello.search(os.environ["TRELLO_TEST_BOARD_NAME"])[0].id - new_star = self._trello.add_star(test_board_id) + board = self._trello.add_board("TEST BOARD") + new_star = self._trello.add_star(board.id) star_list = self._trello.list_stars() self.assertTrue(new_star in star_list, "Star id was not added in list of starred boards") deleted_star = self._trello.delete_star(new_star) star_list = self._trello.list_stars() self.assertFalse(deleted_star in star_list, "Star id was not deleted from list of starred boards") + board.delete() class TrelloClientTestCaseWithoutOAuth(unittest.TestCase): """ From f000a8b0b313eb723239be2099fa465c43c587ac Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 14 Nov 2023 15:58:29 -0500 Subject: [PATCH 06/24] Remove mention of no longer used environment variables from README - Make note that a test Trello account should be used for testing. --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 41874d9..60bc7ba 100644 --- a/README.rst +++ b/README.rst @@ -92,11 +92,10 @@ To run the tests, run ``python -m unittest discover``. Four environment variable * ``TRELLO_API_KEY``: your Trello API key * ``TRELLO_TOKEN``: your Trello OAuth token -* ``TRELLO_TEST_BOARD_COUNT``: the number of boards in your Trello account -* ``TRELLO_TEST_BOARD_NAME``: name of the board to test card manipulation on. Must be unique, or the first match will be used -* ``TRELLO_TEST_STAR_COUNT``: the number of stars on your test Trello board -*WARNING*: The tests will delete all cards on the board called `TRELLO_TEST_BOARD_NAME`! +*NOTE*: **It's recommended to create a separate Trello account for testing. While the tests try to only modify or delete +resources they've created, to remove all possibility of unintentional data loss, we recommend not using a personal +Trello account with existing data.** To run tests across various Python versions, `tox `_ is supported. Install it From 081d09e7b7cf006a907c5094a4b70696a66bc15d Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 12:13:22 -0500 Subject: [PATCH 07/24] Board tests setup and teardown board for each test This makes the test more self contained so they're easier to reason about. --- test/test_board.py | 50 ++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/test/test_board.py b/test/test_board.py index 690f98f..72ec214 100644 --- a/test/test_board.py +++ b/test/test_board.py @@ -13,19 +13,33 @@ class TrelloBoardTestCase(unittest.TestCase): independently. """ - _trello = None - _board = None + _test_board_guid = "c0b8eaf5-d63e-4586-a2be-5d1567e22cc9" @classmethod def setUpClass(cls): - cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], - token=os.environ['TRELLO_TOKEN']) - cls._board = cls._trello.add_board("TEST BOARD") - cls._list = cls._board.add_list(str(datetime.now())) + cls.clean_boards() @classmethod def tearDownClass(cls): - cls._board.delete() + cls.clean_boards() + + @classmethod + def clean_boards(cls): + trello = TrelloClient(os.environ['TRELLO_API_KEY'], + token=os.environ['TRELLO_TOKEN']) + for board in trello.list_boards(): + if cls._test_board_guid in board.name: + board.delete() + + def setUp(self): + self._trello = TrelloClient(os.environ['TRELLO_API_KEY'], + token=os.environ['TRELLO_TOKEN']) + self._board = self._trello.add_board(f"TEST BOARD ({TrelloBoardTestCase._test_board_guid})") + self._list = self._board.add_list(str(datetime.now())) + self._add_card("test_card") + + def tearDown(self): + self._board.delete() def _add_card(self, name, description=None): try: @@ -144,36 +158,28 @@ def test90_get_board(self): self.assertEqual(self._board.name, board.name) def test100_add_board(self): - test_board = self._trello.add_board("test_create_board") - test_list = test_board.add_list("test_list") + test_list = self._board.add_list("test_list") test_list.add_card("test_card") - open_boards = self._trello.list_boards(board_filter="open") - self.assertEqual(len([x for x in open_boards if x.name == "test_create_board"]), 1) + self.assertEqual(self._trello.get_board(self._board.id).id, self._board.id) def test110_copy_board(self): - boards = self._trello.list_boards(board_filter="open") - source_board = next( x for x in boards if x.name == "test_create_board") - self._trello.add_board("copied_board", source_board=source_board) - listed_boards = self._trello.list_boards(board_filter="open") - copied_board = next(iter([x for x in listed_boards if x.name == "copied_board"]), None) + copied_board = self._trello.add_board("Copied " + self._board.name, source_board=self._board) self.assertIsNotNone(copied_board) open_lists = copied_board.open_lists() self.assertEqual(len(open_lists), 4) # default lists plus mine test_list = open_lists[0] self.assertEqual(len(test_list.list_cards()), 1) - test_card = next ( iter([ x for x in test_list.list_cards() if x.name == "test_card"]), None ) + test_card = next(iter([x for x in test_list.list_cards() if x.name == "test_card"]), None) self.assertIsNotNone(test_card) + copied_board.delete() def test120_close_board(self): boards = self._trello.list_boards(board_filter="open") open_count = len(boards) - test_create_board = next( x for x in boards if x.name == "test_create_board") # type: Board - copied_board = next( x for x in boards if x.name == "copied_board") # type: Board - test_create_board.close() - copied_board.close() + self._board.close() still_open_boards = self._trello.list_boards(board_filter="open") still_open_count = len(still_open_boards) - self.assertEqual(still_open_count, open_count - 2) + self.assertEqual(still_open_count, open_count - 1) def test130_get_checklists_board(self): chklists = self._board.get_checklists(cards = 'open') From 1fb7abaaf6de65966b08c4443f382c8b184a8d4d Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 12:28:41 -0500 Subject: [PATCH 08/24] Reformat whitespace in Board class --- trello/board.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/trello/board.py b/trello/board.py index f6fa416..9628cc0 100644 --- a/trello/board.py +++ b/trello/board.py @@ -123,7 +123,7 @@ def open(self): http_method='PUT', post_args={'value': 'false', }, ) self.closed = False - + def delete(self): self.client.fetch_json( '/boards/' + self.id, @@ -192,7 +192,7 @@ def add_custom_field_definition(self, name, type, options=None, display_on_card= :name: name for the field :type: type of field: "checkbox", "list", "number", "text", "date" :options: list of options for field, only valid for "list" type - :display_on_card: boolean whether this field should be shown on the front of cards + :display_on_card: boolean whether this field should be shown on the front of cards :pos: position of the list: "bottom", "top" or a positive number :return: the custom_field_definition :rtype: CustomFieldDefinition @@ -215,7 +215,7 @@ def update_custom_field_definition(self, custom_field_definition_id, name=None, :custom_field_definition_id: the ID of the CustomFieldDefinition to update. :name: new name for the field - :display_on_card: boolean whether this field should be shown on the front of cards + :display_on_card: boolean whether this field should be shown on the front of cards :pos: position of the list: "bottom", "top" or a positive number :return: the custom_field_definition :rtype: CustomFieldDefinition @@ -227,7 +227,7 @@ def update_custom_field_definition(self, custom_field_definition_id, name=None, arguments["display/cardFront"] = u"true" if display_on_card else u"false" if pos: arguments["pos"] = pos - + json_obj = self.client.fetch_json( '/customFields/{0}'.format(custom_field_definition_id), http_method='PUT', @@ -245,7 +245,7 @@ def delete_custom_field_definition(self, custom_field_definition_id): '/customFields/{0}'.format(custom_field_definition_id), http_method='DELETE', ) return json_obj - + def get_custom_field_list_options(self,custom_field_definition_id,values_only=False): """Get custom field definition list options on this board @@ -278,7 +278,7 @@ def add_custom_field_list_option(self,custom_field_definition_id,new_option): post_args={'value': {'text':new_option}, }, ) return json_obj - + def get_custom_field_list_option(self,custom_field_definition_id,option_id): """Get a specific custom field definition list option on this board @@ -322,7 +322,7 @@ def get_labels(self, fields='all', limit=50): '/boards/' + self.id + '/labels', query_params={'fields': fields, 'limit': limit}) return Label.from_json_list(self, json_obj) - + def get_label(self, label_id): """ :label_id: str label id @@ -332,7 +332,7 @@ def get_label(self, label_id): json_obj = self.client.fetch_json( '/boards/' + self.id + '/labels/' + label_id ) - + return Label.from_json(self, json_obj) def get_checklists(self, cards='all'): @@ -458,7 +458,7 @@ def get_cards(self, filters=None, card_filter=""): ) return list([Card.from_json(self, json) for json in json_obj]) - + def get_card(self, card_id): """ :card_id: str card id. From 14aea1a30a8c74c6c870ee72bbbd9ba04626b06e Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 12:32:57 -0500 Subject: [PATCH 09/24] Allow board and list classes to be created from minimal json When requesting resources you can specify only certain fields on those resources are returned. This can be as minimal as only the IDs. This means that when parsing json into resources, we can't assume any field other than the id will be present. All other model resources should also be updated, but only doing List and Board for now. --- trello/board.py | 8 ++++---- trello/trellolist.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/trello/board.py b/trello/board.py index 9628cc0..e8a4797 100644 --- a/trello/board.py +++ b/trello/board.py @@ -57,13 +57,13 @@ def from_json(cls, trello_client=None, organization=None, json_obj=None): :json_obj: the json board object """ if organization is None: - board = Board(client=trello_client, board_id=json_obj['id'], name=json_obj['name']) + board = Board(client=trello_client, board_id=json_obj['id'], name=json_obj.get('name', None)) else: - board = Board(organization=organization, board_id=json_obj['id'], name=json_obj['name']) + board = Board(organization=organization, board_id=json_obj['id'], name=json_obj.get('name', None)) board.description = json_obj.get('desc', '') - board.closed = json_obj['closed'] - board.url = json_obj['url'] + board.closed = json_obj.get('closed', None) + board.url = json_obj.get('url', None) return board diff --git a/trello/trellolist.py b/trello/trellolist.py index 3b18028..c120352 100644 --- a/trello/trellolist.py +++ b/trello/trellolist.py @@ -34,13 +34,13 @@ def from_json(cls, board, json_obj): :board: the board object that the list belongs to :json_obj: the json list object """ - list = List(board, json_obj['id'], name=json_obj['name']) - list.closed = json_obj['closed'] - list.pos = json_obj['pos'] + list_ = List(board, json_obj['id'], name=json_obj.get('name', None)) + list_.closed = json_obj.get('closed', None) + list_.pos = json_obj.get('pos', None) #this method is also called from board.py with a different json object, so we need to make sure 'subscribed' is there if 'subscribed' in json_obj: - list.subscribed = json_obj['subscribed'] - return list + list_.subscribed = json_obj['subscribed'] + return list_ def __repr__(self): return force_str(u'' % self.name) From 994308cdc0083c8b0225821914280f37cfdb2ee9 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 12:38:44 -0500 Subject: [PATCH 10/24] Formally support batch requests for two resource endpoints The batch endpoint allows you to specify multiple requests at once and get a list of responses. This greatly increases performance and can be critical to avoiding Trello rate limiting. This is an incomplete implemetation that only adds support for two Board endpoints. --- test/test_trello_client.py | 30 +++++++++++++++++- trello/batch/batcherror.py | 24 +++++++++++++++ trello/batch/batchresponse.py | 19 ++++++++++++ trello/batch/board.py | 57 +++++++++++++++++++++++++++++++++++ trello/trelloclient.py | 17 +++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 trello/batch/batcherror.py create mode 100644 trello/batch/batchresponse.py create mode 100644 trello/batch/board.py diff --git a/test/test_trello_client.py b/test/test_trello_client.py index 77920ba..7a6ad24 100644 --- a/test/test_trello_client.py +++ b/test/test_trello_client.py @@ -3,7 +3,9 @@ import os import unittest from datetime import datetime -from trello import TrelloClient, Unauthorized, ResourceUnavailable +from trello import TrelloClient, Unauthorized, ResourceUnavailable, Board, List +from trello.batch.board import Board as BatchBoard +from trello.batch.batcherror import BatchError class TrelloClientTestCase(unittest.TestCase): @@ -19,6 +21,32 @@ def setUp(self): self._trello = TrelloClient(os.environ['TRELLO_API_KEY'], token=os.environ['TRELLO_TOKEN']) + def test_fetch_batch(self): + board = self._trello.add_board("TEST BOARD") + + batch_responses = self._trello.fetch_batch([ + BatchBoard.GetLists(board.id, ['id', 'name'], 'open', ['idCard']), + BatchBoard.GetBoard(board.id, ['id', 'name']), + BatchBoard.GetLists('123', ['name']) + ]) + board_lists = batch_responses[0] + boards = batch_responses[1] + batch_error = batch_responses[2] + + self.assertTrue(board_lists.success) + self.assertIsInstance(board_lists.payload[0], List) + self.assertEqual(len(board_lists.payload), 3) + + self.assertTrue(boards.success) + self.assertIsInstance(boards.payload, Board) + self.assertEqual(boards.payload.name, "TEST BOARD") + + self.assertFalse(batch_error.success) + self.assertIsInstance(batch_error.payload, BatchError) + self.assertEqual(batch_error.payload.message, "invalid id") + + board.delete() + def test01_list_boards(self): board = self._trello.add_board("TEST BOARD") boards = self._trello.list_boards(board_filter="open") diff --git a/trello/batch/batcherror.py b/trello/batch/batcherror.py new file mode 100644 index 0000000..145cf41 --- /dev/null +++ b/trello/batch/batcherror.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement, print_function, absolute_import + +from trello import TrelloBase +from trello.compat import force_str +from trello.member import Member + + +class BatchError: + """ + Class representing a BatchError + """ + def __init__(self, status_code, name, message): + super(BatchError, self).__init__() + self.status_code = status_code + self.name = name + self.message = message + + @classmethod + def from_json(cls, json_obj): + return BatchError(json_obj['statusCode'], json_obj['name'], json_obj['message']) + + def __repr__(self): + return force_str(u'' % self.name) diff --git a/trello/batch/batchresponse.py b/trello/batch/batchresponse.py new file mode 100644 index 0000000..4558d3a --- /dev/null +++ b/trello/batch/batchresponse.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement, print_function, absolute_import + +from trello import TrelloBase +from trello.compat import force_str +from trello.member import Member + + +class BatchResponse: + """ + Class representing a BatchError + """ + def __init__(self, payload, success): + super(BatchResponse, self).__init__() + self.payload = payload + self.success = success + + def __repr__(self): + return force_str(u'' % self.success) diff --git a/trello/batch/board.py b/trello/batch/board.py new file mode 100644 index 0000000..a9c0e8b --- /dev/null +++ b/trello/batch/board.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement, print_function, absolute_import + +from trello import Board as TrelloBoard +from trello.trellolist import List +import urllib.parse + + +class Board: + """ + Class representing a Trello board. Board attributes are stored as normal + Python attributes; access to all sub-objects, however, is always + an API call (Lists, Cards). + """ + class GetLists: + def __init__(self, board_id, fields=None, cards=None, card_fields=None): + self.board_id = board_id + self.fields = fields if fields is not None else [] + self.cards = cards + self.card_fields = card_fields if card_fields is not None else [] + + def path(self): + path = f"/boards/{self.board_id}/lists" + params = {} + if self.fields: + fields = ','.join(field for field in self.fields) + params['fields'] = fields + if self.cards: + params['cards'] = self.cards + if self.card_fields: + params['card_fields'] = ','.join(field for field in self.card_fields) + path += "?" + urllib.parse.urlencode(params) if params else "" + return path + + def parse(self, json): + board = TrelloBoard(self.board_id) + lists = [] + for list_ in json: + lists.append(List.from_json(board, list_)) + return lists + + class GetBoard: + def __init__(self, board_id, fields=None): + self.board_id = board_id + self.fields = fields if fields is not None else [] + + def path(self): + path = f"/boards/{self.board_id}" + params = {} + if self.fields: + fields = ','.join(field for field in self.fields) + params['fields'] = fields + path += "?" + urllib.parse.urlencode(params) if params else "" + return path + + def parse(self, json): + return TrelloBoard.from_json(json_obj=json) diff --git a/trello/trelloclient.py b/trello/trelloclient.py index 3765c7a..f9134c9 100644 --- a/trello/trelloclient.py +++ b/trello/trelloclient.py @@ -3,6 +3,9 @@ import json import requests from requests_oauthlib import OAuth1 + +from trello.batch.batcherror import BatchError +from trello.batch.batchresponse import BatchResponse from trello.board import Board from trello.card import Card from trello.trellolist import List @@ -197,6 +200,20 @@ def get_label(self, label_id, board_id): label_json = self.fetch_json('/labels/' + label_id) return Label.from_json(board, label_json) + def fetch_batch(self, batch_requests: list): + batch_responses = self.fetch_json( + "batch", + query_params={"urls": ",".join(batch_request.path() for batch_request in batch_requests)}) + + items = [] + for response, request in zip(batch_responses, batch_requests): + if "200" in response: + item = BatchResponse(request.parse(response['200']), True) + else: + item = BatchResponse(BatchError.from_json(response), False) + items.append(item) + return items + def fetch_json( self, uri_path, From 2f761515d6791f74e7d0cebb5b06dd878998ef67 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 13:43:15 -0500 Subject: [PATCH 11/24] Update project specific details to reference this fork --- README.rst => README.md | 10 +++++----- setup.py | 14 +++++++------- trello/util.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) rename README.rst => README.md (91%) diff --git a/README.rst b/README.md similarity index 91% rename from README.rst rename to README.md index 60bc7ba..596cfe0 100644 --- a/README.rst +++ b/README.md @@ -4,15 +4,12 @@ are cached, but the child objects are not. This can possibly be improved when the API allows for notification subscriptions; this would allow caching (assuming a connection was available to invalidate the cache as appropriate). -I've created a `Trello Board `_ -for feature requests, discussion and some development tracking. - Install ======= :: - pip install py-trello + pip install ha-py-trello Usage ===== @@ -99,4 +96,7 @@ Trello account with existing data.** To run tests across various Python versions, `tox `_ is supported. Install it -and simply run ``tox`` from the ``py-trello`` directory. +and simply run ``tox`` from the ``ha-py-trello`` directory. + +--- +*Forked from original: https://github.com/sarumont/py-trello* diff --git a/setup.py b/setup.py index 113604f..3d97f11 100755 --- a/setup.py +++ b/setup.py @@ -3,15 +3,15 @@ from setuptools import setup, find_packages setup( - name="py-trello", - version="0.19.0", + name="ha-py-trello", + version="0.20.0", - description='Python wrapper around the Trello API', + description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.rst').read(), - author='Richard Kolkovich', - author_email='richard@sigil.org', - url='https://trello.com/board/py-trello/4f145d87b2f9f15d6d027b53', - download_url='https://github.com/sarumont/py-trello', + author='Scott Giminiani (originally Richard Kolkovich)', + author_email='scottg489@gmail.com', + url='https://github.com/ScottG489/ha-py-trello', + download_url='https://github.com/ScottG489/ha-py-trello', keywords='python', license='BSD License', classifiers=[ diff --git a/trello/util.py b/trello/util.py index 80e7fe5..8e896b8 100755 --- a/trello/util.py +++ b/trello/util.py @@ -23,7 +23,7 @@ def create_oauth_token(expiration=None, scope=None, key=None, secret=None, name= scope = scope or os.environ.get('TRELLO_SCOPE', 'read,write') trello_key = key or os.environ['TRELLO_API_KEY'] trello_secret = secret or os.environ['TRELLO_API_SECRET'] - name = name or os.environ.get('TRELLO_NAME', 'py-trello') + name = name or os.environ.get('TRELLO_NAME', 'ha-py-trello') # Step 1: Get a request token. This is a temporary token that is used for # having the user authorize an access token and to sign the request to obtain From 9be3c7f0ea9497e97ad536de946fd02990de3505 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 11:12:02 -0800 Subject: [PATCH 12/24] Create python-publish.yml workflow --- .github/workflows/python-publish.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..f808deb --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,40 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + repository-url: https://test.pypi.org/legacy/ From ea674e1b93300ac3b06ec6b4b9c4671d0fc88244 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 13:43:15 -0500 Subject: [PATCH 13/24] Update project specific details to reference this fork --- MANIFEST.in | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 9561fb1..bb3ec5f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include README.rst +include README.md diff --git a/setup.py b/setup.py index 3d97f11..72f4062 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ version="0.20.0", description='Python wrapper around the Trello API (Home Assistant specific)', - long_description=open('README.rst').read(), + long_description=open('README.md').read(), author='Scott Giminiani (originally Richard Kolkovich)', author_email='scottg489@gmail.com', url='https://github.com/ScottG489/ha-py-trello', From 91f74191e2d4349cde40a806c2c030b454afdb3c Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 14:15:12 -0500 Subject: [PATCH 14/24] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72f4062..dced1bf 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="ha-py-trello", - version="0.20.0", + version="0.20.2", description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.md').read(), From 87c2d82e480c8414c71fee386a218850f0976eb6 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 14:22:04 -0500 Subject: [PATCH 15/24] Remove test repo URL for publishing to pypi --- .github/workflows/python-publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index f808deb..bdaab28 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -37,4 +37,3 @@ jobs: with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - repository-url: https://test.pypi.org/legacy/ From 59f1079c7f395c17f5406063c221b70ad5196261 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 14:24:48 -0500 Subject: [PATCH 16/24] Document how to publish to PyPI --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 596cfe0..4c7cab5 100644 --- a/README.md +++ b/README.md @@ -98,5 +98,21 @@ To run tests across various Python versions, `tox `_ is supported. Install it and simply run ``tox`` from the ``ha-py-trello`` directory. +## Publishing +To publish, simply create a release on GitHub and a workflow will kick off to publish to PyPI. If you'd like to publish +locally, follow the below instructions. + +First ensure the appropriate tools are installed locally: +```shell +python3 -m pip install --upgrade build +python3 -m pip install --upgrade twine +``` +Then build and publish: +```shell +python3 -m build +python3 -m twine upload dist/* +``` +For more information see the [official packaging and publishing docs](https://packaging.python.org/en/latest/tutorials/packaging-projects). + --- *Forked from original: https://github.com/sarumont/py-trello* From 739c409419e6376688b26947fa9881067b668609 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 14:26:07 -0500 Subject: [PATCH 17/24] Bump version to 0.21.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dced1bf..ab13eec 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="ha-py-trello", - version="0.20.2", + version="0.21.0", description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.md').read(), From c252c01c09cae237b1830a52ecc9d5e9d8a885da Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 15:25:51 -0500 Subject: [PATCH 18/24] Allow batch module to be used --- trello/batch/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 trello/batch/__init__.py diff --git a/trello/batch/__init__.py b/trello/batch/__init__.py new file mode 100644 index 0000000..e69de29 From c81fda426a529dafbdd5042b264eef062401a513 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Wed, 22 Nov 2023 15:26:31 -0500 Subject: [PATCH 19/24] Bump version to 0.22.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ab13eec..e901bc5 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="ha-py-trello", - version="0.21.0", + version="0.22.0", description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.md').read(), From 052f4753aa7426e33c7d482babfbbd65dd3ec230 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Sun, 26 Nov 2023 23:24:46 -0500 Subject: [PATCH 20/24] Allow card class to be created from minimal json Similar to recent changes to Board and List. The API can return minimal representations of these with only supplying the ID, so that's all we'll require to create an instance from the from_json method. --- trello/card.py | 37 ++++++++++++++++++++----------------- trello/trellolist.py | 2 ++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/trello/card.py b/trello/card.py index d07ad6c..da95f2c 100644 --- a/trello/card.py +++ b/trello/card.py @@ -139,27 +139,30 @@ def from_json(cls, parent, json_obj): raise Exception("key 'id' is not in json_obj") card = cls(parent, json_obj['id'], - name=json_obj['name']) + name=json_obj.get('name')) card._json_obj = json_obj card.desc = json_obj.get('desc', '') card.due = json_obj.get('due', '') - card.is_due_complete = json_obj['dueComplete'] - card.closed = json_obj['closed'] - card.url = json_obj['url'] - card.pos = json_obj['pos'] - card.shortUrl = json_obj['shortUrl'] - card.idMembers = json_obj['idMembers'] - card.member_ids = json_obj['idMembers'] - card.idLabels = json_obj['idLabels'] - card.idBoard = json_obj['idBoard'] - card.idList = json_obj['idList'] - card.idShort = json_obj['idShort'] - card.badges = json_obj['badges'] + card.is_due_complete = json_obj.get('dueComplete') + card.closed = json_obj.get('closed') + card.url = json_obj.get('url') + card.pos = json_obj.get('pos') + card.shortUrl = json_obj.get('shortUrl') + card.idMembers = json_obj.get('idMembers') + card.member_ids = json_obj.get('idMembers') + card.idLabels = json_obj.get('idLabels') + card.idBoard = json_obj.get('idBoard') + card.idList = json_obj.get('idList') + card.idShort = json_obj.get('idShort') + card.badges = json_obj.get('badges') card.customFields = card.fetch_custom_fields(json_obj=json_obj) - card.countCheckItems = json_obj['badges']['checkItems'] - card.countCheckLists = len(json_obj['idChecklists']) - card._labels = Label.from_json_list(card.board, json_obj['labels']) - card.dateLastActivity = dateparser.parse(json_obj['dateLastActivity']) + if json_obj.get('badges'): + card.countCheckItems = json_obj['badges']['checkItems'] + if json_obj.get('idChecklists'): + card.countCheckLists = len(json_obj['idChecklists']) + card._labels = Label.from_json_list(card.board, json_obj.get('labels', [])) + if json_obj.get('dateLastActivity'): + card.dateLastActivity = dateparser.parse(json_obj.get('dateLastActivity')) if "attachments" in json_obj: card._attachments = [] for attachment_json in json_obj["attachments"]: diff --git a/trello/trellolist.py b/trello/trellolist.py index c120352..5af840c 100644 --- a/trello/trellolist.py +++ b/trello/trellolist.py @@ -40,6 +40,8 @@ def from_json(cls, board, json_obj): #this method is also called from board.py with a different json object, so we need to make sure 'subscribed' is there if 'subscribed' in json_obj: list_.subscribed = json_obj['subscribed'] + if 'cards' in json_obj: + list_.cards = [Card.from_json(list_, card) for card in json_obj['cards']] return list_ def __repr__(self): From f4c45573a6ea85824d4d9acc97a7d1a061221fdb Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Sun, 26 Nov 2023 23:26:46 -0500 Subject: [PATCH 21/24] Bump version to 0.23.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e901bc5..ef7cb7b 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="ha-py-trello", - version="0.22.0", + version="0.23.0", description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.md').read(), From c8c8c386fe814d4505a8486a82d0237637b1c069 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 28 Nov 2023 11:00:28 -0500 Subject: [PATCH 22/24] Initialize trello list object with an empty cards array The list of cards can now be guaranteed to be accessible on a trello list object. --- trello/trellolist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/trello/trellolist.py b/trello/trellolist.py index 5af840c..e7dc970 100644 --- a/trello/trellolist.py +++ b/trello/trellolist.py @@ -25,6 +25,7 @@ def __init__(self, board, list_id, name=''): self.closed = None self.pos = None self.subscribed = None + self.cards = [] @classmethod def from_json(cls, board, json_obj): From a851462e95ea2702940d6ef4ed0de7434f64b596 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 28 Nov 2023 11:02:39 -0500 Subject: [PATCH 23/24] Bump version to 0.23.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ef7cb7b..c507342 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="ha-py-trello", - version="0.23.0", + version="0.23.1", description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.md').read(), From 28efbc40c0a8d12a897f5434afaa705b04d31e29 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Tue, 19 Aug 2025 13:37:42 -0400 Subject: [PATCH 24/24] Bump version to 0.24.1 This brings in fixes from upstream. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c507342..4184483 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="ha-py-trello", - version="0.23.1", + version="0.24.1", description='Python wrapper around the Trello API (Home Assistant specific)', long_description=open('README.md').read(),