Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ jobs:
run: pre-commit run ruff-format
- name: Ruff linting
run: pre-commit run ruff
- run: just test
- run: make test
- run: echo "🍏 This job's status is ${{ job.status }}."
4 changes: 2 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ open NOTEBOOK:
uv run runbook edit {{NOTEBOOK}}

clear-binder-output:
jupyter nbconvert --clear-output --inplace ./runbook/data/*.ipynb
uv run runbook clear-output ./runbook/data/*.ipynb

clear-output *FILES:
jupyter nbconvert --clear-output --inplace {{FILES}}
uv run runbook clear-output {{FILES}}

lint:
pre-commit run
Expand Down
56 changes: 56 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.DEFAULT_GOAL := help

.PHONY: help test test-watch open clear-binder-output clear-output lint lint-all profile release clean build benchmark readme docs docs-release

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'

test: ## Run pytest and deno tests
uv run pytest --disable-warnings -s
deno test -A --reload https://raw.githubusercontent.com/zph/runbook/main/ext/deno/runbook/mod.ts --parallel tests/cli_test.ts

test-watch: ## Run tests on file changes
watchexec -- make test

open: ## Open a notebook (usage: make open NOTEBOOK=path/to/nb)
uv run runbook edit $(NOTEBOOK)

clear-binder-output: ## Clear outputs from binder notebooks
uv run runbook clear-output ./runbook/data/*.ipynb

clear-output: ## Clear outputs from notebooks (usage: make clear-output FILES="a.ipynb b.ipynb")
uv run runbook clear-output $(FILES)

lint: ## Run pre-commit hooks on staged files
pre-commit run

lint-all: ## Run pre-commit hooks on all files
pre-commit run --all-files

profile: ## Profile the CLI with cProfile
uv run python3 -m cProfile runbook/cli/__init__.py

release: ## Create a release via release-it
deno run -A npm:release-it

clean: ## Remove build artifacts
rm -rf ./dist

build: ## Build the package
uv build

benchmark: ## Run benchmark and export to docs/PERFORMANCE.md
hyperfine --export-markdown=docs/PERFORMANCE.md -- runbook

readme: ## Regenerate README from template
.config/templating.sh

docs: ## Build documentation site
cp -f README.md docs/
uvx --with sphinx-click --with myst_parser --with . --from sphinx sphinx-build -b html docs/ site

docs-release: ## Publish documentation
bash .hermit/bin/publish-docs

run: ## Run the CLI (usage: make run ARGS="command args")
uv run runbook $(ARGS)
70 changes: 30 additions & 40 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[project]
name = "runbook"
# Managed via runbook.__version__ and https://pypi.org/project/poetry-version-plugin/
version = "0"
description = "Runbook lib and cli"
requires-python = ">=3.11"
Expand All @@ -9,58 +8,49 @@ authors = [
]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
shx = "^0.4.2"
rich = "^13.9.4"
# papermill = "^2.5.0"
# Used until upstreaming typescript into papermill
papermill = { git = "https://github.com/zph/papermill", branch = "main" }
jupyter = "^1.0.0"
click = "^8.1.7"
bash-kernel = "^0.9.3"
jupyterlab-execute-time = "^3.1.0"
jupytext = "^1.16.1"
nbformat = "^5.9.2"
nbconvert = "^7.14.1"
pyyaml = "^6.0.1"
traitlets = "^5.14.1"
ipywidgets = "^8.1.1"
pre-commit = "^3.6.0"
python-ulid = "^2.2.0"
slack-sdk = "^3.26.2"
jupyterlab = "^4.1.5"
nbdime = "^4.0.1"


[tool.poetry.group.dev.dependencies]
pandas = "^2.1.4"
pyarrow = "^14.0.2"
matplotlib = "^3.8.2"
pytest = "^7.4.4"
dependencies = [
"shx>=0.4.2",
"rich>=13.9.4",
"papermill @ git+https://github.com/zph/papermill@main",
"jupyter>=1.0.0",
"click>=8.1.7",
"bash-kernel>=0.9.3",
"jupyterlab-execute-time>=3.1.0",
"jupytext==1.16.6",
"nbformat>=5.9.2",
"pyyaml>=6.0.1",
"traitlets>=5.14.1",
"ipywidgets>=8.1.1",
"pre-commit>=3.6.0",
"python-ulid>=2.2.0",
"slack-sdk>=3.26.2",
"jupyterlab>=4.1.5",
"nbdime>=4.0.1",
]

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata]
allow-direct-references = true

[dependency-groups]
dev = [
"pytest>=8.3.4",
"ruff>=0.9.5",
]

[tool.poetry.scripts]
[project.scripts]
runbook = "runbook.cli:cli"
# For the sake of installation accessibility for deno notebooks
jupyter = "jupyter_core.command:main"

[tool.poetry-version-plugin]
source = "init"

# https://github.com/python-poetry/poetry/issues/927
# [tool.poetry.plugins."papermill.translators"]
# "typescript" = "translators:runbook.translators.TypescriptTranslator"
[tool.pytest.ini_options]
filterwarnings = [
"ignore::DeprecationWarning:jupyter_client.connect",
"ignore::DeprecationWarning:papermill.parameterize",
]

[tool.ruff]
line-length = 88 # Same default as Black (adjust if needed)
Expand Down
2 changes: 2 additions & 0 deletions runbook/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from runbook.cli.commands import (
check,
clear_output,
convert,
create,
diff,
Expand Down Expand Up @@ -36,6 +37,7 @@ def cli(ctx, cwd):


cli.add_command(init)
cli.add_command(clear_output)
cli.add_command(plan)
cli.add_command(edit)
cli.add_command(create)
Expand Down
1 change: 1 addition & 0 deletions runbook/cli/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from runbook.cli.commands.check import check
from runbook.cli.commands.clear_output import clear_output
from runbook.cli.commands.convert import convert
from runbook.cli.commands.create import create
from runbook.cli.commands.diff import diff
Expand Down
22 changes: 22 additions & 0 deletions runbook/cli/commands/clear_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Clear cell outputs from notebook(s). Replaces jupyter nbconvert --clear-output --inplace."""

import click
import nbformat

from runbook.cli.notebook_io import clear_cell_outputs


@click.command()
@click.argument(
"notebooks",
nargs=-1,
required=True,
type=click.Path(exists=True, path_type=str),
)
def clear_output(notebooks):
"""Clear outputs and execution counts from one or more notebooks (in place)."""
for path in notebooks:
nb = nbformat.read(path, as_version=4)
clear_cell_outputs(nb)
nbformat.write(nb, path)
click.echo(path)
22 changes: 9 additions & 13 deletions runbook/cli/commands/create.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pathlib import Path
from os import path

import click
import nbformat

from runbook.cli.lib import nbconvert_launch_instance
from runbook.cli.notebook_io import clear_cell_outputs
from runbook.cli.validators import (
validate_create_language,
validate_has_notebook_extension,
Expand Down Expand Up @@ -69,18 +71,12 @@ def create(ctx, filename, template, language):
"Supplied filename included more than a basename, should look like 'maintenance-operation.ipynb'"
)
# TODO: remove hardcoding of folder outer name and rely on config file
path.join("runbooks", "binder", filename)
argv = [
template,
"--to",
"notebook",
"--output",
filename,
"--output-dir",
path.join("runbooks", "binder"),
]

nbconvert_launch_instance(argv, clear_output=True)
output_dir = path.join("runbooks", "binder")
dest = path.join(output_dir, filename)
nb = nbformat.read(template, as_version=4)
clear_cell_outputs(nb)
Path(output_dir).mkdir(parents=True, exist_ok=True)
nbformat.write(nb, dest)

click.echo(
click.style(
Expand Down
65 changes: 21 additions & 44 deletions runbook/cli/commands/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,19 @@
import json
import os
import subprocess
from datetime import datetime
from datetime import datetime, timezone
from os import path
from pathlib import Path

import click
import nbformat
import papermill as pm

from runbook.cli.lib import nbconvert_launch_instance
from runbook.cli.notebook_io import get_notebook_language, inject_parameters_and_write
from runbook.cli.validators import validate_plan_params, validate_runbook_file_path
from runbook.constants import RUNBOOK_METADATA


def get_notebook_language(notebook_path: str) -> str:
"""
Determine the language of the notebook by checking the first code cell's metadata.
Returns 'python', 'typescript', or 'unknown'
"""
nb = nbformat.read(notebook_path, as_version=4)
for cell in nb.cells:
if cell.cell_type == "code":
# Check kernel info
if "kernelspec" in nb.metadata:
kernel_name = nb.metadata.kernelspec.name.lower()
if "python" in kernel_name:
return "python"
elif "typescript" in kernel_name or "ts" in kernel_name:
return "typescript"
# Check language info
if "language_info" in nb.metadata:
language = nb.metadata.language_info.name.lower()
if "python" in language:
return "python"
elif "typescript" in language or "ts" in language:
return "typescript"
return "unknown"


def get_parser_by_language(language: str):
if language == "typescript":
return json.loads
Expand Down Expand Up @@ -80,7 +55,7 @@ def get_parser_by_language(language: str):
help="Optional identifier to append to the output filename",
)
@click.option(
"-p",
"-r",
"--prompter",
default="",
type=click.Path(file_okay=True),
Expand Down Expand Up @@ -108,7 +83,7 @@ def plan(ctx, input, embed, identifier="", params={}, prompter=""):
"RUNBOOK_FOLDER": output_folder,
"RUNBOOK_FILE": full_output,
"RUNBOOK_SOURCE": input,
"CREATED_AT": str(datetime.utcnow()),
"CREATED_AT": str(datetime.now(timezone.utc)),
"CREATED_BY": os.environ["USER"],
}
}
Expand Down Expand Up @@ -145,31 +120,33 @@ def plan(ctx, input, embed, identifier="", params={}, prompter=""):
params = json.loads(result.stdout.strip())
else:
for key, value in formatted_params.items():
default_str = value["default"]

def value_proc(user_input, _default=default_str, _parser=value_parser):
if user_input is None or (
isinstance(user_input, str) and user_input.strip() == ""
):
return _parser(_default)
return _parser(user_input)

parsed_value = click.prompt(
f"""Enter value for {key} {value["typing"]} {value["help"]}""",
default=value["default"],
value_proc=value_parser,
default=default_str,
value_proc=value_proc,
)
params[key] = parsed_value
params[key] = parsed_value

injection_params = {**runbook_param_injection, **params}

if not Path(output_folder).exists():
os.makedirs(output_folder, exist_ok=True)

pm.execute_notebook(
input_path=input,
output_path=full_output,
parameters=injection_params,
prepare_only=True,
)

argv = [
"--inplace",
inject_parameters_and_write(
input,
full_output,
]

nbconvert_launch_instance(argv, clear_output=True)
injection_params,
clear_output=True,
)

for f in embed:
shutil.copyfile(src=f, dst=f"{output_folder}/{path.basename(f)}")
Expand Down
2 changes: 1 addition & 1 deletion runbook/cli/commands/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rich.console import Console
from rich.table import Table

from runbook.cli.commands.plan import get_notebook_language
from runbook.cli.notebook_io import get_notebook_language
from runbook.cli.validators import validate_runbook_file_path
from runbook.constants import RUNBOOK_METADATA

Expand Down
Loading