From a146c2423c810575c11568462bc4b9da373ce9d1 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Thu, 11 Sep 2025 23:31:07 +0300 Subject: [PATCH] feat: initial commit --- .gitignore | 1 + import.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++ list_stacks.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 17 ++++++++++ 4 files changed, 200 insertions(+) create mode 100644 .gitignore create mode 100644 import.py create mode 100644 list_stacks.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c04bc49 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +poetry.lock diff --git a/import.py b/import.py new file mode 100644 index 0000000..d1895da --- /dev/null +++ b/import.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +import csv +import requests +import json +import argparse +import getpass + + +def create_card(session, domain, board_id, stack_id, title, description): + url = f"https://{domain}/index.php/apps/deck/api/v1.0/boards/{board_id}/stacks/{stack_id}/cards" + headers = {"OCS-APIRequest": "true", "Content-Type": "application/json"} + payload = { + "title": title, + "type": "plain", + "order": 0, + "description": description, + "duedate": None, + } + + response = session.post(url, headers=headers, data=json.dumps(payload)) + return response + + +def main(): + parser = argparse.ArgumentParser( + description="Import tasks from a CSV file into Nextcloud Deck" + ) + parser.add_argument( + "--domain", + required=True, + help="Nextcloud instance domain (e.g., example.com or https://example.com)", + ) + parser.add_argument( + "--board-id", + required=True, + type=int, + help="Board ID where the cards will be created", + ) + parser.add_argument( + "--stack-id", + required=True, + type=int, + help="Stack ID where the cards will be added", + ) + parser.add_argument( + "--csv-file", required=True, help="Path to the CSV file containing tasks" + ) + 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)", + ) + + args = parser.parse_args() + + # Prompt for credentials if missing + username = args.username or input("Nextcloud username: ") + password = args.password or getpass.getpass( + "Nextcloud password (or app password): " + ) + + session = requests.Session() + session.auth = (username, password) + + with open(args.csv_file, newline="", encoding="utf-8") as csvfile: + reader = csv.DictReader(csvfile) + + for row in reader: + title = row.get("title", "").strip() + description = row.get("description", "").strip() + + if not title: + print(f"Skipping row with missing title: {row}") + continue + + response = create_card( + session, args.domain, args.board_id, args.stack_id, title, description + ) + + if response.status_code in (200, 201): + print(f"✔️ Successfully created card: {title}") + else: + print( + f"❌ Failed to create card: {title} - Error: {response.status_code} {response.text}" + ) + + +if __name__ == "__main__": + main() + diff --git a/list_stacks.py b/list_stacks.py new file mode 100644 index 0000000..d7d9d70 --- /dev/null +++ b/list_stacks.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys +import requests +from urllib.parse import urlparse +import getpass + + +def normalize_base_url(domain_or_url: str) -> str: + parsed = urlparse(domain_or_url) + if parsed.scheme in ("http", "https"): + return domain_or_url.rstrip("/") + return f"https://{domain_or_url.strip().strip('/')}" + + +def list_stacks(session: requests.Session, base_url: str, board_id: int): + url = f"{base_url}/index.php/apps/deck/api/v1.0/boards/{board_id}/stacks" + headers = {"OCS-APIRequest": "true", "Accept": "application/json"} + resp = session.get(url, headers=headers) + if resp.status_code != 200: + raise RuntimeError(f"Deck API error: {resp.status_code} {resp.text}") + return resp.json() + + +def main(): + parser = argparse.ArgumentParser( + description="List stacks (id + name) for a Nextcloud Deck board." + ) + parser.add_argument( + "--domain", + required=True, + help="Nextcloud instance (e.g., example.com or https://example.com)", + ) + parser.add_argument( + "--board-id", required=True, type=int, help="Deck Board ID to list stacks from" + ) + 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)", + ) + parser.add_argument( + "--json", action="store_true", help="Output raw JSON instead of a table" + ) + + args = parser.parse_args() + + # Prompt for missing creds + username = args.username or input("Nextcloud username: ") + password = args.password or getpass.getpass( + "Nextcloud password (or app password): " + ) + + base_url = normalize_base_url(args.domain) + + session = requests.Session() + session.auth = (username, password) + + try: + stacks = list_stacks(session, base_url, args.board_id) + except Exception as e: + print(f"❌ Failed to fetch stacks: {e}", file=sys.stderr) + sys.exit(1) + + if args.json: + print(json.dumps(stacks, ensure_ascii=False, indent=2)) + return + + if not stacks: + print("No stacks found.") + return + + print(f"Stacks for board {args.board_id} at {base_url}:") + print("-" * 60) + print(f"{'ID':<8} {'NAME'}") + print("-" * 60) + for s in stacks: + sid = s.get("id") + title = s.get("title", "") + print(f"{str(sid):<8} {title}") + print("-" * 60) + print("Tip: use the ID in your importer script's --stack-id.") + + +if __name__ == "__main__": + main() + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3da152e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "nextcloud-deck-tools" +version = "0.1.0" +description = "" +authors = [ + {name = "Chen Asraf",email = "casraf@pm.me"} +] +readme = "README.md" +requires-python = "^3.11" +dependencies = [ + "requests (>=2.32.5,<3.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api"