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

1"""Build the script for marimo notebooks. 

2 

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). 

6 

7The script can be run from the command line with optional arguments: 

8 uvx marimushka [--output-dir OUTPUT_DIR] 

9 

10The exported files will be placed in the specified output directory (default: _site). 

11""" 

12 

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# /// 

21 

22from pathlib import Path 

23 

24import jinja2 

25import typer 

26from loguru import logger 

27from rich import print as rich_print 

28 

29from . import __version__ 

30from .notebook import Kind, Notebook, folder2notebooks 

31 

32app = typer.Typer(help=f"Marimushka - Export marimo notebooks in style. Version: {__version__}") 

33 

34 

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() 

43 

44 

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. 

53 

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. 

57 

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. 

65 

66 Returns: 

67 str: The rendered HTML content as a string 

68 

69 """ 

70 # Initialize empty lists if None is provided 

71 notebooks = notebooks or [] 

72 apps = apps or [] 

73 notebooks_wasm = notebooks_wasm or [] 

74 

75 # Export notebooks to WebAssembly 

76 for nb in notebooks: 

77 nb.export(output_dir=output / "notebooks") 

78 

79 # Export apps to WebAssembly 

80 for nb in apps: 

81 nb.export(output_dir=output / "apps") 

82 

83 for nb in notebooks_wasm: 

84 nb.export(output_dir=output / "notebooks_wasm") 

85 

86 # Create the full path for the index.html file 

87 index_path: Path = Path(output) / "index.html" 

88 

89 # Ensure the output directory exists 

90 Path(output).mkdir(parents=True, exist_ok=True) 

91 

92 # Set up Jinja2 environment and load template 

93 template_dir = template_file.parent 

94 template_name = template_file.name 

95 

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) 

103 

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 ) 

110 

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}") 

120 

121 return rendered_html 

122 

123 

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. 

128 

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" 

135 

136 # Convert output_dir explicitly to Path 

137 output_dir: Path = Path(output) 

138 logger.info(f"Output directory: {output_dir}") 

139 

140 # Make sure the output directory exists 

141 output_dir.mkdir(parents=True, exist_ok=True) 

142 

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}") 

149 

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) 

153 

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)}") 

157 

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 "" 

162 

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 ) 

170 

171 

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. 

180 

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". 

197 

198 Returns: 

199 ------- 

200 str 

201 The result returned by the implementation function, representing the 

202 completion of the generation process or final outcome. 

203 

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) 

207 

208 

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) 

232 

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 ) 

241 

242 

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]") 

247 

248 

249def cli(): 

250 """Run the CLI.""" 

251 app()