142 lines
4.7 KiB
Python
Executable File
142 lines
4.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Encode a notebook YAML file into a base64 import URL and open it in the browser.
|
|
|
|
Usage:
|
|
python3 generate_notebook_url.py <notebook_yaml_path> [--mc-base-url URL]
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
|
|
def sanitize_yaml(content: str) -> str:
|
|
"""Replace non-ASCII characters with ASCII equivalents."""
|
|
replacements = {
|
|
"\u2014": "-",
|
|
"\u2013": "-",
|
|
"\u2018": "'",
|
|
"\u2019": "'",
|
|
"\u201c": '"',
|
|
"\u201d": '"',
|
|
"\u2026": "...",
|
|
"\u00a0": " ",
|
|
}
|
|
for char, replacement in replacements.items():
|
|
content = content.replace(char, replacement)
|
|
content = re.sub(r"[^\x00-\x7F]", "?", content)
|
|
return content
|
|
|
|
|
|
def validate_yaml(content: str) -> None:
|
|
"""Parse YAML, validate notebook schema, and exit with context on failure."""
|
|
try:
|
|
doc = yaml.safe_load(content)
|
|
except yaml.YAMLError as e:
|
|
print(f"YAML validation failed: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
errors: list[str] = []
|
|
|
|
# Top-level structure
|
|
if not isinstance(doc, dict):
|
|
errors.append("Root must be a mapping")
|
|
else:
|
|
if "version" not in doc:
|
|
errors.append("Missing top-level 'version'")
|
|
metadata = doc.get("metadata")
|
|
if not isinstance(metadata, dict):
|
|
errors.append("Missing or invalid 'metadata' mapping")
|
|
else:
|
|
for field in ("id", "name", "created_at", "updated_at"):
|
|
if field not in metadata:
|
|
errors.append(f"metadata.{field}: missing required field")
|
|
for bad_field in ("title", "description", "pr_number", "generated_by"):
|
|
if bad_field in metadata:
|
|
errors.append(
|
|
f"metadata.{bad_field}: unexpected field (use 'name' for the notebook title)"
|
|
)
|
|
|
|
cells = doc.get("cells")
|
|
if not isinstance(cells, list):
|
|
errors.append("Missing or invalid 'cells' list")
|
|
else:
|
|
for i, cell in enumerate(cells):
|
|
prefix = f"cells[{i}]"
|
|
if not isinstance(cell, dict):
|
|
errors.append(f"{prefix}: must be a mapping")
|
|
continue
|
|
if "id" not in cell:
|
|
errors.append(f"{prefix}: missing 'id'")
|
|
if "type" not in cell:
|
|
errors.append(f"{prefix}: missing 'type'")
|
|
cell_type = cell.get("type")
|
|
if cell_type not in ("sql", "markdown", "parameter"):
|
|
errors.append(
|
|
f"{prefix}: invalid type '{cell_type}' (must be sql, markdown, or parameter)"
|
|
)
|
|
if "display_type" not in cell:
|
|
errors.append(f"{prefix}: missing 'display_type'")
|
|
if cell_type == "parameter":
|
|
content_val = cell.get("content")
|
|
if not isinstance(content_val, dict):
|
|
errors.append(f"{prefix}: parameter cell 'content' must be a mapping with 'name' and 'config'")
|
|
else:
|
|
if "name" not in content_val:
|
|
errors.append(f"{prefix}: parameter content missing 'name'")
|
|
if "config" not in content_val:
|
|
errors.append(f"{prefix}: parameter content missing 'config'")
|
|
|
|
if errors:
|
|
print("Invalid notebook:", file=sys.stderr)
|
|
for err in errors:
|
|
print(f" - {err}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Encode notebook YAML to import URL")
|
|
parser.add_argument("yaml_path", help="Path to notebook YAML file")
|
|
parser.add_argument(
|
|
"--mc-base-url",
|
|
default="https://getmontecarlo.com",
|
|
help="MC Bridge base URL",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
with open(args.yaml_path) as f:
|
|
notebook_yaml = f.read()
|
|
|
|
yaml_content = sanitize_yaml(notebook_yaml.strip())
|
|
validate_yaml(yaml_content)
|
|
|
|
encoded = base64.b64encode(yaml_content.encode()).decode()
|
|
url = f"{args.mc_base_url}/notebooks/import#{encoded}"
|
|
|
|
print(f"URL length: {len(url)} chars")
|
|
|
|
# Save URL to file alongside the YAML
|
|
url_file = os.path.join(os.path.dirname(os.path.abspath(args.yaml_path)), "notebook_url.txt")
|
|
with open(url_file, "w") as f:
|
|
f.write(url)
|
|
print(f"URL saved to: {url_file}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("NOTEBOOK URL:")
|
|
print("=" * 60)
|
|
print(url)
|
|
print("=" * 60 + "\n")
|
|
|
|
print("Opening notebook in browser...")
|
|
subprocess.run(["open", url])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|