databricks-agent-notebooks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- databricks_agent_notebooks/__init__.py +5 -0
- databricks_agent_notebooks/__main__.py +9 -0
- databricks_agent_notebooks/_constants.py +26 -0
- databricks_agent_notebooks/cli.py +514 -0
- databricks_agent_notebooks/config/__init__.py +1 -0
- databricks_agent_notebooks/config/frontmatter.py +84 -0
- databricks_agent_notebooks/execution/__init__.py +1 -0
- databricks_agent_notebooks/execution/executor.py +112 -0
- databricks_agent_notebooks/execution/injection.py +191 -0
- databricks_agent_notebooks/execution/lineage.py +89 -0
- databricks_agent_notebooks/execution/rendering.py +251 -0
- databricks_agent_notebooks/formats/__init__.py +1 -0
- databricks_agent_notebooks/formats/conversion.py +187 -0
- databricks_agent_notebooks/formats/dbr_source.py +219 -0
- databricks_agent_notebooks/integrations/__init__.py +1 -0
- databricks_agent_notebooks/integrations/databricks/__init__.py +1 -0
- databricks_agent_notebooks/integrations/databricks/clusters.py +128 -0
- databricks_agent_notebooks/runtime/__init__.py +1 -0
- databricks_agent_notebooks/runtime/doctor.py +242 -0
- databricks_agent_notebooks/runtime/home.py +67 -0
- databricks_agent_notebooks/runtime/inventory.py +193 -0
- databricks_agent_notebooks/runtime/kernel.py +577 -0
- databricks_agent_notebooks/runtime/launcher.py +53 -0
- databricks_agent_notebooks/runtime/manifest.py +131 -0
- databricks_agent_notebooks-0.1.0.dist-info/METADATA +39 -0
- databricks_agent_notebooks-0.1.0.dist-info/RECORD +30 -0
- databricks_agent_notebooks-0.1.0.dist-info/WHEEL +5 -0
- databricks_agent_notebooks-0.1.0.dist-info/entry_points.txt +2 -0
- databricks_agent_notebooks-0.1.0.dist-info/licenses/LICENSE +21 -0
- databricks_agent_notebooks-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared constants for the databricks_agent_notebooks library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Kernel metadata per language — single source of truth.
|
|
6
|
+
# Used by _converter.py, _dbr_source.py, and cli.py to set/read kernel metadata.
|
|
7
|
+
KERNELSPECS: dict[str, dict[str, str]] = {
|
|
8
|
+
"python": {
|
|
9
|
+
"name": "python3",
|
|
10
|
+
"display_name": "Python 3",
|
|
11
|
+
"language": "python",
|
|
12
|
+
},
|
|
13
|
+
"scala": {
|
|
14
|
+
"name": "scala212-dbr-connect",
|
|
15
|
+
"display_name": "Scala 2.12 (Databricks Connect)",
|
|
16
|
+
"language": "scala",
|
|
17
|
+
},
|
|
18
|
+
"sql": {
|
|
19
|
+
"name": "python3",
|
|
20
|
+
"display_name": "Python 3 (SQL wrapper)",
|
|
21
|
+
"language": "sql",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
DATABRICKS_CONNECT_VERSION = "16.4.7"
|
|
26
|
+
DATABRICKS_CONNECT_LINE = "16.4"
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""CLI entry point for agent-notebook — normalize, inject, execute, and render notebooks.
|
|
2
|
+
|
|
3
|
+
Provides subcommands for the full notebook pipeline (run), standalone rendering,
|
|
4
|
+
cluster discovery, kernel installation, and environment validation. Follows the
|
|
5
|
+
same argparse/subparsers/dispatch-table pattern as ``libs.continuum.cli``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from databricks_agent_notebooks.config.frontmatter import DatabricksConfig, merge_config
|
|
18
|
+
from databricks_agent_notebooks.execution.executor import execute_notebook
|
|
19
|
+
from databricks_agent_notebooks.execution.injection import inject_cells
|
|
20
|
+
from databricks_agent_notebooks.execution.rendering import render
|
|
21
|
+
from databricks_agent_notebooks.formats.conversion import to_notebook, validate_single_language
|
|
22
|
+
from databricks_agent_notebooks.integrations.databricks.clusters import ClusterError, default_service
|
|
23
|
+
from databricks_agent_notebooks.runtime.doctor import Check, run_checks
|
|
24
|
+
from databricks_agent_notebooks.runtime.inventory import doctor_installed_runtimes, list_installed_runtimes
|
|
25
|
+
from databricks_agent_notebooks.runtime.kernel import (
|
|
26
|
+
KERNEL_DISPLAY_NAME,
|
|
27
|
+
KERNEL_ID,
|
|
28
|
+
install_kernel,
|
|
29
|
+
list_installed_kernels,
|
|
30
|
+
remove_kernel,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
import nbformat
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Parser
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
42
|
+
parser = argparse.ArgumentParser(
|
|
43
|
+
prog="agent-notebook",
|
|
44
|
+
description="Databricks notebook execution: normalize, inject, execute, render.",
|
|
45
|
+
)
|
|
46
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
47
|
+
|
|
48
|
+
# -- run --
|
|
49
|
+
run = subparsers.add_parser("run", help="Normalize, inject, execute, and render a notebook")
|
|
50
|
+
run.add_argument("file", help="Input file (markdown, ipynb, or Databricks source)")
|
|
51
|
+
run.add_argument("--cluster", default=None, help="Cluster name or ID")
|
|
52
|
+
run.add_argument("--profile", default=None, help="Databricks CLI profile")
|
|
53
|
+
run.add_argument("--format", default="all", choices=["all", "md", "html"], dest="fmt", help="Output format (default: all)")
|
|
54
|
+
run.add_argument("--output-dir", default=None, help="Output directory (default: input file's parent)")
|
|
55
|
+
run.add_argument("--timeout", type=int, default=600, help="Per-cell timeout in seconds (default: 600)")
|
|
56
|
+
run.add_argument("--allow-errors", action="store_true", help="Continue execution on cell errors")
|
|
57
|
+
run.add_argument("--no-inject-session", action="store_true", help="Skip Databricks Connect session injection")
|
|
58
|
+
run.add_argument("--language", default=None, help="Override notebook language (python, scala)")
|
|
59
|
+
|
|
60
|
+
# -- clusters --
|
|
61
|
+
clusters = subparsers.add_parser("clusters", help="List Databricks clusters")
|
|
62
|
+
clusters.add_argument("--profile", required=True, help="Databricks CLI profile")
|
|
63
|
+
|
|
64
|
+
# -- install-kernel --
|
|
65
|
+
ik = subparsers.add_parser("install-kernel", help="Install the Databricks Connect Almond kernel")
|
|
66
|
+
ik.add_argument("--kernels-dir", default=None, help="Jupyter kernels directory")
|
|
67
|
+
|
|
68
|
+
# -- kernels --
|
|
69
|
+
kernels = subparsers.add_parser("kernels", help="Manage installed Databricks kernels")
|
|
70
|
+
kernel_subparsers = kernels.add_subparsers(dest="kernels_command", required=True)
|
|
71
|
+
|
|
72
|
+
kernels_install = kernel_subparsers.add_parser("install", help="Install the Databricks Connect Almond kernel")
|
|
73
|
+
kernels_install.add_argument("--id", default=KERNEL_ID, help="Stable kernel identifier")
|
|
74
|
+
kernels_install.add_argument("--display-name", default=KERNEL_DISPLAY_NAME, help="User-facing kernel display name")
|
|
75
|
+
install_location = kernels_install.add_mutually_exclusive_group()
|
|
76
|
+
install_location.add_argument("--user", action="store_true", help="Install into the user Jupyter kernels directory")
|
|
77
|
+
install_location.add_argument("--prefix", default=None, help="Install under PREFIX/share/jupyter/kernels")
|
|
78
|
+
install_location.add_argument("--sys-prefix", action="store_true", help="Install under sys.prefix/share/jupyter/kernels")
|
|
79
|
+
install_location.add_argument("--jupyter-path", default=None, help="Install into an explicit Jupyter kernels directory")
|
|
80
|
+
install_location.add_argument("--kernels-dir", default=None, help=argparse.SUPPRESS)
|
|
81
|
+
kernels_install.add_argument("--force", action="store_true", help="Overwrite an existing kernelspec if present")
|
|
82
|
+
|
|
83
|
+
kernels_list = kernel_subparsers.add_parser("list", help="List installed kernels under runtime-home and overrides")
|
|
84
|
+
kernels_list.add_argument(
|
|
85
|
+
"--jupyter-path",
|
|
86
|
+
action="append",
|
|
87
|
+
default=[],
|
|
88
|
+
help="Additional kernels directory to inspect (can be passed multiple times)",
|
|
89
|
+
)
|
|
90
|
+
kernels_list.add_argument(
|
|
91
|
+
"--kernels-dir",
|
|
92
|
+
action="append",
|
|
93
|
+
default=[],
|
|
94
|
+
help=argparse.SUPPRESS,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
kernels_remove = kernel_subparsers.add_parser("remove", help="Remove a named installed kernel")
|
|
98
|
+
kernels_remove.add_argument("name", help="Kernel directory name to remove")
|
|
99
|
+
kernels_remove.add_argument(
|
|
100
|
+
"--jupyter-path",
|
|
101
|
+
action="append",
|
|
102
|
+
default=[],
|
|
103
|
+
help="Additional kernels directory to search (can be passed multiple times)",
|
|
104
|
+
)
|
|
105
|
+
kernels_remove.add_argument(
|
|
106
|
+
"--kernels-dir",
|
|
107
|
+
action="append",
|
|
108
|
+
default=[],
|
|
109
|
+
help=argparse.SUPPRESS,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
kernels_doctor = kernel_subparsers.add_parser("doctor", help="Validate kernel installation and environment")
|
|
113
|
+
kernels_doctor.add_argument("--id", default=KERNEL_ID, help="Kernel identifier to validate")
|
|
114
|
+
kernels_doctor.add_argument("--profile", default=None, help="Databricks CLI profile to validate")
|
|
115
|
+
kernels_doctor.add_argument("--jupyter-path", default=None, help="Validate an explicit Jupyter kernels directory")
|
|
116
|
+
kernels_doctor.add_argument("--kernels-dir", default=None, help=argparse.SUPPRESS)
|
|
117
|
+
|
|
118
|
+
# -- runtimes --
|
|
119
|
+
runtimes = subparsers.add_parser("runtimes", help="Inspect managed runtimes recorded under runtime-home")
|
|
120
|
+
runtime_subparsers = runtimes.add_subparsers(dest="runtimes_command", required=True)
|
|
121
|
+
|
|
122
|
+
runtime_subparsers.add_parser("list", help="List managed runtimes from runtime-home receipts")
|
|
123
|
+
runtime_subparsers.add_parser("doctor", help="Validate managed runtime receipts and install roots")
|
|
124
|
+
|
|
125
|
+
# -- render --
|
|
126
|
+
rnd = subparsers.add_parser("render", help="Render an already-executed notebook")
|
|
127
|
+
rnd.add_argument("file", help="Path to executed .ipynb notebook")
|
|
128
|
+
rnd.add_argument("--format", default="all", choices=["all", "md", "html"], dest="fmt", help="Output format (default: all)")
|
|
129
|
+
rnd.add_argument("--output-dir", default=None, help="Output directory (default: input file's parent)")
|
|
130
|
+
|
|
131
|
+
# -- doctor --
|
|
132
|
+
doctor = subparsers.add_parser("doctor", help="Validate environment for Databricks Connect")
|
|
133
|
+
doctor.add_argument("--id", default=KERNEL_ID, help="Kernel identifier to validate")
|
|
134
|
+
doctor.add_argument("--profile", default=None, help="Databricks CLI profile to validate")
|
|
135
|
+
|
|
136
|
+
# -- help --
|
|
137
|
+
subparsers.add_parser("help", help="Show usage information")
|
|
138
|
+
|
|
139
|
+
return parser
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Subcommand handlers
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
148
|
+
"""Full pipeline: normalize -> merge config -> inject -> execute -> render."""
|
|
149
|
+
input_path = Path(args.file).resolve()
|
|
150
|
+
if not input_path.is_file():
|
|
151
|
+
print(f"error: file not found: {args.file}", file=sys.stderr)
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
# Step 1: Normalize to notebook
|
|
155
|
+
notebook, frontmatter_config = to_notebook(input_path)
|
|
156
|
+
|
|
157
|
+
# Step 1b: Validate single language (fail fast on mixed-language notebooks)
|
|
158
|
+
try:
|
|
159
|
+
validate_single_language(notebook)
|
|
160
|
+
except ValueError as exc:
|
|
161
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
# Step 2: Merge config
|
|
165
|
+
config = merge_config(
|
|
166
|
+
frontmatter_config or DatabricksConfig(),
|
|
167
|
+
cli_profile=args.profile,
|
|
168
|
+
cli_cluster=args.cluster,
|
|
169
|
+
cli_language=args.language,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Step 3: Resolve cluster
|
|
173
|
+
service = default_service()
|
|
174
|
+
if config.cluster:
|
|
175
|
+
try:
|
|
176
|
+
cluster = service.resolve_cluster(config.cluster, config.profile or "DEFAULT")
|
|
177
|
+
config = DatabricksConfig(
|
|
178
|
+
profile=config.profile,
|
|
179
|
+
cluster=cluster.cluster_id,
|
|
180
|
+
language=config.language,
|
|
181
|
+
)
|
|
182
|
+
except ClusterError as exc:
|
|
183
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
else:
|
|
186
|
+
print(
|
|
187
|
+
"No cluster specified — using serverless compute "
|
|
188
|
+
"(Beta for Scala, some limitations apply).",
|
|
189
|
+
file=sys.stderr,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Step 4: Inject session setup
|
|
193
|
+
if not args.no_inject_session:
|
|
194
|
+
notebook = inject_cells(notebook, config, input_path)
|
|
195
|
+
|
|
196
|
+
# Step 5: Set up output directory
|
|
197
|
+
stem = input_path.stem
|
|
198
|
+
output_dir = Path(args.output_dir).resolve() if args.output_dir else input_path.parent
|
|
199
|
+
run_output_dir = output_dir / f"{stem}_output"
|
|
200
|
+
run_output_dir.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
|
|
202
|
+
# Step 6: Write notebook to temp file for execution
|
|
203
|
+
with tempfile.NamedTemporaryFile(
|
|
204
|
+
suffix=".ipynb", dir=str(run_output_dir), delete=False, mode="w"
|
|
205
|
+
) as tmp:
|
|
206
|
+
nbformat.write(notebook, tmp)
|
|
207
|
+
temp_notebook = Path(tmp.name)
|
|
208
|
+
|
|
209
|
+
# Step 7: If input was not .ipynb, save pre-execution notebook
|
|
210
|
+
if input_path.suffix.lower() != ".ipynb":
|
|
211
|
+
pre_exec_path = run_output_dir / f"{stem}.ipynb"
|
|
212
|
+
shutil.copy2(temp_notebook, pre_exec_path)
|
|
213
|
+
|
|
214
|
+
# Step 8: Execute — use the kernel from the notebook's own metadata
|
|
215
|
+
kernel_name = notebook.metadata.get("kernelspec", {}).get("name", "scala212-dbr-connect")
|
|
216
|
+
result = execute_notebook(
|
|
217
|
+
temp_notebook,
|
|
218
|
+
kernel=kernel_name,
|
|
219
|
+
timeout=args.timeout,
|
|
220
|
+
allow_errors=args.allow_errors,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Step 9: Copy executed notebook to output dir
|
|
224
|
+
executed_path = run_output_dir / f"{stem}.executed.ipynb"
|
|
225
|
+
if result.output_path and result.output_path.is_file():
|
|
226
|
+
shutil.copy2(result.output_path, executed_path)
|
|
227
|
+
elif result.success:
|
|
228
|
+
# output_path might be the same as the temp file
|
|
229
|
+
shutil.copy2(temp_notebook, executed_path)
|
|
230
|
+
|
|
231
|
+
# Step 10: Render
|
|
232
|
+
if executed_path.is_file():
|
|
233
|
+
render_paths = render(executed_path, run_output_dir, args.fmt)
|
|
234
|
+
else:
|
|
235
|
+
render_paths = {}
|
|
236
|
+
|
|
237
|
+
# Step 11: Clean up temp file
|
|
238
|
+
try:
|
|
239
|
+
temp_notebook.unlink(missing_ok=True)
|
|
240
|
+
# Also clean up the default executed output if it exists alongside temp
|
|
241
|
+
default_executed = temp_notebook.with_suffix(".executed.ipynb")
|
|
242
|
+
if default_executed.is_file() and default_executed != executed_path:
|
|
243
|
+
default_executed.unlink(missing_ok=True)
|
|
244
|
+
except OSError:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
# Step 12: Print summary
|
|
248
|
+
if result.success:
|
|
249
|
+
print(f"Execution succeeded ({result.duration_seconds:.1f}s)")
|
|
250
|
+
else:
|
|
251
|
+
print(f"Execution failed ({result.duration_seconds:.1f}s): {result.error}", file=sys.stderr)
|
|
252
|
+
|
|
253
|
+
print(f"Output directory: {run_output_dir}")
|
|
254
|
+
for fmt_name, path in render_paths.items():
|
|
255
|
+
print(f" {fmt_name}: {path}")
|
|
256
|
+
|
|
257
|
+
return 0 if result.success else 1
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _cmd_clusters(args: argparse.Namespace) -> int:
|
|
261
|
+
"""List available Databricks clusters."""
|
|
262
|
+
service = default_service()
|
|
263
|
+
try:
|
|
264
|
+
clusters = service.list_clusters(args.profile)
|
|
265
|
+
except ClusterError as exc:
|
|
266
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
267
|
+
return 1
|
|
268
|
+
|
|
269
|
+
if not clusters:
|
|
270
|
+
print("No clusters found.", file=sys.stderr)
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
# Header
|
|
274
|
+
print(f"{'NAME':<40} {'STATE':<12} {'ID'}")
|
|
275
|
+
print("-" * 80)
|
|
276
|
+
for c in clusters:
|
|
277
|
+
print(f"{c.cluster_name:<40} {c.state:<12} {c.cluster_id}")
|
|
278
|
+
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _cmd_install_kernel(args: argparse.Namespace) -> int:
|
|
283
|
+
shim_args = argparse.Namespace(
|
|
284
|
+
id=KERNEL_ID,
|
|
285
|
+
display_name=KERNEL_DISPLAY_NAME,
|
|
286
|
+
kernels_dir=args.kernels_dir,
|
|
287
|
+
user=False,
|
|
288
|
+
prefix=None,
|
|
289
|
+
sys_prefix=False,
|
|
290
|
+
jupyter_path=None,
|
|
291
|
+
force=True,
|
|
292
|
+
)
|
|
293
|
+
return _cmd_kernels_install(shim_args)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _resolve_kernel_dir_args(args: argparse.Namespace) -> list[Path]:
|
|
297
|
+
raw_dirs = []
|
|
298
|
+
for attribute in ("kernels_dir", "jupyter_path"):
|
|
299
|
+
value = getattr(args, attribute, None)
|
|
300
|
+
if value is None:
|
|
301
|
+
continue
|
|
302
|
+
if isinstance(value, list):
|
|
303
|
+
raw_dirs.extend(value)
|
|
304
|
+
else:
|
|
305
|
+
raw_dirs.append(value)
|
|
306
|
+
|
|
307
|
+
resolved: list[Path] = []
|
|
308
|
+
seen: set[Path] = set()
|
|
309
|
+
for raw_dir in raw_dirs:
|
|
310
|
+
path = Path(raw_dir)
|
|
311
|
+
if path in seen:
|
|
312
|
+
continue
|
|
313
|
+
resolved.append(path)
|
|
314
|
+
seen.add(path)
|
|
315
|
+
return resolved
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _resolve_single_kernel_dir(value: str | None) -> Path | None:
|
|
319
|
+
return Path(value) if value is not None else None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _cmd_kernels_install(args: argparse.Namespace) -> int:
|
|
323
|
+
"""Install the Databricks Connect Almond kernel."""
|
|
324
|
+
try:
|
|
325
|
+
kernel_dir = install_kernel(
|
|
326
|
+
kernel_id=getattr(args, "id", KERNEL_ID),
|
|
327
|
+
display_name=getattr(args, "display_name", KERNEL_DISPLAY_NAME),
|
|
328
|
+
kernels_dir=_resolve_single_kernel_dir(getattr(args, "kernels_dir", None)),
|
|
329
|
+
user=getattr(args, "user", False),
|
|
330
|
+
prefix=_resolve_single_kernel_dir(getattr(args, "prefix", None)),
|
|
331
|
+
sys_prefix=getattr(args, "sys_prefix", False),
|
|
332
|
+
jupyter_path=_resolve_single_kernel_dir(getattr(args, "jupyter_path", None)),
|
|
333
|
+
force=getattr(args, "force", False),
|
|
334
|
+
)
|
|
335
|
+
print(f"Kernel installed: {kernel_dir}")
|
|
336
|
+
return 0
|
|
337
|
+
except (RuntimeError, ValueError, subprocess.CalledProcessError) as exc:
|
|
338
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _cmd_kernels_list(args: argparse.Namespace) -> int:
|
|
343
|
+
"""List installed kernels from runtime-home and any explicit override dirs."""
|
|
344
|
+
kernels = list_installed_kernels(kernels_dirs=_resolve_kernel_dir_args(args))
|
|
345
|
+
if not kernels:
|
|
346
|
+
print("No kernels installed.")
|
|
347
|
+
return 0
|
|
348
|
+
|
|
349
|
+
print(f"{'NAME':<24} {'SOURCE':<20} {'RUNTIME':<24} {'LAUNCHER':<18} {'CONTRACT':<18} {'RECEIPT':<18} DIRECTORY")
|
|
350
|
+
for kernel in kernels:
|
|
351
|
+
runtime_id = kernel.runtime_id or "missing"
|
|
352
|
+
launcher = kernel.launcher_path or "missing"
|
|
353
|
+
contract = str(kernel.launcher_contract_path) if kernel.launcher_contract_path is not None else "missing"
|
|
354
|
+
receipt = str(kernel.receipt_path) if kernel.receipt_path is not None else "missing"
|
|
355
|
+
print(
|
|
356
|
+
f"{kernel.name:<24} {kernel.source:<20} {runtime_id:<24} "
|
|
357
|
+
f"{launcher:<18} {contract:<18} {receipt:<18} {kernel.directory}"
|
|
358
|
+
)
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _cmd_kernels_remove(args: argparse.Namespace) -> int:
|
|
363
|
+
"""Remove a named installed kernel safely."""
|
|
364
|
+
try:
|
|
365
|
+
removed_dir = remove_kernel(args.name, kernels_dirs=_resolve_kernel_dir_args(args))
|
|
366
|
+
print(f"Kernel removed: {removed_dir}")
|
|
367
|
+
return 0
|
|
368
|
+
except (FileNotFoundError, RuntimeError, ValueError) as exc:
|
|
369
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
370
|
+
return 1
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _cmd_render(args: argparse.Namespace) -> int:
|
|
374
|
+
"""Render an already-executed notebook."""
|
|
375
|
+
input_path = Path(args.file).resolve()
|
|
376
|
+
if not input_path.is_file():
|
|
377
|
+
print(f"error: file not found: {args.file}", file=sys.stderr)
|
|
378
|
+
return 1
|
|
379
|
+
|
|
380
|
+
output_dir = Path(args.output_dir).resolve() if args.output_dir else input_path.parent
|
|
381
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
render_paths = render(input_path, output_dir, args.fmt)
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
387
|
+
return 1
|
|
388
|
+
|
|
389
|
+
for fmt_name, path in render_paths.items():
|
|
390
|
+
print(f"{fmt_name}: {path}")
|
|
391
|
+
|
|
392
|
+
return 0
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
396
|
+
return _cmd_kernels_doctor(args)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _cmd_kernels_doctor(args: argparse.Namespace) -> int:
|
|
400
|
+
"""Run environment validation checks."""
|
|
401
|
+
kernel_dirs = _resolve_kernel_dir_args(args)
|
|
402
|
+
if kernel_dirs:
|
|
403
|
+
checks = run_checks(profile=args.profile, kernels_dir=kernel_dirs[0], kernel_id=getattr(args, "id", KERNEL_ID))
|
|
404
|
+
else:
|
|
405
|
+
checks = run_checks(profile=args.profile, kernel_id=getattr(args, "id", KERNEL_ID))
|
|
406
|
+
|
|
407
|
+
status_symbols = {"ok": "[ok]", "warn": "[!!]", "fail": "[FAIL]"}
|
|
408
|
+
|
|
409
|
+
for check in checks:
|
|
410
|
+
symbol = status_symbols.get(check.status, "[??]")
|
|
411
|
+
print(f" {symbol} {check.name}: {check.message}")
|
|
412
|
+
|
|
413
|
+
failures = [c for c in checks if c.status == "fail"]
|
|
414
|
+
if failures:
|
|
415
|
+
print(f"\n{len(failures)} check(s) failed.", file=sys.stderr)
|
|
416
|
+
return 1
|
|
417
|
+
|
|
418
|
+
print("\nAll checks passed.")
|
|
419
|
+
return 0
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _cmd_kernels(args: argparse.Namespace) -> int:
|
|
423
|
+
handler = _KERNEL_HANDLERS.get(args.kernels_command)
|
|
424
|
+
if handler is None:
|
|
425
|
+
print(f"error: unknown kernels command: {args.kernels_command}", file=sys.stderr)
|
|
426
|
+
return 1
|
|
427
|
+
return handler(args)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _cmd_runtimes_list(_args: argparse.Namespace) -> int:
|
|
431
|
+
"""List managed runtimes discovered from runtime-home receipts."""
|
|
432
|
+
runtimes = list_installed_runtimes()
|
|
433
|
+
if not runtimes:
|
|
434
|
+
print("No runtimes installed.")
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
print(f"{'RUNTIME ID':<24} {'STATUS':<16} {'DBR':<8} {'PYTHON':<8} {'RECEIPT':<18} INSTALL ROOT")
|
|
438
|
+
for runtime in runtimes:
|
|
439
|
+
print(
|
|
440
|
+
f"{runtime.runtime_id:<24} {runtime.status:<16} {runtime.databricks_line:<8} "
|
|
441
|
+
f"{runtime.python_line:<8} {runtime.receipt_path!s:<18} {runtime.install_root}"
|
|
442
|
+
)
|
|
443
|
+
return 0
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _cmd_runtimes_doctor(_args: argparse.Namespace) -> int:
|
|
447
|
+
"""Validate managed runtime receipts rooted under runtime-home."""
|
|
448
|
+
checks = doctor_installed_runtimes()
|
|
449
|
+
status_symbols = {"ok": "[ok]", "warn": "[!!]", "fail": "[FAIL]"}
|
|
450
|
+
|
|
451
|
+
for check in checks:
|
|
452
|
+
symbol = status_symbols.get(check.status, "[??]")
|
|
453
|
+
print(f" {symbol} {check.name}: {check.message}")
|
|
454
|
+
|
|
455
|
+
failures = [check for check in checks if check.status == "fail"]
|
|
456
|
+
if failures:
|
|
457
|
+
print(f"\n{len(failures)} check(s) failed.", file=sys.stderr)
|
|
458
|
+
return 1
|
|
459
|
+
|
|
460
|
+
print("\nAll checks passed.")
|
|
461
|
+
return 0
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _cmd_runtimes(args: argparse.Namespace) -> int:
|
|
465
|
+
handler = _RUNTIME_HANDLERS.get(args.runtimes_command)
|
|
466
|
+
if handler is None:
|
|
467
|
+
print(f"error: unknown runtimes command: {args.runtimes_command}", file=sys.stderr)
|
|
468
|
+
return 1
|
|
469
|
+
return handler(args)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
# Dispatch table
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
_HANDLERS = {
|
|
478
|
+
"run": _cmd_run,
|
|
479
|
+
"clusters": _cmd_clusters,
|
|
480
|
+
"install-kernel": _cmd_install_kernel,
|
|
481
|
+
"kernels": _cmd_kernels,
|
|
482
|
+
"runtimes": _cmd_runtimes,
|
|
483
|
+
"render": _cmd_render,
|
|
484
|
+
"doctor": _cmd_doctor,
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
_KERNEL_HANDLERS = {
|
|
488
|
+
"install": _cmd_kernels_install,
|
|
489
|
+
"list": _cmd_kernels_list,
|
|
490
|
+
"remove": _cmd_kernels_remove,
|
|
491
|
+
"doctor": _cmd_kernels_doctor,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
_RUNTIME_HANDLERS = {
|
|
495
|
+
"list": _cmd_runtimes_list,
|
|
496
|
+
"doctor": _cmd_runtimes_doctor,
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def main(argv: list[str] | None = None) -> int:
|
|
501
|
+
"""Entry point for ``python -m databricks_agent_notebooks`` and ``bin/agent-notebook``."""
|
|
502
|
+
parser = _build_parser()
|
|
503
|
+
args = parser.parse_args(argv)
|
|
504
|
+
|
|
505
|
+
if args.command is None or args.command == "help":
|
|
506
|
+
parser.print_help()
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
handler = _HANDLERS.get(args.command)
|
|
510
|
+
if handler is None:
|
|
511
|
+
print(f"error: unknown command: {args.command}", file=sys.stderr)
|
|
512
|
+
return 1
|
|
513
|
+
|
|
514
|
+
return handler(args)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration surfaces for databricks_agent_notebooks."""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""YAML frontmatter parsing for Databricks notebook markdown files.
|
|
2
|
+
|
|
3
|
+
Extracts Databricks connection configuration (profile, cluster, language)
|
|
4
|
+
from ``---`` delimited YAML frontmatter blocks, and merges with CLI overrides.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class DatabricksConfig:
|
|
19
|
+
"""Immutable Databricks connection parameters extracted from frontmatter.
|
|
20
|
+
|
|
21
|
+
All fields default to None, meaning "not specified" — callers decide
|
|
22
|
+
what defaults to apply when a field is absent.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
profile: str | None = None
|
|
26
|
+
cluster: str | None = None
|
|
27
|
+
language: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---", re.DOTALL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_frontmatter(path: Path) -> DatabricksConfig:
|
|
34
|
+
"""Parse YAML frontmatter from a markdown file and extract Databricks config.
|
|
35
|
+
|
|
36
|
+
Looks for a ``---`` delimited YAML block at the very start of the file.
|
|
37
|
+
Within that block, reads the ``databricks:`` mapping for ``profile``,
|
|
38
|
+
``cluster``, and ``language`` keys.
|
|
39
|
+
|
|
40
|
+
Returns a default (all-None) config when:
|
|
41
|
+
- The file has no frontmatter block
|
|
42
|
+
- The frontmatter has no ``databricks:`` key
|
|
43
|
+
- The ``databricks:`` value is not a mapping
|
|
44
|
+
"""
|
|
45
|
+
text = path.read_text(encoding="utf-8")
|
|
46
|
+
match = _FRONTMATTER_RE.match(text)
|
|
47
|
+
if match is None:
|
|
48
|
+
return DatabricksConfig()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
parsed: Any = yaml.safe_load(match.group(1))
|
|
52
|
+
except yaml.YAMLError:
|
|
53
|
+
return DatabricksConfig()
|
|
54
|
+
|
|
55
|
+
if not isinstance(parsed, dict):
|
|
56
|
+
return DatabricksConfig()
|
|
57
|
+
|
|
58
|
+
db = parsed.get("databricks")
|
|
59
|
+
if not isinstance(db, dict):
|
|
60
|
+
return DatabricksConfig()
|
|
61
|
+
|
|
62
|
+
return DatabricksConfig(
|
|
63
|
+
profile=db.get("profile"),
|
|
64
|
+
cluster=db.get("cluster"),
|
|
65
|
+
language=db.get("language"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def merge_config(
|
|
70
|
+
frontmatter: DatabricksConfig,
|
|
71
|
+
cli_profile: str | None = None,
|
|
72
|
+
cli_cluster: str | None = None,
|
|
73
|
+
cli_language: str | None = None,
|
|
74
|
+
) -> DatabricksConfig:
|
|
75
|
+
"""Merge frontmatter config with CLI overrides.
|
|
76
|
+
|
|
77
|
+
CLI arguments take precedence: a non-None CLI value always wins.
|
|
78
|
+
When the CLI value is None, the frontmatter value is preserved.
|
|
79
|
+
"""
|
|
80
|
+
return DatabricksConfig(
|
|
81
|
+
profile=cli_profile if cli_profile is not None else frontmatter.profile,
|
|
82
|
+
cluster=cli_cluster if cli_cluster is not None else frontmatter.cluster,
|
|
83
|
+
language=cli_language if cli_language is not None else frontmatter.language,
|
|
84
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Notebook execution, injection, rendering, and lineage helpers."""
|