Skip to content

Commit f468706

Browse files
authored
Merge pull request #19 from zph/add-auto-prompting
Add auto prompting during plan if no args supplied
2 parents b1325bc + 03b8434 commit f468706

File tree

11 files changed

+217
-30
lines changed

11 files changed

+217
-30
lines changed

.config/README.md.template

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ In this context, a runbook is a mixture of markdown and executable code that con
2828
1. Plan that runbook for a specific run `runbook plan runbook-name.ipynb --embed file.json --parameters '{"arg": 1, "foo": "baz"}'
2929
1. Run the instance of a runbook with either `runbook run runbook-name.ipynb` or use VSCode to run it `code runbooks/run/runbook-name.ipynb`
3030

31+
## Installation
32+
33+
We recommend using [uv](https://docs.astral.sh/uv/) or [pipx](https://pypi.org/project/pipx/) for installing runbook as a cli tool.
34+
35+
```sh
36+
uv tool install git+https://github.com/zph/runbook.git
37+
# or
38+
pipx install git+https://github.com/zph/runbook.git
39+
```
40+
41+
Or pin to a version
42+
43+
```sh
44+
uv tool install git+https://github.com/zph/runbook.git@v0.0.1
45+
46+
pipx install git+https://github.com/zph/runbook.git@v0.0.1
47+
```
48+
3149
## CLI
3250

3351
```sh
@@ -73,9 +91,6 @@ long as executions run through `runbook run ...` command
7391
## Deno / Typescript
7492
1. Parameter cells should use `let` declarations to allow for param overriding
7593
- This is required to correctly support executing the ts version of notebooks.
76-
1. Confirm/prompt functions always return false in notebooks due to lack of support
77-
in deno kernel. We may invest in upstreaming a patch to support this as it has support
78-
in python notebooks
7994

8095
# Readme Changes
8196

NOTES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
- [ ] Use execute and upload output to S3 for non-interactive: https://github.com/nteract/papermill/tree/main?tab=readme-ov-file#execute-via-cli
1212

1313
## P0
14-
- [ ] Setup versioning and bumper (using versioner from npm ecosystem @release-it and @release-it/bumper)
14+
- [x] Setup versioning and bumper (using versioner from npm ecosystem @release-it and @release-it/bumper)
1515
- [x] Fix tag key getting stripped out of planned runbooks b/c it breaks papermill
1616
- [ ] Setup a watcher for auto-exporting html or other formats
17-
- [ ] Setup tagging in the notebooks to auto-set those values
17+
- [x] Setup tagging in the notebooks to auto-set those values
1818
- [ ] Setup git to autoclear cell outputs for a given folder's notebooks (ie templates) https://stackoverflow.com/a/58004619
1919
- [x] Assess if we need shell completions
2020
- [x] Yes, very nice and use https://click.palletsprojects.com/en/8.1.x/shell-completion/#custom-type-completion to define for custom types, ie only find ipynb files for edit, create, etc

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ Use [pipx](https://github.com/pypa/pipx)
3333
1. Plan that runbook for a specific run `runbook plan runbook-name.ipynb --embed file.json --parameters '{"arg": 1, "foo": "baz"}'
3434
1. Run the instance of a runbook with either `runbook run runbook-name.ipynb` or use VSCode to run it `code runbooks/run/runbook-name.ipynb`
3535

36+
## Installation
37+
38+
We recommend using [uv](https://docs.astral.sh/uv/) or [pipx](https://pypi.org/project/pipx/) for installing runbook as a cli tool.
39+
40+
```sh
41+
uv tool install git+https://github.com/zph/runbook.git
42+
# or
43+
pipx install git+https://github.com/zph/runbook.git
44+
```
45+
46+
Or pin to a version
47+
48+
```sh
49+
uv tool install git+https://github.com/zph/runbook.git@v0.0.1
50+
51+
pipx install git+https://github.com/zph/runbook.git@v0.0.1
52+
```
53+
3654
## CLI
3755

3856
```sh
@@ -95,9 +113,6 @@ long as executions run through `runbook run ...` command
95113
## Deno / Typescript
96114
1. Parameter cells should use `let` declarations to allow for param overriding
97115
- This is required to correctly support executing the ts version of notebooks.
98-
1. Confirm/prompt functions always return false in notebooks due to lack of support
99-
in deno kernel. We may invest in upstreaming a patch to support this as it has support
100-
in python notebooks
101116

102117
# Readme Changes
103118

runbook/cli/commands/create.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from os import path
22

33
import click
4-
from nbconvert.nbconvertapp import NbConvertApp
4+
from runbook.cli.lib import nbconvert_launch_instance
55
from runbook.cli.validators import validate_create_language, validate_template
66

77

@@ -11,15 +11,18 @@
1111
"-t",
1212
"--template",
1313
envvar="TEMPLATE",
14-
default="./runbooks/binder/_template-python.ipynb",
14+
default="./runbooks/binder/_template-deno.ipynb",
1515
type=click.Path(exists=True, file_okay=True),
1616
callback=validate_template,
1717
)
18+
19+
# TODO: switch to language and template defaulting to Deno
20+
# based on operational experience at work
1821
@click.option(
1922
"-l",
2023
"--language",
2124
envvar="LANGUAGE",
22-
default="./runbooks/binder/_template-python.ipynb",
25+
default="deno",
2326
callback=validate_create_language,
2427
)
2528
@click.pass_context
@@ -36,9 +39,7 @@ def create(ctx, filename, template, language):
3639
)
3740
# TODO: remove hardcoding of folder outer name and rely on config file
3841
path.join("runbooks", "binder", filename)
39-
# TODO: hide the nbconvert verbose output?
4042
argv = [
41-
"--ClearOutputPreprocessor.enabled=True",
4243
template,
4344
"--to",
4445
"notebook",
@@ -48,7 +49,7 @@ def create(ctx, filename, template, language):
4849
path.join("runbooks", "binder"),
4950
]
5051

51-
NbConvertApp().launch_instance(argv=argv)
52+
nbconvert_launch_instance(argv, clear_output=True)
5253

5354
click.echo(
5455
click.style(

runbook/cli/commands/plan.py

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,56 @@
1+
import json
12
import os
23
from datetime import datetime
34
from os import path
45
from pathlib import Path
56

67
import click
8+
import nbformat
9+
from runbook.cli.lib import nbconvert_launch_instance
710
from runbook.cli.validators import validate_plan_params, validate_runbook_file_path
11+
from runbook.constants import RUNBOOK_METADATA
812

913
import papermill as pm
1014

1115

12-
# TODO: standardize ids in output files through custom processor
13-
# use something like ulid, we don't need full UUIDs
16+
def get_notebook_language(notebook_path: str) -> str:
17+
"""
18+
Determine the language of the notebook by checking the first code cell's metadata.
19+
Returns 'python', 'typescript', or 'unknown'
20+
"""
21+
nb = nbformat.read(notebook_path, as_version=4)
22+
for cell in nb.cells:
23+
if cell.cell_type == "code":
24+
# Check kernel info
25+
if "kernelspec" in nb.metadata:
26+
kernel_name = nb.metadata.kernelspec.name.lower()
27+
if "python" in kernel_name:
28+
return "python"
29+
elif "typescript" in kernel_name or "ts" in kernel_name:
30+
return "typescript"
31+
# Check language info
32+
if "language_info" in nb.metadata:
33+
language = nb.metadata.language_info.name.lower()
34+
if "python" in language:
35+
return "python"
36+
elif "typescript" in language or "ts" in language:
37+
return "typescript"
38+
return "unknown"
39+
40+
41+
import ast
42+
43+
44+
def get_parser_by_language(language: str):
45+
if language == "typescript":
46+
return json.loads
47+
elif language == "python":
48+
return ast.literal_eval
49+
else:
50+
# Default to json.loads for unknown languages
51+
return json.loads
52+
53+
1454
@click.command()
1555
@click.argument(
1656
"input", type=click.Path(file_okay=True), callback=validate_runbook_file_path
@@ -39,7 +79,7 @@ def plan(ctx, input, embed, identifier="", params={}):
3979
full_output = f"{output_folder}/{output_basename_without_ext}.ipynb"
4080

4181
runbook_param_injection = {
42-
"__RUNBOOK_METADATA__": {
82+
RUNBOOK_METADATA: {
4383
"RUNBOOK_FOLDER": output_folder,
4484
"RUNBOOK_FILE": full_output,
4585
"RUNBOOK_SOURCE": input,
@@ -48,6 +88,31 @@ def plan(ctx, input, embed, identifier="", params={}):
4888
}
4989
}
5090

91+
# TODO: add test cases for auto-planning
92+
# As of 2025 Jan it's manual regression testing
93+
if len(params) == 0:
94+
inferred_params = pm.inspect_notebook(input)
95+
notebook_language = get_notebook_language(input)
96+
value_parser = get_parser_by_language(notebook_language)
97+
# Inferred_type_name is language specific
98+
for key, value in inferred_params.items():
99+
if key != RUNBOOK_METADATA:
100+
default = value["default"].rstrip(";")
101+
typing = value["inferred_type_name"]
102+
type_hint = ""
103+
help_hint = ""
104+
if typing:
105+
type_hint = f" ({typing})"
106+
if value["help"]:
107+
help_hint = f"(hint: {value['help']})"
108+
109+
raw_value = click.prompt(
110+
f"""Enter value for {key}{type_hint}{help_hint}""",
111+
default=default,
112+
value_proc=value_parser,
113+
)
114+
params[key] = raw_value
115+
51116
injection_params = {**runbook_param_injection, **params}
52117

53118
if not Path(output_folder).exists():
@@ -64,17 +129,13 @@ def plan(ctx, input, embed, identifier="", params={}):
64129
)
65130

66131
argv = [
67-
"--ClearOutputPreprocessor.enabled=True",
68132
"--inplace",
69133
full_output,
70134
]
71135

72136
# TODO: join the unified logic of create and plan
73137

74-
# TODO: hide the nbconvert verbose output?
75-
from nbconvert.nbconvertapp import NbConvertApp
76-
77-
NbConvertApp().launch_instance(argv=argv)
138+
nbconvert_launch_instance(argv, clear_output=True)
78139

79140
for f in embed:
80141
shutil.copyfile(src=f, dst=f"{output_folder}/{path.basename(f)}")

runbook/cli/lib.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import hashlib
2+
import sys
3+
from io import StringIO
24

5+
from nbconvert.nbconvertapp import NbConvertApp
36
from ulid import ULID
47

58

@@ -14,3 +17,23 @@ def sha256sum(filename):
1417
# use ULID() directly
1518
def ts_id(length=10):
1619
return str(ULID())[0:length]
20+
21+
22+
# Suppresses nbconvert output
23+
def nbconvert_launch_instance(argv, clear_output=True):
24+
if clear_output:
25+
argv.insert(0, "--ClearOutputPreprocessor.enabled=True")
26+
stdout = sys.stdout
27+
stderr = sys.stderr
28+
sys.stdout = StringIO()
29+
sys.stderr = StringIO()
30+
try:
31+
NbConvertApp().launch_instance(argv=argv)
32+
except Exception as e:
33+
print(sys.stdout.getvalue())
34+
print(sys.stderr.getvalue())
35+
raise e
36+
finally:
37+
# Restore stdout/stderr
38+
sys.stdout = stdout
39+
sys.stderr = stderr

runbook/cli/validators.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from pathlib import Path
55

66
import click
7+
8+
# TODO: use click prompt lib instead of questionary to avoid dependency
79
import questionary
810

911

@@ -26,8 +28,10 @@ def validate_template(ctx, param, value):
2628
def validate_create_language(ctx, param, value):
2729
if value == "./runbooks/binder/_template-python.ipynb":
2830
return value
31+
if value == "./runbooks/binder/_template-deno.ipynb":
32+
return value
2933
if value in ["python", "deno"]:
30-
return f"./runbooks/binder/_template-{value}"
34+
return f"./runbooks/binder/_template-{value}.ipynb"
3135
else:
3236
raise click.BadOptionUsage("--language", "options are python or deno")
3337

@@ -36,6 +40,10 @@ def validate_plan_params(ctx, param, value):
3640
if isinstance(value, dict):
3741
return value
3842

43+
# We allow this for the auto-plan case
44+
if value == "" or value == None:
45+
return {}
46+
3947
try:
4048
v = json.loads(value)
4149
if isinstance(v, dict):

runbook/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
RUNBOOK_METADATA = "__RUNBOOK_METADATA__"

0 commit comments

Comments
 (0)