#!/usr/bin/env python3 """Local file uploads to Notion via the `ntn` CLI. Owns all subprocess I/O for the skill. The CLI handles the full File Upload lifecycle (create -> send bytes -> complete) and prints the upload ID. """ from __future__ import annotations import json import shutil import subprocess import sys from pathlib import Path from typing import Dict, List, Optional, Callable, Any _WORKSPACE: Optional[Dict[str, str]] = None class NtnUploadError(Exception): """Raised when `ntn` is unavailable or a file upload fails.""" def __init__(self, message: str, path: str = "", stderr: str = ""): super().__init__(message) self.path = path self.stderr = stderr def preflight() -> Dict[str, str]: """Verify `ntn` is installed and logged in; return its target workspace. Cached for the process. Raises NtnUploadError with an actionable hint on failure. Prints one informational line naming the workspace `ntn` targets (file uploads are workspace-scoped). """ global _WORKSPACE if _WORKSPACE is not None: return _WORKSPACE if shutil.which("ntn") is None: raise NtnUploadError( "ntn CLI not found. Install it with: " "curl -fsSL https://ntn.dev | bash" ) result = subprocess.run( ["ntn", "api", "v1/users/me"], capture_output=True, text=True, ) if result.returncode != 0: raise NtnUploadError( "ntn is not logged in. Run: ntn login\n" + result.stderr.strip() ) try: me = json.loads(result.stdout) bot = me.get("bot", {}) info = { "workspace_name": bot.get("workspace_name", "unknown"), "workspace_id": bot.get("workspace_id", "unknown"), } except (ValueError, AttributeError): info = {"workspace_name": "unknown", "workspace_id": "unknown"} print(f'ntn -> workspace "{info["workspace_name"]}"', file=sys.stderr) _WORKSPACE = info return info def upload(path: Path) -> str: """Upload a local file via `ntn files create --plain`; return its id.""" path = Path(path) with open(path, "rb") as fh: result = subprocess.run( ["ntn", "files", "create", "--plain"], stdin=fh, capture_output=True, text=True, ) if result.returncode != 0: raise NtnUploadError( f"ntn upload failed for {path}", path=str(path), stderr=result.stderr.strip(), ) first_line = result.stdout.strip().splitlines()[0] upload_id = first_line.split("\t")[0].strip() return upload_id