trelloctl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trelloctl/__init__.py +5 -0
- trelloctl/cli.py +91 -0
- trelloctl/client.py +179 -0
- trelloctl/commands/__init__.py +1 -0
- trelloctl/commands/auth/__init__.py +91 -0
- trelloctl/commands/board/__init__.py +209 -0
- trelloctl/commands/card/__init__.py +280 -0
- trelloctl/commands/list/__init__.py +177 -0
- trelloctl/config.py +59 -0
- trelloctl/output.py +125 -0
- trelloctl/resolver.py +148 -0
- trelloctl-0.1.0.dist-info/METADATA +198 -0
- trelloctl-0.1.0.dist-info/RECORD +15 -0
- trelloctl-0.1.0.dist-info/WHEEL +4 -0
- trelloctl-0.1.0.dist-info/entry_points.txt +3 -0
trelloctl/__init__.py
ADDED
trelloctl/cli.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Main CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from trelloctl import __version__
|
|
11
|
+
from trelloctl.client import TrelloClient
|
|
12
|
+
from trelloctl.config import Config
|
|
13
|
+
from trelloctl.output import OutputFormat, print_error
|
|
14
|
+
from trelloctl.resolver import Resolver
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Context:
|
|
18
|
+
"""CLI context object holding shared state."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.profile = os.environ.get("TRELLOCTL_PROFILE", "default")
|
|
22
|
+
self.config = Config(self.profile)
|
|
23
|
+
self.client: TrelloClient | None = None
|
|
24
|
+
self._resolver: Resolver | None = None
|
|
25
|
+
self.format = OutputFormat.TABLE
|
|
26
|
+
|
|
27
|
+
def ensure_client(self) -> TrelloClient:
|
|
28
|
+
"""Ensure we have an authenticated client."""
|
|
29
|
+
if self.client is None:
|
|
30
|
+
api_key = self.config.get_api_key()
|
|
31
|
+
token = self.config.get_token()
|
|
32
|
+
|
|
33
|
+
if not api_key or not token:
|
|
34
|
+
print_error("Not authenticated. Run 'trelloctl auth login' first.")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
self.client = TrelloClient(api_key, token)
|
|
38
|
+
|
|
39
|
+
return self.client
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def resolver(self) -> Resolver:
|
|
43
|
+
"""Get the name resolver."""
|
|
44
|
+
if self._resolver is None:
|
|
45
|
+
self._resolver = Resolver(self.ensure_client())
|
|
46
|
+
return self._resolver
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
pass_context = click.make_pass_decorator(Context, ensure=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.group()
|
|
53
|
+
@click.version_option(version=__version__, prog_name="trelloctl")
|
|
54
|
+
@click.option(
|
|
55
|
+
"--format",
|
|
56
|
+
"-f",
|
|
57
|
+
type=click.Choice(["table", "json", "csv", "plain"]),
|
|
58
|
+
default="table",
|
|
59
|
+
help="Output format",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--profile",
|
|
63
|
+
"-p",
|
|
64
|
+
envvar="TRELLOCTL_PROFILE",
|
|
65
|
+
default="default",
|
|
66
|
+
help="Configuration profile to use",
|
|
67
|
+
)
|
|
68
|
+
@pass_context
|
|
69
|
+
def main(ctx: Context, format: str, profile: str) -> None:
|
|
70
|
+
"""trelloctl - Manage your Trello boards from the command line."""
|
|
71
|
+
ctx.format = OutputFormat(format)
|
|
72
|
+
ctx.profile = profile
|
|
73
|
+
ctx.config = Config(profile)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _register_commands() -> None:
|
|
77
|
+
"""Register command groups with the main CLI."""
|
|
78
|
+
from trelloctl.commands import auth, board, card
|
|
79
|
+
from trelloctl.commands import list as list_cmd
|
|
80
|
+
|
|
81
|
+
main.add_command(auth.auth)
|
|
82
|
+
main.add_command(board.board)
|
|
83
|
+
main.add_command(card.card)
|
|
84
|
+
main.add_command(list_cmd.list_)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_register_commands()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
trelloctl/client.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Trello API client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
BASE_URL = "https://api.trello.com/1"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TrelloClient:
|
|
11
|
+
"""HTTP client for Trello API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: str, token: str) -> None:
|
|
14
|
+
self.api_key = api_key
|
|
15
|
+
self.token = token
|
|
16
|
+
self._client = httpx.Client(timeout=30.0)
|
|
17
|
+
|
|
18
|
+
def _auth_params(self) -> dict[str, str]:
|
|
19
|
+
"""Return authentication parameters."""
|
|
20
|
+
return {"key": self.api_key, "token": self.token}
|
|
21
|
+
|
|
22
|
+
def _request(
|
|
23
|
+
self,
|
|
24
|
+
method: str,
|
|
25
|
+
path: str,
|
|
26
|
+
params: dict | None = None,
|
|
27
|
+
json: dict | None = None,
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Make an authenticated request to Trello API."""
|
|
30
|
+
url = f"{BASE_URL}{path}"
|
|
31
|
+
all_params = self._auth_params()
|
|
32
|
+
if params:
|
|
33
|
+
all_params.update(params)
|
|
34
|
+
|
|
35
|
+
response = self._client.request(method, url, params=all_params, json=json)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
|
|
38
|
+
if response.status_code == 204:
|
|
39
|
+
return None
|
|
40
|
+
return response.json()
|
|
41
|
+
|
|
42
|
+
def get(self, path: str, params: dict | None = None) -> Any:
|
|
43
|
+
"""Make a GET request."""
|
|
44
|
+
return self._request("GET", path, params=params)
|
|
45
|
+
|
|
46
|
+
def post(
|
|
47
|
+
self, path: str, params: dict | None = None, json: dict | None = None
|
|
48
|
+
) -> Any:
|
|
49
|
+
"""Make a POST request."""
|
|
50
|
+
return self._request("POST", path, params=params, json=json)
|
|
51
|
+
|
|
52
|
+
def put(
|
|
53
|
+
self, path: str, params: dict | None = None, json: dict | None = None
|
|
54
|
+
) -> Any:
|
|
55
|
+
"""Make a PUT request."""
|
|
56
|
+
return self._request("PUT", path, params=params, json=json)
|
|
57
|
+
|
|
58
|
+
def delete(self, path: str, params: dict | None = None) -> Any:
|
|
59
|
+
"""Make a DELETE request."""
|
|
60
|
+
return self._request("DELETE", path, params=params)
|
|
61
|
+
|
|
62
|
+
# Board methods
|
|
63
|
+
def get_boards(self, filter: str = "open") -> list[dict]:
|
|
64
|
+
"""Get all boards for the authenticated user."""
|
|
65
|
+
return self.get("/members/me/boards", params={"filter": filter})
|
|
66
|
+
|
|
67
|
+
def get_board(self, board_id: str) -> dict:
|
|
68
|
+
"""Get a board by ID."""
|
|
69
|
+
return self.get(f"/boards/{board_id}")
|
|
70
|
+
|
|
71
|
+
def create_board(self, name: str, desc: str = "") -> dict:
|
|
72
|
+
"""Create a new board."""
|
|
73
|
+
return self.post("/boards", params={"name": name, "desc": desc})
|
|
74
|
+
|
|
75
|
+
def close_board(self, board_id: str, closed: bool = True) -> dict:
|
|
76
|
+
"""Close or reopen a board."""
|
|
77
|
+
return self.put(f"/boards/{board_id}", params={"closed": str(closed).lower()})
|
|
78
|
+
|
|
79
|
+
def delete_board(self, board_id: str) -> None:
|
|
80
|
+
"""Delete a board."""
|
|
81
|
+
self.delete(f"/boards/{board_id}")
|
|
82
|
+
|
|
83
|
+
def get_board_lists(self, board_id: str, filter: str = "open") -> list[dict]:
|
|
84
|
+
"""Get all lists on a board."""
|
|
85
|
+
return self.get(f"/boards/{board_id}/lists", params={"filter": filter})
|
|
86
|
+
|
|
87
|
+
def get_board_labels(self, board_id: str) -> list[dict]:
|
|
88
|
+
"""Get all labels on a board."""
|
|
89
|
+
return self.get(f"/boards/{board_id}/labels")
|
|
90
|
+
|
|
91
|
+
def get_board_members(self, board_id: str) -> list[dict]:
|
|
92
|
+
"""Get all members of a board."""
|
|
93
|
+
return self.get(f"/boards/{board_id}/members")
|
|
94
|
+
|
|
95
|
+
# List methods
|
|
96
|
+
def get_list(self, list_id: str) -> dict:
|
|
97
|
+
"""Get a list by ID."""
|
|
98
|
+
return self.get(f"/lists/{list_id}")
|
|
99
|
+
|
|
100
|
+
def create_list(self, board_id: str, name: str, pos: str = "bottom") -> dict:
|
|
101
|
+
"""Create a new list on a board."""
|
|
102
|
+
return self.post(
|
|
103
|
+
"/lists", params={"idBoard": board_id, "name": name, "pos": pos}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def archive_list(self, list_id: str, closed: bool = True) -> dict:
|
|
107
|
+
"""Archive or unarchive a list."""
|
|
108
|
+
return self.put(f"/lists/{list_id}", params={"closed": str(closed).lower()})
|
|
109
|
+
|
|
110
|
+
def get_list_cards(self, list_id: str) -> list[dict]:
|
|
111
|
+
"""Get all cards in a list."""
|
|
112
|
+
return self.get(f"/lists/{list_id}/cards")
|
|
113
|
+
|
|
114
|
+
# Card methods
|
|
115
|
+
def get_card(self, card_id: str) -> dict:
|
|
116
|
+
"""Get a card by ID."""
|
|
117
|
+
return self.get(f"/cards/{card_id}")
|
|
118
|
+
|
|
119
|
+
def create_card(
|
|
120
|
+
self,
|
|
121
|
+
list_id: str,
|
|
122
|
+
name: str,
|
|
123
|
+
desc: str = "",
|
|
124
|
+
pos: str = "bottom",
|
|
125
|
+
due: str | None = None,
|
|
126
|
+
labels: list[str] | None = None,
|
|
127
|
+
members: list[str] | None = None,
|
|
128
|
+
) -> dict:
|
|
129
|
+
"""Create a new card."""
|
|
130
|
+
params: dict[str, Any] = {
|
|
131
|
+
"idList": list_id,
|
|
132
|
+
"name": name,
|
|
133
|
+
"desc": desc,
|
|
134
|
+
"pos": pos,
|
|
135
|
+
}
|
|
136
|
+
if due:
|
|
137
|
+
params["due"] = due
|
|
138
|
+
if labels:
|
|
139
|
+
params["idLabels"] = ",".join(labels)
|
|
140
|
+
if members:
|
|
141
|
+
params["idMembers"] = ",".join(members)
|
|
142
|
+
return self.post("/cards", params=params)
|
|
143
|
+
|
|
144
|
+
def update_card(self, card_id: str, **kwargs: Any) -> dict:
|
|
145
|
+
"""Update a card."""
|
|
146
|
+
return self.put(f"/cards/{card_id}", params=kwargs)
|
|
147
|
+
|
|
148
|
+
def move_card(self, card_id: str, list_id: str, pos: str = "bottom") -> dict:
|
|
149
|
+
"""Move a card to a different list."""
|
|
150
|
+
return self.put(f"/cards/{card_id}", params={"idList": list_id, "pos": pos})
|
|
151
|
+
|
|
152
|
+
def archive_card(self, card_id: str, closed: bool = True) -> dict:
|
|
153
|
+
"""Archive or unarchive a card."""
|
|
154
|
+
return self.put(f"/cards/{card_id}", params={"closed": str(closed).lower()})
|
|
155
|
+
|
|
156
|
+
def delete_card(self, card_id: str) -> None:
|
|
157
|
+
"""Delete a card."""
|
|
158
|
+
self.delete(f"/cards/{card_id}")
|
|
159
|
+
|
|
160
|
+
def add_card_member(self, card_id: str, member_id: str) -> list[dict]:
|
|
161
|
+
"""Add a member to a card."""
|
|
162
|
+
return self.post(f"/cards/{card_id}/idMembers", params={"value": member_id})
|
|
163
|
+
|
|
164
|
+
def remove_card_member(self, card_id: str, member_id: str) -> list[dict]:
|
|
165
|
+
"""Remove a member from a card."""
|
|
166
|
+
return self.delete(f"/cards/{card_id}/idMembers/{member_id}")
|
|
167
|
+
|
|
168
|
+
def add_card_comment(self, card_id: str, text: str) -> dict:
|
|
169
|
+
"""Add a comment to a card."""
|
|
170
|
+
return self.post(f"/cards/{card_id}/actions/comments", params={"text": text})
|
|
171
|
+
|
|
172
|
+
def get_card_comments(self, card_id: str) -> list[dict]:
|
|
173
|
+
"""Get comments on a card."""
|
|
174
|
+
return self.get(f"/cards/{card_id}/actions", params={"filter": "commentCard"})
|
|
175
|
+
|
|
176
|
+
# Member methods
|
|
177
|
+
def get_me(self) -> dict:
|
|
178
|
+
"""Get the authenticated user."""
|
|
179
|
+
return self.get("/members/me")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command modules for Trello CLI."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Authentication commands."""
|
|
2
|
+
|
|
3
|
+
import webbrowser
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from trelloctl.cli import Context, pass_context
|
|
8
|
+
from trelloctl.output import print_error, print_info, print_success
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def auth() -> None:
|
|
13
|
+
"""Authentication commands."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@auth.command("login")
|
|
18
|
+
@click.option("--api-key", prompt=False, help="Trello API key")
|
|
19
|
+
@click.option("--token", prompt=False, help="Trello token")
|
|
20
|
+
@pass_context
|
|
21
|
+
def login(ctx: Context, api_key: str | None, token: str | None) -> None:
|
|
22
|
+
"""Set up authentication with Trello.
|
|
23
|
+
|
|
24
|
+
You can get your API key at: https://trello.com/app-key
|
|
25
|
+
"""
|
|
26
|
+
if not api_key:
|
|
27
|
+
print_info("Get your API key at: https://trello.com/app-key")
|
|
28
|
+
if click.confirm("Open browser to get API key?", default=True):
|
|
29
|
+
webbrowser.open("https://trello.com/app-key")
|
|
30
|
+
api_key = click.prompt("Enter your API key")
|
|
31
|
+
|
|
32
|
+
ctx.config.set_api_key(api_key)
|
|
33
|
+
print_success("API key saved")
|
|
34
|
+
|
|
35
|
+
if not token:
|
|
36
|
+
token_url = (
|
|
37
|
+
f"https://trello.com/1/authorize?"
|
|
38
|
+
f"expiration=never&scope=read,write,account&"
|
|
39
|
+
f"response_type=token&name=trelloctl&key={api_key}"
|
|
40
|
+
)
|
|
41
|
+
print_info(f"Generate a token at: {token_url}")
|
|
42
|
+
if click.confirm("Open browser to generate token?", default=True):
|
|
43
|
+
webbrowser.open(token_url)
|
|
44
|
+
token = click.prompt("Enter your token")
|
|
45
|
+
|
|
46
|
+
ctx.config.set_token(token)
|
|
47
|
+
print_success("Token saved")
|
|
48
|
+
|
|
49
|
+
# Verify credentials
|
|
50
|
+
try:
|
|
51
|
+
client = ctx.ensure_client()
|
|
52
|
+
me = client.get_me()
|
|
53
|
+
print_success(f"Authenticated as: {me['fullName']} (@{me['username']})")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print_error(f"Authentication failed: {e}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@auth.command("status")
|
|
59
|
+
@pass_context
|
|
60
|
+
def status(ctx: Context) -> None:
|
|
61
|
+
"""Check authentication status."""
|
|
62
|
+
if not ctx.config.is_configured():
|
|
63
|
+
print_error("Not authenticated. Run 'trelloctl auth login' first.")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
client = ctx.ensure_client()
|
|
68
|
+
me = client.get_me()
|
|
69
|
+
print_success(f"Authenticated as: {me['fullName']} (@{me['username']})")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print_error(f"Authentication error: {e}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@auth.command("logout")
|
|
75
|
+
@pass_context
|
|
76
|
+
def logout(ctx: Context) -> None:
|
|
77
|
+
"""Remove stored credentials."""
|
|
78
|
+
import keyring
|
|
79
|
+
from trelloctl.config import SERVICE_NAME
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
keyring.delete_password(SERVICE_NAME, f"{ctx.profile}:api_key")
|
|
83
|
+
except keyring.errors.PasswordDeleteError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
keyring.delete_password(SERVICE_NAME, f"{ctx.profile}:token")
|
|
88
|
+
except keyring.errors.PasswordDeleteError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
print_success("Credentials removed")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Board commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from trelloctl.cli import Context, pass_context
|
|
6
|
+
from trelloctl.output import format_output, print_error, print_success
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def board() -> None:
|
|
11
|
+
"""Board management commands."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@board.command("list")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--filter",
|
|
18
|
+
"-f",
|
|
19
|
+
type=click.Choice(
|
|
20
|
+
["all", "closed", "members", "open", "organization", "public", "starred"]
|
|
21
|
+
),
|
|
22
|
+
default="open",
|
|
23
|
+
help="Filter boards",
|
|
24
|
+
)
|
|
25
|
+
@pass_context
|
|
26
|
+
def list_boards(ctx: Context, filter: str) -> None:
|
|
27
|
+
"""List all accessible boards."""
|
|
28
|
+
client = ctx.ensure_client()
|
|
29
|
+
boards = client.get_boards(filter=filter)
|
|
30
|
+
|
|
31
|
+
data = [
|
|
32
|
+
{
|
|
33
|
+
"id": b["id"],
|
|
34
|
+
"name": b["name"],
|
|
35
|
+
"url": b["url"],
|
|
36
|
+
"closed": "Yes" if b.get("closed") else "No",
|
|
37
|
+
}
|
|
38
|
+
for b in boards
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
format_output(
|
|
42
|
+
data,
|
|
43
|
+
ctx.format,
|
|
44
|
+
columns=[("name", "Name"), ("id", "ID"), ("closed", "Closed")],
|
|
45
|
+
title="Boards",
|
|
46
|
+
template="{name} ({id})",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@board.command("show")
|
|
51
|
+
@click.argument("board")
|
|
52
|
+
@pass_context
|
|
53
|
+
def show_board(ctx: Context, board: str) -> None:
|
|
54
|
+
"""Show details of a board.
|
|
55
|
+
|
|
56
|
+
BOARD can be an ID or name (partial match supported).
|
|
57
|
+
"""
|
|
58
|
+
client = ctx.ensure_client()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
board_id = ctx.resolver.resolve_board(board)
|
|
62
|
+
board_data = client.get_board(board_id)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print_error(str(e))
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
data = {
|
|
68
|
+
"id": board_data["id"],
|
|
69
|
+
"name": board_data["name"],
|
|
70
|
+
"description": board_data.get("desc", ""),
|
|
71
|
+
"url": board_data["url"],
|
|
72
|
+
"closed": "Yes" if board_data.get("closed") else "No",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
format_output(
|
|
76
|
+
data,
|
|
77
|
+
ctx.format,
|
|
78
|
+
columns=[
|
|
79
|
+
("name", "Name"),
|
|
80
|
+
("id", "ID"),
|
|
81
|
+
("description", "Description"),
|
|
82
|
+
("url", "URL"),
|
|
83
|
+
("closed", "Closed"),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@board.command("create")
|
|
89
|
+
@click.option("--name", "-n", required=True, help="Board name")
|
|
90
|
+
@click.option("--description", "-d", default="", help="Board description")
|
|
91
|
+
@pass_context
|
|
92
|
+
def create_board(ctx: Context, name: str, description: str) -> None:
|
|
93
|
+
"""Create a new board."""
|
|
94
|
+
client = ctx.ensure_client()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
board_data = client.create_board(name, description)
|
|
98
|
+
print_success(f"Created board: {board_data['name']} ({board_data['id']})")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print_error(f"Failed to create board: {e}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@board.command("close")
|
|
104
|
+
@click.argument("board")
|
|
105
|
+
@click.option("--reopen", is_flag=True, help="Reopen instead of close")
|
|
106
|
+
@pass_context
|
|
107
|
+
def close_board(ctx: Context, board: str, reopen: bool) -> None:
|
|
108
|
+
"""Close (archive) a board.
|
|
109
|
+
|
|
110
|
+
BOARD can be an ID or name (partial match supported).
|
|
111
|
+
"""
|
|
112
|
+
client = ctx.ensure_client()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
board_id = ctx.resolver.resolve_board(board)
|
|
116
|
+
client.close_board(board_id, closed=not reopen)
|
|
117
|
+
action = "Reopened" if reopen else "Closed"
|
|
118
|
+
print_success(f"{action} board: {board}")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print_error(str(e))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@board.command("delete")
|
|
124
|
+
@click.argument("board")
|
|
125
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this board?")
|
|
126
|
+
@pass_context
|
|
127
|
+
def delete_board(ctx: Context, board: str) -> None:
|
|
128
|
+
"""Delete a board (permanently).
|
|
129
|
+
|
|
130
|
+
BOARD can be an ID or name (partial match supported).
|
|
131
|
+
"""
|
|
132
|
+
client = ctx.ensure_client()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
board_id = ctx.resolver.resolve_board(board)
|
|
136
|
+
client.delete_board(board_id)
|
|
137
|
+
print_success(f"Deleted board: {board}")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print_error(str(e))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@board.command("labels")
|
|
143
|
+
@click.argument("board")
|
|
144
|
+
@pass_context
|
|
145
|
+
def board_labels(ctx: Context, board: str) -> None:
|
|
146
|
+
"""List labels on a board.
|
|
147
|
+
|
|
148
|
+
BOARD can be an ID or name (partial match supported).
|
|
149
|
+
"""
|
|
150
|
+
client = ctx.ensure_client()
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
board_id = ctx.resolver.resolve_board(board)
|
|
154
|
+
labels = client.get_board_labels(board_id)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print_error(str(e))
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
data = [
|
|
160
|
+
{
|
|
161
|
+
"id": label["id"],
|
|
162
|
+
"name": label.get("name", ""),
|
|
163
|
+
"color": label.get("color", ""),
|
|
164
|
+
}
|
|
165
|
+
for label in labels
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
format_output(
|
|
169
|
+
data,
|
|
170
|
+
ctx.format,
|
|
171
|
+
columns=[("name", "Name"), ("color", "Color"), ("id", "ID")],
|
|
172
|
+
title="Labels",
|
|
173
|
+
template="{name} ({color})",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@board.command("members")
|
|
178
|
+
@click.argument("board")
|
|
179
|
+
@pass_context
|
|
180
|
+
def board_members(ctx: Context, board: str) -> None:
|
|
181
|
+
"""List members of a board.
|
|
182
|
+
|
|
183
|
+
BOARD can be an ID or name (partial match supported).
|
|
184
|
+
"""
|
|
185
|
+
client = ctx.ensure_client()
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
board_id = ctx.resolver.resolve_board(board)
|
|
189
|
+
members = client.get_board_members(board_id)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print_error(str(e))
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
data = [
|
|
195
|
+
{
|
|
196
|
+
"id": m["id"],
|
|
197
|
+
"username": m.get("username", ""),
|
|
198
|
+
"fullName": m.get("fullName", ""),
|
|
199
|
+
}
|
|
200
|
+
for m in members
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
format_output(
|
|
204
|
+
data,
|
|
205
|
+
ctx.format,
|
|
206
|
+
columns=[("fullName", "Name"), ("username", "Username"), ("id", "ID")],
|
|
207
|
+
title="Members",
|
|
208
|
+
template="{fullName} (@{username})",
|
|
209
|
+
)
|