Coverage for src/marimushka/export.py: 100%
78 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"""Build the script for marimo notebooks.
3This script exports marimo notebooks to HTML/WebAssembly format and generates
4an index.html file that lists all the notebooks. It handles both regular notebooks
5(from the notebooks/ directory) and apps (from the apps/ directory).
7The script can be run from the command line with optional arguments:
8 uvx marimushka [--output-dir OUTPUT_DIR]
10The exported files will be placed in the specified output directory (default: _site).
11"""
13# /// script
14# requires-python = ">=3.12"
15# dependencies = [
16# "jinja2==3.1.3",
17# "typer==0.16.0",
18# "loguru==0.7.0"
19# ]
20# ///
22from pathlib import Path
24import jinja2
25import typer
26from loguru import logger
27from rich import print as rich_print
29from . import __version__
30from .notebook import Kind, Notebook, folder2notebooks
32app = typer.Typer(help=f"Marimushka - Export marimo notebooks in style. Version: {__version__}")
35@app.callback(invoke_without_command=True)
36def callback(ctx: typer.Context):
37 """Run before any command and display help if no command is provided."""
38 # If no command is provided, show help
39 if ctx.invoked_subcommand is None:
40 print(ctx.get_help())
41 # Exit with code 0 to indicate success
42 raise typer.Exit()
45def _generate_index(
46 output: Path,
47 template_file: Path,
48 notebooks: list[Notebook] | None = None,
49 apps: list[Notebook] | None = None,
50 notebooks_wasm: list[Notebook] | None = None,
51) -> str:
52 """Generate an index.html file that lists all the notebooks.
54 This function creates an HTML index page that displays links to all the exported
55 notebooks. The index page includes the marimo logo and displays each notebook
56 with a formatted title and a link to open it.
58 Args:
59 notebooks_wasm:
60 notebooks (List[Notebook]): List of notebooks with data for notebooks
61 apps (List[Notebook]): List of notebooks with data for apps
62 notebooks_wasm (List[Notebook]): List of notebooks with data for notebooks_wasm
63 output (Path): Directory where the index.html file will be saved
64 template_file (Path, optional): Path to the template file. If None, uses the default template.
66 Returns:
67 str: The rendered HTML content as a string
69 """
70 # Initialize empty lists if None is provided
71 notebooks = notebooks or []
72 apps = apps or []
73 notebooks_wasm = notebooks_wasm or []
75 # Export notebooks to WebAssembly
76 for nb in notebooks:
77 nb.export(output_dir=output / "notebooks")
79 # Export apps to WebAssembly
80 for nb in apps:
81 nb.export(output_dir=output / "apps")
83 for nb in notebooks_wasm:
84 nb.export(output_dir=output / "notebooks_wasm")
86 # Create the full path for the index.html file
87 index_path: Path = Path(output) / "index.html"
89 # Ensure the output directory exists
90 Path(output).mkdir(parents=True, exist_ok=True)
92 # Set up Jinja2 environment and load template
93 template_dir = template_file.parent
94 template_name = template_file.name
96 rendered_html = ""
97 try:
98 # Create Jinja2 environment and load template
99 env = jinja2.Environment(
100 loader=jinja2.FileSystemLoader(template_dir), autoescape=jinja2.select_autoescape(["html", "xml"])
101 )
102 template = env.get_template(template_name)
104 # Render the template with notebook and app data
105 rendered_html = template.render(
106 notebooks=notebooks,
107 apps=apps,
108 notebooks_wasm=notebooks_wasm,
109 )
111 # Write the rendered HTML to the index.html file
112 try:
113 with Path.open(index_path, "w") as f:
114 f.write(rendered_html)
115 logger.info(f"Successfully generated index file at {index_path}")
116 except OSError as e:
117 logger.error(f"Error writing index file to {index_path}: {e}")
118 except jinja2.exceptions.TemplateError as e:
119 logger.error(f"Error rendering template {template_file}: {e}")
121 return rendered_html
124def _main_impl(
125 output: str | Path, template: str | Path, notebooks: str | Path, apps: str | Path, notebooks_wasm: str | Path
126) -> str:
127 """Implement the main function.
129 This function contains the actual implementation of the main functionality.
130 It is called by the main() function, which handles the Typer options.
131 """
132 logger.info("Starting marimushka build process")
133 logger.info(f"Version of Marimushka: {__version__}")
134 output = output or "_site"
136 # Convert output_dir explicitly to Path
137 output_dir: Path = Path(output)
138 logger.info(f"Output directory: {output_dir}")
140 # Make sure the output directory exists
141 output_dir.mkdir(parents=True, exist_ok=True)
143 # Convert template to Path if provided
144 template_file: Path = Path(template)
145 logger.info(f"Using template file: {template_file}")
146 logger.info(f"Notebooks: {notebooks}")
147 logger.info(f"Apps: {apps}")
148 logger.info(f"Notebooks-wasm: {notebooks_wasm}")
150 notebooks_data = folder2notebooks(folder=notebooks, kind=Kind.NB)
151 apps_data = folder2notebooks(folder=apps, kind=Kind.APP)
152 notebooks_wasm_data = folder2notebooks(folder=notebooks_wasm, kind=Kind.NB_WASM)
154 logger.info(f"# notebooks_data: {len(notebooks_data)}")
155 logger.info(f"# apps_data: {len(apps_data)}")
156 logger.info(f"# notebooks_wasm_data: {len(notebooks_wasm_data)}")
158 # Exit if no notebooks or apps were found
159 if not notebooks_data and not apps_data and not notebooks_wasm_data:
160 logger.warning("No notebooks or apps found!")
161 return ""
163 return _generate_index(
164 output=output_dir,
165 template_file=template_file,
166 notebooks=notebooks_data,
167 apps=apps_data,
168 notebooks_wasm=notebooks_wasm_data,
169 )
172def main(
173 output: str | Path = "_site",
174 template: str | Path = Path(__file__).parent / "templates" / "tailwind.html.j2",
175 notebooks: str | Path = "notebooks",
176 apps: str | Path = "apps",
177 notebooks_wasm: str | Path = "notebooks",
178) -> str:
179 """Call the implementation function with the provided parameters and return its result.
181 Parameters
182 ----------
183 output: str | Path
184 The output directory where generated files will be stored.
185 Defaults to "_site".
186 template: str | Path
187 Path to the template file used during the generation process.
188 Defaults to a predefined "tailwind.html.j2" file.
189 notebooks: str | Path
190 Directory containing the notebooks to be processed.
191 Defaults to "notebooks".
192 apps: str | Path
193 Directory containing application files. Defaults to "apps".
194 notebooks_wasm: str | Path
195 Directory containing WebAssembly-related files for notebooks.
196 Defaults to "notebooks".
198 Returns:
199 -------
200 str
201 The result returned by the implementation function, representing the
202 completion of the generation process or final outcome.
204 """
205 # Call the implementation function with the provided parameters and return its result
206 return _main_impl(output=output, template=template, notebooks=notebooks, apps=apps, notebooks_wasm=notebooks_wasm)
209@app.command(name="export")
210def _main_typer(
211 output: str = typer.Option("_site", "--output", "-o", help="Directory where the exported files will be saved"),
212 template: str = typer.Option(
213 str(Path(__file__).parent / "templates" / "tailwind.html.j2"),
214 "--template",
215 "-t",
216 help="Path to the template file",
217 ),
218 notebooks: str = typer.Option("notebooks", "--notebooks", "-n", help="Directory containing marimo notebooks"),
219 apps: str = typer.Option("apps", "--apps", "-a", help="Directory containing marimo apps"),
220 notebooks_wasm: str = typer.Option(
221 "notebooks_wasm", "--notebooks-wasm", "-nw", help="Directory containing marimo notebooks"
222 ),
223) -> None:
224 """Export marimo notebooks and build an HTML index page linking to them."""
225 # When called through Typer, the parameters might be typer.Option objects
226 # Extract the default values from the Option objects if necessary
227 output_val = getattr(output, "default", output)
228 template_val = getattr(template, "default", template)
229 notebooks_val = getattr(notebooks, "default", notebooks)
230 apps_val = getattr(apps, "default", apps)
231 notebooks_wasm_val = getattr(notebooks_wasm, "default", notebooks_wasm)
233 # Call the main function with the resolved parameter values
234 main(
235 output=output_val,
236 template=template_val,
237 notebooks=notebooks_val,
238 apps=apps_val,
239 notebooks_wasm=notebooks_wasm_val,
240 )
243@app.command(name="version")
244def version():
245 """Show the version of Marimushka."""
246 rich_print(f"[bold green]Marimushka[/bold green] version: [bold blue]{__version__}[/bold blue]")
249def cli():
250 """Run the CLI."""
251 app()