mirror of
https://github.com/chenasraf/nextcloud-deck-tools.git
synced 2026-05-17 17:28:07 +00:00
feat: create tags on board feat: main flow asks for task instead of import by default fix: create board requires color
298 lines
8.5 KiB
Python
298 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
import argparse
|
|
import getpass
|
|
import os
|
|
from typing import Callable, Iterable, Optional, TypeVar
|
|
from urllib.parse import urlparse
|
|
|
|
import inquirer
|
|
import requests
|
|
|
|
T = TypeVar("T")
|
|
|
|
# ---------- color presets / picker ----------
|
|
|
|
DECK_COLOR_PRESETS: list[tuple[str, str]] = [
|
|
("Nextcloud Blue", "0082c9"),
|
|
("Red", "e74c3c"),
|
|
("Orange", "f39c12"),
|
|
("Yellow", "f1c40f"),
|
|
("Green", "27ae60"),
|
|
("Teal", "1abc9c"),
|
|
("Purple", "9b59b6"),
|
|
("Pink", "e91e63"),
|
|
("Gray", "95a5a6"),
|
|
]
|
|
|
|
|
|
def select_existing_board(
|
|
session: requests.Session,
|
|
base_url: str,
|
|
message: str = "Which board?",
|
|
) -> tuple[int, str]:
|
|
"""
|
|
Fetch boards via the Deck API and let the user pick one interactively.
|
|
Returns (board_id, board_title).
|
|
"""
|
|
# Lazy import to avoid circular module load.
|
|
from list_boards import list_boards
|
|
|
|
boards = list_boards(session, base_url)
|
|
if not boards:
|
|
raise RuntimeError("No boards found on this instance.")
|
|
|
|
choices: list[str] = []
|
|
board_map: dict[str, tuple[int, str]] = {}
|
|
for board in boards:
|
|
bid = board.get("id")
|
|
title = board.get("title", "Untitled")
|
|
label = f"{title} (ID: {bid})"
|
|
choices.append(label)
|
|
board_map[label] = (bid, title)
|
|
|
|
answer = inquirer.prompt(
|
|
[inquirer.List("board", message=message, choices=choices)]
|
|
)
|
|
if not answer:
|
|
raise RuntimeError("Board selection cancelled")
|
|
|
|
return board_map[answer["board"]]
|
|
|
|
|
|
def interactive_color_selection(message: str = "Pick a color") -> str:
|
|
"""
|
|
Show interactive menu to pick a hex color from presets, or enter a custom hex.
|
|
Returns 6-char lowercase hex string (no '#').
|
|
"""
|
|
choices = [f"{name} (#{hex_})" for name, hex_ in DECK_COLOR_PRESETS]
|
|
custom = "Custom hex..."
|
|
choices.append(custom)
|
|
|
|
answer = inquirer.prompt(
|
|
[inquirer.List("color", message=message, choices=choices)]
|
|
)
|
|
if not answer:
|
|
raise RuntimeError("Color selection cancelled")
|
|
|
|
selected = answer["color"]
|
|
if selected == custom:
|
|
custom_answer = inquirer.prompt(
|
|
[
|
|
inquirer.Text(
|
|
"hex",
|
|
message="Enter hex color (6 chars, no '#')",
|
|
default="0082c9",
|
|
)
|
|
]
|
|
)
|
|
if not custom_answer:
|
|
raise RuntimeError("Color entry cancelled")
|
|
hex_value = custom_answer["hex"].strip().lstrip("#")
|
|
if len(hex_value) != 6 or not all(
|
|
c in "0123456789abcdefABCDEF" for c in hex_value
|
|
):
|
|
raise RuntimeError(f"Invalid hex color: {hex_value}")
|
|
return hex_value.lower()
|
|
|
|
for name, hex_ in DECK_COLOR_PRESETS:
|
|
if selected == f"{name} (#{hex_})":
|
|
return hex_
|
|
raise RuntimeError(f"Unexpected color selection: {selected}")
|
|
|
|
# ---------- URL & session helpers ----------
|
|
|
|
|
|
def normalize_base_url(domain_or_url: str) -> str:
|
|
"""
|
|
Accepts either a bare domain (example.com) or a full URL (https://example.com[/...]).
|
|
Returns a normalized base URL without trailing slash (e.g., https://example.com).
|
|
"""
|
|
val = (domain_or_url or "").strip().rstrip("/")
|
|
if not val:
|
|
return val
|
|
parsed = urlparse(val)
|
|
if parsed.scheme in ("http", "https"):
|
|
return val
|
|
return f"https://{val}"
|
|
|
|
|
|
def build_session(username: str, password: str) -> requests.Session:
|
|
s = requests.Session()
|
|
s.auth = (username, password)
|
|
return s
|
|
|
|
|
|
# ---------- argparse helpers ----------
|
|
|
|
|
|
def add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
"""
|
|
Adds commonly shared CLI flags. Not marked as required because we support env vars + prompts.
|
|
"""
|
|
parser.add_argument(
|
|
"--domain",
|
|
help="Nextcloud instance domain or base URL (e.g., example.com or https://example.com)",
|
|
)
|
|
parser.add_argument(
|
|
"--board-id",
|
|
type=int,
|
|
help="Deck Board ID",
|
|
)
|
|
parser.add_argument(
|
|
"--username",
|
|
help="Nextcloud username (if not provided, will be prompted)",
|
|
)
|
|
parser.add_argument(
|
|
"--password",
|
|
help="Nextcloud password or app password (if not provided, will be prompted)",
|
|
)
|
|
|
|
|
|
# ---------- resolution helpers ----------
|
|
|
|
|
|
def _first_env_match(keys: Iterable[str]) -> Optional[str]:
|
|
for k in keys:
|
|
v = os.getenv(k)
|
|
if v is not None and str(v).strip() != "":
|
|
return v
|
|
return None
|
|
|
|
|
|
def resolve_arg(
|
|
args_value: Optional[str],
|
|
env_keys: Iterable[str],
|
|
*,
|
|
prompt_text: Optional[str] = None,
|
|
secret: bool = False,
|
|
cast: Optional[Callable[[str], T]] = None,
|
|
allow_empty: bool = False,
|
|
) -> T | str:
|
|
"""
|
|
Resolution order: ENV -> CLI arg -> prompt (if provided) -> error.
|
|
- env_keys: list/iterable of env var names to try in order.
|
|
- secret: use getpass for prompt if True.
|
|
- cast: optional converter (e.g., int). Cast is applied to whichever source was used.
|
|
- allow_empty: if True, an empty prompt is accepted (returns empty string).
|
|
"""
|
|
raw = _first_env_match(env_keys)
|
|
if raw is None and args_value is not None:
|
|
raw = args_value
|
|
|
|
if raw is None:
|
|
if prompt_text is not None:
|
|
raw = (
|
|
getpass.getpass(prompt_text) if secret else input(prompt_text)
|
|
).strip()
|
|
if not allow_empty and raw == "":
|
|
raise ValueError(f"Missing value for: {', '.join(env_keys)}")
|
|
else:
|
|
# No prompt provided -> treat as missing.
|
|
raise ValueError(
|
|
f"Missing required value. Provide one of env({', '.join(env_keys)}) or CLI option."
|
|
)
|
|
|
|
return cast(raw) if cast else raw
|
|
|
|
|
|
def resolve_common(
|
|
*,
|
|
cli_domain: Optional[str],
|
|
cli_board_id: Optional[int],
|
|
cli_username: Optional[str],
|
|
cli_password: Optional[str],
|
|
) -> tuple[str, int, str, str]:
|
|
"""
|
|
Resolves common inputs from ENV/CLI/prompt:
|
|
Returns: (base_url, board_id, username, password)
|
|
"""
|
|
domain = resolve_arg(
|
|
None if cli_domain is None else str(cli_domain),
|
|
["NEXTCLOUD_DOMAIN", "NEXTCLOUD_BASE_URL"],
|
|
prompt_text="Nextcloud domain (e.g., example.com or https://example.com): ",
|
|
secret=False,
|
|
cast=str,
|
|
)
|
|
base_url = normalize_base_url(domain)
|
|
|
|
board_id = resolve_arg(
|
|
None if cli_board_id is None else str(cli_board_id),
|
|
["NEXTCLOUD_BOARD_ID", "BOARD_ID"],
|
|
prompt_text="Deck Board ID: ",
|
|
secret=False,
|
|
cast=lambda s: int(s.strip()),
|
|
)
|
|
|
|
username = resolve_arg(
|
|
cli_username,
|
|
["NEXTCLOUD_USERNAME", "NC_USERNAME"],
|
|
prompt_text="Nextcloud username: ",
|
|
secret=False,
|
|
cast=str,
|
|
)
|
|
password = resolve_arg(
|
|
cli_password,
|
|
["NEXTCLOUD_PASSWORD", "NC_PASSWORD"],
|
|
prompt_text="Nextcloud password (or app password): ",
|
|
secret=True,
|
|
cast=str,
|
|
)
|
|
|
|
return base_url, board_id, username, password
|
|
|
|
|
|
def add_domain_and_auth_args(parser: argparse.ArgumentParser) -> None:
|
|
"""
|
|
Like add_common_args, but WITHOUT --board-id (useful for scripts that don't need a board yet).
|
|
"""
|
|
parser.add_argument(
|
|
"--domain",
|
|
help="Nextcloud instance domain or base URL (e.g., example.com or https://example.com)",
|
|
)
|
|
parser.add_argument(
|
|
"--username",
|
|
help="Nextcloud username (if not provided, will be prompted)",
|
|
)
|
|
parser.add_argument(
|
|
"--password",
|
|
help="Nextcloud password or app password (if not provided, will be prompted)",
|
|
)
|
|
|
|
|
|
def resolve_domain_and_auth(
|
|
*,
|
|
cli_domain: str | None,
|
|
cli_username: str | None,
|
|
cli_password: str | None,
|
|
) -> tuple[str, str, str]:
|
|
"""
|
|
Resolve base_url, username, password via ENV -> CLI -> prompt.
|
|
Returns: (base_url, username, password)
|
|
"""
|
|
domain = resolve_arg(
|
|
None if cli_domain is None else str(cli_domain),
|
|
["NEXTCLOUD_DOMAIN", "NEXTCLOUD_BASE_URL"],
|
|
prompt_text="Nextcloud domain (e.g., example.com or https://example.com): ",
|
|
secret=False,
|
|
cast=str,
|
|
)
|
|
base_url = normalize_base_url(domain)
|
|
|
|
username = resolve_arg(
|
|
cli_username,
|
|
["NEXTCLOUD_USERNAME", "NC_USERNAME"],
|
|
prompt_text="Nextcloud username: ",
|
|
secret=False,
|
|
cast=str,
|
|
)
|
|
password = resolve_arg(
|
|
cli_password,
|
|
["NEXTCLOUD_PASSWORD", "NC_PASSWORD"],
|
|
prompt_text="Nextcloud password (or app password): ",
|
|
secret=True,
|
|
cast=str,
|
|
)
|
|
return base_url, username, password
|
|
|