From 5ba18ea7d5c7970ed6635eb1139e87803f074de3 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 27 Dec 2019 15:55:56 +0100 Subject: [PATCH] Added Trello integration --- docs/source/conf.py | 1 + platypush/__init__.py | 10 +- platypush/message/__init__.py | 15 +- platypush/message/response/__init__.py | 10 +- platypush/message/response/trello.py | 185 +++++ platypush/plugins/trello.py | 908 +++++++++++++++++++++++++ requirements.txt | 6 +- setup.py | 4 +- 8 files changed, 1126 insertions(+), 13 deletions(-) create mode 100644 platypush/message/response/trello.py create mode 100644 platypush/plugins/trello.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 63610d5b0..73508d367 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -230,6 +230,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers', 'avs', 'PyOBEX', 'todoist', + 'trello', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/__init__.py b/platypush/__init__.py index aa9433ad5..9be538833 100644 --- a/platypush/__init__.py +++ b/platypush/__init__.py @@ -10,7 +10,6 @@ import logging import os import sys -from .bus import Bus from .bus.redis import RedisBus from .config import Config from .context import register_backends @@ -164,12 +163,9 @@ class Daemon: print('---- Starting platypush v.{}'.format(__version__)) - redis_conf = Config.get('backend.redis') - if redis_conf: - self.bus = RedisBus(on_message=self.on_message(), - **redis_conf.get('redis_args', {})) - else: - self.bus = Bus(on_message=self.on_message()) + redis_conf = Config.get('backend.redis', {}) + self.bus = RedisBus(on_message=self.on_message(), + **redis_conf.get('redis_args', {})) # Initialize the backends and link them to the bus self.backends = register_backends(bus=self.bus, global_scope=True) diff --git a/platypush/message/__init__.py b/platypush/message/__init__.py index 5103bac15..36a915332 100644 --- a/platypush/message/__init__.py +++ b/platypush/message/__init__.py @@ -1,3 +1,4 @@ +import datetime import logging import inspect import json @@ -23,7 +24,7 @@ class Message(object): for attr in self.__dir__() if (attr != '_timestamp' or not attr.startswith('_')) and not inspect.ismethod(getattr(self, attr)) - }).replace('\n', ' ') + }, cls=MessageEncoder).replace('\n', ' ') def __bytes__(self): """ @@ -77,6 +78,8 @@ class Message(object): class Mapping(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + for k, v in kwargs.items(): + self.__setattr__(k, v) def __setitem__(self, key, item): self.__dict__[key] = item @@ -130,4 +133,14 @@ class Mapping(dict): return str(self.__dict__) +class MessageEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime.datetime) or \ + isinstance(obj, datetime.date) or \ + isinstance(obj, datetime.time): + return obj.isoformat() + + return super().default(obj) + + # vim:sw=4:ts=4:et: diff --git a/platypush/message/response/__init__.py b/platypush/message/response/__init__.py index f42e254e7..c539115b1 100644 --- a/platypush/message/response/__init__.py +++ b/platypush/message/response/__init__.py @@ -1,7 +1,7 @@ import json import time -from platypush.message import Message +from platypush.message import Message, MessageEncoder class Response(Message): @@ -67,6 +67,10 @@ class Response(Message): the message into a UTF-8 JSON string """ + output = self.output if self.output is not None and self.output != {} else { + 'status': 'ok' if not self.errors else 'error' + } + response_dict = { 'id': self.id, 'type': 'response', @@ -74,7 +78,7 @@ class Response(Message): 'origin': self.origin if hasattr(self, 'origin') else None, '_timestamp': self.timestamp, 'response': { - 'output': self.output, + 'output': output, 'errors': self.errors, }, } @@ -82,7 +86,7 @@ class Response(Message): if self.disable_logging: response_dict['_disable_logging'] = self.disable_logging - return json.dumps(response_dict) + return json.dumps(response_dict, cls=MessageEncoder) # vim:sw=4:ts=4:et: diff --git a/platypush/message/response/trello.py b/platypush/message/response/trello.py new file mode 100644 index 000000000..9f397d330 --- /dev/null +++ b/platypush/message/response/trello.py @@ -0,0 +1,185 @@ +import datetime + +from typing import Optional, List, Union + +from platypush.message import Mapping +from platypush.message.response import Response + + +class TrelloResponse(Response): + pass + + +class TrelloList(Mapping): + def __init__(self, + id: str, + name: str, + closed: bool, + subscribed: bool, + *args, **kwargs): + super().__init__(id=id, name=name, closed=closed, subscribed=subscribed, *args, **kwargs) + + +class TrelloBoard(Mapping): + def __init__(self, + id: str, + name: str, + url: str, + closed: bool, + lists: Optional[List[TrelloList]] = None, + description: Optional[str] = None, + date_last_activity: Optional[datetime.datetime] = None, + *args, **kwargs): + super().__init__(id=id, name=name, url=url, closed=closed, description=description, lists=lists, + date_last_activity=date_last_activity, *args, **kwargs) + + +class TrelloBoardResponse(TrelloResponse): + def __init__(self, board: TrelloBoard, **kwargs): + super().__init__(output=board, **kwargs) + + +class TrelloBoardsResponse(TrelloResponse): + def __init__(self, boards: List[TrelloBoard], **kwargs): + super().__init__(output=boards, **kwargs) + + +class TrelloListsResponse(TrelloResponse): + def __init__(self, lists: List[TrelloList], **kwargs): + super().__init__(output=lists, **kwargs) + + +class TrelloLabel(Mapping): + def __init__(self, + id: str, + name: str, + color: Optional[str] = None, + *args, **kwargs): + super().__init__(id=id, name=name, color=color, *args, **kwargs) + + +class TrelloUser(Mapping): + def __init__(self, + id: str, + username: str, + fullname: str, + initials: Optional[str] = None, + avatar_url: Optional[str] = None, + *args, **kwargs): + super().__init__(id=id, username=username, fullname=fullname, initials=initials, + avatar_url=avatar_url, *args, **kwargs) + + +class TrelloComment(Mapping): + # noinspection PyShadowingBuiltins + def __init__(self, + id: str, + text: str, + type: str, + creator: TrelloUser, + date: Union[str, datetime.datetime], + *args, **kwargs): + super().__init__(id=id, text=text, type=type, creator=creator, date=date, *args, **kwargs) + + +class TrelloCard(Mapping): + # noinspection PyShadowingBuiltins + def __init__(self, + id: str, + name: str, + url: str, + closed: bool, + board: TrelloBoard, + is_due_complete: bool, + list: Optional[TrelloList] = None, + comments: Optional[List[TrelloComment]] = None, + labels: Optional[List[TrelloLabel]] = None, + description: Optional[str] = None, + due_date: Optional[Union[datetime.datetime, str]] = None, + latest_card_move_date: Optional[Union[datetime.datetime, str]] = None, + date_last_activity: Optional[Union[datetime.datetime, str]] = None, + *args, **kwargs): + super().__init__(id=id, name=name, url=url, closed=closed, board=board, is_due_complete=is_due_complete, + description=description, date_last_activity=date_last_activity, due_date=due_date, list=list, + comments=comments, labels=labels, latest_card_move_date=latest_card_move_date, *args, **kwargs) + + +class TrelloCardResponse(TrelloResponse): + def __init__(self, card: TrelloCard, **kwargs): + super().__init__(output=card, **kwargs) + + +class TrelloCardsResponse(TrelloResponse): + def __init__(self, cards: List[TrelloCard], **kwargs): + super().__init__(output=cards, **kwargs) + + +class TrelloPreview(Mapping): + # noinspection PyShadowingBuiltins + def __init__(self, + id: str, + scaled: bool, + url: str, + bytes: int, + height: int, + width: int, + *args, **kwargs): + super().__init__(id=id, scaled=scaled, url=url, bytes=bytes, height=height, width=width, *args, **kwargs) + + +class TrelloAttachment(Mapping): + # noinspection PyShadowingBuiltins + def __init__(self, + id: str, + bytes: int, + date: str, + edge_color: str, + id_member: str, + is_upload: bool, + name: str, + previews: List[TrelloPreview], + url: str, + mime_type: Optional[str] = None, + *args, **kwargs): + super().__init__(id=id, bytes=bytes, date=date, edge_color=edge_color, id_member=id_member, is_upload=is_upload, + name=name, previews=previews, url=url, mime_type=mime_type, *args, **kwargs) + + +class TrelloChecklistItem(Mapping): + def __init__(self, + id: str, + name: str, + checked: bool, + *args, **kwargs): + super().__init__(id=id, name=name, checked=checked, *args, **kwargs) + + +class TrelloChecklist(Mapping): + def __init__(self, + id: str, + name: str, + checklist_items: List[TrelloChecklistItem], + *args, **kwargs): + super().__init__(id=id, name=name, checklist_items=checklist_items, *args, **kwargs) + + +class TrelloMember(Mapping): + def __init__(self, + id: str, + full_name: str, + bio: Optional[str], + url: Optional[str], + username: Optional[str], + initials: Optional[str], + member_type: Optional[str] = None, + *args, **kwargs): + super().__init__(id=id, full_name=full_name, bio=bio, url=url, username=username, initials=initials, + member_type=member_type, *args, **kwargs) + + +class TrelloMembersResponse(Mapping): + def __init__(self, members: List[TrelloMember], **kwargs): + super().__init__(output=members, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/trello.py b/platypush/plugins/trello.py new file mode 100644 index 000000000..5db5d473d --- /dev/null +++ b/platypush/plugins/trello.py @@ -0,0 +1,908 @@ +import datetime + +from typing import Optional, Dict, List, Union + +# noinspection PyPackageRequirements +import trello +# noinspection PyPackageRequirements +from trello.board import Board, List as List_ +# noinspection PyPackageRequirements +from trello.exceptions import ResourceUnavailable + +from platypush.message.response.trello import TrelloBoard, TrelloBoardsResponse, TrelloCardsResponse, TrelloCard, \ + TrelloAttachment, TrelloPreview, TrelloChecklist, TrelloChecklistItem, TrelloUser, TrelloComment, TrelloLabel, \ + TrelloList, TrelloBoardResponse, TrelloListsResponse, TrelloMembersResponse, TrelloMember, TrelloCardResponse + +from platypush.plugins import Plugin, action + + +class TrelloPlugin(Plugin): + """ + Trello integration. + + Requires: + + * **py-trello** (``pip install py-trello``) + + You'll also need a Trello API key. You can get it `here `. + You'll also need an auth token if you want to view/change private resources. You can generate a permanent token + linked to your account on + https://trello.com/1/connect?key=&name=platypush&response_type=token&expiration=never&scope=read,write + """ + + def __init__(self, api_key: str, api_secret: Optional[str] = None, token: Optional[str] = None, **kwargs): + """ + :param api_key: Trello API key. You can get it `here `. + :param api_secret: Trello API secret. You can get it `here `. + :param token: Trello token. It is required if you want to access or modify private resources. You can get + a permanent token on + https://trello.com/1/connect?key=&name=platypush&response_type=token&expiration=never&scope=read,write + """ + + super().__init__(**kwargs) + self.api_key = api_key + self.api_secret = api_secret + self.token = token + self._client = None + + def _get_client(self) -> trello.TrelloClient: + if not self._client: + self._client = trello.TrelloClient( + api_key=self.api_key, + api_secret=self.api_secret, + token=self.token, + ) + + return self._client + + def _get_board(self, board: str) -> Board: + client = self._get_client() + try: + return client.get_board(board) + except ResourceUnavailable: + boards = [b for b in client.list_boards() if b.name == board] + assert boards, 'No such board: {}'.format(board) + return boards[0] + + # noinspection PyShadowingBuiltins + @action + def get_boards(self, all: bool = False) -> TrelloBoardsResponse: + """ + Get the list of boards. + + :param all: If True, return all the boards included those that have been closed/archived/deleted. Otherwise, + only return open/active boards (default: False). + """ + client = self._get_client() + + return TrelloBoardsResponse([ + TrelloBoard( + id=b.id, + name=b.name, + description=b.description, + url=b.url, + date_last_activity=b.date_last_activity, + closed=b.closed, + ) + for b in client.list_boards(board_filter='all' if all else 'open') + ]) + + @action + def get_board(self, board: str) -> TrelloBoardResponse: + """ + Get the info about a board. + + :param board: Board ID or name + """ + + board = self._get_board(board) + return TrelloBoardResponse( + TrelloBoard( + id=board.id, + name=board.name, + url=board.url, + closed=board.closed, + description=board.description, + date_last_activity=board.date_last_activity, + lists=[ + TrelloList(id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed) + for ll in board.list_lists() + ] + ) + ) + + @action + def open_board(self, board: str): + """ + Re-open/un-archive a board. + + :param board: Board ID or name + """ + board = self._get_board(board) + board.open() + + @action + def close_board(self, board: str): + """ + Close/archive a board. + + :param board: Board ID or name + """ + board = self._get_board(board) + board.close() + + @action + def set_board_name(self, board: str, name: str): + """ + Change the name of a board. + + :param board: Board ID or name. + :param name: New name. + """ + board = self._get_board(board) + board.set_name(name) + return self.get_board(name) + + @action + def set_board_description(self, board: str, description: str): + """ + Change the description of a board. + + :param board: Board ID or name. + :param description: New description. + """ + board = self._get_board(board) + board.set_description(description) + return self.get_board(description) + + @action + def create_label(self, board: str, name: str, color: Optional[str] = None): + """ + Add a label to a board. + + :param board: Board ID or name + :param name: Label name + :param color: Optional HTML color + """ + board = self._get_board(board) + board.add_label(name=name, color=color) + + @action + def delete_label(self, board: str, label: str): + """ + Delete a label from a board. + + :param board: Board ID or name + :param label: Label ID or name + """ + + board = self._get_board(board) + + try: + board.delete_label(label) + except ResourceUnavailable: + labels = [ + ll for ll in board.get_labels() + if ll.name == label + ] + + assert labels, 'No such label: {}'.format(label) + label = labels[0].id + board.delete_label(label) + + @action + def add_member(self, board: str, member_id: str, member_type: str = 'normal'): + """ + Add a member to a board. + + :param board: Board ID or name. + :param member_id: Member ID to add. + :param member_type: Member type - can be 'normal' or 'admin' (default: 'normal'). + """ + board = self._get_board(board) + board.add_member(member_id, member_type=member_type) + + @action + def remove_member(self, board: str, member_id: str): + """ + Remove a member from a board. + + :param board: Board ID or name. + :param member_id: Member ID to remove. + """ + board = self._get_board(board) + board.remove_member(member_id) + + def _get_members(self, board: str, only_admin: bool = False) -> TrelloMembersResponse: + board = self._get_board(board) + members = board.admin_members() if only_admin else board.get_members() + + return TrelloMembersResponse([ + TrelloMember( + id=m.id, + full_name=m.full_name, + bio=m.bio, + url=m.url, + username=m.username, + initials=m.initials, + member_type=getattr(m, 'member_type') if hasattr(m, 'member_type') else None + ) + for m in members + ]) + + @action + def get_members(self, board: str) -> TrelloMembersResponse: + """ + Get the list of all the members of a board. + :param board: Board ID or name. + """ + return self._get_members(board, only_admin=False) + + @action + def get_admin_members(self, board: str) -> TrelloMembersResponse: + """ + Get the list of the admin members of a board. + :param board: Board ID or name. + """ + return self._get_members(board, only_admin=True) + + # noinspection PyShadowingBuiltins + @action + def get_lists(self, board: str, all: bool = False) -> TrelloListsResponse: + """ + Get the list of lists on a board. + + :param board: Board ID or name + :param all: If True, return all the lists, included those that have been closed/archived/deleted. Otherwise, + only return open/active lists (default: False). + """ + + board = self._get_board(board) + return TrelloListsResponse([ + TrelloList(id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed) + for ll in board.list_lists('all' if all else 'open') + ]) + + @action + def add_list(self, board: str, name: str, pos: Optional[int] = None): + """ + Add a list to a board. + + :param board: Board ID or name + :param name: List name + :param pos: Optional position (default: last) + """ + + board = self._get_board(board) + board.add_list(name=name, pos=pos) + + # noinspection PyShadowingBuiltins + def _get_list(self, board: str, list: str) -> List_: + board = self._get_board(board) + + try: + return board.get_list(list) + except ResourceUnavailable: + lists = [ll for ll in board.list_lists() if ll.name == list] + assert lists, 'No such list: {}'.format(list) + return lists[0] + + # noinspection PyShadowingBuiltins + @action + def set_list_name(self, board: str, list: str, name: str): + """ + Change the name of a board list. + + :param board: Board ID or name + :param list: List ID or name + :param name: New name + """ + list = self._get_list(board, list) + list.set_name(name) + + # noinspection PyShadowingBuiltins + @action + def list_subscribe(self, board: str, list: str): + """ + Subscribe to a list. + + :param board: Board ID or name + :param list: List ID or name + """ + list = self._get_list(board, list) + list.subscribe() + + # noinspection PyShadowingBuiltins + @action + def list_unsubscribe(self, board: str, list: str): + """ + Unsubscribe from a list. + + :param board: Board ID or name + :param list: List ID or name + """ + list = self._get_list(board, list) + list.unsubscribe() + + # noinspection PyShadowingBuiltins + @action + def archive_all_cards(self, board: str, list: str): + """ + Archive all the cards on a list. + + :param board: Board ID or name + :param list: List ID or name + """ + list = self._get_list(board, list) + list.archive_all_cards() + + @action + def move_all_cards(self, board: str, src: str, dest: str): + """ + Move all the cards from a list to another. + + :param board: Board ID or name + :param src: Source list + :param dest: Target list + """ + src = self._get_list(board, src) + dest = self._get_list(board, dest) + src.move_all_cards(dest.id) + + # noinspection PyShadowingBuiltins + @action + def open_list(self, board: str, list: str): + """ + Open/un-archive a list. + + :param board: Board ID or name + :param list: List ID or name + """ + list = self._get_list(board, list) + list.open() + + # noinspection PyShadowingBuiltins + @action + def close_list(self, board: str, list: str): + """ + Close/archive a list. + + :param board: Board ID or name + :param list: List ID or name + """ + list = self._get_list(board, list) + list.close() + + # noinspection PyShadowingBuiltins + @action + def move_list(self, board: str, list: str, position: int): + """ + Move a list to another position. + + :param board: Board ID or name + :param list: List ID or name + :param position: New position index + """ + list = self._get_list(board, list) + list.move(position) + + # noinspection PyShadowingBuiltins + @action + def add_card(self, board: str, list: str, name: str, description: Optional[str] = None, + position: Optional[int] = None, labels: Optional[List[str]] = None, + due: Optional[Union[str, datetime.datetime]] = None, source: Optional[str] = None, + assign: Optional[List[str]] = None) -> TrelloCardResponse: + """ + Add a card to a list. + + :param board: Board ID or name + :param list: List ID or name + :param name: Card name + :param description: Card description + :param position: Card position index + :param labels: List of labels + :param due: Due date (``datetime.datetime`` object or ISO-format string) + :param source: Card ID to clone from + :param assign: List of assignee member IDs + """ + list = self._get_list(board, list) + + if labels: + labels = [ + ll for ll in list.board.get_labels() + if ll.id in labels or ll.name in labels + ] + + card = list.add_card(name=name, desc=description, labels=labels, due=due, source=source, position=position, + assign=assign) + + return TrelloCardResponse(TrelloCard(id=card.id, + name=card.name, + url=card.url, + closed=card.closed, + board=TrelloBoard( + id=list.board.id, + name=list.board.name, + url=list.board.url, + closed=list.board.closed, + description=list.board.description, + date_last_activity=list.board.date_last_activity + ), + + is_due_complete=card.is_due_complete, + list=None, + comments=[], + labels=[ + TrelloLabel( + id=lb.id, + name=lb.name, + color=lb.color + ) + for lb in (card.labels or []) + ], + description=card.description, + due_date=card.due_date, + latest_card_move_date=card.latestCardMove_date, + date_last_activity=card.date_last_activity + )) + + @action + def delete_card(self, card_id: str): + """ + Permanently delete a card. + + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.delete() + + @action + def open_card(self, card_id: str): + """ + Open/un-archive a card. + + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.set_closed(False) + + @action + def close_card(self, card_id: str): + """ + Close/archive a card. + + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.set_closed(True) + + @action + def add_checklist(self, card_id: str, title: str, items: List[str], states: Optional[List[bool]] = None): + """ + Add a checklist to a card. + + :param card_id: Card ID + :param title: Checklist title + :param items: List of items in the checklist + :param states: State of each item, True for checked, False for unchecked + """ + client = self._get_client() + card = client.get_card(card_id) + card.add_checklist(title, items, states) + + @action + def add_label(self, card_id: str, label: str): + """ + Add a label to a card. + + :param card_id: Card ID + :param label: Label name + """ + client = self._get_client() + card = client.get_card(card_id) + + labels = [ + ll for ll in card.board.get_labels() + if ll.name == label + ] + + assert labels, 'No such label: {}'.format(label) + label = labels[0] + card.add_label(label) + + @action + def remove_label(self, card_id: str, label: str): + """ + Remove a label from a card. + + :param card_id: Card ID + :param label: Label name + """ + client = self._get_client() + card = client.get_card(card_id) + + labels = [ + ll for ll in card.board.get_labels() + if ll.name == label + ] + + assert labels, 'No such label: {}'.format(label) + label = labels[0] + card.remove_label(label) + + @action + def assign_card(self, card_id: str, member_id: str): + """ + Assign a card. + + :param card_id: Card ID + :param member_id: Member ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.assign(member_id) + + @action + def unassign_card(self, card_id: str, member_id: str): + """ + Un-assign a card. + + :param card_id: Card ID + :param member_id: Member ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.unassign(member_id) + + @action + def attach_card(self, card_id: str, name: Optional[str] = None, mime_type: Optional[str] = None, + file: Optional[str] = None, url: Optional[str] = None): + """ + Add an attachment to a card. It can be either a local file or a remote URL. + + :param card_id: Card ID + :param name: File name + :param mime_type: MIME type + :param file: Path to the file + :param url: URL to the file + """ + client = self._get_client() + card = client.get_card(card_id) + card.attach(name=name, mimeType=mime_type, file=file, url=url) + + @action + def remove_attachment(self, card_id: str, attachment_id: str): + """ + Remove an attachment from a card. + + :param card_id: Card ID + :param attachment_id: Attachment ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.remove_attachment(attachment_id) + + # noinspection PyShadowingBuiltins + @action + def change_card_board(self, card_id: str, board: str, list: str = None): + """ + Move a card to a new board. + + :param card_id: Card ID + :param board: New board ID or name + :param list: Optional target list ID or name + """ + client = self._get_client() + card = client.get_card(card_id) + board = self._get_board(board) + + list_id = None + if list: + list_id = self._get_list(board.id, list).id + + card.change_board(board.id, list_id) + + # noinspection PyShadowingBuiltins + @action + def change_card_list(self, card_id: str, list: str): + """ + Move a card to a new list. + + :param card_id: Card ID + :param list: List ID or name + """ + client = self._get_client() + card = client.get_card(card_id) + list = self._get_list(card.board.id, list) + card.change_list(list.id) + + @action + def change_card_pos(self, card_id: str, position: int): + """ + Move a card to a new position. + + :param card_id: Card ID + :param position: New position index + """ + client = self._get_client() + card = client.get_card(card_id) + card.change_pos(position) + + @action + def comment_card(self, card_id: str, text: str): + """ + Add a comment to a card. + + :param card_id: Card ID + :param text: Comment text + """ + client = self._get_client() + card = client.get_card(card_id) + card.comment(text) + + @action + def update_comment(self, card_id: str, comment_id: str, text: str): + """ + Update the content of a comment. + + :param card_id: Card ID + :param comment_id: Comment ID + :param text: New comment text + """ + client = self._get_client() + card = client.get_card(card_id) + card.update_comment(comment_id, text) + + @action + def delete_comment(self, card_id: str, comment_id: str): + """ + Delete a comment. + + :param card_id: Card ID + :param comment_id: Comment ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.delete_comment(comment_id) + + @action + def set_card_name(self, card_id: str, name: str): + """ + Change the name of a card. + + :param card_id: Card ID + :param name: New name + """ + client = self._get_client() + card = client.get_card(card_id) + card.set_name(name) + + @action + def set_card_description(self, card_id: str, description: str): + """ + Change the description of a card. + + :param card_id: Card ID + :param description: New description + """ + client = self._get_client() + card = client.get_card(card_id) + card.set_description(description) + + @action + def add_card_member(self, card_id: str, member_id: str): + """ + Add a member to a card. + + :param card_id: Card ID + :param member_id: Member ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.add_member(member_id) + + @action + def remove_card_member(self, card_id: str, member_id: str): + """ + Remove a member from a card. + + :param card_id: Card ID + :param member_id: Member ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.remove_member(member_id) + + @action + def set_card_due(self, card_id: str, due: Union[str, datetime.datetime]): + """ + Set the due date for a card. + + :param card_id: Card ID + :param due: Due date, as a datetime.datetime object or an ISO string + """ + client = self._get_client() + card = client.get_card(card_id) + + if isinstance(due, str): + due = datetime.datetime.fromisoformat(due) + + card.set_due(due) + + @action + def remove_card_due(self, card_id: str): + """ + Remove the due date from a card. + + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.remove_due() + + @action + def set_card_due_complete(self, card_id: str): + """ + Set the due date of a card as completed. + + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.set_due_complete() + + @action + def remove_card_due_complete(self, card_id: str): + """ + Remove the due complete flag from a card. + + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.remove_due_complete() + + @action + def card_subscribe(self, card_id: str): + """ + Subscribe to a card. + :param card_id: Card ID + """ + client = self._get_client() + card = client.get_card(card_id) + card.subscribe() + + # noinspection PyShadowingBuiltins + @action + def get_cards(self, board: str, list: Optional[str] = None, all: bool = False) -> TrelloCardsResponse: + """ + Get the list of cards on a board. + + :param board: Board ID or name + :param list: List ID or name. If set then the method will only return the cards found on that list + (default: None) + :param all: If True, return all the cards included those that have been closed/archived/deleted. Otherwise, + only return open/active cards (default: False). + """ + + board = self._get_board(board) + lists: Dict[str, TrelloList] = { + ll.id: TrelloList(id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed) + for ll in board.list_lists() + } + + list_id = None + + if list: + if list in lists: + list_id = list + else: + # noinspection PyUnresolvedReferences + ll = [l1 for l1 in lists.values() if l1.name == list] + assert ll, 'No such list ID/name: {}'.format(list) + # noinspection PyUnresolvedReferences + list_id = ll[0].id + + # noinspection PyUnresolvedReferences + return TrelloCardsResponse([ + TrelloCard( + id=c.id, + name=c.name, + url=c.url, + closed=c.closed, + list=lists.get(c.list_id), + + board=TrelloBoard( + id=c.board.id, + name=c.board.name, + url=c.board.url, + closed=c.board.closed, + description=c.board.description, + date_last_activity=c.board.date_last_activity + ), + + attachments=[ + TrelloAttachment( + id=a.get('id'), + bytes=a.get('bytes'), + date=a.get('date'), + edge_color=a.get('edgeColor'), + id_member=a.get('idMember'), + is_upload=a.get('isUpload'), + name=a.get('name'), + previews=[ + TrelloPreview( + id=p.get('id'), + scaled=p.get('scaled'), + url=p.get('url'), + bytes=p.get('bytes'), + height=p.get('height'), + width=p.get('width') + ) + for p in a.get('previews', []) + ], + url=a.get('url'), + mime_type=a.get('mimeType') + ) + for a in c.attachments + ], + + checklists=[ + TrelloChecklist( + id=ch.id, + name=ch.name, + checklist_items=[ + TrelloChecklistItem( + id=i.get('id'), + name=i.get('name'), + checked=i.get('checked') + ) + for i in ch.items + ] + ) + for ch in c.checklists + ], + + comments=[ + TrelloComment( + id=co.get('id'), + text=co.get('data', {}).get('text'), + type=co.get('type'), + date=co.get('date'), + creator=TrelloUser( + id=co.get('memberCreator', {}).get('id'), + username=co.get('memberCreator', {}).get('username'), + fullname=co.get('memberCreator', {}).get('fullName'), + initials=co.get('memberCreator', {}).get('initials'), + avatar_url=co.get('memberCreator', {}).get('avatarUrl') + ) + ) + for co in c.comments + ], + + labels=[ + TrelloLabel( + id=lb.id, + name=lb.name, + color=lb.color + ) + for lb in (c.labels or []) + ], + + is_due_complete=c.is_due_complete, + due_date=c.due_date, + description=c.description, + latest_card_move_date=c.latestCardMove_date, + date_last_activity=c.date_last_activity + ) + for c in board.all_cards() if ( + (all or not c.closed) and + (not list or c.list_id == list_id) + ) + ]) + + +# vim:sw=4:ts=4:et: diff --git a/requirements.txt b/requirements.txt index 5f90130a1..67df6b809 100644 --- a/requirements.txt +++ b/requirements.txt @@ -190,4 +190,8 @@ croniter # nodered # Support for Todoist integration -# todoist +# todoist-python + +# Support for Trello integration +# py-trello + diff --git a/setup.py b/setup.py index 71f39201a..26f55d618 100755 --- a/setup.py +++ b/setup.py @@ -253,7 +253,9 @@ setup( # Support for Node-RED integration 'nodered': ['pynodered'], # Support for Todoist integration - 'nodered': ['todoist-python'], + 'todoist': ['todoist-python'], + # Support for Trello integration + 'trello': ['py-trello'], }, )