databricks-agent-notebooks 0.2.2__tar.gz → 0.4.0__tar.gz
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-0.2.2 → databricks_agent_notebooks-0.4.0}/PKG-INFO +1 -1
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/pyproject.toml +8 -1
- databricks_agent_notebooks-0.4.0/src/databricks_agent_notebooks/__init__.py +10 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/_constants.py +9 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/cli.py +120 -32
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/config/frontmatter.py +11 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/execution/executor.py +63 -19
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/execution/injection.py +130 -13
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/execution/rendering.py +68 -4
- databricks_agent_notebooks-0.4.0/src/databricks_agent_notebooks/for_agents/README.md +497 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/agent_doctor.md +57 -28
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/scala_development.md +49 -0
- databricks_agent_notebooks-0.4.0/src/databricks_agent_notebooks/for_agents/scripts/agent-nb-run.sh +122 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/connect.py +27 -2
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/doctor.py +44 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks.egg-info/PKG-INFO +1 -1
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks.egg-info/SOURCES.txt +1 -0
- databricks_agent_notebooks-0.2.2/src/databricks_agent_notebooks/__init__.py +0 -5
- databricks_agent_notebooks-0.2.2/src/databricks_agent_notebooks/for_agents/README.md +0 -289
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/LICENSE +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/README.md +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/setup.cfg +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/__main__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/config/__init__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/execution/__init__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/execution/lineage.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/examples/scala/package_cells_scaffold.md +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/examples/scala/src/api/implementation/Api.scala +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/examples/scala/src/api/stable/Api.scala +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/examples/smoke/python_select_one.md +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/examples/smoke/scala_select_one.md +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/for_agents/package_cell_instructions.md +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/formats/__init__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/formats/conversion.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/formats/dbr_source.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/integrations/__init__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/integrations/databricks/__init__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/integrations/databricks/clusters.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/__init__.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/home.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/inventory.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/kernel.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/launcher.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/manifest.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks/runtime/scala_connect.py +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks.egg-info/dependency_links.txt +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks.egg-info/entry_points.txt +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks.egg-info/requires.txt +0 -0
- {databricks_agent_notebooks-0.2.2 → databricks_agent_notebooks-0.4.0}/src/databricks_agent_notebooks.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "databricks-agent-notebooks"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Standalone notebook conversion and local runtime tooling for the databricks-agent-notebooks repository."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -52,6 +52,13 @@ agent-notebook = "databricks_agent_notebooks.cli:main"
|
|
|
52
52
|
testpaths = ["tests"]
|
|
53
53
|
addopts = "-q"
|
|
54
54
|
pythonpath = ["src"]
|
|
55
|
+
markers = [
|
|
56
|
+
"integration: Fast integration tests (CLI smoke, no side effects)",
|
|
57
|
+
"slow: Slow tests (kernel downloads, package installs, real execution)",
|
|
58
|
+
"spark: Tests requiring local Spark execution (PySpark or Almond kernel)",
|
|
59
|
+
"databricks: Tests requiring live Databricks workspace credentials",
|
|
60
|
+
"scala: Tests requiring Almond/Scala kernel and Java",
|
|
61
|
+
]
|
|
55
62
|
|
|
56
63
|
[tool.setuptools]
|
|
57
64
|
package-dir = { "" = "src" }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Standalone Databricks notebook conversion and execution tooling."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("databricks-agent-notebooks")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.0.0-dev"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -103,3 +103,12 @@ def scala_variant_for_dbr(major: int) -> ScalaVariant:
|
|
|
103
103
|
if major >= 17:
|
|
104
104
|
return SCALA_213
|
|
105
105
|
return SCALA_212
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# LOCAL_SPARK — reserved profile for local PySpark / vanilla Spark execution
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
LOCAL_SPARK_PROFILE = "LOCAL_SPARK"
|
|
113
|
+
LOCAL_SPARK_DEFAULT_MASTER = "local[*]"
|
|
114
|
+
LOCAL_SPARK_DEFAULT_VERSION = "3.5.4"
|
|
@@ -8,6 +8,7 @@ same argparse/subparsers/dispatch-table pattern as ``libs.continuum.cli``.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
|
+
import re
|
|
11
12
|
import shutil
|
|
12
13
|
import subprocess
|
|
13
14
|
import sys
|
|
@@ -15,7 +16,8 @@ import tempfile
|
|
|
15
16
|
from importlib import resources
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
18
|
-
from databricks_agent_notebooks
|
|
19
|
+
from databricks_agent_notebooks import __version__
|
|
20
|
+
from databricks_agent_notebooks.config.frontmatter import DatabricksConfig, is_local_spark, merge_config
|
|
19
21
|
from databricks_agent_notebooks.execution.executor import RawProgressValue, emit_progress_signal, execute_notebook
|
|
20
22
|
from databricks_agent_notebooks.execution.injection import inject_cells
|
|
21
23
|
from databricks_agent_notebooks.execution.rendering import render
|
|
@@ -23,7 +25,14 @@ from databricks_agent_notebooks.formats.conversion import to_notebook, validate_
|
|
|
23
25
|
from databricks_agent_notebooks.integrations.databricks.clusters import ClusterError, default_service
|
|
24
26
|
from databricks_agent_notebooks.runtime.connect import ensure_cluster_runtime, ensure_serverless_runtime
|
|
25
27
|
from databricks_agent_notebooks.runtime.home import resolve_runtime_home
|
|
26
|
-
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
from databricks_agent_notebooks._constants import (
|
|
31
|
+
DEFAULT_SCALA_VARIANT,
|
|
32
|
+
LOCAL_SPARK_DEFAULT_VERSION,
|
|
33
|
+
SCALA_212,
|
|
34
|
+
SCALA_VARIANTS,
|
|
35
|
+
)
|
|
27
36
|
from databricks_agent_notebooks.runtime.scala_connect import prefetch_scala_connect, resolve_scala_connect
|
|
28
37
|
from databricks_agent_notebooks.runtime.doctor import Check, doctor_scala_connect_readiness, run_checks
|
|
29
38
|
from databricks_agent_notebooks.runtime.inventory import doctor_installed_runtimes, list_installed_runtimes
|
|
@@ -39,6 +48,30 @@ from databricks_agent_notebooks.runtime.kernel import (
|
|
|
39
48
|
|
|
40
49
|
import nbformat
|
|
41
50
|
|
|
51
|
+
_VALID_SCALA_LOCAL_MASTER_RE = re.compile(r"^local(\[(\*|\d+)(,\d+)?\])?$")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _validate_scala_local_spark(master: str, executor_memory: str | None) -> str | None:
|
|
55
|
+
"""Validate LOCAL_SPARK configuration for Scala notebooks.
|
|
56
|
+
|
|
57
|
+
Returns an error message string if validation fails, or None if valid.
|
|
58
|
+
"""
|
|
59
|
+
if not _VALID_SCALA_LOCAL_MASTER_RE.match(master):
|
|
60
|
+
return (
|
|
61
|
+
f"error: Spark master URL '{master}' is not supported for Scala notebooks. "
|
|
62
|
+
"Scala LOCAL_SPARK only supports local[*], local[N], local[N,M], or local. "
|
|
63
|
+
"The local-cluster mode is incompatible with the Almond kernel's classloader "
|
|
64
|
+
"(ClassNotFoundException in executors). "
|
|
65
|
+
"Use local[N] for parallelism control instead."
|
|
66
|
+
)
|
|
67
|
+
if executor_memory:
|
|
68
|
+
return (
|
|
69
|
+
"error: AGENT_NOTEBOOK_LOCAL_SPARK_EXECUTOR_MEMORY is not supported for Scala notebooks. "
|
|
70
|
+
"In local mode there are no separate executor processes — the driver IS the executor. "
|
|
71
|
+
"Use AGENT_NOTEBOOK_LOCAL_SPARK_DRIVER_MEMORY to control the total JVM heap size instead."
|
|
72
|
+
)
|
|
73
|
+
return None
|
|
74
|
+
|
|
42
75
|
|
|
43
76
|
# ---------------------------------------------------------------------------
|
|
44
77
|
# Parser
|
|
@@ -75,6 +108,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
75
108
|
epilog=_agent_docs_epilog(),
|
|
76
109
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
77
110
|
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--version", action="version",
|
|
113
|
+
version=f"%(prog)s {__version__}",
|
|
114
|
+
)
|
|
78
115
|
subparsers = parser.add_subparsers(dest="command")
|
|
79
116
|
|
|
80
117
|
# -- run --
|
|
@@ -207,9 +244,85 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
207
244
|
language = _resolve_execution_language(notebook, config)
|
|
208
245
|
inject_session = not args.no_inject_session
|
|
209
246
|
|
|
247
|
+
# Step 2b: LOCAL_SPARK validation and branching
|
|
248
|
+
local_spark = is_local_spark(config)
|
|
249
|
+
if local_spark and config.cluster:
|
|
250
|
+
print("error: --profile LOCAL_SPARK and --cluster are mutually exclusive", file=sys.stderr)
|
|
251
|
+
return 1
|
|
252
|
+
|
|
210
253
|
# Step 3: Resolve cluster
|
|
211
254
|
cluster = None
|
|
212
|
-
if
|
|
255
|
+
if local_spark and inject_session:
|
|
256
|
+
emit_progress_signal("compute", mode=RawProgressValue("local-spark"))
|
|
257
|
+
# Pre-flight: verify pyspark is importable for Python LOCAL_SPARK
|
|
258
|
+
if language != "scala":
|
|
259
|
+
try:
|
|
260
|
+
import pyspark # noqa: F401, PLC0415
|
|
261
|
+
except ImportError:
|
|
262
|
+
print(
|
|
263
|
+
"error: pyspark is required for Python LOCAL_SPARK but not importable. "
|
|
264
|
+
"Install it with: pip install pyspark (or uv pip install pyspark)",
|
|
265
|
+
file=sys.stderr,
|
|
266
|
+
)
|
|
267
|
+
return 1
|
|
268
|
+
# Validate and set up Scala LOCAL_SPARK
|
|
269
|
+
if language == "scala":
|
|
270
|
+
local_spark_master = os.environ.get("AGENT_NOTEBOOK_LOCAL_SPARK_MASTER", "local[*]")
|
|
271
|
+
executor_memory = os.environ.get("AGENT_NOTEBOOK_LOCAL_SPARK_EXECUTOR_MEMORY")
|
|
272
|
+
validation_error = _validate_scala_local_spark(local_spark_master, executor_memory)
|
|
273
|
+
if validation_error:
|
|
274
|
+
print(validation_error, file=sys.stderr)
|
|
275
|
+
return 1
|
|
276
|
+
spark_ver = os.environ.get(
|
|
277
|
+
"AGENT_NOTEBOOK_LOCAL_SPARK_VERSION", LOCAL_SPARK_DEFAULT_VERSION,
|
|
278
|
+
)
|
|
279
|
+
scala_variant = SCALA_VARIANTS.get(
|
|
280
|
+
"2.13" if spark_ver.startswith("4.") else "2.12", SCALA_212,
|
|
281
|
+
)
|
|
282
|
+
notebook.metadata["kernelspec"] = {
|
|
283
|
+
"name": scala_variant.kernel_id,
|
|
284
|
+
"display_name": scala_variant.kernel_display_name,
|
|
285
|
+
"language": "scala",
|
|
286
|
+
}
|
|
287
|
+
# Spark 3.x on Java 17+ needs --add-opens for the Almond kernel JVM.
|
|
288
|
+
# PySpark handles this in its own launcher; for Scala we propagate via
|
|
289
|
+
# JDK_JAVA_OPTIONS (standard since Java 9). Unlike JAVA_TOOL_OPTIONS,
|
|
290
|
+
# JDK_JAVA_OPTIONS does not print "Picked up ..." to stderr.
|
|
291
|
+
existing_jdk = os.environ.get("JDK_JAVA_OPTIONS", "")
|
|
292
|
+
existing_jto = os.environ.get("JAVA_TOOL_OPTIONS", "")
|
|
293
|
+
if spark_ver.startswith("3.") and "--add-opens" not in existing_jdk and "--add-opens" not in existing_jto:
|
|
294
|
+
_opens = (
|
|
295
|
+
"--add-opens=java.base/java.lang=ALL-UNNAMED "
|
|
296
|
+
"--add-opens=java.base/java.lang.invoke=ALL-UNNAMED "
|
|
297
|
+
"--add-opens=java.base/java.lang.reflect=ALL-UNNAMED "
|
|
298
|
+
"--add-opens=java.base/java.io=ALL-UNNAMED "
|
|
299
|
+
"--add-opens=java.base/java.net=ALL-UNNAMED "
|
|
300
|
+
"--add-opens=java.base/java.nio=ALL-UNNAMED "
|
|
301
|
+
"--add-opens=java.base/java.util=ALL-UNNAMED "
|
|
302
|
+
"--add-opens=java.base/java.util.concurrent=ALL-UNNAMED "
|
|
303
|
+
"--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED "
|
|
304
|
+
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED "
|
|
305
|
+
"--add-opens=java.base/sun.nio.cs=ALL-UNNAMED "
|
|
306
|
+
"--add-opens=java.base/sun.security.action=ALL-UNNAMED "
|
|
307
|
+
"--add-opens=java.base/sun.util.calendar=ALL-UNNAMED "
|
|
308
|
+
"--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED"
|
|
309
|
+
)
|
|
310
|
+
os.environ["JDK_JAVA_OPTIONS"] = f"{existing_jdk} {_opens}".strip()
|
|
311
|
+
# Inject driver memory as -Xmx into JDK_JAVA_OPTIONS for Scala.
|
|
312
|
+
# The SparkConf injection (in injection.py) generates
|
|
313
|
+
# spark.driver.memory but that is decorative for Scala — the JVM
|
|
314
|
+
# is already running by the time SparkSession.builder reads it.
|
|
315
|
+
existing_jdk = os.environ.get("JDK_JAVA_OPTIONS", "")
|
|
316
|
+
driver_memory = os.environ.get("AGENT_NOTEBOOK_LOCAL_SPARK_DRIVER_MEMORY")
|
|
317
|
+
if driver_memory and "-Xmx" not in existing_jdk and "-Xmx" not in existing_jto:
|
|
318
|
+
os.environ["JDK_JAVA_OPTIONS"] = f"{existing_jdk} -Xmx{driver_memory}".strip()
|
|
319
|
+
print(
|
|
320
|
+
f"warning: In local mode, spark.driver.memory controls the total JVM heap "
|
|
321
|
+
f"shared by driver and all task threads — there are no separate executor "
|
|
322
|
+
f"processes. -Xmx{driver_memory} has been set on the JVM.",
|
|
323
|
+
file=sys.stderr,
|
|
324
|
+
)
|
|
325
|
+
elif config.cluster and inject_session:
|
|
213
326
|
service = default_service()
|
|
214
327
|
try:
|
|
215
328
|
effective_profile = config.profile or "DEFAULT"
|
|
@@ -224,7 +337,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
224
337
|
emit_progress_signal("failed", error=str(exc))
|
|
225
338
|
print(f"error: {exc}", file=sys.stderr)
|
|
226
339
|
return 1
|
|
227
|
-
elif not config.cluster:
|
|
340
|
+
elif not config.cluster and not local_spark:
|
|
228
341
|
emit_progress_signal("compute", mode=RawProgressValue("serverless"))
|
|
229
342
|
|
|
230
343
|
managed_python_executable = None
|
|
@@ -252,7 +365,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
252
365
|
"display_name": scala_variant.kernel_display_name,
|
|
253
366
|
"language": "scala",
|
|
254
367
|
}
|
|
255
|
-
elif cluster is None and inject_session and language == "python":
|
|
368
|
+
elif cluster is None and inject_session and not local_spark and language == "python":
|
|
256
369
|
try:
|
|
257
370
|
managed_runtime = ensure_serverless_runtime(
|
|
258
371
|
profile=config.profile,
|
|
@@ -263,7 +376,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
263
376
|
print(f"error: {exc}", file=sys.stderr)
|
|
264
377
|
return 1
|
|
265
378
|
managed_python_executable = managed_runtime.python_executable
|
|
266
|
-
elif cluster is None and inject_session and language == "scala":
|
|
379
|
+
elif cluster is None and inject_session and not local_spark and language == "scala":
|
|
267
380
|
scala_variant = DEFAULT_SCALA_VARIANT
|
|
268
381
|
notebook.metadata["kernelspec"] = {
|
|
269
382
|
"name": scala_variant.kernel_id,
|
|
@@ -275,6 +388,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
275
388
|
if inject_session:
|
|
276
389
|
notebook = inject_cells(
|
|
277
390
|
notebook, config, input_path,
|
|
391
|
+
local_spark=local_spark,
|
|
278
392
|
scala_connect_version=scala_connect_version,
|
|
279
393
|
scala_variant=scala_variant,
|
|
280
394
|
)
|
|
@@ -565,18 +679,6 @@ def _print_doctor_checks(checks: list[Check]) -> list[Check]:
|
|
|
565
679
|
return [check for check in checks if check.status == "fail"]
|
|
566
680
|
|
|
567
681
|
|
|
568
|
-
def _cmd_kernels_doctor(args: argparse.Namespace) -> int:
|
|
569
|
-
"""Run environment validation checks."""
|
|
570
|
-
checks = _run_kernels_doctor_checks(args)
|
|
571
|
-
failures = _print_doctor_checks(checks)
|
|
572
|
-
if failures:
|
|
573
|
-
print(f"\n{len(failures)} check(s) failed.", file=sys.stderr)
|
|
574
|
-
return 1
|
|
575
|
-
|
|
576
|
-
print("\nAll checks passed.")
|
|
577
|
-
return 0
|
|
578
|
-
|
|
579
|
-
|
|
580
682
|
def _cmd_kernels(args: argparse.Namespace) -> int:
|
|
581
683
|
handler = _KERNEL_HANDLERS.get(args.kernels_command)
|
|
582
684
|
if handler is None:
|
|
@@ -601,18 +703,6 @@ def _cmd_runtimes_list(_args: argparse.Namespace) -> int:
|
|
|
601
703
|
return 0
|
|
602
704
|
|
|
603
705
|
|
|
604
|
-
def _cmd_runtimes_doctor(_args: argparse.Namespace) -> int:
|
|
605
|
-
"""Validate managed runtime receipts rooted under runtime-home."""
|
|
606
|
-
checks = _run_runtimes_doctor_checks()
|
|
607
|
-
failures = _print_doctor_checks(checks)
|
|
608
|
-
if failures:
|
|
609
|
-
print(f"\n{len(failures)} check(s) failed.", file=sys.stderr)
|
|
610
|
-
return 1
|
|
611
|
-
|
|
612
|
-
print("\nAll checks passed.")
|
|
613
|
-
return 0
|
|
614
|
-
|
|
615
|
-
|
|
616
706
|
def _cmd_runtimes(args: argparse.Namespace) -> int:
|
|
617
707
|
handler = _RUNTIME_HANDLERS.get(args.runtimes_command)
|
|
618
708
|
if handler is None:
|
|
@@ -640,12 +730,10 @@ _KERNEL_HANDLERS = {
|
|
|
640
730
|
"install": _cmd_kernels_install,
|
|
641
731
|
"list": _cmd_kernels_list,
|
|
642
732
|
"remove": _cmd_kernels_remove,
|
|
643
|
-
"doctor": _cmd_kernels_doctor,
|
|
644
733
|
}
|
|
645
734
|
|
|
646
735
|
_RUNTIME_HANDLERS = {
|
|
647
736
|
"list": _cmd_runtimes_list,
|
|
648
|
-
"doctor": _cmd_runtimes_doctor,
|
|
649
737
|
}
|
|
650
738
|
|
|
651
739
|
|
|
@@ -13,6 +13,8 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import yaml
|
|
15
15
|
|
|
16
|
+
from databricks_agent_notebooks._constants import LOCAL_SPARK_PROFILE
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
@dataclass(frozen=True)
|
|
18
20
|
class DatabricksConfig:
|
|
@@ -82,3 +84,12 @@ def merge_config(
|
|
|
82
84
|
cluster=cli_cluster if cli_cluster is not None else frontmatter.cluster,
|
|
83
85
|
language=cli_language if cli_language is not None else frontmatter.language,
|
|
84
86
|
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_local_spark(config: DatabricksConfig) -> bool:
|
|
90
|
+
"""Detect whether *config* specifies the reserved LOCAL_SPARK profile.
|
|
91
|
+
|
|
92
|
+
Case-insensitive: ``local_spark``, ``Local_Spark``, ``LOCAL_SPARK``
|
|
93
|
+
all match.
|
|
94
|
+
"""
|
|
95
|
+
return config.profile is not None and config.profile.upper() == LOCAL_SPARK_PROFILE
|
|
@@ -19,11 +19,14 @@ from collections.abc import Sequence
|
|
|
19
19
|
from contextlib import contextmanager
|
|
20
20
|
from dataclasses import dataclass
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from unittest.mock import patch
|
|
22
23
|
|
|
23
24
|
from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel
|
|
24
25
|
from nbconvert.preprocessors import ExecutePreprocessor
|
|
25
26
|
|
|
26
27
|
import nbformat
|
|
28
|
+
import nbclient.client as _nbclient_mod
|
|
29
|
+
import nbformat.v4.nbbase as _nbbase
|
|
27
30
|
|
|
28
31
|
from databricks_agent_notebooks.runtime.home import resolve_runtime_home
|
|
29
32
|
|
|
@@ -33,8 +36,7 @@ except ModuleNotFoundError: # pragma: no cover - exercised via packaging verifi
|
|
|
33
36
|
install_ipykernel = None
|
|
34
37
|
|
|
35
38
|
|
|
36
|
-
HEARTBEAT_INTERVAL_SECONDS =
|
|
37
|
-
REDACTED_CELL_SNIPPET = "[source redacted]"
|
|
39
|
+
HEARTBEAT_INTERVAL_SECONDS = 60.0
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
class RawProgressValue(str):
|
|
@@ -133,18 +135,11 @@ def _build_cell_label(cell: nbformat.NotebookNode, lines: list[str]) -> str:
|
|
|
133
135
|
return f"[{cell_type} cell]"
|
|
134
136
|
|
|
135
137
|
|
|
136
|
-
def _build_cell_snippet(_cell: nbformat.NotebookNode, lines: list[str]) -> str:
|
|
137
|
-
if not lines:
|
|
138
|
-
return "[empty cell]"
|
|
139
|
-
return REDACTED_CELL_SNIPPET
|
|
140
|
-
|
|
141
|
-
|
|
142
138
|
def _describe_cell(cell: nbformat.NotebookNode, *, cell_index: int) -> dict[str, object]:
|
|
143
139
|
lines = _iter_meaningful_lines(cell.source)
|
|
144
140
|
return {
|
|
145
141
|
"cell_index": cell_index + 1,
|
|
146
142
|
"cell_label": _build_cell_label(cell, lines),
|
|
147
|
-
"cell_snippet": _build_cell_snippet(cell, lines),
|
|
148
143
|
}
|
|
149
144
|
|
|
150
145
|
|
|
@@ -243,6 +238,36 @@ def _patched_execution_environment(runtime_data_dir: str):
|
|
|
243
238
|
os.environ.pop("JUPYTER_PATH", None)
|
|
244
239
|
|
|
245
240
|
|
|
241
|
+
@contextmanager
|
|
242
|
+
def _robust_kernel_error_output():
|
|
243
|
+
"""Ensure kernel error messages have ``traceback``, ``ename``, and ``evalue``.
|
|
244
|
+
|
|
245
|
+
Some Jupyter kernels (notably Almond/Scala) omit ``traceback`` from IOPub
|
|
246
|
+
``error`` messages on compile errors. ``nbformat.v4.nbbase.output_from_msg``
|
|
247
|
+
accesses these fields via direct dict lookup, so a missing key raises
|
|
248
|
+
``KeyError("traceback")`` which propagates as a useless error string.
|
|
249
|
+
|
|
250
|
+
This context manager monkey-patches ``output_from_msg`` at its call site in
|
|
251
|
+
``nbclient.client`` (where the bound reference lives) to default missing
|
|
252
|
+
fields before the original function sees them, keeping the normal
|
|
253
|
+
``CellExecutionError`` path intact so callers get ename/evalue in the error
|
|
254
|
+
message.
|
|
255
|
+
"""
|
|
256
|
+
_original = _nbclient_mod.output_from_msg
|
|
257
|
+
|
|
258
|
+
def _patched(msg): # noqa: ANN001, ANN202
|
|
259
|
+
msg_type = msg.get("header", {}).get("msg_type") or msg.get("msg_type", "")
|
|
260
|
+
if msg_type == "error":
|
|
261
|
+
content = msg.get("content", {})
|
|
262
|
+
content.setdefault("traceback", [])
|
|
263
|
+
content.setdefault("ename", "UnknownError")
|
|
264
|
+
content.setdefault("evalue", "")
|
|
265
|
+
return _original(msg)
|
|
266
|
+
|
|
267
|
+
with patch.object(_nbclient_mod, "output_from_msg", _patched):
|
|
268
|
+
yield
|
|
269
|
+
|
|
270
|
+
|
|
246
271
|
def _execute_notebook_local(
|
|
247
272
|
notebook_path: Path,
|
|
248
273
|
*,
|
|
@@ -302,7 +327,10 @@ def _execute_notebook_local(
|
|
|
302
327
|
)
|
|
303
328
|
|
|
304
329
|
try:
|
|
305
|
-
with
|
|
330
|
+
with (
|
|
331
|
+
_patched_execution_environment(str(runtime_home.kernels_dir.parent)),
|
|
332
|
+
_robust_kernel_error_output(),
|
|
333
|
+
):
|
|
306
334
|
executed_notebook, _ = executor.preprocess(notebook, resources)
|
|
307
335
|
except Exception as exc:
|
|
308
336
|
return ExecutionResult(
|
|
@@ -366,7 +394,7 @@ def _execute_notebook_subprocess(
|
|
|
366
394
|
result_path = Path(result_file.name)
|
|
367
395
|
|
|
368
396
|
try:
|
|
369
|
-
|
|
397
|
+
process = subprocess.Popen(
|
|
370
398
|
_subprocess_command(
|
|
371
399
|
python_executable=python_executable,
|
|
372
400
|
notebook_path=notebook_path,
|
|
@@ -376,14 +404,30 @@ def _execute_notebook_subprocess(
|
|
|
376
404
|
timeout=timeout,
|
|
377
405
|
allow_errors=allow_errors,
|
|
378
406
|
),
|
|
379
|
-
|
|
407
|
+
stdout=subprocess.PIPE,
|
|
408
|
+
stderr=subprocess.PIPE,
|
|
380
409
|
text=True,
|
|
381
|
-
check=False,
|
|
382
410
|
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
411
|
+
|
|
412
|
+
stderr_lines: list[str] = []
|
|
413
|
+
|
|
414
|
+
def _stream_stderr() -> None:
|
|
415
|
+
assert process.stderr is not None
|
|
416
|
+
for line in process.stderr:
|
|
417
|
+
print(line, end="", file=sys.stderr, flush=True)
|
|
418
|
+
stderr_lines.append(line)
|
|
419
|
+
|
|
420
|
+
stderr_thread = threading.Thread(target=_stream_stderr, daemon=True)
|
|
421
|
+
stderr_thread.start()
|
|
422
|
+
|
|
423
|
+
stdout_text = process.stdout.read() if process.stdout else ""
|
|
424
|
+
process.wait()
|
|
425
|
+
stderr_thread.join()
|
|
426
|
+
|
|
427
|
+
if stdout_text:
|
|
428
|
+
print(stdout_text, end="", file=sys.stdout)
|
|
429
|
+
|
|
430
|
+
stderr_text = "".join(stderr_lines).strip()
|
|
387
431
|
|
|
388
432
|
if result_path.is_file():
|
|
389
433
|
payload = json.loads(result_path.read_text(encoding="utf-8"))
|
|
@@ -399,7 +443,7 @@ def _execute_notebook_subprocess(
|
|
|
399
443
|
success=False,
|
|
400
444
|
output_path=output_path,
|
|
401
445
|
duration_seconds=time.monotonic() - start,
|
|
402
|
-
error=
|
|
446
|
+
error=stderr_text or f"Managed runtime execution failed with exit code {process.returncode}",
|
|
403
447
|
)
|
|
404
448
|
finally:
|
|
405
449
|
result_path.unlink(missing_ok=True)
|
|
@@ -417,7 +461,7 @@ def execute_notebook(
|
|
|
417
461
|
if output_path is None:
|
|
418
462
|
output_path = notebook_path.with_suffix(".executed.ipynb")
|
|
419
463
|
|
|
420
|
-
if python_executable is not None and python_executable.resolve() !=
|
|
464
|
+
if python_executable is not None and str(python_executable.resolve().parent.parent) != sys.prefix:
|
|
421
465
|
return _execute_notebook_subprocess(
|
|
422
466
|
notebook_path,
|
|
423
467
|
output_path=output_path,
|