From 5edcf16538e3c197628675bf85a8076f8fa92488 Mon Sep 17 00:00:00 2001 From: Ryan Barton Date: Thu, 8 Oct 2026 11:27:29 -0400 Subject: [PATCH] Expose geometry-only KCL sessions in Python --- rust/kcl-python-bindings/README.md | 10 +++ rust/kcl-python-bindings/kcl.pyi | 6 +- rust/kcl-python-bindings/src/connection.rs | 74 ++++++++++------------ rust/kcl-python-bindings/tests/tests.py | 25 ++++++-- 4 files changed, 70 insertions(+), 45 deletions(-) diff --git a/rust/kcl-python-bindings/README.md b/rust/kcl-python-bindings/README.md index 86c52b7ed50..b92f274d79f 100644 --- a/rust/kcl-python-bindings/README.md +++ b/rust/kcl-python-bindings/README.md @@ -47,6 +47,16 @@ Omitted values use the client's existing environment defaults (`ZOO_API_TOKEN` or `KITTYCAD_API_TOKEN` for the token; `ZOO_HOST` or `KITTYCAD_HOST` for the API origin). Mock sessions do not create an API client. +For exports and measurements that do not need images, pass `geometry_only=True` +to either constructor. This selects the CPU engine pool and skips rendering setup: + +```python +async with await kcl.new_kcl_session( + "main.kcl", geometry_only=True, token=api_token, base_url=api_origin +) as session: + files = await session.export(kcl.FileExportFormat.Step) +``` + `session.outcome` is an `ExecOutcome` with the same diagnostics, constraint reports, and sketch rendering methods returned by `execute()`. Accessing it shares the saved result without copying the execution state or running KCL again. Use diff --git a/rust/kcl-python-bindings/kcl.pyi b/rust/kcl-python-bindings/kcl.pyi index d1ec705f4df..4c3d0bd4ba7 100644 --- a/rust/kcl-python-bindings/kcl.pyi +++ b/rust/kcl-python-bindings/kcl.pyi @@ -1455,20 +1455,22 @@ async def mock_execute_code(code: builtins.str) -> zooExecOutcome: Mock execute the kcl code. """ -async def new_kcl_session(path: builtins.str, *, mock: builtins.bool = ..., highlight_edges: typing.Optional[builtins.bool] = None, video_res_width: typing.Optional[builtins.int] = None, video_res_height: typing.Optional[builtins.int] = None, token: typing.Optional[builtins.str] = None, base_url: typing.Optional[builtins.str] = None) -> KclSession: +async def new_kcl_session(path: builtins.str, *, mock: builtins.bool = ..., geometry_only: builtins.bool = ..., highlight_edges: typing.Optional[builtins.bool] = None, video_res_width: typing.Optional[builtins.int] = None, video_res_height: typing.Optional[builtins.int] = None, token: typing.Optional[builtins.str] = None, base_url: typing.Optional[builtins.str] = None) -> KclSession: r""" Execute this KCL project. Return an executed KCL project with its connection still available. You can call follow-up methods, like exporting or snapshotting or measuring, on the returned session. + `geometry_only` uses the CPU engine pool without initializing rendering. `token` and `base_url` override the client environment settings for this session. Omitted values retain the client's existing environment defaults. """ -async def new_kcl_session_code(code: builtins.str, *, mock: builtins.bool = ..., highlight_edges: typing.Optional[builtins.bool] = None, video_res_width: typing.Optional[builtins.int] = None, video_res_height: typing.Optional[builtins.int] = None, token: typing.Optional[builtins.str] = None, base_url: typing.Optional[builtins.str] = None) -> KclSession: +async def new_kcl_session_code(code: builtins.str, *, mock: builtins.bool = ..., geometry_only: builtins.bool = ..., highlight_edges: typing.Optional[builtins.bool] = None, video_res_width: typing.Optional[builtins.int] = None, video_res_height: typing.Optional[builtins.int] = None, token: typing.Optional[builtins.str] = None, base_url: typing.Optional[builtins.str] = None) -> KclSession: r""" Execute this KCL source code string. Return an executed KCL project with its connection still available. You can call follow-up methods, like exporting or snapshotting or measuring, on the returned session. + `geometry_only` uses the CPU engine pool without initializing rendering. `token` and `base_url` override the client environment settings for this session. Omitted values retain the client's existing environment defaults. """ diff --git a/rust/kcl-python-bindings/src/connection.rs b/rust/kcl-python-bindings/src/connection.rs index d35a06cacc3..440cf1e39f7 100644 --- a/rust/kcl-python-bindings/src/connection.rs +++ b/rust/kcl-python-bindings/src/connection.rs @@ -195,14 +195,17 @@ impl KclSession { /// Execute this KCL project. /// Return an executed KCL project with its connection still available. /// You can call follow-up methods, like exporting or snapshotting or measuring, on the returned session. +/// `geometry_only` uses the CPU engine pool without initializing rendering. /// `token` and `base_url` override the client environment settings for this session. /// Omitted values retain the client's existing environment defaults. #[pyo3_stub_gen::derive::gen_stub_pyfunction] #[gen_stub(override_return_type(type_repr = "KclSession"))] -#[pyfunction(signature = (path, *, mock=false, highlight_edges=None, video_res_width=None, video_res_height=None, token=None, base_url=None))] +#[pyfunction(signature = (path, *, mock=false, geometry_only=false, highlight_edges=None, video_res_width=None, video_res_height=None, token=None, base_url=None))] +#[allow(clippy::too_many_arguments)] // Independent Python keyword options. pub async fn new_kcl_session( path: String, mock: bool, + geometry_only: bool, highlight_edges: Option, video_res_width: Option, video_res_height: Option, @@ -213,12 +216,16 @@ pub async fn new_kcl_session( spawn_py(async move { new_kcl_session_impl( input, - mock, - highlight_edges, - video_res_width, - video_res_height, - token, - base_url, + crate::ContextParams { + mock, + geometry_only, + highlight_edges, + video_res_width, + video_res_height, + token, + base_url, + ..Default::default() + }, ) .await }) @@ -228,14 +235,17 @@ pub async fn new_kcl_session( /// Execute this KCL source code string. /// Return an executed KCL project with its connection still available. /// You can call follow-up methods, like exporting or snapshotting or measuring, on the returned session. +/// `geometry_only` uses the CPU engine pool without initializing rendering. /// `token` and `base_url` override the client environment settings for this session. /// Omitted values retain the client's existing environment defaults. #[pyo3_stub_gen::derive::gen_stub_pyfunction] #[gen_stub(override_return_type(type_repr = "KclSession"))] -#[pyfunction(signature = (code, *, mock=false, highlight_edges=None, video_res_width=None, video_res_height=None, token=None, base_url=None))] +#[pyfunction(signature = (code, *, mock=false, geometry_only=false, highlight_edges=None, video_res_width=None, video_res_height=None, token=None, base_url=None))] +#[allow(clippy::too_many_arguments)] // Independent Python keyword options. pub async fn new_kcl_session_code( code: String, mock: bool, + geometry_only: bool, highlight_edges: Option, video_res_width: Option, video_res_height: Option, @@ -246,12 +256,16 @@ pub async fn new_kcl_session_code( spawn_py(async move { new_kcl_session_impl( input, - mock, - highlight_edges, - video_res_width, - video_res_height, - token, - base_url, + crate::ContextParams { + mock, + geometry_only, + highlight_edges, + video_res_width, + video_res_height, + token, + base_url, + ..Default::default() + }, ) .await }) @@ -260,15 +274,7 @@ pub async fn new_kcl_session_code( /// Execute this KCL project. /// Return an executed KCL project with its connection still available. -pub async fn new_kcl_session_impl( - input: KclInput, - mock: bool, - highlight_edges: Option, - video_res_width: Option, - video_res_height: Option, - token: Option, - base_url: Option, -) -> PyResult { +async fn new_kcl_session_impl(input: KclInput, mut params: crate::ContextParams) -> PyResult { // I/O or parse failures should raise an exception. // There's no more useful data to include. // So it's fine to use ? here. @@ -278,6 +284,7 @@ pub async fn new_kcl_session_impl( path, filename, } = load_and_parse(input).await?; + params.current_file = path; // Connect to the engine. // If you can't even connect to the engine, just raise an exception. @@ -286,16 +293,7 @@ pub async fn new_kcl_session_impl( program .language_version() .map_err(|err| into_miette_for_parse(&filename, &code, err))?, - crate::ContextParams { - current_file: path, - mock, - highlight_edges, - geometry_only: false, - video_res_width, - video_res_height, - token, - base_url, - }, + params, ) .await .map_err(to_py_exception)?; @@ -348,12 +346,10 @@ mod tests { async fn follow_up_error_drains_responses_and_close_releases_context() { let mut session = new_kcl_session_impl( KclInput::Code("@settings(kclVersion = 2.0)\nvalue = 1".to_owned()), - true, - None, - None, - None, - None, - None, + crate::ContextParams { + mock: true, + ..Default::default() + }, ) .await .unwrap(); diff --git a/rust/kcl-python-bindings/tests/tests.py b/rust/kcl-python-bindings/tests/tests.py index 6ea93472a14..d6ee14c8417 100755 --- a/rust/kcl-python-bindings/tests/tests.py +++ b/rust/kcl-python-bindings/tests/tests.py @@ -4,10 +4,10 @@ import sys from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Thread - -import pytest +from urllib.parse import parse_qs, urlsplit import kcl +import pytest from kcl import Point3d # Get the path to this script's parent directory. @@ -185,8 +185,13 @@ def session_constructor(request, tmp_path): @pytest.mark.asyncio @pytest.mark.parametrize("environment_configured", [False, True]) +@pytest.mark.parametrize("geometry_only", [False, True]) async def test_kcl_session_explicit_credentials( - session_constructor, modeling_api, monkeypatch, environment_configured + session_constructor, + modeling_api, + monkeypatch, + environment_configured, + geometry_only, ): for name in ("ZOO_API_TOKEN", "KITTYCAD_API_TOKEN", "ZOO_HOST", "KITTYCAD_HOST"): monkeypatch.delenv(name, raising=False) @@ -202,7 +207,12 @@ async def test_kcl_session_explicit_credentials( async def connect(token): async with asyncio.timeout(10): with pytest.raises(Exception, match="401"): - await constructor(source, token=token, base_url=base_url) + await constructor( + source, + geometry_only=geometry_only, + token=token, + base_url=base_url, + ) await asyncio.gather( connect("first-session-token"), connect("second-session-token") @@ -213,6 +223,10 @@ async def connect(token): "Bearer first-session-token", "Bearer second-session-token", } + for path, _ in requests: + query = parse_qs(urlsplit(path).query) + assert query["geometry_only"] == [str(geometry_only).lower()] + assert query.get("pool") == (["cpu"] if geometry_only else None) assert dict(os.environ) == before @@ -236,6 +250,9 @@ async def test_kcl_session_credentials_environment_fallback( path, authorization = requests[0] assert path.startswith("/ws/modeling/commands?") assert authorization == "Bearer environment-token" + query = parse_qs(urlsplit(path).query) + assert query["geometry_only"] == ["false"] + assert "pool" not in query @pytest.mark.asyncio