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
- jessfraz
- package-ecosystem: 'cargo' # See documentation for possible values
directory: '/src/wasm-lib/' # Location of package manifests
directory: '/rust/' # Location of package manifests
schedule:
interval: weekly
day: monday
@ -39,3 +39,11 @@ updates:
wasm-bindgen-deps:
patterns:
- "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
.eslintcache
venv
.vite/
# electron
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",
]
[[package]]
name = "kcl-python-bindings"
version = "0.3.45"
dependencies = [
"anyhow",
"kcl-lib",
"kittycad-modeling-cmds",
"miette",
"pyo3",
"serde",
"serde_json",
"tokio",
"uuid",
]
[[package]]
name = "kcl-test-server"
version = "0.1.45"
@ -1833,9 +1848,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "828a0c74476533e6258ea7dd70cfc7d63a5df4b37753d30ef198e0689eaac4eb"
checksum = "fb5a824cb9bb4c602962ecbaca5ce71225938aa1abc24103bf46c222f468dd26"
dependencies = [
"anyhow",
"chrono",
@ -2530,6 +2545,7 @@ dependencies = [
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"serde",
"unindent",
]

View File

@ -3,6 +3,7 @@ resolver = "2"
members = [
"kcl-derive-docs",
"kcl-lib",
"kcl-python-bindings",
"kcl-test-server",
"kcl-to-core",
"kcl-wasm-lib"
@ -23,10 +24,14 @@ similar = { opt-level = 3 }
debug = "line-tables-only"
[workspace.dependencies]
async-trait = "0.1.85"
anyhow = { version = "1" }
http = "1"
indexmap = "2.7.0"
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_json = { version = "1" }
tokio = { version = "1" }

View File

@ -46,7 +46,9 @@ test:
publish-kcl version:
git tag kcl-{{version}}
git push origin kcl-{{version}}
cargo publish -p kcl-derive-docs
cargo publish -p kcl-lib
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"] }
[dev-dependencies]
anyhow = "1.0.95"
anyhow = { workspace = true }
expectorate = "1.1.0"
pretty_assertions = "1.4.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
[dependencies]
anyhow = { version = "1.0.95", features = ["backtrace"] }
anyhow = { workspace = true, features = ["backtrace"] }
async-recursion = "1.1.1"
async-trait = "0.1.85"
async-trait = {workspace = true}
base64 = "0.22.1"
chrono = "0.4.38"
clap = { version = "4.5.27", default-features = false, optional = true, features = [
@ -37,10 +37,10 @@ kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
lazy_static = "1.5.0"
measurements = "0.11.0"
miette = "7.5.0"
miette = { workspace = true }
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.6", optional = true }
pyo3 = { workspace = true, optional = true }
regex = "1.11.1"
reqwest = { version = "0.12", default-features = false, features = [
"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.

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"
[dependencies]
anyhow = "1"
async-trait = "0.1.85"
anyhow = { workspace = true }
async-trait = { workspace = true }
indexmap = { workspace = true }
kcl-lib = { path = "../kcl-lib" }
kittycad = { workspace = true, features = ["clap"] }