2019-12-27 15:55:56 +01:00
|
|
|
import datetime
|
2023-11-17 02:05:11 +01:00
|
|
|
import json
|
|
|
|
from threading import Event
|
2019-12-27 15:55:56 +01:00
|
|
|
from typing import Optional, Dict, List, Union
|
|
|
|
|
|
|
|
import trello
|
2023-11-17 02:05:11 +01:00
|
|
|
from trello.board import Board, List as List_ # type: ignore
|
|
|
|
from trello.exceptions import ResourceUnavailable # type: ignore
|
|
|
|
from websocket import WebSocketApp
|
|
|
|
|
|
|
|
from platypush.context import get_bus
|
|
|
|
from platypush.message.event.trello import (
|
|
|
|
MoveCardEvent,
|
|
|
|
NewCardEvent,
|
|
|
|
ArchivedCardEvent,
|
|
|
|
UnarchivedCardEvent,
|
|
|
|
)
|
|
|
|
from platypush.plugins import RunnablePlugin, action
|
|
|
|
from platypush.schemas.trello import (
|
|
|
|
TrelloBoardSchema,
|
|
|
|
TrelloCardSchema,
|
|
|
|
TrelloListSchema,
|
|
|
|
TrelloMemberSchema,
|
|
|
|
)
|
2023-09-24 16:54:43 +02:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
from ._model import (
|
2023-09-24 16:54:43 +02:00
|
|
|
TrelloBoard,
|
|
|
|
TrelloCard,
|
|
|
|
TrelloAttachment,
|
|
|
|
TrelloPreview,
|
|
|
|
TrelloChecklist,
|
|
|
|
TrelloChecklistItem,
|
|
|
|
TrelloUser,
|
|
|
|
TrelloComment,
|
|
|
|
TrelloLabel,
|
|
|
|
TrelloList,
|
|
|
|
TrelloMember,
|
|
|
|
)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
class TrelloPlugin(RunnablePlugin):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Trello integration.
|
|
|
|
|
2023-11-20 02:43:18 +01:00
|
|
|
You'll need a Trello API key. You can get it `here
|
|
|
|
<https://trello.com/app-key>`_.
|
2023-11-17 02:05:11 +01:00
|
|
|
|
|
|
|
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=<KEY>&name=platypush&response_type=token&expiration=never&scope=read,write``.
|
|
|
|
|
|
|
|
Also, polling of events requires you to follow a separate procedure to
|
|
|
|
retrieve the Websocket tokens, since Trello uses a different (and
|
|
|
|
undocumented) authorization mechanism:
|
|
|
|
|
|
|
|
1. Open https://trello.com in your browser.
|
2023-11-17 10:27:33 +01:00
|
|
|
2. Open the developer tools (F12).
|
|
|
|
3. Go to the Cookies tab.
|
|
|
|
4. Copy the value of the ``cloud.session.token`` cookie.
|
2023-11-17 02:05:11 +01:00
|
|
|
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
_websocket_url_base = 'wss://trello.com/1/Session/socket?clientVersion=build-194674'
|
|
|
|
|
2023-09-24 16:54:43 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
api_key: str,
|
|
|
|
api_secret: Optional[str] = None,
|
|
|
|
token: Optional[str] = None,
|
2023-11-17 02:05:11 +01:00
|
|
|
cloud_session_token: Optional[str] = None,
|
|
|
|
boards: Optional[List[str]] = None,
|
|
|
|
**kwargs,
|
2023-09-24 16:54:43 +02:00
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
2023-11-20 02:43:18 +01:00
|
|
|
:param api_key: Trello API key. You can get it `here
|
2023-11-20 02:44:35 +01:00
|
|
|
<https://trello.com/app-key>`_.
|
|
|
|
:param api_secret: Trello API secret. You can get it `here
|
|
|
|
<https://trello.com/app-key>`_.
|
2023-11-17 10:27:33 +01:00
|
|
|
:param token: Trello token. It is required if you want to access or
|
|
|
|
modify private resources. You can get a permanent token on
|
2023-11-17 02:05:11 +01:00
|
|
|
``https://trello.com/1/connect?key=<KEY>&name=platypush&response_type=token&expiration=never&scope=read,write``.
|
|
|
|
:param cloud_session_token: Cloud session token. It is required
|
|
|
|
if you want to monitor your boards for changes. See
|
|
|
|
:class:`platypush.plugins.trello.TrelloPlugin` for the procedure to
|
|
|
|
retrieve it.
|
|
|
|
:param boards: List of boards to subscribe, by ID or name. If
|
|
|
|
specified, then the plugin will listen for changes only on these boards.
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self.api_key = api_key
|
|
|
|
self.api_secret = api_secret
|
|
|
|
self.token = token
|
2023-11-17 02:05:11 +01:00
|
|
|
self.cloud_session_token = cloud_session_token
|
2019-12-27 15:55:56 +01:00
|
|
|
self._client = None
|
2023-11-17 02:05:11 +01:00
|
|
|
self._req_id = 0
|
|
|
|
self._boards_by_id = {}
|
|
|
|
self._boards_by_name = {}
|
|
|
|
self._monitored_boards = boards
|
|
|
|
self.url = None
|
|
|
|
|
|
|
|
if token:
|
|
|
|
self.url = self._websocket_url_base
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
self._connected = Event()
|
|
|
|
self._items = {}
|
|
|
|
self._event_handled = False
|
|
|
|
self._ws: Optional[WebSocketApp] = None
|
|
|
|
|
|
|
|
def _get_client(self) -> trello.TrelloClient: # type: ignore
|
2019-12-27 15:55:56 +01:00
|
|
|
if not self._client:
|
2023-11-17 02:05:11 +01:00
|
|
|
self._client = trello.TrelloClient( # type: ignore
|
2019-12-27 15:55:56 +01:00
|
|
|
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]
|
2023-11-17 02:05:11 +01:00
|
|
|
assert boards, f'No such board: {board}'
|
2019-12-27 15:55:56 +01:00
|
|
|
return boards[0]
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
def _get_boards(
|
|
|
|
self, all: bool = False # pylint: disable=redefined-builtin
|
|
|
|
) -> List[Board]:
|
|
|
|
client = self._get_client()
|
|
|
|
return client.list_boards(board_filter='all' if all else 'open')
|
|
|
|
|
2019-12-27 15:55:56 +01:00
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def get_boards(self, all: bool = False): # pylint: disable=redefined-builtin
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Get the list of boards.
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
:param all: If True, return all the boards included those that have
|
|
|
|
been closed/archived/deleted. Otherwise, only return open/active
|
|
|
|
boards (default: False).
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloBoardSchema(many=True)
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
return TrelloBoardSchema().dump(
|
2023-09-24 16:54:43 +02:00
|
|
|
[
|
|
|
|
TrelloBoard(
|
|
|
|
id=b.id,
|
|
|
|
name=b.name,
|
|
|
|
description=b.description,
|
|
|
|
url=b.url,
|
|
|
|
date_last_activity=b.date_last_activity,
|
|
|
|
closed=b.closed,
|
|
|
|
)
|
2023-11-17 02:05:11 +01:00
|
|
|
for b in self._get_boards(all=all)
|
|
|
|
],
|
|
|
|
many=True,
|
2023-09-24 16:54:43 +02:00
|
|
|
)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def get_board(self, board: str):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Get the info about a board.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloBoardSchema
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
b = self._get_board(board)
|
|
|
|
return TrelloBoardSchema().dump(
|
2019-12-27 15:55:56 +01:00
|
|
|
TrelloBoard(
|
2023-11-17 02:05:11 +01:00
|
|
|
id=b.id,
|
|
|
|
name=b.name,
|
|
|
|
url=b.url,
|
|
|
|
closed=b.closed,
|
|
|
|
description=b.description,
|
|
|
|
date_last_activity=b.date_last_activity,
|
2019-12-27 15:55:56 +01:00
|
|
|
lists=[
|
2023-09-24 16:54:43 +02:00
|
|
|
TrelloList(
|
|
|
|
id=ll.id,
|
|
|
|
name=ll.name,
|
|
|
|
closed=ll.closed,
|
|
|
|
subscribed=ll.subscribed,
|
|
|
|
)
|
2023-11-17 02:05:11 +01:00
|
|
|
for ll in b.list_lists()
|
2023-09-24 16:54:43 +02:00
|
|
|
],
|
2019-12-27 15:55:56 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
@action
|
|
|
|
def open_board(self, board: str):
|
|
|
|
"""
|
|
|
|
Re-open/un-archive a board.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).open()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def close_board(self, board: str):
|
|
|
|
"""
|
|
|
|
Close/archive a board.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).close()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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.
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).set_name(name)
|
2019-12-27 15:55:56 +01:00
|
|
|
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.
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).set_description(description)
|
2019-12-27 15:55:56 +01:00
|
|
|
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
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).add_label(name=name, color=color)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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
|
|
|
|
"""
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
b = self._get_board(board)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
try:
|
2023-11-17 02:05:11 +01:00
|
|
|
b.delete_label(label)
|
2019-12-27 15:55:56 +01:00
|
|
|
except ResourceUnavailable:
|
2023-11-17 02:05:11 +01:00
|
|
|
label_ = next(iter(ll for ll in b.get_labels() if ll.name == label), None)
|
|
|
|
assert label_, f'No such label: {label}'
|
|
|
|
label = label_.id
|
|
|
|
b.delete_label(label)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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').
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).add_member(member_id, member_type=member_type)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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.
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).remove_member(member_id)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
def _get_members(self, board: str, only_admin: bool = False):
|
|
|
|
b = self._get_board(board)
|
|
|
|
members = b.admin_members() if only_admin else b.get_members()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
return TrelloMemberSchema().dump(
|
2023-09-24 16:54:43 +02:00
|
|
|
[
|
|
|
|
TrelloMember(
|
|
|
|
id=m.id,
|
2023-11-17 02:05:11 +01:00
|
|
|
fullname=m.full_name,
|
2023-09-24 16:54:43 +02:00
|
|
|
bio=m.bio,
|
|
|
|
url=m.url,
|
|
|
|
username=m.username,
|
|
|
|
initials=m.initials,
|
|
|
|
member_type=getattr(m, 'member_type', None),
|
|
|
|
)
|
|
|
|
for m in members
|
2023-11-17 02:05:11 +01:00
|
|
|
],
|
|
|
|
many=True,
|
2023-09-24 16:54:43 +02:00
|
|
|
)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def get_members(self, board: str):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Get the list of all the members of a board.
|
2023-11-17 22:30:30 +01:00
|
|
|
|
2019-12-27 15:55:56 +01:00
|
|
|
:param board: Board ID or name.
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloMemberSchema(many=True)
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
return self._get_members(board, only_admin=False)
|
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def get_admin_members(self, board: str):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Get the list of the admin members of a board.
|
2023-11-17 22:30:30 +01:00
|
|
|
|
2019-12-27 15:55:56 +01:00
|
|
|
:param board: Board ID or name.
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloMemberSchema(many=True)
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
return self._get_members(board, only_admin=True)
|
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def get_lists(
|
|
|
|
self, board: str, all: bool = False # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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).
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloListSchema(many=True)
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
return TrelloListSchema().dump(
|
2023-09-24 16:54:43 +02:00
|
|
|
[
|
|
|
|
TrelloList(
|
|
|
|
id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed
|
|
|
|
)
|
2023-11-17 02:05:11 +01:00
|
|
|
for ll in self._get_board(board).list_lists('all' if all else 'open')
|
|
|
|
],
|
|
|
|
many=True,
|
2023-09-24 16:54:43 +02:00
|
|
|
)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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)
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_board(board).add_list(name=name, pos=pos)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
def _get_list(
|
|
|
|
self, board: str, list: str # pylint: disable=redefined-builtin
|
|
|
|
) -> List_:
|
|
|
|
b = self._get_board(board)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
try:
|
2023-11-17 02:05:11 +01:00
|
|
|
return b.get_list(list)
|
2019-12-27 15:55:56 +01:00
|
|
|
except ResourceUnavailable:
|
2023-11-17 02:05:11 +01:00
|
|
|
lists = [ll for ll in b.list_lists() if ll.name == list]
|
|
|
|
assert lists, f'No such list: {list}'
|
2019-12-27 15:55:56 +01:00
|
|
|
return lists[0]
|
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def set_list_name(
|
|
|
|
self, board: str, list: str, name: str # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Change the name of a board list.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
:param name: New name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).set_name(name)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def list_subscribe(
|
|
|
|
self, board: str, list: str # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Subscribe to a list.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).subscribe()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def list_unsubscribe(
|
|
|
|
self, board: str, list: str # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Unsubscribe from a list.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).unsubscribe()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def archive_all_cards(
|
|
|
|
self, board: str, list: str # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Archive all the cards on a list.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).archive_all_cards()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
src_list = self._get_list(board, src)
|
|
|
|
dest_list = self._get_list(board, dest)
|
|
|
|
src_list.move_all_cards(dest_list.id)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def open_list(self, board: str, list: str): # pylint: disable=redefined-builtin
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Open/un-archive a list.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).open()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def close_list(self, board: str, list: str): # pylint: disable=redefined-builtin
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Close/archive a list.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).close()
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def move_list(
|
|
|
|
self, board: str, list: str, position: int # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
Move a list to another position.
|
|
|
|
|
|
|
|
:param board: Board ID or name
|
|
|
|
:param list: List ID or name
|
|
|
|
:param position: New position index
|
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
self._get_list(board, list).move(position)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-09-24 16:54:43 +02:00
|
|
|
def add_card(
|
|
|
|
self,
|
|
|
|
board: str,
|
2023-11-17 02:05:11 +01:00
|
|
|
list: str, # pylint: disable=redefined-builtin
|
2023-09-24 16:54:43 +02:00
|
|
|
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,
|
2023-11-17 02:05:11 +01:00
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloCardSchema
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
2023-11-17 02:05:11 +01:00
|
|
|
list_ = self._get_list(board, list)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
if labels:
|
|
|
|
labels = [
|
2023-09-24 16:54:43 +02:00
|
|
|
ll
|
2023-11-17 02:05:11 +01:00
|
|
|
for ll in list_.board.get_labels()
|
2019-12-27 15:55:56 +01:00
|
|
|
if ll.id in labels or ll.name in labels
|
|
|
|
]
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
card = list_.add_card(
|
2023-09-24 16:54:43 +02:00
|
|
|
name=name,
|
|
|
|
desc=description,
|
|
|
|
labels=labels,
|
|
|
|
due=due,
|
|
|
|
source=source,
|
|
|
|
position=position,
|
|
|
|
assign=assign,
|
|
|
|
)
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
return TrelloCardSchema().dump(
|
2023-09-24 16:54:43 +02:00
|
|
|
TrelloCard(
|
|
|
|
id=card.id,
|
|
|
|
name=card.name,
|
|
|
|
url=card.url,
|
|
|
|
closed=card.closed,
|
|
|
|
board=TrelloBoard(
|
2023-11-17 02:05:11 +01:00
|
|
|
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,
|
2023-09-24 16:54:43 +02:00
|
|
|
),
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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
|
2023-09-24 16:54:43 +02:00
|
|
|
def add_checklist(
|
|
|
|
self,
|
|
|
|
card_id: str,
|
|
|
|
title: str,
|
|
|
|
items: List[str],
|
|
|
|
states: Optional[List[bool]] = None,
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
2023-09-24 16:54:43 +02:00
|
|
|
labels = [ll for ll in card.board.get_labels() if ll.name == label]
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
assert labels, f'No such label: {label}'
|
2019-12-27 15:55:56 +01:00
|
|
|
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)
|
|
|
|
|
2023-09-24 16:54:43 +02:00
|
|
|
labels = [ll for ll in card.board.get_labels() if ll.name == label]
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
assert labels, f'No such label: {label}'
|
2019-12-27 15:55:56 +01:00
|
|
|
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
|
2023-09-24 16:54:43 +02:00
|
|
|
def attach_card(
|
|
|
|
self,
|
|
|
|
card_id: str,
|
|
|
|
name: Optional[str] = None,
|
|
|
|
mime_type: Optional[str] = None,
|
|
|
|
file: Optional[str] = None,
|
|
|
|
url: Optional[str] = None,
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def change_card_board(
|
|
|
|
self,
|
|
|
|
card_id: str,
|
|
|
|
board: str,
|
|
|
|
list: Optional[str] = None, # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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)
|
2023-11-17 02:05:11 +01:00
|
|
|
board_id = self._get_board(board).id
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
list_id = None
|
|
|
|
if list:
|
2023-11-17 02:05:11 +01:00
|
|
|
list_id = self._get_list(board_id, list).id
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
card.change_board(board_id, list_id)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-17 02:05:11 +01:00
|
|
|
def change_card_list(
|
|
|
|
self, card_id: str, list: str # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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)
|
2023-11-17 02:05:11 +01:00
|
|
|
card.change_list(self._get_list(card.board.id, list).id)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
@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()
|
|
|
|
|
|
|
|
@action
|
2023-09-24 16:54:43 +02:00
|
|
|
def get_cards(
|
2023-11-17 02:05:11 +01:00
|
|
|
self,
|
|
|
|
board: str,
|
|
|
|
list: Optional[str] = None, # pylint: disable=redefined-builtin
|
|
|
|
all: bool = False, # pylint: disable=redefined-builtin
|
|
|
|
):
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
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).
|
2023-11-17 22:30:30 +01:00
|
|
|
:return: .. schema:: trello.TrelloCardSchema(many=True)
|
2019-12-27 15:55:56 +01:00
|
|
|
"""
|
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
b = self._get_board(board)
|
2019-12-27 15:55:56 +01:00
|
|
|
lists: Dict[str, TrelloList] = {
|
2023-09-24 16:54:43 +02:00
|
|
|
ll.id: TrelloList(
|
|
|
|
id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed
|
|
|
|
)
|
2023-11-17 02:05:11 +01:00
|
|
|
for ll in b.list_lists()
|
2019-12-27 15:55:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
list_id = None
|
|
|
|
|
|
|
|
if list:
|
|
|
|
if list in lists:
|
|
|
|
list_id = list
|
|
|
|
else:
|
2023-11-17 02:05:11 +01:00
|
|
|
ll = next(
|
|
|
|
iter(
|
|
|
|
l1 for l1 in lists.values() if l1.name == list # type: ignore
|
|
|
|
),
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
assert ll, f'No such list ID/name: {list}'
|
|
|
|
list_id = ll.id # type: ignore
|
|
|
|
|
|
|
|
return TrelloCardSchema().dump(
|
2023-09-24 16:54:43 +02:00
|
|
|
[
|
|
|
|
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'),
|
2023-11-17 02:05:11 +01:00
|
|
|
url=a.get('url'),
|
|
|
|
size=a.get('bytes'),
|
2023-09-24 16:54:43 +02:00
|
|
|
date=a.get('date'),
|
|
|
|
edge_color=a.get('edgeColor'),
|
2023-11-17 02:05:11 +01:00
|
|
|
member_id=a.get('idMember'),
|
2023-09-24 16:54:43 +02:00
|
|
|
is_upload=a.get('isUpload'),
|
|
|
|
name=a.get('name'),
|
|
|
|
previews=[
|
|
|
|
TrelloPreview(
|
|
|
|
id=p.get('id'),
|
|
|
|
scaled=p.get('scaled'),
|
|
|
|
url=p.get('url'),
|
2023-11-17 02:05:11 +01:00
|
|
|
size=p.get('bytes'),
|
2023-09-24 16:54:43 +02:00
|
|
|
height=p.get('height'),
|
|
|
|
width=p.get('width'),
|
|
|
|
)
|
|
|
|
for p in a.get('previews', [])
|
|
|
|
],
|
|
|
|
mime_type=a.get('mimeType'),
|
2019-12-27 15:55:56 +01:00
|
|
|
)
|
2023-09-24 16:54:43 +02:00
|
|
|
for a in c.attachments
|
|
|
|
],
|
|
|
|
checklists=[
|
|
|
|
TrelloChecklist(
|
|
|
|
id=ch.id,
|
|
|
|
name=ch.name,
|
2023-11-17 02:05:11 +01:00
|
|
|
items=[
|
2023-09-24 16:54:43 +02:00
|
|
|
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,
|
|
|
|
)
|
2023-11-17 02:05:11 +01:00
|
|
|
for c in b.all_cards()
|
2023-09-24 16:54:43 +02:00
|
|
|
if ((all or not c.closed) and (not list or c.list_id == list_id))
|
2023-11-17 02:05:11 +01:00
|
|
|
],
|
|
|
|
many=True,
|
2023-09-24 16:54:43 +02:00
|
|
|
)
|
2019-12-27 15:55:56 +01:00
|
|
|
|
2023-11-17 02:05:11 +01:00
|
|
|
def _initialize_connection(self, ws: WebSocketApp):
|
|
|
|
boards = [
|
|
|
|
b
|
|
|
|
for b in (self._get_boards() or [])
|
|
|
|
if not self._monitored_boards
|
|
|
|
or b.id in self._monitored_boards
|
|
|
|
or b.name in self._monitored_boards
|
|
|
|
]
|
|
|
|
|
|
|
|
for b in boards:
|
|
|
|
self._boards_by_id[b.id] = b
|
|
|
|
self._boards_by_name[b.name] = b
|
|
|
|
|
|
|
|
for board_id in self._boards_by_id.keys():
|
|
|
|
self._send(
|
|
|
|
ws,
|
|
|
|
{
|
|
|
|
'type': 'subscribe',
|
|
|
|
'modelType': 'Board',
|
|
|
|
'idModel': board_id,
|
|
|
|
'tags': ['clientActions', 'updates'],
|
|
|
|
'invitationTokens': [],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
self.logger.info('Trello boards subscribed')
|
|
|
|
|
|
|
|
def _on_msg(self, *args): # pylint: disable=too-many-return-statements
|
|
|
|
if len(args) < 2:
|
|
|
|
self.logger.warning(
|
|
|
|
'Missing websocket argument - make sure that you are using '
|
|
|
|
'a version of websocket-client < 0.53.0 or >= 0.58.0'
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
ws, msg = args[:2]
|
|
|
|
if not msg:
|
|
|
|
# Reply back with an empty message when the server sends an empty message
|
|
|
|
ws.send('')
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
msg = json.loads(msg)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.warning(
|
|
|
|
'Received invalid JSON message from Trello: %s: %s', msg, e
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
if 'error' in msg:
|
|
|
|
self.logger.warning('Trello error: %s', msg['error'])
|
|
|
|
return
|
|
|
|
|
|
|
|
if msg.get('reqid') == 0:
|
|
|
|
self.logger.debug('Ping response received, subscribing boards')
|
|
|
|
self._initialize_connection(ws)
|
|
|
|
return
|
|
|
|
|
|
|
|
notify = msg.get('notify')
|
|
|
|
if not notify:
|
|
|
|
return
|
|
|
|
|
|
|
|
if notify['event'] != 'updateModels' or notify['typeName'] != 'Action':
|
|
|
|
return
|
|
|
|
|
|
|
|
for delta in notify['deltas']:
|
|
|
|
args = {
|
|
|
|
'card_id': delta['data']['card']['id'],
|
|
|
|
'card_name': delta['data']['card']['name'],
|
|
|
|
'list_id': (
|
|
|
|
delta['data'].get('list') or delta['data'].get('listAfter', {})
|
|
|
|
).get('id'),
|
|
|
|
'list_name': (
|
|
|
|
delta['data'].get('list') or delta['data'].get('listAfter', {})
|
|
|
|
).get('name'),
|
|
|
|
'board_id': delta['data']['board']['id'],
|
|
|
|
'board_name': delta['data']['board']['name'],
|
|
|
|
'closed': delta.get('closed'),
|
|
|
|
'member_id': delta['memberCreator']['id'],
|
|
|
|
'member_username': delta['memberCreator']['username'],
|
|
|
|
'member_fullname': delta['memberCreator']['fullName'],
|
|
|
|
'date': delta['date'],
|
|
|
|
}
|
|
|
|
|
|
|
|
if delta.get('type') == 'createCard':
|
|
|
|
get_bus().post(NewCardEvent(**args))
|
|
|
|
elif delta.get('type') == 'updateCard':
|
|
|
|
if 'listBefore' in delta['data']:
|
|
|
|
args.update(
|
|
|
|
{
|
|
|
|
'old_list_id': delta['data']['listBefore']['id'],
|
|
|
|
'old_list_name': delta['data']['listBefore']['name'],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
get_bus().post(MoveCardEvent(**args))
|
|
|
|
elif 'closed' in delta['data'].get('old', {}):
|
|
|
|
cls = (
|
|
|
|
UnarchivedCardEvent
|
|
|
|
if delta['data']['old']['closed']
|
|
|
|
else ArchivedCardEvent
|
|
|
|
)
|
|
|
|
get_bus().post(cls(**args))
|
|
|
|
|
|
|
|
def _on_error(self, *args):
|
|
|
|
error = args[1] if len(args) > 1 else args[0]
|
|
|
|
self.logger.warning('Trello websocket error: %s', error)
|
|
|
|
|
|
|
|
def _on_close(self, *_):
|
|
|
|
self.logger.warning('Trello websocket connection closed')
|
|
|
|
self._connected.clear()
|
|
|
|
self._req_id = 0
|
|
|
|
|
|
|
|
while not self.should_stop():
|
|
|
|
try:
|
|
|
|
self._connect(reconnect=True)
|
|
|
|
self._connected.wait(timeout=10)
|
|
|
|
break
|
|
|
|
except TimeoutError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
def _on_open(self, *args):
|
|
|
|
ws = args[0] if args else None
|
|
|
|
self._connected.set()
|
|
|
|
if ws:
|
|
|
|
self._send(ws, {'type': 'ping'})
|
|
|
|
self.logger.info('Trello websocket connected')
|
|
|
|
|
|
|
|
def _send(self, ws: WebSocketApp, msg: dict):
|
|
|
|
msg['reqid'] = self._req_id
|
|
|
|
ws.send(json.dumps(msg))
|
|
|
|
self._req_id += 1
|
|
|
|
|
|
|
|
def _connect(self, reconnect: bool = False) -> WebSocketApp:
|
|
|
|
assert self.url, 'Trello websocket URL not set'
|
|
|
|
if reconnect:
|
|
|
|
self.stop()
|
|
|
|
|
|
|
|
if not self._ws:
|
|
|
|
self._ws = WebSocketApp(
|
|
|
|
self.url,
|
|
|
|
header={
|
|
|
|
'Cookie': (
|
|
|
|
f'token={self.token}; '
|
|
|
|
f'cloud.session.token={self.cloud_session_token}'
|
|
|
|
)
|
|
|
|
},
|
|
|
|
on_open=self._on_open,
|
|
|
|
on_message=self._on_msg,
|
|
|
|
on_error=self._on_error,
|
|
|
|
on_close=self._on_close,
|
|
|
|
)
|
|
|
|
|
|
|
|
return self._ws
|
|
|
|
|
|
|
|
def main(self):
|
|
|
|
if not self.url:
|
|
|
|
self.logger.info(
|
|
|
|
"token/cloud_session_token not set: your Trello boards won't be monitored for changes"
|
|
|
|
)
|
|
|
|
self.wait_stop()
|
|
|
|
else:
|
|
|
|
ws = self._connect()
|
|
|
|
ws.run_forever()
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
if self._ws:
|
|
|
|
self._ws.close()
|
|
|
|
self._ws = None
|
|
|
|
|
2019-12-27 15:55:56 +01:00
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|