Coverage for src/marimushka/notebook.py: 97%
61 statements
« prev ^ index » next coverage.py v7.10.4, created at 2025-09-29 05:14 +0000
« prev ^ index » next coverage.py v7.10.4, created at 2025-09-29 05:14 +0000
1"""Notebook module for handling marimo notebooks.
3This module provides the Notebook class for representing and exporting marimo notebooks.
4"""
6import dataclasses
7import subprocess
8from enum import Enum
9from pathlib import Path
11from loguru import logger
14class Kind(Enum):
15 """Kind of notebook."""
17 NB = "notebook"
18 NB_WASM = "notebook_wasm"
19 APP = "app"
21 @classmethod
22 def from_str(cls, value: str) -> "Kind":
23 """Represent a factory method to parse a string into a Kind enumeration instance.
25 This method attempts to match the input string to an existing kind defined
26 in the Kind enumeration. If the input string does not match any valid kind,
27 an error is raised detailing the invalid value and listing acceptable kinds.
29 Args:
30 value (str): A string representing the kind to parse into a Kind instance.
32 Returns:
33 Kind: An instance of the Kind enumeration corresponding to the input string.
35 Raises:
36 ValueError: If the input string does not match any valid Kind value.
38 """
39 try:
40 return Kind(value)
41 except ValueError as e:
42 raise ValueError(f"Invalid Kind: {value!r}. Must be one of {[k.value for k in Kind]}") from e
44 @property
45 def command(self) -> list[str]:
46 """Get the command list associated with a specific Kind instance.
48 The command property returns a list of command strings that correspond
49 to different kinds of operations based on the Kind instance.
51 Attributes:
52 command: A list of strings representing the command.
54 Returns:
55 list[str]: A list of command strings for the corresponding Kind instance.
57 """
58 commands = {
59 Kind.NB: ["uvx", "marimo", "export", "html", "--sandbox"],
60 Kind.NB_WASM: ["uvx", "marimo", "export", "html-wasm", "--sandbox", "--mode", "edit"],
61 Kind.APP: ["uvx", "marimo", "export", "html-wasm", "--sandbox", "--mode", "run", "--no-show-code"],
62 }
63 return commands[self]
65 @property
66 def html_path(self) -> Path:
67 """Provide a property to determine the HTML path for different kinds of objects.
69 This property computes the corresponding directory path based on the kind
70 of the object, such as notebooks, notebooks_wasm, or apps.
72 @return: A Path object representing the relevant directory path for the
73 current kind.
75 @rtype: Path
76 """
77 paths = {
78 Kind.NB: Path("notebooks"),
79 Kind.NB_WASM: Path("notebooks_wasm"),
80 Kind.APP: Path("apps"),
81 }
82 return paths[self]
85@dataclasses.dataclass(frozen=True)
86class Notebook:
87 """Represents a marimo notebook.
89 This class encapsulates a marimo notebook (.py file) and provides methods
90 for exporting it to HTML/WebAssembly format.
92 Attributes:
93 path (Path): Path to the marimo notebook (.py file)
94 kind (Kind): How the notebook ts treated
96 """
98 path: Path
99 kind: Kind = Kind.NB
101 def __post_init__(self):
102 """Validate the notebook path after initialization.
104 Raises:
105 FileNotFoundError: If the file does not exist
106 ValueError: If the path is not a file or not a Python file
108 """
109 if not self.path.exists():
110 raise FileNotFoundError(f"File not found: {self.path}")
111 if not self.path.is_file():
112 raise ValueError(f"Path is not a file: {self.path}")
113 if not self.path.suffix == ".py":
114 raise ValueError(f"File is not a Python file: {self.path}")
116 def export(self, output_dir: Path) -> bool:
117 """Export the notebook to HTML/WebAssembly format.
119 This method exports the marimo notebook to HTML/WebAssembly format.
120 If is_app is True, the notebook is exported in "run" mode with code hidden,
121 suitable for applications. Otherwise, it's exported in "edit" mode,
122 suitable for interactive notebooks.
124 Args:
125 output_dir (Path): Directory where the exported HTML file will be saved
127 Returns:
128 bool: True if export succeeded, False otherwise
130 """
131 cmd = self.kind.command
133 try:
134 # Create the full output path and ensure the directory exists
135 output_file: Path = output_dir / f"{self.path.stem}.html"
136 output_file.parent.mkdir(parents=True, exist_ok=True)
138 # Add the notebook path and output file to command
139 cmd.extend([str(self.path), "-o", str(output_file)])
141 # Run marimo export command
142 logger.debug(f"Running command: {cmd}")
143 subprocess.run(cmd, capture_output=True, text=True, check=True)
144 return True
145 except subprocess.CalledProcessError as e:
146 # Handle marimo export errors
147 logger.error(f"Error exporting {self.path}:")
148 logger.error(f"Command output: {e.stderr}")
149 return False
150 except Exception as e:
151 # Handle unexpected errors
152 logger.error(f"Unexpected error exporting {self.path}: {e}")
153 return False
155 @property
156 def display_name(self) -> str:
157 """Return the display name for the notebook."""
158 return self.path.stem.replace("_", " ")
160 @property
161 def html_path(self) -> Path:
162 """Return the path to the exported HTML file."""
163 return self.kind.html_path / f"{self.path.stem}.html"
166def folder2notebooks(folder: Path | str | None, kind: Kind = Kind.NB) -> list[Notebook]:
167 """Find all marimo notebooks in a directory."""
168 if folder is None or folder == "":
169 return []
171 # which files are included here?
172 notebooks = list(Path(folder).glob("*.py"))
174 # uvx marimo export html-wasm / html --sandbox (--mode edit/run) (
175 return [Notebook(path=nb, kind=kind) for nb in notebooks]