Files
nextcloud-deck-tools/create_board.py
Chen Asraf 8aa1213f61 feat: create board fix, create tags, main cmd flow
feat: create tags on board
feat: main flow asks for task instead of import by default
fix: create board requires color
2026-05-01 00:06:29 +03:00

200 lines
5.9 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from typing import Iterable
import requests
from common import (
add_domain_and_auth_args,
build_session,
resolve_arg,
resolve_domain_and_auth,
)
DEFAULT_STACKS: list[str] = [
"🐞 Bugs",
"📋 To Do",
"🚧 In Progress",
"✅ Done",
"📄 Backlog",
]
def get_stack_names() -> list[str]:
"""
Get stack names from NEW_BOARD_COLUMNS env var (comma-separated),
or fall back to DEFAULT_STACKS if not set.
Tolerates shell-style escapes that may leak into the value (e.g.,
"🐞\\ Bugs\\,📋\\ To\\ Do") by stripping single-backslash escapes
from each piece after splitting on commas.
"""
columns_env = os.getenv("NEW_BOARD_COLUMNS")
if columns_env:
names = []
for col in columns_env.split(","):
# Remove backslash escapes: "\<c>" -> "<c>"; trailing "\" -> ""
unescaped = re.sub(r"\\(.?)", r"\1", col).strip()
if unescaped:
names.append(unescaped)
return names
return DEFAULT_STACKS
def create_board(
session: requests.Session, base_url: str, title: str, color: str = "0082c9"
) -> dict:
"""
POST /boards -> { id, title, ... }
"""
url = f"{base_url}/index.php/apps/deck/api/v1.0/boards"
headers = {"OCS-APIRequest": "true", "Content-Type": "application/json"}
payload = {"title": title, "color": color}
resp = session.post(url, headers=headers, data=json.dumps(payload))
if resp.status_code not in (200, 201):
raise RuntimeError(
f"Deck API error creating board: {resp.status_code} {resp.text}"
)
return resp.json()
def create_stack(
session: requests.Session, base_url: str, board_id: int, title: str, order: int
) -> dict:
"""
POST /boards/{board_id}/stacks -> { id, title, order, ... }
"""
url = f"{base_url}/index.php/apps/deck/api/v1.0/boards/{board_id}/stacks"
headers = {"OCS-APIRequest": "true", "Content-Type": "application/json"}
payload = {"title": title, "order": order}
resp = session.post(url, headers=headers, data=json.dumps(payload))
if resp.status_code not in (200, 201):
raise RuntimeError(
f"Deck API error creating stack '{title}': {resp.status_code} {resp.text}"
)
return resp.json()
def create_stacks_in_order(
session: requests.Session,
base_url: str,
board_id: int,
stacks: Iterable[str],
) -> list[dict]:
created = []
for idx, title in enumerate(stacks):
created.append(create_stack(session, base_url, board_id, title, idx))
print(f" ✔️ Created stack [{idx}]: {title}")
return created
def main() -> None:
parser = argparse.ArgumentParser(
description="Create a Nextcloud Deck board and pre-populate it with standard stacks."
)
# Only domain/username/password (no board-id since we are creating it)
add_domain_and_auth_args(parser)
# Script-specific
parser.add_argument(
"--board-name",
help="New board name/title (e.g., 'Team Kanban')",
)
parser.add_argument(
"--color",
help="Board color as 6-char hex (no '#'), e.g., 0082c9",
)
parser.add_argument(
"--no-default-stacks",
action="store_true",
help="Create the board without the default stacks.",
)
args = parser.parse_args()
# Resolve domain + auth (ENV -> CLI -> prompt)
try:
base_url, username, password = resolve_domain_and_auth(
cli_domain=args.domain,
cli_username=args.username,
cli_password=args.password,
)
except Exception as e:
print(f"{e}", file=sys.stderr)
sys.exit(2)
# Resolve board name
try:
board_name = resolve_arg(
args.board_name,
["NEXTCLOUD_BOARD_NAME", "BOARD_NAME"],
prompt_text="New board name: ",
cast=str,
)
except Exception as e:
print(f"{e}", file=sys.stderr)
sys.exit(2)
# Resolve board color
try:
color_raw = resolve_arg(
args.color,
["NEXTCLOUD_BOARD_COLOR", "BOARD_COLOR"],
prompt_text="Board color hex (6 chars, no '#') [0082c9]: ",
cast=str,
allow_empty=True,
)
color = (color_raw or "").strip().lstrip("#") or "0082c9"
if len(color) != 6 or not all(c in "0123456789abcdefABCDEF" for c in color):
raise ValueError(f"Invalid hex color: {color}")
color = color.lower()
except Exception as e:
print(f"{e}", file=sys.stderr)
sys.exit(2)
session = build_session(username, password)
# Create board
try:
board = create_board(session, base_url, board_name, color)
board_id = board.get("id")
if board_id is None:
raise RuntimeError(f"Board created but no id returned: {board}")
print(f"✔️ Created board '{board_name}' (id: {board_id}) at {base_url}")
except Exception as e:
print(f"❌ Failed to create board: {e}", file=sys.stderr)
sys.exit(1)
# Create default stacks (unless suppressed)
created_stacks = []
if not args.no_default_stacks:
try:
stack_names = get_stack_names()
print("→ Creating stacks in order:")
created_stacks = create_stacks_in_order(
session, base_url, int(board_id), stack_names
)
except Exception as e:
print(f"❌ Failed to create stacks: {e}", file=sys.stderr)
sys.exit(1)
# Summary
print("\nSummary")
print("-------")
print(f"Board ID: {board_id}")
if created_stacks:
print("Stacks (in order):")
for s in created_stacks:
print(f" - [{s.get('id')}] {s.get('title')}")
if __name__ == "__main__":
main()