Skip to content

Commit dab0794

Browse files
committed
v0.4.1: fix notebook install — Python cell + install() function
v0.4.0's notebook install recipe used `%sh python3 genie-install`, which loses the notebook kernel context. The Databricks SDK's runtime-auth provider partially detects "looks like a notebook" (Spark Py4J connects fine in the subprocess) but then fails the IPython context lookup with `'NoneType' object has no attribute 'parent_header'` — and the SDK breaks out of the auth chain instead of falling through to env vars. Result: install fails on the very first turnkey notebook recipe people try. scripts/genie-install: refactor `main()` to delegate to a shared `_run(args, client)` helper and expose a new public entry point: def install(*, shared=False, main=False, version=None, bundle=None, target=None, repo=DEFAULT_REPO, keep_temp=False, client=None) -> int: ... Callable from any Python context. CLI semantics unchanged. docs/install/databricks-genie.md: TL;DR now leads with the Python- cell + runpy pattern: %pip install --quiet databricks-sdk PyYAML import urllib.request, runpy urllib.request.urlretrieve( "https://github.com/easel/helix/releases/latest/download/genie-install", "/tmp/genie_install.py", ) g = runpy.run_path("/tmp/genie_install.py") g["install"]() Plus an explicit warning callout about why `%sh` doesn't work. Tested via runpy.run_path + install() against /Users/.../helix-fn-test: 229/229 uploaded, 14/14 verify checks pass.
1 parent e0f5566 commit dab0794

3 files changed

Lines changed: 76 additions & 16 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helix",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "HELIX methodology, artifact catalog, and routing skill for AI-assisted development.",
55
"author": {
66
"name": "Erik LaBianca",

docs/install/databricks-genie.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,33 @@ design, review — over a project's governing artifacts.
88

99
### From inside a Databricks notebook (recommended — no setup)
1010

11-
Paste this into a notebook cell. The Databricks runtime supplies
12-
implicit workspace credentials; no PAT, env vars, or CLI required.
11+
Paste this into a Python notebook cell. The kernel has implicit
12+
workspace credentials; no PAT, env vars, or CLI required.
1313

1414
```python
15-
%sh
16-
curl -fsSL https://github.com/easel/helix/releases/latest/download/genie-install -o /tmp/genie-install
17-
python3 /tmp/genie-install # user-scoped install
18-
# python3 /tmp/genie-install --shared # workspace-wide (admin)
19-
# python3 /tmp/genie-install --main # track main branch
15+
%pip install --quiet databricks-sdk PyYAML
16+
17+
import urllib.request, runpy
18+
urllib.request.urlretrieve(
19+
"https://github.com/easel/helix/releases/latest/download/genie-install",
20+
"/tmp/genie_install.py",
21+
)
22+
g = runpy.run_path("/tmp/genie_install.py")
23+
24+
g["install"]() # user-scoped (current notebook user)
25+
# g["install"](shared=True) # workspace-wide (admin)
26+
# g["install"](main=True) # track main branch
2027
```
2128

22-
The installer auto-detects the workspace from the notebook's runtime
23-
context and uses your notebook identity for auth. Re-run any time to
24-
refresh.
29+
The `install()` function runs in the notebook's Python kernel where
30+
the Databricks SDK can use implicit notebook-runtime auth. Re-run any
31+
time to refresh.
32+
33+
> **Don't use `%sh`** for the install. A `%sh` subprocess loses the
34+
> notebook's kernel context — the SDK partially detects "looks like a
35+
> notebook" (Spark Py4J connects) but then fails the IPython context
36+
> lookup with `'NoneType' object has no attribute 'parent_header'`.
37+
> Run the installer in a Python cell instead.
2538
2639
### From a dev box or CI
2740

scripts/genie-install

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,14 +438,15 @@ def resolve_target(args: argparse.Namespace, client) -> str:
438438
# --- main -----------------------------------------------------------------
439439

440440

441-
def main() -> int:
442-
args = parse_args()
443-
441+
def _run(args: argparse.Namespace, client=None) -> int:
442+
"""Shared install body. Used by both the CLI entry point (`main`) and
443+
the programmatic entry point (`install`)."""
444444
print(f"HELIX Genie installer")
445445
print()
446446

447-
info("connecting to workspace")
448-
client = make_client()
447+
if client is None:
448+
info("connecting to workspace")
449+
client = make_client()
449450

450451
target = resolve_target(args, client)
451452
print(f"target: {target}")
@@ -481,5 +482,51 @@ def main() -> int:
481482
return 0
482483

483484

485+
def install(
486+
*,
487+
shared: bool = False,
488+
main: bool = False,
489+
version: str | None = None,
490+
bundle: "Path | str | None" = None,
491+
target: str | None = None,
492+
repo: str = DEFAULT_REPO,
493+
keep_temp: bool = False,
494+
client=None,
495+
) -> int:
496+
"""Programmatic entry point. Same semantics as the CLI.
497+
498+
Designed to be invoked from a Databricks notebook cell where the
499+
kernel has implicit workspace credentials:
500+
501+
import urllib.request, runpy
502+
urllib.request.urlretrieve(
503+
"https://github.com/easel/helix/releases/latest/download/genie-install",
504+
"/tmp/genie_install.py",
505+
)
506+
g = runpy.run_path("/tmp/genie_install.py")
507+
g["install"]() # user-scoped, latest release
508+
# g["install"](shared=True) # workspace-wide
509+
# g["install"](main=True) # track main branch
510+
"""
511+
args = argparse.Namespace(
512+
shared=shared,
513+
main=main,
514+
version=version,
515+
bundle=Path(bundle) if bundle else None,
516+
target=target,
517+
repo=repo,
518+
keep_temp=keep_temp,
519+
)
520+
if sum([bool(args.main), bool(args.version), bool(args.bundle)]) > 1:
521+
fail(2, "--main, --version, --bundle are mutually exclusive")
522+
if args.shared and args.target:
523+
fail(2, "--shared and --target are mutually exclusive")
524+
return _run(args, client=client)
525+
526+
527+
def main() -> int:
528+
return _run(parse_args())
529+
530+
484531
if __name__ == "__main__":
485532
sys.exit(main())

0 commit comments

Comments
 (0)