Setting the file. One moment. New Notebook · Jupyter Notebook · openai/skills · Skills Docsscripts/new_notebook.py
Python·130 lines·4 KB
text.strip().lower()
12 cleaned = re.sub(r"[^a-z0-9]+", "-", lowered)
13 collapsed = re.sub(r"-+", "-", cleaned).strip("-")
14 return collapsed or "notebook"
15
16
17def find_repo_root(start: Path) -> Path:
18 for candidate in (start, *start.parents):
19 if (candidate / ".git").exists():
20 return candidate
21 return start
22
23
24def load_template(skill_dir: Path, kind: str) -> dict[str, Any]:
25 asset_name = "experiment-template.ipynb" if kind == "experiment" else "tutorial-template.ipynb"
26 template_path = skill_dir / "assets" / asset_name
27 if not template_path.exists():
28 raise SystemExit(f"Missing template: {template_path}")
29 with template_path.open("r", encoding="utf-8") as f:
30 data = json.load(f)
31 if not isinstance(data, dict):
32 raise SystemExit(f"Unexpected template shape: {template_path}")
33 return data
34
35
36def update_title(notebook: dict[str, Any], kind: str, title: str) -> None:
37 prefix = "Experiment" if kind == "experiment" else "Tutorial"
38 expected = f"# {prefix}: {title}\n"
39
40 cells = notebook.get("cells")
41 if not isinstance(cells, list) or not cells:
42 raise SystemExit("Template notebook has no cells")
43
44 first_cell = cells[0]
45 if not isinstance(first_cell, dict) or first_cell.get("cell_type") != "markdown":
46 raise SystemExit("Template notebook must start with a markdown title cell")
47
48 source = first_cell.get("source", [])
49 if isinstance(source, str):
50 source_lines = [source]
51 elif isinstance(source, list):
52 source_lines = [str(line) for line in source]
53 else:
54 source_lines = []
55
56 if source_lines:
57 source_lines[0] = expected
58 else:
59 source_lines = [expected]
60
61 first_cell["source"] = source_lines
62
63 metadata = notebook.setdefault("metadata", {})
64 if not isinstance(metadata, dict):
65 raise SystemExit("Notebook metadata must be a mapping")
66
67 language_info = metadata.setdefault("language_info", {})
68 if isinstance(language_info, dict):
69 language_info.setdefault("name", "python")
70 language_info.setdefault("version", "3.12")
71
72
73def default_output(repo_root: Path, title: str) -> Path:
74 filename = f"{slugify(title)}.ipynb"
75 return repo_root / "output" / "jupyter-notebook" / filename
76
77
78def parse_args() -> argparse.Namespace:
79 parser = argparse.ArgumentParser(description="Scaffold a Jupyter notebook for experiments or tutorials.")
80 parser.add_argument(
81 "--kind",
82 choices=["experiment", "tutorial"],
83 default="experiment",
84 help="Notebook style to scaffold (default: experiment).",
85 )
86 parser.add_argument(
87 "--title",
88 required=True,
89 help="Human-readable notebook title used in the first markdown cell.",
90 )
91 parser.add_argument(
92 "--out",
93 type=Path,
94 default=None,
95 help="Output path for the notebook. Defaults to output/jupyter-notebook/<slug>.ipynb.",
96 )
97 parser.add_argument(
98 "--force",
99 action="store_true",
100 help="Overwrite the output file if it already exists.",
101 )
102 return parser.parse_args()
103
104
105def main() -> None:
106 args = parse_args()
107
108 script_path = Path(__file__).resolve()
109 skill_dir = script_path.parents[1]
110 repo_root = find_repo_root(skill_dir)
111
112 notebook = load_template(skill_dir, args.kind)
113 update_title(notebook, args.kind, args.title)
114
115 out_path = args.out or default_output(repo_root, args.title)
116 out_path = out_path.resolve()
117
118 if out_path.exists() and not args.force:
119 raise SystemExit(f"Refusing to overwrite existing file without --force: {out_path}")
120
121 out_path.parent.mkdir(parents=True, exist_ok=True)
122 with out_path.open("w", encoding="utf-8") as f:
123 json.dump(notebook, f, indent=2)
124 f.write("\n")
125
126 print(f"Wrote {out_path} using kind={args.kind}.")
127
128
129if __name__ == "__main__":
130 main()