#!/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 requests T = TypeVar("T") # ---------- 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