move kcl.py to this repo (#5587)

* ci for kcl-python-bindings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* bettter concurrency

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup files

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updaets

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updaets

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updaets

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updaets

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* format

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* format

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-03-01 16:38:25 -08:00
committed by GitHub
parent 66f3500ca9
commit a4db302174
30 changed files with 1541 additions and 14 deletions

View File

@ -25,7 +25,7 @@ updates:
- adamchalmers - adamchalmers
- jessfraz - jessfraz
- package-ecosystem: 'cargo' # See documentation for possible values - package-ecosystem: 'cargo' # See documentation for possible values
directory: '/src/wasm-lib/' # Location of package manifests directory: '/rust/' # Location of package manifests
schedule: schedule:
interval: weekly interval: weekly
day: monday day: monday
@ -39,3 +39,11 @@ updates:
wasm-bindgen-deps: wasm-bindgen-deps:
patterns: patterns:
- "wasm-bindgen*" - "wasm-bindgen*"
- package-ecosystem: "pip" # See documentation for possible values
directory: "/rust/kcl-python-bindings/" # Location of package manifests
schedule:
interval: weekly
day: monday
reviewers:
- adamchalmers
- jessfraz

View File

@ -0,0 +1,179 @@
# This file is autogenerated by maturin v1.6.0 and then modified
# To update, run
#
# maturin generate-ci github
#
name: kcl-python-bindings
on:
push:
branches:
- main
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'rust/kcl-python-bindings/**'
- '**.rs'
- .github/workflows/kcl-python-bindings.yml
tags:
- 'kcl-*'
pull_request:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'rust/kcl-python-bindings/**'
- '**.rs'
- .github/workflows/kcl-python-bindings.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
linux-x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: rust/kcl-python-bindings
target: x86_64
args: --release --out dist --find-interpreter
sccache: 'true'
manylinux: auto
before-script-linux: |
yum install openssl-devel -y
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-linux-x86_64
path: rust/kcl-python-bindings/dist
windows:
runs-on: windows-16-cores
strategy:
matrix:
target:
- x64
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: rust/kcl-python-bindings
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-windows-${{ matrix.target }}
path: rust/kcl-python-bindings/dist
macos:
runs-on: macos-latest
strategy:
matrix:
target:
- x86_64
- aarch64
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: rust/kcl-python-bindings
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.target }}
path: rust/kcl-python-bindings/dist
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
- uses: taiki-e/install-action@just
- name: Run tests
run: |
cd rust/kcl-python-bindings
just setup-uv
just test
env:
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
- name: Install codespell
run: |
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH
uv pip install pip --upgrade
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
working-directory: rust/kcl-python-bindings
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: rust/kcl-python-bindings/dist
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
needs: [linux-x86_64, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v4
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
- name: Install codespell
run: |
cd rust/kcl-python-bindings
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH
uv pip install pip --upgrade
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
working-directory: rust/kcl-python-bindings
command: upload
args: --non-interactive --skip-existing wheels-*/*

24
.github/workflows/ruff.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: ruff
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: main
paths:
- '**.py'
- .github/workflows/ruff.yml
pull_request:
paths:
- '**.py'
- .github/workflows/ruff.yml
permissions:
contents: read
pull-requests: write
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3

7
.gitignore vendored
View File

@ -63,8 +63,13 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
.eslintcache .eslintcache
venv
.vite/ .vite/
# electron # electron
out/ out/
# python
__pycache__/
uv.lock
rust/kcl-python-bindings/dist
venv

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

20
rust/Cargo.lock generated
View File

@ -1744,6 +1744,21 @@ dependencies = [
"zip", "zip",
] ]
[[package]]
name = "kcl-python-bindings"
version = "0.3.45"
dependencies = [
"anyhow",
"kcl-lib",
"kittycad-modeling-cmds",
"miette",
"pyo3",
"serde",
"serde_json",
"tokio",
"uuid",
]
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.45" version = "0.1.45"
@ -1833,9 +1848,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.2.99" version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "828a0c74476533e6258ea7dd70cfc7d63a5df4b37753d30ef198e0689eaac4eb" checksum = "fb5a824cb9bb4c602962ecbaca5ce71225938aa1abc24103bf46c222f468dd26"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -2530,6 +2545,7 @@ dependencies = [
"pyo3-build-config", "pyo3-build-config",
"pyo3-ffi", "pyo3-ffi",
"pyo3-macros", "pyo3-macros",
"serde",
"unindent", "unindent",
] ]

View File

@ -3,6 +3,7 @@ resolver = "2"
members = [ members = [
"kcl-derive-docs", "kcl-derive-docs",
"kcl-lib", "kcl-lib",
"kcl-python-bindings",
"kcl-test-server", "kcl-test-server",
"kcl-to-core", "kcl-to-core",
"kcl-wasm-lib" "kcl-wasm-lib"
@ -23,10 +24,14 @@ similar = { opt-level = 3 }
debug = "line-tables-only" debug = "line-tables-only"
[workspace.dependencies] [workspace.dependencies]
async-trait = "0.1.85"
anyhow = { version = "1" }
http = "1" http = "1"
indexmap = "2.7.0" indexmap = "2.7.0"
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.99", features = ["ts-rs", "websocket"] } kittycad-modeling-cmds = { version = "0.2.100", features = ["ts-rs", "websocket"] }
miette = "7.5.0"
pyo3 = { version = "0.22.6" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" } serde_json = { version = "1" }
tokio = { version = "1" } tokio = { version = "1" }

View File

@ -46,7 +46,9 @@ test:
publish-kcl version: publish-kcl version:
git tag kcl-{{version}} git tag kcl-{{version}}
git push origin kcl-{{version}}
cargo publish -p kcl-derive-docs cargo publish -p kcl-derive-docs
cargo publish -p kcl-lib cargo publish -p kcl-lib
cargo publish -p kcl-test-server cargo publish -p kcl-test-server
# We push the tag at the end of publish since pushing the tag
# will trigger CI to release the kcl python bindings.
git push origin kcl-{{version}}

View File

@ -23,7 +23,7 @@ serde_tokenstream = "0.2"
syn = { version = "2.0.96", features = ["full"] } syn = { version = "2.0.96", features = ["full"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.95" anyhow = { workspace = true }
expectorate = "1.1.0" expectorate = "1.1.0"
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
rustfmt-wrapper = "0.2.1" rustfmt-wrapper = "0.2.1"

View File

@ -11,9 +11,9 @@ keywords = ["kcl", "KittyCAD", "CAD"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = { version = "1.0.95", features = ["backtrace"] } anyhow = { workspace = true, features = ["backtrace"] }
async-recursion = "1.1.1" async-recursion = "1.1.1"
async-trait = "0.1.85" async-trait = {workspace = true}
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.27", default-features = false, optional = true, features = [ clap = { version = "4.5.27", default-features = false, optional = true, features = [
@ -37,10 +37,10 @@ kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true } kittycad-modeling-cmds = { workspace = true }
lazy_static = "1.5.0" lazy_static = "1.5.0"
measurements = "0.11.0" measurements = "0.11.0"
miette = "7.5.0" miette = { workspace = true }
mime_guess = "2.0.5" mime_guess = "2.0.5"
parse-display = "0.9.1" parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true } pyo3 = { workspace = true, optional = true }
regex = "1.11.1" regex = "1.11.1"
reqwest = { version = "0.12", default-features = false, features = [ reqwest = { version = "0.12", default-features = false, features = [
"stream", "stream",

View File

@ -1,4 +1,4 @@
# KCL # kcl-lib
Our language for defining geometry and working with our Geometry Engine efficiently. Short for KittyCAD Language, named after our Design API. Our language for defining geometry and working with our Geometry Engine efficiently. Short for KittyCAD Language, named after our Design API.

View File

@ -0,0 +1,26 @@
[package]
name = "kcl-python-bindings"
version = "0.3.45"
edition = "2021"
repository = "https://github.com/kittycad/modeling-app"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "kcl"
crate-type = ["cdylib"]
[dependencies]
anyhow = { workspace = true }
kcl-lib = { path = "../kcl-lib", features = [
"pyo3",
"engine",
"disable-println",
] }
#kcl-lib = { path = "../modeling-app/src/wasm-lib/kcl", default-features = false, features = ["pyo3", "engine", "disable-println"] }
kittycad-modeling-cmds = { workspace = true }
miette = { workspace = true, features = ["fancy"] }
pyo3 = { workspace = true, features = ["serde", "experimental-async"] }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

View File

@ -0,0 +1,39 @@
# kcl-python-bindings
Python bindings to the rust kcl-lib crate.
## Usage
The [tests.py](tests/tests.py) file contains examples of how to use the library.
## Development
We use [maturin](https://github.com/PyO3/maturin) for this project.
You can either download binaries from the [latest release](https://github.com/PyO3/maturin/releases/latest) or install it with [pipx](https://pypa.github.io/pipx/):
```shell
pipx install maturin
```
> [!NOTE]
>
> `pip install maturin` should also work if you don't want to use pipx.
There are four main commands:
- `maturin publish` builds the crate into python packages and publishes them to pypi.
- `maturin build` builds the wheels and stores them in a folder (`target/wheels` by default), but doesn't upload them. It's possible to upload those with [twine](https://github.com/pypa/twine) or `maturin upload`.
- `maturin develop` builds the crate and installs it as a python module directly in the current virtualenv. Note that while `maturin develop` is faster, it doesn't support all the feature that running `pip install` after `maturin build` supports.
`pyo3` bindings are automatically detected.
`maturin` doesn't need extra configuration files and doesn't clash with an existing setuptools-rust or milksnake configuration.
### Releasing a new version
1. Make sure the `Cargo.toml` has the new version you want to release.
2. Run `make tag` this is just an easy command for making a tag formatted
correctly with the version.
3. Push the tag (the result of `make tag` gives instructions for this)
4. Everything else is triggered from the tag push. Just make sure all the tests
pass on the `main` branch before making and pushing a new tag.

View File

@ -0,0 +1,14 @@
// A 25x25x50 box
const box_width = 25
const box_depth = 25
const box_height = 50
const box_sketch = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> xLine(box_width, %, $line1)
|> yLine(box_depth, %, $line2)
|> xLineTo(profileStartX(%), %, $line3)
|> close(%, $line4)
const box3D = extrude(box_sketch, length = box_height)

View File

@ -0,0 +1 @@
import thing from 'thing.kcl'

View File

@ -0,0 +1 @@
lksjndflsskjfnak;jfna##

View File

@ -0,0 +1,50 @@
// Antenna
// Set units
@settings(defaultLengthUnit = in)
// import constants
import height, width, antennaBaseWidth, antennaBaseHeight, antennaTopWidth, antennaTopHeight from "globals.kcl"
// Calculate the origin
origin = [-width / 2 + .45, -0.10]
// Create the antenna
antennaX = origin[0]
antennaY = origin[1]
antennaPlane = {
plane = {
origin = { x = 0, y = 0, z = height / 2 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}
}
// Create the antenna base sketch
sketch001 = startSketchOn(antennaPlane)
|> startProfileAt([origin[0], origin[1]], %)
|> line(end = [antennaBaseWidth, 0])
|> line(end = [0, -antennaBaseHeight])
|> line(end = [-antennaBaseWidth, 0])
|> close()
// Create the antenna top sketch
loftPlane = offsetPlane('XY', offset = height / 2 + 3)
sketch002 = startSketchOn(loftPlane)
|> startProfileAt([
origin[0] + (antennaBaseWidth - antennaTopWidth) / 2,
origin[1] - ((antennaBaseHeight - antennaTopHeight) / 2)
], %)
|> xLine(antennaTopWidth, %)
|> yLine(-antennaTopHeight, %)
|> xLine(-antennaTopWidth, %)
|> close()
// Create the antenna using a loft
loft([sketch001, sketch002])
|> appearance(color = "#000000")

View File

@ -0,0 +1,80 @@
// Walkie talkie body
// Set units
@settings(defaultLengthUnit = in)
// Import constants
import height, width, thickness, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight from "globals.kcl"
bodySketch = startSketchOn('XZ')
|> startProfileAt([-width / 2, height / 2], %)
|> xLine(width, %, $chamfer1)
|> yLine(-height, %, $chamfer2)
|> xLine(-width, %, $chamfer3)
|> close(tag = $chamfer4)
bodyExtrude = extrude(bodySketch, length = thickness)
|> chamfer(
length = chamferLength,
tags = [
getNextAdjacentEdge(chamfer1),
getNextAdjacentEdge(chamfer2),
getNextAdjacentEdge(chamfer3),
getNextAdjacentEdge(chamfer4)
]
)
// Define the offset for the indentation
sketch002 = startSketchOn(bodyExtrude, 'END')
|> startProfileAt([
-width / 2 + offset,
height / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({ angle = 45, to = height / 2 - offset }, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + offset / 2 * cos(toRadians(45))),
height / 2 - offset
])
|> angledLineToX({ angle = -45, to = width / 2 - offset }, %)
|> line(endAbsolute = [
width / 2 - offset,
-(height / 2 - (chamferLength + offset / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))),
-height / 2 + offset
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset
}, %)
|> close()
extrude002 = extrude(sketch002, length = -0.0625)
// Create the pocket for the screen
sketch003 = startSketchOn(extrude002, 'start')
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|> xLine(screenWidth, %, $seg01)
|> yLine(-screenHeight, %)
|> xLine(-segLen(seg01), %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude003 = extrude(sketch003, length = screenDepth)
// Create the speaker box
sketch004 = startSketchOn(extrude002, 'start')
|> startProfileAt([-1.25 / 2, -.125], %)
|> xLine(speakerBoxWidth, %)
|> yLine(-speakerBoxHeight, %)
|> xLine(-speakerBoxWidth, %)
|> close()
extrude(sketch004, length = -.5)
|> appearance(
color = "#277bb0",
)

View File

@ -0,0 +1,38 @@
// Walkie Talkie button
// Set units
@settings(defaultLengthUnit = in)
// Import constants
import screenHeight, buttonWidth, tolerance, buttonHeight, buttonThickness from 'globals.kcl'
// Create a function for the button
export fn button(origin, rotation, plane) {
buttonSketch = startSketchOn(plane)
|> startProfileAt([origin[0], origin[1]], %)
|> angledLine({
angle = 180 + rotation,
length = buttonWidth
}, %, $tag1)
|> angledLine({
angle = 270 + rotation,
length = buttonHeight
}, %, $tag2)
|> angledLine({
angle = 0 + rotation,
length = buttonWidth
}, %)
|> close()
buttonExtrude = extrude(buttonSketch, length = buttonThickness)
|> chamfer(
length = .050,
tags = [
getNextAdjacentEdge(tag1),
getNextAdjacentEdge(tag2)
]
)
|> appearance(color = "#ff0000")
return buttonExtrude
}

View File

@ -0,0 +1,85 @@
// Walkie talkie case
// Set units
@settings(defaultLengthUnit = in)
// Import constants and Zoo logo
import width, height, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight, squareHoleSideLength, caseTolerance from "globals.kcl"
import zLogo, oLogo, oLogo2 from "zoo-logo.kcl"
plane = offsetPlane("XZ", offset = 1)
fn screenHole(sketchStart) {
sketch006 = startSketchOn(sketchStart)
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|> xLine(screenWidth, %)
|> yLine(-screenHeight, %)
|> xLine(-screenWidth, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
return sketch006
}
fn squareHolePattern(plane, x, y) {
fn transformX(i) {
return { translate = [.125 * i, 0] }
}
fn transformY(i) {
return { translate = [0, -.125 * i] }
}
squareHolePatternSketch = startSketchOn(plane)
|> startProfileAt([-x, -y], %)
|> line(end = [squareHoleSideLength / 2, 0])
|> line(end = [0, -squareHoleSideLength / 2])
|> line(end = [-squareHoleSideLength / 2, 0])
|> close()
|> patternTransform2d(instances = 13, transform = transformX)
|> patternTransform2d(instances = 11, transform = transformY)
return squareHolePatternSketch
}
sketch005 = startSketchOn(offsetPlane("XZ", offset = 1))
|> startProfileAt([
-width / 2 + offset + caseTolerance,
height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({
angle = 45,
to = height / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))),
height / 2 - (offset + caseTolerance)
])
|> angledLineToX({
angle = -45,
to = width / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (offset + caseTolerance),
-(height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset + caseTolerance
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))),
-height / 2 + offset + caseTolerance
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset + caseTolerance
}, %)
|> close()
|> hole(screenHole(plane), %)
|> hole(squareHolePattern(plane, .75, .125), %)
|> hole(zLogo(plane, [-.30, -1.825], .20), %)
|> hole(oLogo(plane, [-.075, -1.825], .20), %)
|> hole(oLogo2(plane, [-.075, -1.825], .20), %)
|> hole(oLogo(plane, [.175, -1.825], .20), %)
|> hole(oLogo2(plane, [.175, -1.825], .20), %)
extrude(sketch005, length = -0.0625)
|> appearance(color = '#D0FF01', metalness = 0, roughness = 50)

View File

@ -0,0 +1,42 @@
// Global constants for the walkie talkie
// Set units
@settings(defaultLengthUnit = in)
// body
export height = 4
export width = 2.5
export thickness = 1
export chamferLength = .325
export offset = .125
export screenWidth = 1.75
export screenHeight = 1
export screenYPosition = height / 2 - 0.75
export screenDepth = -.0625
export speakerBoxWidth = 1.25
export speakerBoxHeight = 1.25
// antenna
export antennaBaseWidth = .5
export antennaBaseHeight = .25
export antennaTopWidth = .30
export antennaTopHeight = .05
// button
export buttonWidth = .15
export tolerance = 0.020
export buttonHeight = screenHeight / 2 - tolerance
export buttonThickness = .040
// case
export squareHoleSideLength = 0.0625
export caseTolerance = 0.010
// knob
export knobDiameter = .5
export knobHeight = .25
export knobRadius = 0.050
// talk-button
export talkButtonSideLength = 0.5
export talkButtonHeight = 0.050

View File

@ -0,0 +1,38 @@
// Walkie talkie knob
// Set units
@settings(defaultLengthUnit = in)
// Import constants
import width, thickness, height, knobDiameter, knobHeight, knobRadius from "globals.kcl"
// Define the plane for the knob
knobPlane = {
plane = {
origin = {
x = width / 2 - 0.70,
y = -thickness / 2,
z = height / 2
},
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 0, y = 1, z = 0 }
}
}
// Create the knob sketch and revolve
startSketchOn(knobPlane)
|> startProfileAt([0.0001, 0], %)
|> xLine(knobDiameter / 2, %)
|> yLine(knobHeight - 0.05, %)
|> arc({
angleStart = 0,
angleEnd = 90,
radius = .05
}, %)
|> xLineTo(0.0001, %)
|> close()
|> revolve({ axis = "Y" }, %)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 50)

View File

@ -0,0 +1,50 @@
// Walkie Talkie
// A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings.
// Set units
@settings(defaultLengthUnit = in)
// Import parts and constants
import 'body.kcl'
import 'antenna.kcl'
import 'case.kcl'
import 'talk-button.kcl' as talkButton
import 'knob.kcl'
import button from "button.kcl"
import width, height, thickness, screenWidth, screenHeight, screenYPosition, tolerance from "globals.kcl"
// Import the body
body
// Import the case
case
// Import the antenna
antenna
// Import the buttons
button([
-(screenWidth / 2 + tolerance),
screenYPosition
], 0, offsetPlane("XZ", offset = thickness))
button([
-(screenWidth / 2 + tolerance),
screenYPosition - (screenHeight / 2)
], 0, offsetPlane("XZ", offset = thickness))
button([
screenWidth / 2 + tolerance,
screenYPosition - screenHeight
], 180, offsetPlane("XZ", offset = thickness))
button([
screenWidth / 2 + tolerance,
screenYPosition - (screenHeight / 2)
], 180, offsetPlane("XZ", offset = thickness))
// Import the talk button
talkButton
// Import the frequency knob
knob

View File

@ -0,0 +1,46 @@
// Walkie talkie talk button
// Set units
@settings(defaultLengthUnit = in)
// Import constants
import width, thickness, talkButtonSideLength, talkButtonHeight from "globals.kcl"
talkButtonPlane = {
plane = {
origin = {
x = width / 2,
y = -thickness / 2,
z = .5
},
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
}
}
// Create the talk button sketch
talkButtonSketch = startSketchOn(talkButtonPlane)
|> startProfileAt([
-talkButtonSideLength / 2,
talkButtonSideLength / 2
], %)
|> xLine(talkButtonSideLength, %, $tag1)
|> yLine(-talkButtonSideLength, %, $tag2)
|> xLine(-talkButtonSideLength, %, $tag3)
|> close(tag = $tag4)
// Create the talk button and apply fillets
extrude(talkButtonSketch, length = talkButtonHeight)
|> fillet(
radius = 0.050,
tags = [
getNextAdjacentEdge(tag1),
getNextAdjacentEdge(tag2),
getNextAdjacentEdge(tag3),
getNextAdjacentEdge(tag4)
]
)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 90)

View File

@ -0,0 +1,83 @@
// Zoo logo
// Define a function to draw the ZOO "Z"
export fn zLogo(surface, origin, scale) {
zSketch = surface
|> startProfileAt([
0 + origin[0],
0.15 * scale + origin[1]
], %)
|> yLine(-0.15 * scale, %)
|> xLine(0.15 * scale, %)
|> angledLineToX({
angle = 47.15,
to = 0.3 * scale + origin[0]
}, %, $seg1)
|> yLineTo(0 + origin[1], %, $seg3)
|> xLine(0.63 * scale, %)
|> yLine(0.225 * scale, %)
|> xLine(-0.57 * scale, %)
|> angledLineToX({
angle = 47.15,
to = 0.93 * scale + origin[0]
}, %)
|> yLine(0.15 * scale, %)
|> xLine(-0.15 * scale, %)
|> angledLine({
angle = 47.15,
length = -segLen(seg1)
}, %, $seg2)
|> yLine(segLen(seg3), %)
|> xLineTo(0 + origin[0], %)
|> yLine(-0.225 * scale, %)
|> angledLineThatIntersects({
angle = 0,
intersectTag = seg2,
offset = 0
}, %)
|> close()
return zSketch
}
// Define a function to draw the ZOO "O"
export fn oLogo(surface, origin, scale) {
oSketch001 = surface
|> startProfileAt([
.788 * scale + origin[0],
.921 * scale + origin[1]
], %)
|> arc({
angleStart = 47.15 + 6,
angleEnd = 47.15 - 6 + 180,
radius = .525 * scale
}, %)
|> angledLine({ angle = 47.15, length = .24 * scale }, %)
|> arc({
angleStart = 47.15 - 11 + 180,
angleEnd = 47.15 + 11,
radius = .288 * scale
}, %)
|> close()
return oSketch001
}
export fn oLogo2(surface, origin, scale) {
oSketch002 = surface
|> startProfileAt([
.16 * scale + origin[0],
.079 * scale + origin[1]
], %)
|> arc({
angleStart = 47.15 + 6 - 180,
angleEnd = 47.15 - 6,
radius = .525 * scale
}, %)
|> angledLine({ angle = 47.15, length = -.24 * scale }, %)
|> arc({
angleStart = 47.15 - 11,
angleEnd = 47.15 + 11 - 180,
radius = .288 * scale
}, %)
|> close()
return oSketch002
}

View File

@ -0,0 +1,8 @@
test:
uv pip install .[test]
uv run pytest tests/tests.py
setup-uv:
uv python install
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH

View File

@ -0,0 +1,22 @@
[build-system]
requires = ["maturin>=1.6,<2.0"]
build-backend = "maturin"
[project]
name = "zoo-kcl"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[project.optional-dependencies]
test = [
"pytest",
"pytest-asyncio",
]
[tool.maturin]
features = ["pyo3/extension-module"]

View File

@ -0,0 +1,525 @@
#![allow(clippy::useless_conversion)]
use anyhow::Result;
use kcl_lib::{
lint::{checks, Discovered},
ExecutorContext, UnitLength,
};
use pyo3::{
prelude::PyModuleMethods, pyclass, pyfunction, pymethods, pymodule, types::PyModule, wrap_pyfunction, Bound, PyErr,
PyResult,
};
use serde::{Deserialize, Serialize};
fn tokio() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock;
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(|| tokio::runtime::Runtime::new().unwrap())
}
fn into_miette(error: kcl_lib::KclErrorWithOutputs) -> PyErr {
let report = error.clone().into_miette_report_with_outputs().unwrap();
let report = miette::Report::new(report);
pyo3::exceptions::PyException::new_err(format!("{:?}", report))
}
fn into_miette_for_parse(filename: &str, input: &str, error: kcl_lib::KclError) -> PyErr {
let report = kcl_lib::Report {
kcl_source: input.to_string(),
error: error.clone(),
filename: filename.to_string(),
};
let report = miette::Report::new(report);
pyo3::exceptions::PyException::new_err(format!("{:?}", report))
}
/// The variety of image formats snapshots may be exported to.
#[derive(Serialize, Deserialize, PartialEq, Hash, Debug, Clone, Copy)]
#[pyclass(eq, eq_int)]
#[serde(rename_all = "lowercase")]
pub enum ImageFormat {
/// .png format
Png,
/// .jpeg format
Jpeg,
}
impl From<ImageFormat> for kittycad_modeling_cmds::ImageFormat {
fn from(format: ImageFormat) -> Self {
match format {
ImageFormat::Png => kittycad_modeling_cmds::ImageFormat::Png,
ImageFormat::Jpeg => kittycad_modeling_cmds::ImageFormat::Jpeg,
}
}
}
/// A file that was exported from the engine.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[pyclass]
pub struct ExportFile {
/// Binary contents of the file.
pub contents: Vec<u8>,
/// Name of the file.
pub name: String,
}
impl From<kittycad_modeling_cmds::shared::ExportFile> for ExportFile {
fn from(file: kittycad_modeling_cmds::shared::ExportFile) -> Self {
ExportFile {
contents: file.contents.0,
name: file.name,
}
}
}
impl From<kittycad_modeling_cmds::websocket::RawFile> for ExportFile {
fn from(file: kittycad_modeling_cmds::websocket::RawFile) -> Self {
ExportFile {
contents: file.contents,
name: file.name,
}
}
}
#[pymethods]
impl ExportFile {
#[getter]
fn contents(&self) -> Vec<u8> {
self.contents.clone()
}
#[getter]
fn name(&self) -> String {
self.name.clone()
}
}
/// The valid types of output file formats.
#[derive(Serialize, Deserialize, PartialEq, Hash, Debug, Clone)]
#[pyclass(eq, eq_int)]
#[serde(rename_all = "lowercase")]
pub enum FileExportFormat {
/// Autodesk Filmbox (FBX) format. <https://en.wikipedia.org/wiki/FBX>
Fbx,
/// Binary glTF 2.0.
///
/// This is a single binary with .glb extension.
///
/// This is better if you want a compressed format as opposed to the human readable glTF that lacks
/// compression.
Glb,
/// glTF 2.0. Embedded glTF 2.0 (pretty printed).
///
/// Single JSON file with .gltf extension binary data encoded as base64 data URIs.
///
/// The JSON contents are pretty printed.
///
/// It is human readable, single file, and you can view the diff easily in a
/// git commit.
Gltf,
/// The OBJ file format. <https://en.wikipedia.org/wiki/Wavefront_.obj_file> It may or
/// may not have an an attached material (mtl // mtllib) within the file, but we
/// interact with it as if it does not.
Obj,
/// The PLY file format. <https://en.wikipedia.org/wiki/PLY_(file_format)>
Ply,
/// The STEP file format. <https://en.wikipedia.org/wiki/ISO_10303-21>
Step,
/// The STL file format. <https://en.wikipedia.org/wiki/STL_(file_format)>
Stl,
}
fn get_output_format(
format: &FileExportFormat,
src_unit: kittycad_modeling_cmds::units::UnitLength,
) -> kittycad_modeling_cmds::format::OutputFormat3d {
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
let coords = kittycad_modeling_cmds::coord::System {
forward: kittycad_modeling_cmds::coord::AxisDirectionPair {
axis: kittycad_modeling_cmds::coord::Axis::Y,
direction: kittycad_modeling_cmds::coord::Direction::Negative,
},
up: kittycad_modeling_cmds::coord::AxisDirectionPair {
axis: kittycad_modeling_cmds::coord::Axis::Z,
direction: kittycad_modeling_cmds::coord::Direction::Positive,
},
};
match format {
FileExportFormat::Fbx => {
kittycad_modeling_cmds::format::OutputFormat3d::Fbx(kittycad_modeling_cmds::format::fbx::export::Options {
storage: kittycad_modeling_cmds::format::fbx::export::Storage::Binary,
created: None,
})
}
FileExportFormat::Glb => kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
kittycad_modeling_cmds::format::gltf::export::Options {
storage: kittycad_modeling_cmds::format::gltf::export::Storage::Binary,
presentation: kittycad_modeling_cmds::format::gltf::export::Presentation::Compact,
},
),
FileExportFormat::Gltf => kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
kittycad_modeling_cmds::format::gltf::export::Options {
storage: kittycad_modeling_cmds::format::gltf::export::Storage::Embedded,
presentation: kittycad_modeling_cmds::format::gltf::export::Presentation::Pretty,
},
),
FileExportFormat::Obj => {
kittycad_modeling_cmds::format::OutputFormat3d::Obj(kittycad_modeling_cmds::format::obj::export::Options {
coords,
units: src_unit,
})
}
FileExportFormat::Ply => {
kittycad_modeling_cmds::format::OutputFormat3d::Ply(kittycad_modeling_cmds::format::ply::export::Options {
storage: kittycad_modeling_cmds::format::ply::export::Storage::Ascii,
coords,
selection: kittycad_modeling_cmds::format::Selection::DefaultScene,
units: src_unit,
})
}
FileExportFormat::Step => kittycad_modeling_cmds::format::OutputFormat3d::Step(
kittycad_modeling_cmds::format::step::export::Options { coords, created: None },
),
FileExportFormat::Stl => {
kittycad_modeling_cmds::format::OutputFormat3d::Stl(kittycad_modeling_cmds::format::stl::export::Options {
storage: kittycad_modeling_cmds::format::stl::export::Storage::Ascii,
coords,
units: src_unit,
selection: kittycad_modeling_cmds::format::Selection::DefaultScene,
})
}
}
}
/// Get the path to the current file from the path given, and read the code.
async fn get_code_and_file_path(path: &str) -> Result<(String, std::path::PathBuf)> {
let mut path = std::path::PathBuf::from(path);
// Check if the path is a directory, if so we want to look for a main.kcl inside.
if path.is_dir() {
path = path.join("main.kcl");
if !path.exists() {
return Err(anyhow::anyhow!("Directory must contain a main.kcl file"));
}
} else {
// Otherwise be sure we have a kcl file.
if let Some(ext) = path.extension() {
if ext != "kcl" {
return Err(anyhow::anyhow!("File must have a .kcl extension"));
}
}
}
let code = tokio::fs::read_to_string(&path).await?;
Ok((code, path))
}
async fn new_context_state(current_file: Option<std::path::PathBuf>) -> Result<(ExecutorContext, kcl_lib::ExecState)> {
let mut settings: kcl_lib::ExecutorSettings = Default::default();
if let Some(current_file) = current_file {
settings.with_current_file(current_file);
}
let state = kcl_lib::ExecState::new(&settings);
let ctx = ExecutorContext::new_with_client(settings, None, None).await?;
Ok((ctx, state))
}
/// Execute the kcl code from a file path.
#[pyfunction]
async fn execute(path: String) -> PyResult<()> {
tokio()
.spawn(async move {
let (code, path) = get_code_and_file_path(&path)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let (ctx, mut state) = new_context_state(Some(path))
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run_with_ui_outputs(&program, &mut state)
.await
.map_err(into_miette)?;
Ok(())
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Execute the kcl code.
#[pyfunction]
async fn execute_code(code: String) -> PyResult<()> {
tokio()
.spawn(async move {
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let (ctx, mut state) = new_context_state(None)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run_with_ui_outputs(&program, &mut state)
.await
.map_err(into_miette)?;
Ok(())
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Execute a kcl file and snapshot it in a specific format.
#[pyfunction]
async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResult<Vec<u8>> {
tokio()
.spawn(async move {
let (code, path) = get_code_and_file_path(&path)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let (ctx, mut state) = new_context_state(Some(path))
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run_with_ui_outputs(&program, &mut state)
.await
.map_err(into_miette)?;
// Zoom to fit.
ctx.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::ZoomToFit(kittycad_modeling_cmds::ZoomToFit {
object_ids: Default::default(),
padding: 0.1,
animated: false,
}),
)
.await?;
// Send a snapshot request to the engine.
let resp = ctx
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::TakeSnapshot(kittycad_modeling_cmds::TakeSnapshot {
format: image_format.into(),
}),
)
.await?;
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Modeling {
modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::TakeSnapshot(data),
} = resp
else {
return Err(pyo3::exceptions::PyException::new_err(format!(
"Unexpected response from engine: {:?}",
resp
)));
};
Ok(data.contents.0)
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Execute the kcl code and snapshot it in a specific format.
#[pyfunction]
async fn execute_code_and_snapshot(code: String, image_format: ImageFormat) -> PyResult<Vec<u8>> {
tokio()
.spawn(async move {
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let (ctx, mut state) = new_context_state(None)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run_with_ui_outputs(&program, &mut state)
.await
.map_err(into_miette)?;
// Zoom to fit.
ctx.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::ZoomToFit(kittycad_modeling_cmds::ZoomToFit {
object_ids: Default::default(),
padding: 0.1,
animated: false,
}),
)
.await?;
// Send a snapshot request to the engine.
let resp = ctx
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::TakeSnapshot(kittycad_modeling_cmds::TakeSnapshot {
format: image_format.into(),
}),
)
.await?;
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Modeling {
modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::TakeSnapshot(data),
} = resp
else {
return Err(pyo3::exceptions::PyException::new_err(format!(
"Unexpected response from engine: {:?}",
resp
)));
};
Ok(data.contents.0)
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Execute a kcl file and export it to a specific file format.
#[pyfunction]
async fn execute_and_export(path: String, export_format: FileExportFormat) -> PyResult<Vec<ExportFile>> {
tokio()
.spawn(async move {
let (code, path) = get_code_and_file_path(&path)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
let program = kcl_lib::Program::parse_no_errs(&code)
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
let settings = program.meta_settings()?.unwrap_or_default();
let units: UnitLength = settings.default_length_units.into();
let (ctx, mut state) = new_context_state(Some(path))
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run_with_ui_outputs(&program, &mut state)
.await
.map_err(into_miette)?;
// This will not return until there are files.
let resp = ctx
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
entity_ids: vec![],
format: get_output_format(&export_format, units.into()),
}),
)
.await?;
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
return Err(pyo3::exceptions::PyException::new_err(format!(
"Unexpected response from engine: {:?}",
resp
)));
};
Ok(files.into_iter().map(ExportFile::from).collect())
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Execute the kcl code and export it to a specific file format.
#[pyfunction]
async fn execute_code_and_export(code: String, export_format: FileExportFormat) -> PyResult<Vec<ExportFile>> {
tokio()
.spawn(async move {
let program =
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let settings = program.meta_settings()?.unwrap_or_default();
let units: UnitLength = settings.default_length_units.into();
let (ctx, mut state) = new_context_state(None)
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
// Execute the program.
ctx.run_with_ui_outputs(&program, &mut state)
.await
.map_err(into_miette)?;
// This will not return until there are files.
let resp = ctx
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
kcl_lib::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
entity_ids: vec![],
format: get_output_format(&export_format, units.into()),
}),
)
.await?;
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
return Err(pyo3::exceptions::PyException::new_err(format!(
"Unexpected response from engine: {:?}",
resp
)));
};
Ok(files.into_iter().map(ExportFile::from).collect())
})
.await
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
}
/// Format the kcl code.
#[pyfunction]
fn format(code: String) -> PyResult<String> {
let program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let recasted = program.recast();
Ok(recasted)
}
/// Lint the kcl code.
#[pyfunction]
fn lint(code: String) -> PyResult<Vec<Discovered>> {
let program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
let lints = program
.lint(checks::lint_variables)
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
Ok(lints)
}
/// The kcl python module.
#[pymodule]
fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
// Add our types to the module.
m.add_class::<ImageFormat>()?;
m.add_class::<ExportFile>()?;
m.add_class::<FileExportFormat>()?;
m.add_class::<UnitLength>()?;
m.add_class::<Discovered>()?;
// Add our functions to the module.
m.add_function(wrap_pyfunction!(execute, m)?)?;
m.add_function(wrap_pyfunction!(execute_code, m)?)?;
m.add_function(wrap_pyfunction!(execute_and_snapshot, m)?)?;
m.add_function(wrap_pyfunction!(execute_code_and_snapshot, m)?)?;
m.add_function(wrap_pyfunction!(execute_and_export, m)?)?;
m.add_function(wrap_pyfunction!(execute_code_and_export, m)?)?;
m.add_function(wrap_pyfunction!(format, m)?)?;
m.add_function(wrap_pyfunction!(lint, m)?)?;
Ok(())
}

View File

@ -0,0 +1,140 @@
#!/usr/bin/env python3
import os
import kcl
import pytest
# Get the path to this script's parent directory.
files_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "files")
kcl_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "..", "..", "kcl-lib"
)
lego_file = os.path.join(kcl_dir, "e2e", "executor", "inputs", "lego.kcl")
walkie_talkie_dir = os.path.join(files_dir, "walkie-talkie")
@pytest.mark.asyncio
async def test_kcl_execute_with_exception():
# Read from a file.
try:
await kcl.execute(os.path.join(files_dir, "parse_file_error"))
except Exception as e:
assert e is not None
assert len(str(e)) > 0
assert "lksjndflsskjfnak;jfna##" in str(e)
@pytest.mark.asyncio
async def test_kcl_execute():
# Read from a file.
await kcl.execute(lego_file)
@pytest.mark.asyncio
async def test_kcl_execute_code():
# Read from a file.
with open(lego_file, "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
await kcl.execute_code(code)
@pytest.mark.asyncio
async def test_kcl_execute_code_and_snapshot():
# Read from a file.
with open(lego_file, "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
image_bytes = await kcl.execute_code_and_snapshot(code, kcl.ImageFormat.Jpeg)
assert image_bytes is not None
assert len(image_bytes) > 0
@pytest.mark.asyncio
async def test_kcl_execute_code_and_export():
# Read from a file.
with open(lego_file, "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
files = await kcl.execute_code_and_export(code, kcl.FileExportFormat.Step)
assert files is not None
assert len(files) > 0
assert files[0] is not None
name = files[0].name
contents = files[0].contents
assert name is not None
assert len(name) > 0
assert contents is not None
assert len(contents) > 0
@pytest.mark.asyncio
async def test_kcl_execute_dir_assembly():
# Read from a file.
await kcl.execute(walkie_talkie_dir)
@pytest.mark.asyncio
async def test_kcl_execute_and_snapshot():
# Read from a file.
image_bytes = await kcl.execute_and_snapshot(lego_file, kcl.ImageFormat.Jpeg)
assert image_bytes is not None
assert len(image_bytes) > 0
@pytest.mark.asyncio
async def test_kcl_execute_and_snapshot_dir():
# Read from a file.
image_bytes = await kcl.execute_and_snapshot(
walkie_talkie_dir, kcl.ImageFormat.Jpeg
)
assert image_bytes is not None
assert len(image_bytes) > 0
@pytest.mark.asyncio
async def test_kcl_execute_and_export():
# Read from a file.
files = await kcl.execute_and_export(lego_file, kcl.FileExportFormat.Step)
assert files is not None
assert len(files) > 0
assert files[0] is not None
name = files[0].name
contents = files[0].contents
assert name is not None
assert len(name) > 0
assert contents is not None
assert len(contents) > 0
def test_kcl_format():
# Read from a file.
with open(lego_file, "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
formatted_code = kcl.format(code)
assert formatted_code is not None
assert len(formatted_code) > 0
def test_kcl_lint():
# Read from a file.
with open(os.path.join(files_dir, "box_with_linter_errors.kcl"), "r") as f:
code = str(f.read())
assert code is not None
assert len(code) > 0
lints = kcl.lint(code)
assert lints is not None
assert len(lints) > 0
description = lints[0].description
assert description is not None
assert len(description) > 0
finding = lints[0].finding
assert finding is not None
finding_title = finding.title
assert finding_title is not None
assert len(finding_title) > 0

View File

@ -13,8 +13,8 @@ name = "kcl-to-core"
path = "src/tool.rs" path = "src/tool.rs"
[dependencies] [dependencies]
anyhow = "1" anyhow = { workspace = true }
async-trait = "0.1.85" async-trait = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
kcl-lib = { path = "../kcl-lib" } kcl-lib = { path = "../kcl-lib" }
kittycad = { workspace = true, features = ["clap"] } kittycad = { workspace = true, features = ["clap"] }