├── .envrc.example ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── ci_install.yml │ └── publish.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── bin ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ ├── invoke.py │ ├── main.rs │ ├── opt.rs │ ├── options.rs │ ├── py.rs │ └── shim.rs ├── build.py ├── example-schema.yaml ├── examples ├── count-vowels.py ├── imports.py └── imports_example.py ├── extism.pyi ├── install.sh ├── lib ├── .cargo │ └── config.toml ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ ├── lib.rs │ ├── prelude.py │ └── py_module.rs ├── pyproject.toml ├── rust-toolchain.toml └── uv.lock /.envrc.example: -------------------------------------------------------------------------------- 1 | export EXTISM_ENABLE_WASI_OUTPUT=1 # Enable WASI output for debugging -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zshipko @bhelx 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | include: 11 | - name: linux 12 | os: ubuntu-latest 13 | # - name: windows 14 | # os: windows-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | profile: default 20 | toolchain: 1.81.0 21 | target: wasm32-wasip1 22 | default: true 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: '1.x' 28 | 29 | - name: Setup Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: '3.12' 33 | 34 | - name: Update deps (Linux) 35 | run: | 36 | go install github.com/extism/cli/extism@v1.6.0 37 | cd /tmp 38 | # get just wasm-merge and wasm-opt 39 | curl -L https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-linux.tar.gz > binaryen.tar.gz 40 | tar xvzf binaryen.tar.gz 41 | sudo cp binaryen-version_117/bin/wasm-merge /usr/local/bin 42 | sudo cp binaryen-version_117/bin/wasm-opt /usr/local/bin 43 | if: runner.os != 'Windows' 44 | 45 | - name: Update deps (Windows) 46 | run: | 47 | powershell -executionpolicy bypass -File .\install-wasi-sdk.ps1 48 | go install github.com/extism/cli/extism@c1eb1fc 49 | Remove-Item -Recurse -Path "c:\Program files\Binaryen" -Force -ErrorAction SilentlyContinue > $null 2>&1 50 | New-Item -ItemType Directory -Force -Path "c:\Program files\Binaryen" -ErrorAction Stop > $null 2>&1 51 | Invoke-WebRequest -Uri "https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_117-x86_64-windows.tar.gz" -OutFile "$env:TMP\binaryen-version_116-x86_64-windows.tar.gz" 52 | 7z x "$env:TMP\binaryen-version_117-x86_64-windows.tar.gz" -o"$env:TMP\" >$null 2>&1 53 | 7z x -ttar "$env:TMP\binaryen-version_117-x86_64-windows.tar" -o"$env:TMP\" >$null 2>&1 54 | Copy-Item -Path "$env:TMP\binaryen-version_117\bin\wasm-opt.exe" -Destination "c:\Program files\Binaryen" -ErrorAction Stop > $null 2>&1 55 | Copy-Item -Path "$env:TMP\binaryen-version_117\bin\wasm-merge.exe" -Destination "c:\Program files\Binaryen" -ErrorAction Stop > $null 2>&1 56 | if: runner.os == 'Windows' 57 | 58 | - name: Run Tests (Linux) 59 | run: | 60 | make 61 | make test 62 | if: runner.os != 'Windows' 63 | 64 | - name: Run Tests (Windows) 65 | run: | 66 | set PATH="c:\Program files\Binaryen\";%PATH% 67 | make 68 | make test 69 | shell: cmd 70 | if: runner.os == 'Windows' 71 | 72 | -------------------------------------------------------------------------------- /.github/workflows/ci_install.yml: -------------------------------------------------------------------------------- 1 | name: "CI Install Script" 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | include: 11 | - name: linux 12 | os: ubuntu-latest 13 | - name: macos 14 | os: macos-latest 15 | # - name: windows 16 | # os: windows-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Test Install Script 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | run: | 24 | bash ./install.sh 25 | if: runner.os != 'Windows' 26 | 27 | - name: Test Installation 28 | run: | 29 | which extism-py 30 | extism-py --version 31 | if: runner.os != 'Windows' 32 | 33 | 34 | # - name: Test Install Script Part1 (Windows) 35 | # run: | 36 | # powershell -executionpolicy bypass -File .\install-windows.ps1 37 | # if: runner.os == 'Windows' 38 | 39 | # - name: Test Install Script Part2 (Windows) 40 | # run: | 41 | # $env:Path = "C:\Program Files\Extism\;C:\Program Files\Binaryen\;" + $env:Path 42 | # extism-py --version 43 | # if: runner.os == 'Windows' 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | compile_core: 11 | name: Compile Core 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Install Rust 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: default 20 | toolchain: 1.81.0 21 | target: wasm32-wasip1 22 | default: true 23 | 24 | - name: Setup Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.12' 28 | 29 | - name: Get wasm-opt 30 | run: | 31 | curl -L https://github.com/WebAssembly/binaryen/releases/download/version_116/binaryen-version_116-x86_64-linux.tar.gz > binaryen.tar.gz 32 | tar xvzf binaryen.tar.gz 33 | sudo cp binaryen-version_116/bin/wasm-opt /usr/local/bin 34 | sudo chmod +x /usr/local/bin/wasm-opt 35 | 36 | - name: Make core 37 | run: make core 38 | 39 | - name: Opt core 40 | run: wasm-opt --enable-reference-types --enable-bulk-memory --strip -O3 lib/target/wasm32-wasip1/release/core.wasm -o lib/target/wasm32-wasip1/release/core.wasm 41 | 42 | - name: Upload core binary to artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: engine 46 | path: lib/target/wasm32-wasip1/release/core.wasm 47 | 48 | - name: Upload wasi-deps 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: wasi-deps 52 | path: lib/target/wasm32-wasi/wasi-deps/ 53 | 54 | compile_cli: 55 | name: Compile CLI 56 | needs: compile_core 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | matrix: 60 | include: 61 | - name: linux 62 | os: ubuntu-latest 63 | path: bin/target/x86_64-unknown-linux-gnu/release/extism-py 64 | asset_name: extism-py-x86_64-linux-${{ github.event.release.tag_name }} 65 | shasum_cmd: sha256sum 66 | target: x86_64-unknown-linux-gnu 67 | # - name: linux-arm64 68 | # os: ubuntu-latest 69 | # path: bin/target/aarch64-unknown-linux-gnu/release/extism-py 70 | # asset_name: extism-py-aarch64-linux-${{ github.event.release.tag_name }} 71 | # shasum_cmd: sha256sum 72 | # target: aarch64-unknown-linux-gnu 73 | - name: macos 74 | os: macos-latest 75 | path: bin/target/x86_64-apple-darwin/release/extism-py 76 | asset_name: extism-py-x86_64-macos-${{ github.event.release.tag_name }} 77 | shasum_cmd: shasum -a 256 78 | target: x86_64-apple-darwin 79 | - name: macos-arm64 80 | os: macos-latest 81 | path: bin/target/aarch64-apple-darwin/release/extism-py 82 | asset_name: extism-py-aarch64-macos-${{ github.event.release.tag_name }} 83 | shasum_cmd: shasum -a 256 84 | target: aarch64-apple-darwin 85 | # - name: windows 86 | # os: windows-latest 87 | # path: bin\target\x86_64-pc-windows-msvc\release\extism-py.exe 88 | # asset_name: extism-py-x86_64-windows-${{ github.event.release.tag_name }} 89 | # target: x86_64-pc-windows-msvc 90 | # - name: windows-arm64 91 | # os: windows-latest 92 | # path: bin\target\aarch64-pc-windows-msvc\release\extism-py.exe 93 | # asset_name: extism-py-aarch64-windows-${{ github.event.release.tag_name }} 94 | # target: aarch64-pc-windows-msvc 95 | 96 | steps: 97 | - uses: actions/checkout@v1 98 | 99 | - uses: actions/download-artifact@v4 100 | with: 101 | name: engine 102 | path: bin 103 | 104 | - uses: actions/download-artifact@v4 105 | with: 106 | name: wasi-deps 107 | path: wasi-deps 108 | 109 | - name: Install Rust 110 | uses: actions-rs/toolchain@v1 111 | with: 112 | profile: default 113 | toolchain: stable 114 | target: ${{ matrix.target }} 115 | default: true 116 | 117 | - name: Setup Python 118 | uses: actions/setup-python@v5 119 | with: 120 | python-version: '3.12' 121 | 122 | - name: Install gnu gcc 123 | run: | 124 | sudo apt-get update 125 | sudo apt-get install g++-aarch64-linux-gnu 126 | sudo apt-get install gcc-aarch64-linux-gnu 127 | if: matrix.os == 'ubuntu-latest' 128 | 129 | - name: Build CLI ${{ matrix.os }} 130 | env: 131 | EXTISM_ENGINE_PATH: core.wasm 132 | run: cd bin && rustup target add ${{ matrix.target }} && cargo build --release --target ${{ matrix.target }} --package extism-py 133 | 134 | - name: Create directory structure 135 | run: mkdir -p extism-py/bin extism-py/share && mv wasi-deps extism-py/share/extism-py && mv ${{ matrix.path }} extism-py/bin/ 136 | 137 | - name: Archive assets 138 | run: tar czf ${{ matrix.asset_name }}.tar.gz extism-py 139 | 140 | - name: Upload assets to artifacts 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: ${{ matrix.asset_name }}.tar.gz 144 | path: ${{ matrix.asset_name }}.tar.gz 145 | 146 | - name: Upload assets to release 147 | uses: actions/upload-release-asset@v1 148 | env: 149 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 150 | with: 151 | upload_url: ${{ github.event.release.upload_url }} 152 | asset_path: ./${{ matrix.asset_name }}.tar.gz 153 | asset_name: ${{ matrix.asset_name }}.tar.gz 154 | asset_content_type: application/gzip 155 | 156 | - name: Generate asset hash (Linux/MacOS) 157 | run: ${{ matrix.shasum_cmd }} ${{ matrix.asset_name }}.tar.gz | awk '{ print $1 }' > ${{ matrix.asset_name }}.tar.gz.sha256 158 | if: runner.os != 'Windows' 159 | 160 | - name: Generate asset hash (Windows) 161 | run: Get-FileHash -Path ${{ matrix.asset_name }}.tar.gz -Algorithm SHA256 | Select-Object -ExpandProperty Hash > ${{ matrix.asset_name }}.tar.gz.sha256 162 | shell: pwsh 163 | if: runner.os == 'Windows' 164 | 165 | - name: Upload asset hash to artifacts 166 | uses: actions/upload-artifact@v4 167 | with: 168 | name: ${{ matrix.asset_name }}.tar.gz.sha256 169 | path: ${{ matrix.asset_name }}.tar.gz.sha256 170 | 171 | - name: Upload asset hash to release 172 | uses: actions/upload-release-asset@v1 173 | env: 174 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 175 | with: 176 | upload_url: ${{ github.event.release.upload_url }} 177 | asset_path: ./${{ matrix.asset_name }}.tar.gz.sha256 178 | asset_name: ${{ matrix.asset_name }}.tar.gz.sha256 179 | asset_content_type: plain/text 180 | 181 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | extism-py 4 | examples/*.wasm 5 | .envrc -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | exclude = ["lib", "bin"] 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON_FILES=lib/src/prelude.py bin/src/invoke.py build.py 2 | 3 | build: 4 | ./build.py build 5 | 6 | install: 7 | ./build.py install 8 | 9 | format: 10 | uv run ruff format $(PYTHON_FILES) 11 | 12 | check: 13 | uv run ruff check $(PYTHON_FILES) 14 | 15 | clean: 16 | rm -rf bin/target lib/target 17 | 18 | core: 19 | cd lib && cargo build --release 20 | 21 | test: examples 22 | EXTISM_ENABLE_WASI_OUTPUT=1 extism call ./examples/count-vowels.wasm count_vowels --wasi --input "this is a test" 23 | EXTISM_ENABLE_WASI_OUTPUT=1 extism call ./examples/imports.wasm count_vowels --wasi --input "this is a test" --link example=./examples/imports_example.wasm 24 | 25 | 26 | .PHONY: examples 27 | examples: build 28 | ./extism-py -o examples/count-vowels.wasm examples/count-vowels.py 29 | ./extism-py -o examples/imports.wasm examples/imports.py 30 | ./extism-py -o examples/imports_example.wasm examples/imports_example.py 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism Python PDK 2 | 3 | ![GitHub License](https://img.shields.io/github/license/extism/extism) 4 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/python-pdk) 5 | 6 | This project contains a tool that can be used to create 7 | [Extism Plug-ins](https://extism.org/docs/concepts/plug-in) in Python. 8 | 9 | ## Overview 10 | 11 | This PDK uses [PyO3](https://github.com/PyO3/pyo3) and 12 | [wizer](https://github.com/bytecodealliance/wizer) to run Python code as an 13 | Extism Plug-in. 14 | 15 | ## Install the compiler 16 | 17 | We release the compiler as native binaries you can download and run. Check the 18 | [releases](https://github.com/extism/python-pdk/releases) page for the latest. 19 | 20 | ## Install Script 21 | 22 | ### Linux, macOS 23 | 24 | ```bash 25 | curl -Ls https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash 26 | ``` 27 | 28 | This will install `extism-py` (and `wasm-merge`/`wasm-opt` if not already 29 | installed) to `$HOME/.local/bin` and create `$HOME/.local/share/extism-py` 30 | 31 | ### Testing the Install 32 | 33 | > _Note_: [Binaryen](https://github.com/WebAssembly/binaryen), specifically the 34 | > `wasm-merge` and `wasm-opt` tools are required as a dependency. We will try to 35 | > package this up eventually but for now it must be reachable on your machine. 36 | > You can install on mac with `brew install binaryen` or see their 37 | > [releases page](https://github.com/WebAssembly/binaryen/releases). 38 | 39 | Then run command with no args to see the help: 40 | 41 | ``` 42 | extism-py 43 | error: The following required arguments were not provided: 44 | 45 | 46 | USAGE: 47 | extism-py -o 48 | 49 | For more information try --help 50 | ``` 51 | 52 | > **Note**: If you are using mac, you may need to tell your security system this 53 | > unsigned binary is fine. If you think this is dangerous, or can't get it to 54 | > work, see the "compile from source" section below. 55 | 56 | ## Getting Started 57 | 58 | The goal of writing an 59 | [Extism plug-in](https://extism.org/docs/concepts/plug-in) is to compile your 60 | Python code to a Wasm module with exported functions that the host application 61 | can invoke. The first thing you should understand is creating an export. 62 | 63 | ## Python Dependencies 64 | 65 | It is possible to add directories to the Python search path using the `PYTHONPATH` 66 | environment variable. This means you should be able to install pure Python dependencies 67 | into a virtual environment and add the library path to `PYTHONPATH` before executing 68 | `extism-py`, for example: 69 | 70 | Given code with an import from an external package 71 | 72 | ```python 73 | import extism 74 | import toml # this is from pypi 75 | ``` 76 | 77 | You can add your dependencies to a virtual environment 78 | 79 | ```bash 80 | $ python3 -m virtualenv ./deps 81 | $ source ./deps/bin/activate 82 | $ pip install toml 83 | ``` 84 | 85 | And then point `extism-py` to the dependencies (still using the active virtualenv shell) 86 | 87 | ```bash 88 | $ PYTHONPATH=$(python3 -c "import sys; print(sys.path[-1])") extism-py -o a.wasm plugin.py 89 | ``` 90 | 91 | It's also possible to reference the `site-packages` path directly, even from outside the 92 | virtual environment: 93 | 94 | ```bash 95 | $ PYTHONPATH=./deps/lib/python3.12/site-packages extism-py -o a.wasm plugin.py 96 | ``` 97 | 98 | **Note**: This only works with pure Python dependencies, packages that require native shared libraries 99 | aren't supported. 100 | 101 | ### Exports 102 | 103 | Let's write a simple program that exports a `greet` function which will take a 104 | name as a string and return a greeting string. Paste this into a file 105 | `plugin.py`: 106 | 107 | ```python 108 | import extism 109 | 110 | @extism.plugin_fn 111 | def greet(): 112 | name = extism.input_str() 113 | extism.output_str(f"Hello, {name}") 114 | ``` 115 | 116 | Some things to note about this code: 117 | 118 | 1. We can export functions by name using the `extism.plugin_fn` decorator. This 119 | allows the host to invoke this function. 120 | 2. In this PDK we code directly to the ABI. We get input from using the 121 | `extism.input*` functions and we return data back with the `extism.output*` 122 | functions. 123 | 124 | Let's compile this to Wasm now using the `extism-py` tool: 125 | 126 | ```bash 127 | extism-py plugin.py -o plugin.wasm 128 | ``` 129 | 130 | We can now test `plugin.wasm` using the 131 | [Extism CLI](https://github.com/extism/cli)'s `run` command: 132 | 133 | ```bash 134 | extism call plugin.wasm greet --input="Benjamin" --wasi 135 | # => Hello, Benjamin! 136 | ``` 137 | 138 | > **Note**: Currently `wasi` must be provided for all Python plug-ins even if 139 | > they don't need system access. 140 | 141 | > **Note**: We also have a web-based, plug-in tester called the 142 | > [Extism Playground](https://playground.extism.org/) 143 | 144 | ### More Exports: Error Handling 145 | 146 | We catch any exceptions thrown and return them as errors to the host. Suppose we 147 | want to re-write our greeting module to never greet Benjamins: 148 | 149 | ```python 150 | import extism 151 | 152 | @extism.plugin_fn 153 | def greet(): 154 | name = extism.input_str() 155 | if name == "Benjamin": 156 | raise Exception("Sorry, we don't greet Benjamins!") 157 | extism.output_str(f"Hello, {name}") 158 | ``` 159 | 160 | Now compile and run: 161 | 162 | ```bash 163 | extism-py plugin.py -o plugin.wasm 164 | extism call plugin.wasm greet --input="Benjamin" --wasi 165 | # => Error: Sorry, we don't greet Benjamins!: 166 | # => File "", line 17, in __invoke 167 | # => File "", line 9, in greet 168 | echo $? # print last status code 169 | # => 1 170 | extism call plugin.wasm greet --input="Zach" --wasi 171 | # => Hello, Zach! 172 | echo $? 173 | # => 0 174 | ``` 175 | 176 | ### JSON 177 | 178 | ```python 179 | import extism 180 | 181 | @extism.plugin_fn 182 | def sum(): 183 | params = extism.input_json() 184 | extism.output_json({"sum": params['a'] + params['b']}) 185 | ``` 186 | 187 | ```bash 188 | extism call plugin.wasm sum --input='{"a": 20, "b": 21}' --wasi 189 | # => {"sum":41} 190 | ``` 191 | 192 | For automatic deserialization of input types and serialization of output types, 193 | see [XTP Python Bindgen](https://github.com/dylibso/xtp-python-bindgen/) . The 194 | `extism.Json` dataclass serialization has been removed in-favor of the 195 | [Dataclass Wizard](https://dataclass-wizard.readthedocs.io/en/latest/index.html) 196 | based solution there. 197 | 198 | ### Configs 199 | 200 | Configs are key-value pairs that can be passed in by the host when creating a 201 | plug-in. These can be useful to statically configure the plug-in with some data 202 | that exists across every function call. Here is a trivial example using 203 | `Config.get`: 204 | 205 | ```python 206 | import extism 207 | 208 | @extism.plugin_fn 209 | def greet(): 210 | user = extism.Config.get_str("user") 211 | extism.output_str(f"Hello, {user}!") 212 | ``` 213 | 214 | To test it, the [Extism CLI](https://github.com/extism/cli) has a `--config` 215 | option that lets you pass in `key=value` pairs: 216 | 217 | ```bash 218 | extism call plugin.wasm greet --config user=Benjamin --wasi 219 | # => Hello, Benjamin! 220 | ``` 221 | 222 | ### Logging 223 | 224 | At the current time, calling `console.log` emits an `info` log. Please file an 225 | issue or PR if you want to expose the raw logging interface: 226 | 227 | ```python 228 | import extism 229 | 230 | @extism.plugin_fn 231 | def log_stuff(): 232 | extism.log(Extism.LogLevel.Info, "Hello, world!") 233 | ``` 234 | 235 | Running it, you need to pass a log-level flag: 236 | 237 | ``` 238 | extism call plugin.wasm logStuff --wasi --log-level=info 239 | # => 2023/10/17 14:25:00 Hello, World! 240 | ``` 241 | 242 | ### Using Host Functions 243 | 244 | You can defer logic to host functions from your plugin code. 245 | Create a function stub using the `import_fn` decorator: 246 | 247 | ```python 248 | # plugin.py 249 | import extism 250 | 251 | @extism.import_fn("app", "kv_store") # (namespace, function name) 252 | def kv_store(input: str): ... 253 | 254 | @extism.plugin_fn 255 | def do_something(): 256 | data = extism.input(...) 257 | kv_store(data) 258 | ``` 259 | 260 | Implement the host function using the `host_fn` decorator: 261 | 262 | ```python 263 | # app.py 264 | import extism 265 | 266 | RECORDS = [] 267 | 268 | @extism.host_fn(name="kv_store", namespace="app") 269 | def kv_store(data: ...): 270 | RECORDS.append(data) 271 | 272 | with extism.Plugin(...) as plugin: 273 | plugin.call("do_something", ...) 274 | ``` 275 | 276 | ## Generating Bindings 277 | 278 | It's often very useful to define a schema to describe the function signatures 279 | and types you want to use between Extism SDK and PDK languages. 280 | 281 | [XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source 282 | framework to generate PDK bindings for Extism plug-ins. It's used by the 283 | [XTP Platform](https://www.getxtp.com/), but can be used outside of the platform 284 | to define any Extism compatible plug-in system. 285 | 286 | ### 1. Install the `xtp` CLI. 287 | 288 | See installation instructions 289 | [here](https://docs.xtp.dylibso.com/docs/cli#installation). 290 | 291 | ### 2. Create a schema using our OpenAPI-inspired IDL: 292 | 293 | ```yaml 294 | version: v1-draft 295 | exports: 296 | CountVowels: 297 | input: 298 | type: string 299 | contentType: text/plain; charset=utf-8 300 | output: 301 | $ref: "#/components/schemas/VowelReport" 302 | contentType: application/json 303 | # components.schemas defined in example-schema.yaml... 304 | ``` 305 | 306 | > See an example in [example-schema.yaml](./example-schema.yaml), or a full 307 | > "kitchen sink" example on 308 | > [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/). 309 | 310 | ### 3. Generate bindings to use from your plugins: 311 | 312 | ``` 313 | xtp plugin init --schema-file ./example-schema.yaml 314 | 1. TypeScript 315 | 2. Go 316 | 3. Rust 317 | > 4. Python 318 | 5. C# 319 | 6. Zig 320 | 7. C++ 321 | 8. GitHub Template 322 | 9. Local Template 323 | ``` 324 | 325 | This will create an entire boilerplate plugin project for you to get started 326 | with: 327 | 328 | ```python 329 | # returns VowelReport (The result of counting vowels on the Vowels input.) 330 | def count_vowels(input: str) -> VowelReport: 331 | raise Exception("Unimplemented: CountVowels") 332 | ``` 333 | 334 | Implement the empty function(s), and run `xtp plugin build` to compile your 335 | plugin. 336 | 337 | > For more information about XTP Bindgen, see the 338 | > [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and 339 | > the official 340 | > [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema). 341 | 342 | ## Compiling the compiler from source 343 | 344 | ### Prerequisites 345 | 346 | Before compiling the compiler, you need to install prerequisites. 347 | 348 | 1. Install Rust using [rustup](https://rustup.rs) 349 | 2. Install the WASI target platform via 350 | `rustup target add --toolchain stable wasm32-wasip1` 351 | 3. Install [CMake](https://cmake.org/install/) (on macOS with homebrew, 352 | `brew install cmake`) 353 | 4. Install [Binaryen](https://github.com/WebAssembly/binaryen/) and add it's 354 | install location to your PATH (only wasm-opt is required for build process) 355 | 5. Install [7zip](https://www.7-zip.org/)(only for Windows) 356 | 357 | ### Compiling from source 358 | 359 | Run make to compile the core crate (the engine) and the cli: 360 | 361 | ``` 362 | ./build.py 363 | ``` 364 | 365 | To test the built compiler (ensure you have Extism installed): 366 | 367 | ```bash 368 | ./extism-py examples/count-vowels.py -o count-vowels.wasm 369 | extism call out.wasm count_vowels --wasi --input='Hello World Test!' 370 | # => "{\"count\":4}" 371 | ``` 372 | 373 | ### Debugging 374 | 375 | To improve your debugging experience and get more information for panics 376 | and crashes, you should set the `EXTISM_ENABLE_WASI_OUTPUT` env var. 377 | 378 | ## How it works 379 | 380 | This works a little differently than other PDKs. You cannot compile Python to 381 | Wasm because it doesn't have an appropriate type system to do this. The 382 | `extism-py` command we have provided here is a little compiler / wrapper that 383 | does a series of things for you: 384 | 385 | 1. It loads an "engine" Wasm program containing the Python runtime 386 | 2. It initializes the Python runtime 387 | 3. It loads your Python source code into memory 388 | 4. It parses the Python source code for exports and generates 1-to-1 proxy 389 | export functions in Wasm 390 | 5. It freezes and emits the machine state as a new Wasm file at this 391 | post-initialized point in time 392 | 393 | This new Wasm file can be used just like any other Extism plugin. 394 | -------------------------------------------------------------------------------- /bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "extism-py" 3 | version = "0.1.5" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.86" 8 | directories = "5.0.1" 9 | env_logger = "0.11.5" 10 | log = "0.4.22" 11 | rustpython-parser = "0.4.0" 12 | structopt = "0.3.26" 13 | tempfile = "3.12.0" 14 | wagen = "0.2.0" 15 | wizer = "7.0.0" 16 | -------------------------------------------------------------------------------- /bin/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo::rerun-if-changed=src/invoke.py"); 3 | 4 | let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("core.wasm"); 5 | if let Ok(path) = std::env::var("EXTISM_ENGINE_PATH") { 6 | println!("cargo::rerun-if-changed={path}"); 7 | std::fs::copy(path, out).unwrap(); 8 | } else { 9 | println!("cargo::rerun-if-changed=../lib/target/wasm32-wasip1/release/core.wasm"); 10 | std::fs::copy("../lib/target/wasm32-wasip1/release/core.wasm", out).unwrap(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/src/invoke.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | 4 | def __invoke(index, shared, *args): 5 | import extism 6 | 7 | try: 8 | f = extism.__exports[index] 9 | 10 | if shared: 11 | a = [] 12 | argnames = f.__code__.co_varnames 13 | for i, arg in enumerate(args): 14 | t = f.__annotations__.get(argnames[i], extism.memory.MemoryHandle) 15 | a.append(extism._load(t, arg)) 16 | else: 17 | a = [extism._store(x) for x in args] 18 | 19 | res = f(*a) 20 | if shared and res is not None: 21 | return extism._store(res) 22 | if res is not None and "return" in f.__annotations__: 23 | return extism._load(f.__annotations__["return"], res) 24 | else: 25 | return res 26 | except BaseException as exc: 27 | tb = "".join(traceback.format_tb(exc.__traceback__)) 28 | err = f"{str(exc)}:\n{tb}" 29 | extism.ffi.set_error(err) 30 | raise exc 31 | -------------------------------------------------------------------------------- /bin/src/main.rs: -------------------------------------------------------------------------------- 1 | mod opt; 2 | mod options; 3 | mod py; 4 | mod shim; 5 | 6 | use anyhow::{bail, Error}; 7 | use log::LevelFilter; 8 | use options::Options; 9 | use structopt::StructOpt; 10 | use tempfile::TempDir; 11 | 12 | use std::borrow::Cow; 13 | use std::env; 14 | use std::io::Write; 15 | use std::process::{Command, Stdio}; 16 | 17 | const CORE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/core.wasm")); 18 | const INVOKE: &str = include_str!("invoke.py"); 19 | 20 | #[derive(Debug, Clone)] 21 | struct Import { 22 | module: String, 23 | name: String, 24 | params: Vec, 25 | results: Vec, 26 | } 27 | 28 | #[derive(Debug, Clone)] 29 | struct Export { 30 | name: String, 31 | is_plugin_fn: bool, 32 | params: Vec, 33 | results: Vec, 34 | } 35 | 36 | fn main() -> Result<(), Error> { 37 | // Setup logging 38 | let mut builder = env_logger::Builder::new(); 39 | builder 40 | .filter(None, LevelFilter::Info) 41 | .target(env_logger::Target::Stdout) 42 | .init(); 43 | 44 | // Parse CLI arguments 45 | let opts = Options::from_args(); 46 | let core: Cow<[u8]> = if let Ok(path) = std::env::var("EXTISM_ENGINE_PATH") { 47 | Cow::Owned(std::fs::read(path)?) 48 | } else { 49 | Cow::Borrowed(CORE) 50 | }; 51 | 52 | // Generate core module if `core` flag is set 53 | if opts.core { 54 | opt::Optimizer::new(&core) 55 | .wizen(true) 56 | .debug(opts.debug) 57 | .write_optimized_wasm(opts.output)?; 58 | return Ok(()); 59 | } 60 | 61 | let mut user_code = std::fs::read_to_string(&opts.input_py)?; 62 | user_code.push('\n'); 63 | user_code += INVOKE; 64 | 65 | let tmp_dir = TempDir::new()?; 66 | let core_path = tmp_dir.path().join("core.wasm"); 67 | let shim_path = tmp_dir.path().join("shim.wasm"); 68 | 69 | let self_cmd = env::args().next().expect("Expected a command argument"); 70 | { 71 | let mut command = Command::new(self_cmd); 72 | command 73 | .arg("-c") 74 | .arg(&opts.input_py) 75 | .arg("-o") 76 | .arg(&core_path); 77 | 78 | command.env_clear(); 79 | 80 | if opts.debug { 81 | command.arg("-g"); 82 | } 83 | let mut command = command.stdin(Stdio::piped()).spawn()?; 84 | command 85 | .stdin 86 | .take() 87 | .expect("Expected to get writeable stdin") 88 | .write_all(user_code.as_bytes())?; 89 | let status = command.wait()?; 90 | if !status.success() { 91 | bail!("Couldn't create wasm from input"); 92 | } 93 | } 94 | 95 | let (imports, exports) = py::find_imports_and_exports(user_code)?; 96 | if exports.is_empty() { 97 | anyhow::bail!( 98 | "No exports found, use the @extism.plugin_fn decorator to specify exported functions" 99 | ) 100 | } 101 | 102 | shim::generate(&exports, &imports, &shim_path)?; 103 | 104 | let output = Command::new("wasm-merge") 105 | .arg("--version") 106 | .stdout(Stdio::null()) 107 | .stderr(Stdio::null()) 108 | .status(); 109 | if output.is_err() { 110 | bail!("Failed to detect wasm-merge. Please install binaryen and make sure wasm-merge is on your path: https://github.com/WebAssembly/binaryen"); 111 | } 112 | 113 | // Merge the shim with the core module 114 | let mut cmd = Command::new("wasm-merge"); 115 | cmd.arg(&core_path) 116 | .arg("core") 117 | .arg(&shim_path) 118 | .arg("shim") 119 | .arg("-o") 120 | .arg(&opts.output) 121 | .arg("--enable-reference-types") 122 | .arg("--enable-bulk-memory"); 123 | if opts.debug { 124 | cmd.arg("-g"); 125 | } 126 | 127 | let status = cmd.status()?; 128 | if !status.success() { 129 | bail!("wasm-merge failed. Couldn't merge shim"); 130 | } 131 | 132 | opt::optimize_wasm_file(opts.output, opts.debug)?; 133 | Ok(()) 134 | } 135 | -------------------------------------------------------------------------------- /bin/src/opt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use std::{ 3 | path::{Path, PathBuf}, 4 | process::{Command, Stdio}, 5 | }; 6 | use wizer::Wizer; 7 | 8 | pub(crate) struct Optimizer<'a> { 9 | wizen: bool, 10 | optimize: bool, 11 | wasm: &'a [u8], 12 | debug: bool, 13 | } 14 | 15 | fn find_deps() -> PathBuf { 16 | if let Ok(path) = std::env::var("EXTISM_PYTHON_WASI_DEPS_DIR") { 17 | return PathBuf::from(path); 18 | } 19 | 20 | let in_repo = PathBuf::from("../lib/target/wasm32-wasi/wasi-deps"); 21 | if in_repo.exists() { 22 | return in_repo; 23 | } 24 | 25 | let in_repo_root = PathBuf::from("lib/target/wasm32-wasi/wasi-deps"); 26 | if in_repo_root.exists() { 27 | return in_repo_root; 28 | } 29 | 30 | let usr_local_share = PathBuf::from("/usr/local/share/extism-py"); 31 | if usr_local_share.exists() { 32 | return usr_local_share; 33 | } 34 | 35 | let usr_share = PathBuf::from("/usr/share/extism-py"); 36 | if usr_share.exists() { 37 | return usr_share; 38 | } 39 | 40 | directories::BaseDirs::new() 41 | .unwrap() 42 | .data_dir() 43 | .join("extism-py") 44 | } 45 | 46 | impl<'a> Optimizer<'a> { 47 | pub fn new(wasm: &'a [u8]) -> Self { 48 | Self { 49 | wasm, 50 | optimize: false, 51 | wizen: false, 52 | debug: false, 53 | } 54 | } 55 | 56 | #[allow(unused)] 57 | pub fn optimize(self, optimize: bool) -> Self { 58 | Self { optimize, ..self } 59 | } 60 | 61 | #[allow(unused)] 62 | pub fn debug(self, debug: bool) -> Self { 63 | Self { debug, ..self } 64 | } 65 | 66 | pub fn wizen(self, wizen: bool) -> Self { 67 | Self { wizen, ..self } 68 | } 69 | 70 | #[cfg(target_os = "windows")] 71 | fn convert_windows_paths(&self, paths: Vec<(String, PathBuf)>) -> Vec<(String, PathBuf)> { 72 | use std::path::Component; 73 | let mut ret = vec![]; 74 | for (_, path) in paths { 75 | let new_path = 76 | path.components() 77 | .filter_map(|comp| match comp { 78 | Component::Normal(part) => Some(part.to_string_lossy().to_string()), 79 | _ => None, // Skip root, prefix, or other non-normal components 80 | }) 81 | .collect::>() 82 | .join("/"); 83 | let normalized = format!("/{}", new_path); 84 | ret.push((normalized, path)); 85 | } 86 | ret 87 | } 88 | 89 | pub fn write_optimized_wasm(self, dest: impl AsRef) -> Result<(), Error> { 90 | let python_path = std::env::var("PYTHONPATH").unwrap_or_else(|_| String::from(".")); 91 | let split_paths = std::env::split_paths(&python_path); 92 | let paths: Vec<(String, PathBuf)> = split_paths.map(|p| (p.to_string_lossy().to_string(), p)).collect(); 93 | 94 | #[cfg(target_os = "windows")] 95 | let paths = self.convert_windows_paths(paths); 96 | #[cfg(target_os = "windows")] 97 | std::env::set_var("PYTHONPATH", paths.iter().map(|p| p.0.clone()).collect::>().join(":")); 98 | 99 | // Ensure compatibility with old releases 100 | let mut deps = find_deps().join("usr"); 101 | if !deps.exists() { 102 | let parent = deps.parent().unwrap(); 103 | if parent.join("local").exists() { 104 | deps = parent.to_path_buf(); 105 | } else { 106 | anyhow::bail!("wasi-deps path doesn't exist: {}", deps.display()); 107 | } 108 | } 109 | 110 | if self.wizen { 111 | let mut w = Wizer::new(); 112 | w.allow_wasi(true)? 113 | .inherit_stdio(true) 114 | .inherit_env(true) 115 | .wasm_bulk_memory(true) 116 | .map_dir("/usr", deps); 117 | for (mapped, path) in paths { 118 | if !path.exists() { 119 | continue; 120 | } 121 | w.map_dir(mapped, path); 122 | } 123 | let wasm = w.run(self.wasm)?; 124 | std::fs::write(&dest, wasm)?; 125 | } else { 126 | std::fs::write(&dest, self.wasm)?; 127 | } 128 | 129 | if self.optimize { 130 | optimize_wasm_file(dest, self.debug)?; 131 | } 132 | 133 | Ok(()) 134 | } 135 | } 136 | 137 | pub(crate) fn optimize_wasm_file(dest: impl AsRef, debug: bool) -> Result<(), Error> { 138 | let output = Command::new("wasm-opt") 139 | .arg("--version") 140 | .stdout(Stdio::null()) 141 | .stderr(Stdio::null()) 142 | .status(); 143 | if output.is_err() { 144 | anyhow::bail!("Failed to detect wasm-opt. Please install binaryen and make sure wasm-opt is on your path: https://github.com/WebAssembly/binaryen"); 145 | } 146 | let mut cmd = Command::new("wasm-opt"); 147 | cmd.arg("--enable-reference-types") 148 | .arg("--enable-bulk-memory") 149 | .arg("-O2"); 150 | if debug { 151 | cmd.arg("-g"); 152 | } else { 153 | cmd.arg("--strip"); 154 | } 155 | cmd.arg(dest.as_ref()) 156 | .arg("-o") 157 | .arg(dest.as_ref()) 158 | .status()?; 159 | Ok(()) 160 | } 161 | -------------------------------------------------------------------------------- /bin/src/options.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use structopt::StructOpt; 3 | 4 | #[derive(Debug, StructOpt)] 5 | #[structopt(name = "extism-py", about = "Extism Python PDK compiler")] 6 | pub struct Options { 7 | #[structopt(parse(from_os_str))] 8 | pub input_py: PathBuf, 9 | 10 | #[structopt(short = "o", parse(from_os_str), default_value = "index.wasm")] 11 | pub output: PathBuf, 12 | 13 | #[structopt(short = "c")] 14 | pub core: bool, 15 | 16 | #[structopt(short = "g")] 17 | pub debug: bool, 18 | } 19 | -------------------------------------------------------------------------------- /bin/src/py.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use anyhow::Error; 3 | 4 | fn get_import( 5 | f: &rustpython_parser::ast::StmtFunctionDef, 6 | call: &rustpython_parser::ast::ExprCall, 7 | ) -> Result { 8 | // println!("{:?} {:?}", f, call); 9 | let mut module = None; 10 | let mut func = None; 11 | 12 | let n_args = f.args.args.len(); 13 | let has_return = f.returns.is_some(); 14 | 15 | if let Some(module_name) = call.args[0].as_constant_expr() { 16 | if let Some(module_name) = module_name.value.as_str() { 17 | module = Some(module_name.to_string()); 18 | } 19 | } 20 | 21 | if let Some(func_name) = call.args[1].as_constant_expr() { 22 | if let Some(func_name) = func_name.value.as_str() { 23 | func = Some(func_name.to_string()); 24 | } 25 | } 26 | 27 | // println!("IMPORT {:?}::{:?}: {n_args} -> {has_return}", module, func); 28 | match (module, func) { 29 | (Some(module), Some(func)) => Ok(Import { 30 | module, 31 | name: func, 32 | params: vec![wagen::ValType::I64; n_args], 33 | results: if has_return { 34 | vec![wagen::ValType::I64] 35 | } else { 36 | vec![] 37 | }, 38 | }), 39 | _ => { 40 | anyhow::bail!("Invalid import, import_fn must include a module name and function name") 41 | } 42 | } 43 | } 44 | 45 | fn get_export( 46 | f: &rustpython_parser::ast::StmtFunctionDef, 47 | is_plugin_fn: bool, 48 | ) -> Result { 49 | let func = f.name.to_string(); 50 | 51 | let n_args = f.args.args.len(); 52 | let has_return = f.returns.is_some(); 53 | 54 | if is_plugin_fn && n_args > 0 { 55 | anyhow::bail!( 56 | "plugin_fn expects a function with no arguments, {func} should have no arguments" 57 | ); 58 | } 59 | Ok(Export { 60 | name: func, 61 | is_plugin_fn, 62 | params: vec![wagen::ValType::I64; n_args], 63 | results: if is_plugin_fn { 64 | vec![wagen::ValType::I32] 65 | } else if has_return { 66 | vec![wagen::ValType::I64] 67 | } else { 68 | vec![] 69 | }, 70 | }) 71 | } 72 | 73 | fn get_import_fn_decorator( 74 | f: &rustpython_parser::ast::StmtFunctionDef, 75 | ) -> Result, Error> { 76 | for d in f.decorator_list.iter() { 77 | if let Some(call) = d.as_call_expr() { 78 | if let Some(name) = call.func.as_attribute_expr() { 79 | if let Some(n) = name.value.as_name_expr() { 80 | if n.id.as_str() == "import_fn" 81 | || n.id.as_str() == "extism" && name.attr.as_str() == "import_fn" 82 | { 83 | return get_import(f, call).map(Some); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | Ok(None) 91 | } 92 | 93 | fn get_export_decorator( 94 | f: &rustpython_parser::ast::StmtFunctionDef, 95 | ) -> Result, Error> { 96 | for d in f.decorator_list.iter() { 97 | if let Some(call) = d.as_call_expr() { 98 | if let Some(name) = call.func.as_attribute_expr() { 99 | if let Some(n) = name.value.as_name_expr() { 100 | if n.id.as_str() == "plugin_fn" 101 | || n.id.as_str() == "extism" && name.attr.as_str() == "plugin_fn" 102 | { 103 | anyhow::bail!("extism.plugin_fn takes no arguments"); 104 | } else if n.id.as_str() == "shared_fn" 105 | || n.id.as_str() == "extism" && name.attr.as_str() == "shared_fn" 106 | { 107 | anyhow::bail!("extism.shared_fn takes no arguments"); 108 | } 109 | } 110 | } 111 | } else if let Some(attr) = d.as_attribute_expr() { 112 | if let Some(n) = attr.value.as_name_expr() { 113 | if n.id.as_str() == "plugin_fn" 114 | || n.id.as_str() == "extism" && attr.attr.as_str() == "plugin_fn" 115 | { 116 | return get_export(f, true).map(Some); 117 | } else if n.id.as_str() == "shared_fn" 118 | || n.id.as_str() == "extism" && attr.attr.as_str() == "shared_fn" 119 | { 120 | return get_export(f, false).map(Some); 121 | } 122 | } 123 | } 124 | } 125 | 126 | Ok(None) 127 | } 128 | 129 | fn collect( 130 | stmt: rustpython_parser::ast::Stmt, 131 | exports: &mut Vec, 132 | imports: &mut Vec, 133 | ) -> Result<(), Error> { 134 | if let Some(f) = stmt.as_function_def_stmt() { 135 | if let Some(import) = get_import_fn_decorator(f)? { 136 | imports.push(import); 137 | } else if let Some(export) = get_export_decorator(f)? { 138 | exports.push(export); 139 | } 140 | } 141 | 142 | Ok(()) 143 | } 144 | 145 | pub(crate) fn find_imports_and_exports(data: String) -> Result<(Vec, Vec), Error> { 146 | let parsed = rustpython_parser::parse(&data, rustpython_parser::Mode::Module, "")? 147 | .expect_module(); 148 | 149 | let mut exports = vec![]; 150 | let mut imports = vec![]; 151 | for stmt in parsed.body { 152 | collect(stmt, &mut exports, &mut imports)?; 153 | } 154 | Ok((imports, exports)) 155 | } 156 | -------------------------------------------------------------------------------- /bin/src/shim.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use wagen::{Instr, ValType}; 3 | 4 | pub(crate) fn generate( 5 | exports: &[Export], 6 | imports: &[Import], 7 | shim_path: &std::path::Path, 8 | ) -> Result<(), Error> { 9 | let mut module = wagen::Module::new(); 10 | 11 | let n_imports = imports.len(); 12 | let import_table = module.tables().push(wagen::TableType { 13 | element_type: wagen::RefType::FUNCREF, 14 | minimum: n_imports as u64, 15 | maximum: None, 16 | table64: false, 17 | }); 18 | 19 | let __arg_start = module.import("core", "__arg_start", None, [], []); 20 | let __arg_i32 = module.import("core", "__arg_i32", None, [ValType::I32], []); 21 | let __arg_i64 = module.import("core", "__arg_i64", None, [ValType::I64], []); 22 | let __arg_f32 = module.import("core", "__arg_f32", None, [ValType::F32], []); 23 | let __arg_f64 = module.import("core", "__arg_f64", None, [ValType::F64], []); 24 | 25 | let __invoke = module.import( 26 | "core", 27 | "__invoke", 28 | None, 29 | [wagen::ValType::I32, wagen::ValType::I32], 30 | [], 31 | ); 32 | 33 | let __invoke_i64 = module.import( 34 | "core", 35 | "__invoke_i64", 36 | None, 37 | [wagen::ValType::I32, wagen::ValType::I32], 38 | [wagen::ValType::I64], 39 | ); 40 | 41 | let __invoke_i32 = module.import( 42 | "core", 43 | "__invoke_i32", 44 | None, 45 | [wagen::ValType::I32, wagen::ValType::I32], 46 | [wagen::ValType::I32], 47 | ); 48 | 49 | let mut import_elements = Vec::new(); 50 | for import in imports.iter() { 51 | let index = module.import( 52 | &import.module, 53 | &import.name, 54 | None, 55 | import.params.clone(), 56 | import.results.clone(), 57 | ); 58 | import_elements.push(index.index()); 59 | } 60 | 61 | for p in 0..=5 { 62 | for q in 0..=1 { 63 | let indirect_type = module 64 | .types() 65 | .push(|t| t.function(vec![ValType::I64; p], vec![ValType::I64; q])); 66 | let name = format!("__invokeHostFunc_{p}_{q}"); 67 | let mut params = vec![ValType::I32]; 68 | for _ in 0..p { 69 | params.push(ValType::I64); 70 | } 71 | let invoke_host = module 72 | .func(&name, params, vec![ValType::I64; q], []) 73 | .export(&name); 74 | 75 | let builder = invoke_host.builder(); 76 | for i in 1..=p { 77 | builder.push(Instr::LocalGet(i as u32)); 78 | } 79 | builder.push(Instr::LocalGet(0)); 80 | builder.push(Instr::CallIndirect { 81 | ty: indirect_type, 82 | table: import_table, 83 | }); 84 | } 85 | } 86 | 87 | module.active_element( 88 | Some(import_table), 89 | wagen::Elements::Functions(&import_elements), 90 | ); 91 | 92 | for (index, export) in exports.iter().enumerate() { 93 | if export.results.len() > 1 { 94 | anyhow::bail!( 95 | "Multiple return arguments are not currently supported but used in exported function {}", 96 | export.name 97 | ); 98 | } 99 | let func = module 100 | .func( 101 | &export.name, 102 | export.params.clone(), 103 | export.results.clone(), 104 | [], 105 | ) 106 | .export(&export.name); 107 | let builder = func.builder(); 108 | builder.push(Instr::Call(__arg_start.index())); 109 | for (parami, param) in export.params.iter().enumerate() { 110 | builder.push(Instr::LocalGet(parami as u32)); 111 | 112 | match param { 113 | ValType::I32 => { 114 | builder.push(Instr::Call(__arg_i32.index())); 115 | } 116 | ValType::I64 => { 117 | builder.push(Instr::Call(__arg_i64.index())); 118 | } 119 | ValType::F32 => { 120 | builder.push(Instr::Call(__arg_f32.index())); 121 | } 122 | ValType::F64 => { 123 | builder.push(Instr::Call(__arg_f64.index())); 124 | } 125 | r => { 126 | anyhow::bail!("Unsupported param type: {:?}", r); 127 | } 128 | } 129 | } 130 | 131 | builder.push(Instr::I32Const(index as i32)); 132 | builder.push(Instr::I32Const(!export.is_plugin_fn as i32)); 133 | match export.results.first() { 134 | None => { 135 | builder.push(Instr::Call(__invoke.index())); 136 | } 137 | Some(ValType::I32) => { 138 | builder.push(Instr::Call(__invoke_i32.index())); 139 | } 140 | Some(ValType::I64) => { 141 | builder.push(Instr::Call(__invoke_i64.index())); 142 | } 143 | // Some(ValType::F32) => { 144 | // builder.push(Instr::Call(__invoke_f32.index())); 145 | // } 146 | // Some(ValType::F64) => { 147 | // builder.push(Instr::Call(__invoke_f64.index())); 148 | // } 149 | Some(r) => { 150 | anyhow::bail!("Unsupported result type: {:?}", r); 151 | } 152 | } 153 | } 154 | 155 | module.validate_save(shim_path)?; 156 | Ok(()) 157 | } 158 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import platform 5 | import shutil 6 | import subprocess 7 | import logging 8 | from pathlib import Path 9 | import argparse 10 | import sys 11 | 12 | system = platform.system().lower() 13 | home = Path.home() 14 | 15 | 16 | def set_log_level(log_level): 17 | numeric_level = getattr(logging, log_level.upper(), None) 18 | if not isinstance(numeric_level, int): 19 | raise ValueError(f"Invalid log level: {log_level}") 20 | logging.basicConfig( 21 | level=numeric_level, format="%(asctime)s - %(levelname)s - %(message)s" 22 | ) 23 | 24 | 25 | def bin_dir() -> Path: 26 | if system in ["linux", "darwin"]: 27 | return home / ".local" / "bin" 28 | elif system == "windows": 29 | return Path(os.getenv("USERHOME")) 30 | else: 31 | raise OSError(f"Unsupported OS {system}") 32 | 33 | 34 | def data_dir() -> Path: 35 | if system == "linux": 36 | return home / ".local" / "share" / "extism-py" 37 | elif system == "darwin": 38 | return home / "Library" / "Application Support" / "extism-py" 39 | elif system == "windows": 40 | return Path(os.getenv("APPDATA")) / "extism-py" 41 | else: 42 | raise OSError(f"Unsupported OS {system}") 43 | 44 | 45 | def exe_file() -> str: 46 | if system == "windows": 47 | return "extism-py.exe" 48 | else: 49 | return "extism-py" 50 | 51 | 52 | def run_subprocess(command, cwd=None, quiet=False): 53 | try: 54 | logging.info(f"Running command: {' '.join(command)} in {cwd or '.'}") 55 | stdout = subprocess.DEVNULL if quiet else None 56 | stderr = subprocess.DEVNULL if quiet else None 57 | subprocess.run(command, cwd=cwd, check=True, stdout=stdout, stderr=stderr) 58 | except subprocess.CalledProcessError as e: 59 | logging.error(f"Command '{' '.join(command)}' failed with error: {e}") 60 | raise 61 | 62 | 63 | def check_rust_installed(): 64 | try: 65 | subprocess.run( 66 | ["rustc", "--version"], 67 | check=True, 68 | stdout=subprocess.DEVNULL, 69 | stderr=subprocess.DEVNULL, 70 | ) 71 | subprocess.run( 72 | ["cargo", "--version"], 73 | check=True, 74 | stdout=subprocess.DEVNULL, 75 | stderr=subprocess.DEVNULL, 76 | ) 77 | except subprocess.CalledProcessError: 78 | logging.error( 79 | "Rust and Cargo are required but not found. Please install Rust: https://www.rust-lang.org/tools/install" 80 | ) 81 | sys.exit(1) 82 | 83 | 84 | def do_build(args): 85 | check_rust_installed() 86 | run_subprocess(["cargo", "build", "--release"], cwd="./lib", quiet=args.quiet) 87 | run_subprocess(["cargo", "build", "--release"], cwd="./bin", quiet=args.quiet) 88 | shutil.copy2(Path("./bin/target/release") / exe_file(), Path(".") / exe_file()) 89 | logging.info("Build completed successfully.") 90 | 91 | 92 | def do_install(args): 93 | do_build(args) 94 | bin_dir = Path(args.bin_dir) 95 | data_dir = Path(args.data_dir) 96 | bin_dir.mkdir(parents=True, exist_ok=True) 97 | data_dir.mkdir(parents=True, exist_ok=True) 98 | 99 | dest_path = bin_dir / exe_file() 100 | logging.info(f"Copying binary to {dest_path}") 101 | shutil.copy2(Path("./bin/target/release") / exe_file(), dest_path) 102 | 103 | logging.info(f"Copying data files to {data_dir}") 104 | shutil.copytree( 105 | Path("./lib/target/wasm32-wasip1/wasi-deps"), data_dir, dirs_exist_ok=True 106 | ) 107 | 108 | logging.info(f"{exe_file()} installed to {bin_dir}") 109 | logging.info(f"Data files installed to {data_dir}") 110 | logging.info("Installation completed successfully.") 111 | 112 | 113 | def do_clean(args): 114 | logging.info("Cleaning build artifacts...") 115 | shutil.rmtree("./lib/target", ignore_errors=True) 116 | shutil.rmtree("./bin/target", ignore_errors=True) 117 | if (Path(".") / exe_file()).exists(): 118 | (Path(".") / exe_file()).unlink() 119 | logging.info("Clean completed successfully.") 120 | 121 | 122 | def get_version(path): 123 | try: 124 | result = subprocess.run( 125 | [path / exe_file(), "--version"], capture_output=True, text=True, check=True 126 | ) 127 | return result.stdout.strip() 128 | except subprocess.CalledProcessError: 129 | return "Unknown" 130 | 131 | 132 | def main(): 133 | parser = argparse.ArgumentParser( 134 | prog="build.py", description="Extism Python PDK builder" 135 | ) 136 | parser.add_argument( 137 | "command", 138 | choices=["build", "install", "clean"], 139 | default="build", 140 | nargs="?", 141 | help="Command to run", 142 | ) 143 | parser.add_argument( 144 | "--bin-dir", 145 | default=bin_dir(), 146 | dest="bin_dir", 147 | help="Directory to install binaries", 148 | ) 149 | parser.add_argument( 150 | "--data-dir", 151 | default=data_dir(), 152 | dest="data_dir", 153 | help="Directory to install data files", 154 | ) 155 | parser.add_argument( 156 | "--log-level", 157 | default="INFO", 158 | choices=["debug", "info", "warning", "error", "critical"], 159 | help="Set the logging level", 160 | ) 161 | parser.add_argument("--quiet", "-q", action="store_true", help="Suppress output") 162 | 163 | args = parser.parse_args() 164 | 165 | if not args.quiet: 166 | set_log_level(args.log_level) 167 | 168 | try: 169 | if args.command == "build": 170 | do_build(args) 171 | elif args.command == "install": 172 | do_install(args) 173 | elif args.command == "clean": 174 | do_clean(args) 175 | 176 | if args.command in ["install"]: 177 | version = get_version(Path(args.bin_dir)) 178 | logging.info(f"extism-py version: {version}") 179 | except Exception as e: 180 | logging.error(f"An error occurred: {e}") 181 | sys.exit(1) 182 | 183 | 184 | if __name__ == "__main__": 185 | main() 186 | -------------------------------------------------------------------------------- /example-schema.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://xtp.dylibso.com/assets/wasm/schema.json 2 | # Learn more at https://docs.xtp.dylibso.com/docs/concepts/xtp-schema 3 | version: v1-draft 4 | exports: 5 | CountVowels: 6 | input: 7 | type: string 8 | contentType: text/plain; charset=utf-8 9 | output: 10 | $ref: "#/components/schemas/VowelReport" 11 | contentType: application/json 12 | components: 13 | schemas: 14 | VowelReport: 15 | description: The result of counting vowels on the Vowels input. 16 | properties: 17 | count: 18 | type: integer 19 | format: int32 20 | description: The count of vowels for input string. 21 | total: 22 | type: integer 23 | format: int32 24 | description: The cumulative amount of vowels counted, if this keeps state across multiple function calls. 25 | nullable: true 26 | vowels: 27 | type: string 28 | description: The set of vowels used to get the count, e.g. "aAeEiIoOuU" 29 | -------------------------------------------------------------------------------- /examples/count-vowels.py: -------------------------------------------------------------------------------- 1 | import extism 2 | 3 | @extism.plugin_fn 4 | def count_vowels(): 5 | input = extism.input_str() 6 | total = 0 7 | for ch in input: 8 | if ch in ['A', 'a', 'E', 'e', 'I', 'i', 'O', 'o', 'U', 'u']: 9 | total += 1 10 | extism.log(extism.LogLevel.Info, "Hello!") 11 | extism.output({"count": total}) 12 | 13 | -------------------------------------------------------------------------------- /examples/imports.py: -------------------------------------------------------------------------------- 1 | import extism 2 | 3 | @extism.import_fn("example", "do_something") 4 | def do_something(): 5 | pass 6 | 7 | @extism.import_fn("example", "reflect") 8 | def reflect(x: str) -> str: 9 | pass 10 | 11 | @extism.import_fn("example", "update_dict") 12 | def update_dict(x: dict) -> dict: 13 | pass 14 | 15 | @extism.plugin_fn 16 | def count_vowels(): 17 | input = reflect(extism.input_str()) 18 | do_something() 19 | total = 0 20 | for ch in input: 21 | if ch in ['A', 'a', 'E', 'e', 'I', 'i', 'O', 'o', 'U', 'u']: 22 | total += 1 23 | extism.log(extism.LogLevel.Info, "Hello!") 24 | extism.output(update_dict({"count": total})) 25 | 26 | -------------------------------------------------------------------------------- /examples/imports_example.py: -------------------------------------------------------------------------------- 1 | import extism 2 | 3 | @extism.shared_fn 4 | def do_something(): 5 | print("Something") 6 | 7 | @extism.shared_fn 8 | def reflect(x: str) -> str: 9 | return x 10 | 11 | @extism.shared_fn 12 | def update_dict(d: dict) -> dict: 13 | d["abc"] = 123 14 | return d 15 | -------------------------------------------------------------------------------- /extism.pyi: -------------------------------------------------------------------------------- 1 | # WARNING THIS FILE IS AI GENERATED from 2 | # https://github.com/extism/python-pdk/blob/main/lib/src/prelude.py and 3 | # https://github.com/extism/python-pdk/blob/main/lib/src/py_module.rs 4 | # It is meant purely for developer/IDE usage and should not be made available to 5 | # extism-py 6 | # 7 | # Prompt used with Claude 3.5 Sonnet: 8 | # `prelude.py` defines an `extism` module. Generate a dummy module (`.pyi`) for 9 | # `extism` so IDEs can understand. Be sure to preserve the original comments. 10 | 11 | """ 12 | Extism Python Plugin Development Kit (PDK) 13 | 14 | This module provides the interface for developing Extism plugins in Python. 15 | It includes functionality for handling plugin I/O, HTTP requests, memory management, 16 | configuration, and host function interactions. 17 | """ 18 | 19 | from typing import Any, TypeVar, Callable, Optional, Union, Dict, List, Type, TypeAlias, overload 20 | from enum import Enum 21 | 22 | class LogLevel(Enum): 23 | Trace: LogLevel 24 | Debug: LogLevel 25 | Info: LogLevel 26 | Warn: LogLevel 27 | Error: LogLevel 28 | 29 | class MemoryHandle: 30 | offset: int 31 | length: int 32 | def __init__(self, offset: int, length: int) -> None: ... 33 | 34 | class memory: 35 | @staticmethod 36 | def find(offs: int) -> Optional[MemoryHandle]: ... 37 | 38 | @staticmethod 39 | def bytes(mem: MemoryHandle) -> bytes: ... 40 | 41 | @staticmethod 42 | def string(mem: MemoryHandle) -> str: ... 43 | 44 | @staticmethod 45 | def free(mem: MemoryHandle) -> None: ... 46 | 47 | @staticmethod 48 | def alloc(data: bytes) -> MemoryHandle: ... 49 | 50 | class HttpRequest: 51 | url: str 52 | method: Optional[str] 53 | headers: Optional[Dict[str, str]] 54 | 55 | def __init__(self, url: str, method: Optional[str] = None, headers: Optional[Dict[str, str]] = None) -> None: ... 56 | 57 | class _HttpResponseInternal: 58 | def status_code(self) -> int: ... 59 | def data(self) -> bytes: ... 60 | headers: Dict[str, str] 61 | 62 | class HttpResponse: 63 | _inner: _HttpResponseInternal 64 | 65 | def __init__(self, res: _HttpResponseInternal) -> None: ... 66 | 67 | @property 68 | def status_code(self) -> int: 69 | """Get HTTP status code""" 70 | ... 71 | 72 | def data_bytes(self) -> bytes: 73 | """Get response body bytes""" 74 | ... 75 | 76 | def data_str(self) -> str: 77 | """Get response body string""" 78 | ... 79 | 80 | def data_json(self) -> Any: 81 | """Get response body JSON""" 82 | ... 83 | 84 | def headers(self) -> Dict[str, str]: 85 | """Get HTTP response headers""" 86 | ... 87 | 88 | class Http: 89 | @staticmethod 90 | def request( 91 | url: str, 92 | meth: str = "GET", 93 | body: Optional[Union[bytes, str]] = None, 94 | headers: Optional[Dict[str, str]] = None 95 | ) -> HttpResponse: 96 | """Make an HTTP request""" 97 | ... 98 | 99 | T = TypeVar('T') 100 | 101 | def log(level: LogLevel, msg: Union[str, bytes, Any]) -> None: ... 102 | 103 | def input_bytes() -> bytes: ... 104 | def output_bytes(result: bytes) -> None: ... 105 | def input_str() -> str: ... 106 | def output_str(result: str) -> None: ... 107 | 108 | def import_fn(module: str, name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: 109 | """Annotate an import function""" 110 | ... 111 | 112 | def plugin_fn(func: Callable[[], Any]) -> Callable[[], Any]: 113 | """Annotate a function that will be called by Extism""" 114 | ... 115 | 116 | def shared_fn(f: Callable[..., Any]) -> Callable[..., Any]: 117 | """Annotate a an export that won't be called directly by Extism""" 118 | ... 119 | 120 | def input_json(t: Optional[Type[T]] = None) -> Union[T, Any]: 121 | """Get input as JSON""" 122 | ... 123 | 124 | def output_json(x: Any) -> None: 125 | """Set JSON output""" 126 | ... 127 | 128 | def input(t: Optional[Type[T]] = None) -> Optional[T]: 129 | ... 130 | 131 | def output(x: Optional[Union[str, bytes, Dict[str, Any], List[Any], Enum]] = None) -> None: 132 | ... 133 | 134 | class Var: 135 | @staticmethod 136 | def get_bytes(key: str) -> Optional[bytes]: 137 | """Get variable as bytes""" 138 | ... 139 | 140 | @staticmethod 141 | def get_str(key: str) -> Optional[str]: 142 | """Get variable as string""" 143 | ... 144 | 145 | @staticmethod 146 | def get_json(key: str) -> Optional[Any]: 147 | """Get variable as JSON""" 148 | ... 149 | 150 | @staticmethod 151 | def set(key: str, value: Union[bytes, str]) -> None: 152 | """Set a variable with a string or bytes value""" 153 | ... 154 | 155 | class Config: 156 | @staticmethod 157 | def get_str(key: str) -> Optional[str]: 158 | """Get a config value as string""" 159 | ... 160 | 161 | @staticmethod 162 | def get_json(key: str) -> Optional[Any]: 163 | """Get a config value as JSON""" 164 | ... -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -z "${GITHUB_TOKEN}" ]]; then 4 | GITHUB_FLAGS=() 5 | else 6 | GITHUB_FLAGS=('--header' "Authorization: Bearer $GITHUB_TOKEN" '--header' 'X-GitHub-Api-Version: 2022-11-28') 7 | fi 8 | 9 | set -eou pipefail 10 | 11 | # Get the latest release 12 | RELEASE_API_URL="https://api.github.com/repos/extism/python-pdk/releases/latest" 13 | if [[ ${#GITHUB_FLAGS[@]} -eq 0 ]]; then 14 | response=$(curl -s "$RELEASE_API_URL") 15 | else 16 | response=$(curl "${GITHUB_FLAGS[@]}" -s "$RELEASE_API_URL") 17 | fi 18 | if [ -z "$response" ]; then 19 | echo "Error: Failed to fetch the latest release from GitHub API." 20 | exit 1 21 | fi 22 | 23 | # try to parse tag 24 | LATEST_TAG=$(echo "$response" | grep -m 1 '"tag_name":' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') 25 | 26 | if [ -z "$LATEST_TAG" ]; then 27 | echo "Error: Could not find the latest release tag." 28 | exit 1 29 | fi 30 | 31 | echo "Installing extism-py release with tag: $LATEST_TAG" 32 | 33 | USER_DATA_DIR='' 34 | OS='' 35 | case `uname` in 36 | Darwin*) 37 | OS="macos" 38 | USER_DATA_DIR="$HOME/Library/Application Support" ;; 39 | Linux*) 40 | OS="linux" 41 | USER_DATA_DIR="$HOME/.local/share" ;; 42 | *) echo "unknown os: $OSTYPE" && exit 1 ;; 43 | esac 44 | 45 | ARCH=`uname -m` 46 | case "$ARCH" in 47 | ix86*|x86_64*) ARCH="x86_64" ;; 48 | arm64*|aarch64*) ARCH="aarch64" ;; 49 | *) echo "unknown arch: $ARCH" && exit 1 ;; 50 | esac 51 | 52 | BINARYEN_TAG="version_116" 53 | DOWNLOAD_URL="https://github.com/extism/python-pdk/releases/download/$LATEST_TAG/extism-py-$ARCH-$OS-$LATEST_TAG.tar.gz" 54 | 55 | # Function to check if a directory is in PATH and writable 56 | is_valid_install_dir() { 57 | [[ ":$PATH:" == *":$1:"* ]] && [ -w "$1" ] 58 | } 59 | 60 | INSTALL_DIR="" 61 | USE_SUDO="" 62 | 63 | # Check for common user-writable directories in PATH 64 | for dir in "$HOME/bin" "$HOME/.local/bin" "$HOME/.bin"; do 65 | if is_valid_install_dir "$dir"; then 66 | INSTALL_DIR="$dir" 67 | break 68 | fi 69 | done 70 | 71 | # If no user-writable directory found, use system directory 72 | if [ -z "$INSTALL_DIR" ]; then 73 | INSTALL_DIR="/usr/local/bin" 74 | USE_SUDO=1 75 | fi 76 | 77 | echo "Checking for binaryen..." 78 | 79 | if ! which "wasm-merge" > /dev/null || ! which "wasm-opt" > /dev/null; then 80 | echo "Missing binaryen tool(s)" 81 | 82 | # binaryen use arm64 instead where as extism-py uses aarch64 for release file naming 83 | case "$ARCH" in 84 | aarch64*) ARCH="arm64" ;; 85 | esac 86 | 87 | # matches the case where the user installs extism-pdk in a Linux-based Docker image running on mac m1 88 | # binaryen didn't have arm64 release file for linux 89 | if [ $ARCH = "arm64" ] && [ $OS = "linux" ]; then 90 | ARCH="x86_64" 91 | fi 92 | 93 | if [ $OS = "macos" ]; then 94 | echo "Installing binaryen and wasm-merge using homebrew" 95 | brew install binaryen 96 | else 97 | if [ ! -e "binaryen-$BINARYEN_TAG-$ARCH-$OS.tar.gz" ]; then 98 | echo 'Downloading binaryen...' 99 | curl -L -O "https://github.com/WebAssembly/binaryen/releases/download/$BINARYEN_TAG/binaryen-$BINARYEN_TAG-$ARCH-$OS.tar.gz" 100 | fi 101 | rm -rf 'binaryen' "binaryen-$BINARYEN_TAG" 102 | tar xf "binaryen-$BINARYEN_TAG-$ARCH-$OS.tar.gz" 103 | mv "binaryen-$BINARYEN_TAG"/ binaryen/ 104 | sudo mkdir -p /usr/local/binaryen 105 | if ! which 'wasm-merge' > /dev/null; then 106 | echo "Installing wasm-merge..." 107 | rm -f /usr/local/binaryen/wasm-merge 108 | sudo mv binaryen/bin/wasm-merge /usr/local/binaryen/wasm-merge 109 | sudo ln -s /usr/local/binaryen/wasm-merge /usr/local/bin/wasm-merge 110 | else 111 | echo "wasm-merge is already installed" 112 | fi 113 | if ! which 'wasm-opt' > /dev/null; then 114 | echo "Installing wasm-opt..." 115 | rm -f /usr/local/bin/wasm-opt 116 | sudo mv binaryen/bin/wasm-opt /usr/local/binaryen/wasm-opt 117 | sudo ln -s /usr/local/binaryen/wasm-opt /usr/local/bin/wasm-opt 118 | else 119 | echo "wasm-opt is already installed" 120 | fi 121 | fi 122 | else 123 | echo "binaryen tools are already installed" 124 | fi 125 | 126 | TARGET="$INSTALL_DIR/extism-py" 127 | echo "Downloading extism-py from: $DOWNLOAD_URL" 128 | 129 | if curl -fsSL --output /tmp/extism-py.tar.gz "$DOWNLOAD_URL"; then 130 | tar xzf /tmp/extism-py.tar.gz -C /tmp 131 | rm -rf "$USER_DATA_DIR/extism-py" 132 | mkdir -p "$USER_DATA_DIR" 133 | 134 | if [ "$USE_SUDO" = "1" ]; then 135 | echo "No user-writable bin directory found in PATH. Using sudo to install in $INSTALL_DIR" 136 | sudo mv /tmp/extism-py/bin/extism-py "$TARGET" 137 | mv /tmp/extism-py/share/extism-py "$USER_DATA_DIR/extism-py" 138 | else 139 | mv /tmp/extism-py/bin/extism-py "$TARGET" 140 | mv /tmp/extism-py/share/extism-py "$USER_DATA_DIR/extism-py" 141 | fi 142 | chmod +x "$TARGET" 143 | 144 | echo "Successfully installed extism-py to $TARGET" 145 | else 146 | echo "Failed to download or install extism-py. Curl exit code: $?" 147 | exit 148 | fi 149 | 150 | # Warn the user if the chosen path is not in the path 151 | if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then 152 | echo "Note: $INSTALL_DIR is not in your PATH. You may need to add it to your PATH or use the full path to run extism-py." 153 | fi 154 | 155 | echo "Installation complete. Try to run 'extism-py --version' to ensure it was correctly installed." 156 | 157 | 158 | -------------------------------------------------------------------------------- /lib/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip1" 3 | rustflags = ["-C","link-args=--no-entry"] 4 | -------------------------------------------------------------------------------- /lib/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.90" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 31 | 32 | [[package]] 33 | name = "backtrace" 34 | version = "0.3.74" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 37 | dependencies = [ 38 | "addr2line", 39 | "cfg-if", 40 | "libc", 41 | "miniz_oxide", 42 | "object", 43 | "rustc-demangle", 44 | "windows-targets 0.52.6", 45 | ] 46 | 47 | [[package]] 48 | name = "base64" 49 | version = "0.21.7" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 52 | 53 | [[package]] 54 | name = "base64" 55 | version = "0.22.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 58 | 59 | [[package]] 60 | name = "bitflags" 61 | version = "1.3.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.6.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 70 | 71 | [[package]] 72 | name = "bumpalo" 73 | version = "3.16.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 76 | 77 | [[package]] 78 | name = "bytemuck" 79 | version = "1.19.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" 82 | 83 | [[package]] 84 | name = "byteorder" 85 | version = "1.5.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 88 | 89 | [[package]] 90 | name = "bytes" 91 | version = "1.7.2" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" 94 | 95 | [[package]] 96 | name = "cc" 97 | version = "1.1.31" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" 100 | dependencies = [ 101 | "shlex", 102 | ] 103 | 104 | [[package]] 105 | name = "cfg-if" 106 | version = "1.0.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 109 | 110 | [[package]] 111 | name = "core-foundation" 112 | version = "0.9.4" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 115 | dependencies = [ 116 | "core-foundation-sys", 117 | "libc", 118 | ] 119 | 120 | [[package]] 121 | name = "core-foundation-sys" 122 | version = "0.8.7" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 125 | 126 | [[package]] 127 | name = "crc32fast" 128 | version = "1.4.2" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 131 | dependencies = [ 132 | "cfg-if", 133 | ] 134 | 135 | [[package]] 136 | name = "either" 137 | version = "1.13.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 140 | 141 | [[package]] 142 | name = "encoding_rs" 143 | version = "0.8.34" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 146 | dependencies = [ 147 | "cfg-if", 148 | ] 149 | 150 | [[package]] 151 | name = "equivalent" 152 | version = "1.0.1" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 155 | 156 | [[package]] 157 | name = "errno" 158 | version = "0.3.9" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 161 | dependencies = [ 162 | "libc", 163 | "windows-sys 0.52.0", 164 | ] 165 | 166 | [[package]] 167 | name = "extism-convert" 168 | version = "1.7.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "6b2042dab1fdb408d7504446cfb10079815da8b45e192a8954bf568c8a43e65d" 171 | dependencies = [ 172 | "anyhow", 173 | "base64 0.22.1", 174 | "bytemuck", 175 | "extism-convert-macros", 176 | "prost", 177 | "rmp-serde", 178 | "serde", 179 | "serde_json", 180 | ] 181 | 182 | [[package]] 183 | name = "extism-convert-macros" 184 | version = "1.7.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "8bb2f0038f2c3b14daa95b132f4fc1bad786a92946c7c9c06e120985a1fc4028" 187 | dependencies = [ 188 | "manyhow", 189 | "proc-macro-crate", 190 | "proc-macro2", 191 | "quote", 192 | "syn", 193 | ] 194 | 195 | [[package]] 196 | name = "extism-manifest" 197 | version = "1.7.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "675c0d7e15bb5e6e2a520ea26c4309c047c30b16de852f26373de1906677a58d" 200 | dependencies = [ 201 | "base64 0.22.1", 202 | "serde", 203 | "serde_json", 204 | ] 205 | 206 | [[package]] 207 | name = "extism-pdk" 208 | version = "1.3.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "ccd50f3ebae4132239ee188c00d91bda2b283db007213dd4bd6f3b4be4d3f82c" 211 | dependencies = [ 212 | "anyhow", 213 | "base64 0.22.1", 214 | "extism-convert", 215 | "extism-manifest", 216 | "extism-pdk-derive", 217 | "serde", 218 | "serde_json", 219 | ] 220 | 221 | [[package]] 222 | name = "extism-pdk-derive" 223 | version = "1.3.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "b3a225e26f498571c273e095d9c2437acb0fd5d6ef3cb88d284d7e2d037d4d10" 226 | dependencies = [ 227 | "proc-macro2", 228 | "quote", 229 | "syn", 230 | ] 231 | 232 | [[package]] 233 | name = "extism-python-pdk" 234 | version = "0.1.5" 235 | dependencies = [ 236 | "anyhow", 237 | "extism-pdk", 238 | "pyo3", 239 | "wlr-libpy", 240 | ] 241 | 242 | [[package]] 243 | name = "filetime" 244 | version = "0.2.25" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 247 | dependencies = [ 248 | "cfg-if", 249 | "libc", 250 | "libredox", 251 | "windows-sys 0.59.0", 252 | ] 253 | 254 | [[package]] 255 | name = "flate2" 256 | version = "1.0.34" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" 259 | dependencies = [ 260 | "crc32fast", 261 | "miniz_oxide", 262 | ] 263 | 264 | [[package]] 265 | name = "fnv" 266 | version = "1.0.7" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 269 | 270 | [[package]] 271 | name = "form_urlencoded" 272 | version = "1.2.1" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 275 | dependencies = [ 276 | "percent-encoding", 277 | ] 278 | 279 | [[package]] 280 | name = "futures-channel" 281 | version = "0.3.31" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 284 | dependencies = [ 285 | "futures-core", 286 | ] 287 | 288 | [[package]] 289 | name = "futures-core" 290 | version = "0.3.31" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 293 | 294 | [[package]] 295 | name = "futures-io" 296 | version = "0.3.31" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 299 | 300 | [[package]] 301 | name = "futures-sink" 302 | version = "0.3.31" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 305 | 306 | [[package]] 307 | name = "futures-task" 308 | version = "0.3.31" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 311 | 312 | [[package]] 313 | name = "futures-util" 314 | version = "0.3.31" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 317 | dependencies = [ 318 | "futures-core", 319 | "futures-io", 320 | "futures-task", 321 | "memchr", 322 | "pin-project-lite", 323 | "pin-utils", 324 | "slab", 325 | ] 326 | 327 | [[package]] 328 | name = "getrandom" 329 | version = "0.2.15" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 332 | dependencies = [ 333 | "cfg-if", 334 | "libc", 335 | "wasi", 336 | ] 337 | 338 | [[package]] 339 | name = "gimli" 340 | version = "0.31.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 343 | 344 | [[package]] 345 | name = "h2" 346 | version = "0.3.26" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 349 | dependencies = [ 350 | "bytes", 351 | "fnv", 352 | "futures-core", 353 | "futures-sink", 354 | "futures-util", 355 | "http", 356 | "indexmap", 357 | "slab", 358 | "tokio", 359 | "tokio-util", 360 | "tracing", 361 | ] 362 | 363 | [[package]] 364 | name = "hashbrown" 365 | version = "0.15.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 368 | 369 | [[package]] 370 | name = "heck" 371 | version = "0.5.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 374 | 375 | [[package]] 376 | name = "hermit-abi" 377 | version = "0.3.9" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 380 | 381 | [[package]] 382 | name = "http" 383 | version = "0.2.12" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 386 | dependencies = [ 387 | "bytes", 388 | "fnv", 389 | "itoa", 390 | ] 391 | 392 | [[package]] 393 | name = "http-body" 394 | version = "0.4.6" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 397 | dependencies = [ 398 | "bytes", 399 | "http", 400 | "pin-project-lite", 401 | ] 402 | 403 | [[package]] 404 | name = "httparse" 405 | version = "1.9.5" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 408 | 409 | [[package]] 410 | name = "httpdate" 411 | version = "1.0.3" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 414 | 415 | [[package]] 416 | name = "hyper" 417 | version = "0.14.31" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" 420 | dependencies = [ 421 | "bytes", 422 | "futures-channel", 423 | "futures-core", 424 | "futures-util", 425 | "h2", 426 | "http", 427 | "http-body", 428 | "httparse", 429 | "httpdate", 430 | "itoa", 431 | "pin-project-lite", 432 | "socket2", 433 | "tokio", 434 | "tower-service", 435 | "tracing", 436 | "want", 437 | ] 438 | 439 | [[package]] 440 | name = "hyper-rustls" 441 | version = "0.24.2" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" 444 | dependencies = [ 445 | "futures-util", 446 | "http", 447 | "hyper", 448 | "rustls", 449 | "tokio", 450 | "tokio-rustls", 451 | ] 452 | 453 | [[package]] 454 | name = "idna" 455 | version = "0.5.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 458 | dependencies = [ 459 | "unicode-bidi", 460 | "unicode-normalization", 461 | ] 462 | 463 | [[package]] 464 | name = "indexmap" 465 | version = "2.6.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 468 | dependencies = [ 469 | "equivalent", 470 | "hashbrown", 471 | ] 472 | 473 | [[package]] 474 | name = "indoc" 475 | version = "2.0.5" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 478 | 479 | [[package]] 480 | name = "ipnet" 481 | version = "2.10.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 484 | 485 | [[package]] 486 | name = "itertools" 487 | version = "0.13.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 490 | dependencies = [ 491 | "either", 492 | ] 493 | 494 | [[package]] 495 | name = "itoa" 496 | version = "1.0.11" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 499 | 500 | [[package]] 501 | name = "js-sys" 502 | version = "0.3.72" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 505 | dependencies = [ 506 | "wasm-bindgen", 507 | ] 508 | 509 | [[package]] 510 | name = "libc" 511 | version = "0.2.161" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 514 | 515 | [[package]] 516 | name = "libredox" 517 | version = "0.1.3" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 520 | dependencies = [ 521 | "bitflags 2.6.0", 522 | "libc", 523 | "redox_syscall", 524 | ] 525 | 526 | [[package]] 527 | name = "linux-raw-sys" 528 | version = "0.4.14" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 531 | 532 | [[package]] 533 | name = "log" 534 | version = "0.4.22" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 537 | 538 | [[package]] 539 | name = "manyhow" 540 | version = "0.11.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" 543 | dependencies = [ 544 | "manyhow-macros", 545 | "proc-macro2", 546 | "quote", 547 | "syn", 548 | ] 549 | 550 | [[package]] 551 | name = "manyhow-macros" 552 | version = "0.11.4" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" 555 | dependencies = [ 556 | "proc-macro-utils", 557 | "proc-macro2", 558 | "quote", 559 | ] 560 | 561 | [[package]] 562 | name = "memchr" 563 | version = "2.7.4" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 566 | 567 | [[package]] 568 | name = "memoffset" 569 | version = "0.9.1" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 572 | dependencies = [ 573 | "autocfg", 574 | ] 575 | 576 | [[package]] 577 | name = "mime" 578 | version = "0.3.17" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 581 | 582 | [[package]] 583 | name = "miniz_oxide" 584 | version = "0.8.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 587 | dependencies = [ 588 | "adler2", 589 | ] 590 | 591 | [[package]] 592 | name = "mio" 593 | version = "1.0.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 596 | dependencies = [ 597 | "hermit-abi", 598 | "libc", 599 | "wasi", 600 | "windows-sys 0.52.0", 601 | ] 602 | 603 | [[package]] 604 | name = "num-traits" 605 | version = "0.2.19" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 608 | dependencies = [ 609 | "autocfg", 610 | ] 611 | 612 | [[package]] 613 | name = "object" 614 | version = "0.36.5" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 617 | dependencies = [ 618 | "memchr", 619 | ] 620 | 621 | [[package]] 622 | name = "once_cell" 623 | version = "1.20.2" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 626 | 627 | [[package]] 628 | name = "paste" 629 | version = "1.0.15" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 632 | 633 | [[package]] 634 | name = "path-absolutize" 635 | version = "3.1.1" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" 638 | dependencies = [ 639 | "path-dedot", 640 | ] 641 | 642 | [[package]] 643 | name = "path-dedot" 644 | version = "3.1.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" 647 | dependencies = [ 648 | "once_cell", 649 | ] 650 | 651 | [[package]] 652 | name = "percent-encoding" 653 | version = "2.3.1" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 656 | 657 | [[package]] 658 | name = "pin-project-lite" 659 | version = "0.2.14" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 662 | 663 | [[package]] 664 | name = "pin-utils" 665 | version = "0.1.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 668 | 669 | [[package]] 670 | name = "portable-atomic" 671 | version = "1.9.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" 674 | 675 | [[package]] 676 | name = "proc-macro-crate" 677 | version = "3.2.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 680 | dependencies = [ 681 | "toml_edit", 682 | ] 683 | 684 | [[package]] 685 | name = "proc-macro-utils" 686 | version = "0.10.0" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" 689 | dependencies = [ 690 | "proc-macro2", 691 | "quote", 692 | "smallvec", 693 | ] 694 | 695 | [[package]] 696 | name = "proc-macro2" 697 | version = "1.0.88" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" 700 | dependencies = [ 701 | "unicode-ident", 702 | ] 703 | 704 | [[package]] 705 | name = "prost" 706 | version = "0.13.3" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" 709 | dependencies = [ 710 | "bytes", 711 | "prost-derive", 712 | ] 713 | 714 | [[package]] 715 | name = "prost-derive" 716 | version = "0.13.3" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" 719 | dependencies = [ 720 | "anyhow", 721 | "itertools", 722 | "proc-macro2", 723 | "quote", 724 | "syn", 725 | ] 726 | 727 | [[package]] 728 | name = "pyo3" 729 | version = "0.22.5" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51" 732 | dependencies = [ 733 | "cfg-if", 734 | "indoc", 735 | "libc", 736 | "memoffset", 737 | "once_cell", 738 | "portable-atomic", 739 | "pyo3-build-config", 740 | "pyo3-ffi", 741 | "pyo3-macros", 742 | "unindent", 743 | ] 744 | 745 | [[package]] 746 | name = "pyo3-build-config" 747 | version = "0.22.5" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179" 750 | dependencies = [ 751 | "once_cell", 752 | "target-lexicon", 753 | ] 754 | 755 | [[package]] 756 | name = "pyo3-ffi" 757 | version = "0.22.5" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d" 760 | dependencies = [ 761 | "libc", 762 | "pyo3-build-config", 763 | ] 764 | 765 | [[package]] 766 | name = "pyo3-macros" 767 | version = "0.22.5" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e" 770 | dependencies = [ 771 | "proc-macro2", 772 | "pyo3-macros-backend", 773 | "quote", 774 | "syn", 775 | ] 776 | 777 | [[package]] 778 | name = "pyo3-macros-backend" 779 | version = "0.22.5" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce" 782 | dependencies = [ 783 | "heck", 784 | "proc-macro2", 785 | "pyo3-build-config", 786 | "quote", 787 | "syn", 788 | ] 789 | 790 | [[package]] 791 | name = "quote" 792 | version = "1.0.37" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 795 | dependencies = [ 796 | "proc-macro2", 797 | ] 798 | 799 | [[package]] 800 | name = "redox_syscall" 801 | version = "0.5.7" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 804 | dependencies = [ 805 | "bitflags 2.6.0", 806 | ] 807 | 808 | [[package]] 809 | name = "reqwest" 810 | version = "0.11.27" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 813 | dependencies = [ 814 | "base64 0.21.7", 815 | "bytes", 816 | "encoding_rs", 817 | "futures-core", 818 | "futures-util", 819 | "h2", 820 | "http", 821 | "http-body", 822 | "hyper", 823 | "hyper-rustls", 824 | "ipnet", 825 | "js-sys", 826 | "log", 827 | "mime", 828 | "once_cell", 829 | "percent-encoding", 830 | "pin-project-lite", 831 | "rustls", 832 | "rustls-pemfile", 833 | "serde", 834 | "serde_json", 835 | "serde_urlencoded", 836 | "sync_wrapper", 837 | "system-configuration", 838 | "tokio", 839 | "tokio-rustls", 840 | "tower-service", 841 | "url", 842 | "wasm-bindgen", 843 | "wasm-bindgen-futures", 844 | "web-sys", 845 | "webpki-roots", 846 | "winreg", 847 | ] 848 | 849 | [[package]] 850 | name = "ring" 851 | version = "0.17.8" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 854 | dependencies = [ 855 | "cc", 856 | "cfg-if", 857 | "getrandom", 858 | "libc", 859 | "spin", 860 | "untrusted", 861 | "windows-sys 0.52.0", 862 | ] 863 | 864 | [[package]] 865 | name = "rmp" 866 | version = "0.8.14" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" 869 | dependencies = [ 870 | "byteorder", 871 | "num-traits", 872 | "paste", 873 | ] 874 | 875 | [[package]] 876 | name = "rmp-serde" 877 | version = "1.3.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" 880 | dependencies = [ 881 | "byteorder", 882 | "rmp", 883 | "serde", 884 | ] 885 | 886 | [[package]] 887 | name = "rustc-demangle" 888 | version = "0.1.24" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 891 | 892 | [[package]] 893 | name = "rustix" 894 | version = "0.38.37" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 897 | dependencies = [ 898 | "bitflags 2.6.0", 899 | "errno", 900 | "libc", 901 | "linux-raw-sys", 902 | "windows-sys 0.52.0", 903 | ] 904 | 905 | [[package]] 906 | name = "rustls" 907 | version = "0.21.12" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 910 | dependencies = [ 911 | "log", 912 | "ring", 913 | "rustls-webpki", 914 | "sct", 915 | ] 916 | 917 | [[package]] 918 | name = "rustls-pemfile" 919 | version = "1.0.4" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 922 | dependencies = [ 923 | "base64 0.21.7", 924 | ] 925 | 926 | [[package]] 927 | name = "rustls-webpki" 928 | version = "0.101.7" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 931 | dependencies = [ 932 | "ring", 933 | "untrusted", 934 | ] 935 | 936 | [[package]] 937 | name = "ryu" 938 | version = "1.0.18" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 941 | 942 | [[package]] 943 | name = "sct" 944 | version = "0.7.1" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 947 | dependencies = [ 948 | "ring", 949 | "untrusted", 950 | ] 951 | 952 | [[package]] 953 | name = "serde" 954 | version = "1.0.210" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 957 | dependencies = [ 958 | "serde_derive", 959 | ] 960 | 961 | [[package]] 962 | name = "serde_derive" 963 | version = "1.0.210" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 966 | dependencies = [ 967 | "proc-macro2", 968 | "quote", 969 | "syn", 970 | ] 971 | 972 | [[package]] 973 | name = "serde_json" 974 | version = "1.0.132" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 977 | dependencies = [ 978 | "itoa", 979 | "memchr", 980 | "ryu", 981 | "serde", 982 | ] 983 | 984 | [[package]] 985 | name = "serde_urlencoded" 986 | version = "0.7.1" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 989 | dependencies = [ 990 | "form_urlencoded", 991 | "itoa", 992 | "ryu", 993 | "serde", 994 | ] 995 | 996 | [[package]] 997 | name = "shlex" 998 | version = "1.3.0" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1001 | 1002 | [[package]] 1003 | name = "slab" 1004 | version = "0.4.9" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1007 | dependencies = [ 1008 | "autocfg", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "smallvec" 1013 | version = "1.13.2" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1016 | 1017 | [[package]] 1018 | name = "socket2" 1019 | version = "0.5.7" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1022 | dependencies = [ 1023 | "libc", 1024 | "windows-sys 0.52.0", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "spin" 1029 | version = "0.9.8" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1032 | 1033 | [[package]] 1034 | name = "syn" 1035 | version = "2.0.82" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" 1038 | dependencies = [ 1039 | "proc-macro2", 1040 | "quote", 1041 | "unicode-ident", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "sync_wrapper" 1046 | version = "0.1.2" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1049 | 1050 | [[package]] 1051 | name = "system-configuration" 1052 | version = "0.5.1" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1055 | dependencies = [ 1056 | "bitflags 1.3.2", 1057 | "core-foundation", 1058 | "system-configuration-sys", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "system-configuration-sys" 1063 | version = "0.5.0" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1066 | dependencies = [ 1067 | "core-foundation-sys", 1068 | "libc", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "tar" 1073 | version = "0.4.42" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" 1076 | dependencies = [ 1077 | "filetime", 1078 | "libc", 1079 | "xattr", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "target-lexicon" 1084 | version = "0.12.16" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 1087 | 1088 | [[package]] 1089 | name = "tinyvec" 1090 | version = "1.8.0" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1093 | dependencies = [ 1094 | "tinyvec_macros", 1095 | ] 1096 | 1097 | [[package]] 1098 | name = "tinyvec_macros" 1099 | version = "0.1.1" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1102 | 1103 | [[package]] 1104 | name = "tokio" 1105 | version = "1.40.0" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 1108 | dependencies = [ 1109 | "backtrace", 1110 | "bytes", 1111 | "libc", 1112 | "mio", 1113 | "pin-project-lite", 1114 | "socket2", 1115 | "windows-sys 0.52.0", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "tokio-rustls" 1120 | version = "0.24.1" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 1123 | dependencies = [ 1124 | "rustls", 1125 | "tokio", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "tokio-util" 1130 | version = "0.7.12" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 1133 | dependencies = [ 1134 | "bytes", 1135 | "futures-core", 1136 | "futures-sink", 1137 | "pin-project-lite", 1138 | "tokio", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "toml_datetime" 1143 | version = "0.6.8" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1146 | 1147 | [[package]] 1148 | name = "toml_edit" 1149 | version = "0.22.22" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 1152 | dependencies = [ 1153 | "indexmap", 1154 | "toml_datetime", 1155 | "winnow", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "tower-service" 1160 | version = "0.3.3" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1163 | 1164 | [[package]] 1165 | name = "tracing" 1166 | version = "0.1.40" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1169 | dependencies = [ 1170 | "pin-project-lite", 1171 | "tracing-core", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "tracing-core" 1176 | version = "0.1.32" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1179 | dependencies = [ 1180 | "once_cell", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "try-lock" 1185 | version = "0.2.5" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1188 | 1189 | [[package]] 1190 | name = "unicode-bidi" 1191 | version = "0.3.17" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" 1194 | 1195 | [[package]] 1196 | name = "unicode-ident" 1197 | version = "1.0.13" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1200 | 1201 | [[package]] 1202 | name = "unicode-normalization" 1203 | version = "0.1.24" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 1206 | dependencies = [ 1207 | "tinyvec", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "unindent" 1212 | version = "0.2.3" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 1215 | 1216 | [[package]] 1217 | name = "untrusted" 1218 | version = "0.9.0" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1221 | 1222 | [[package]] 1223 | name = "url" 1224 | version = "2.5.2" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1227 | dependencies = [ 1228 | "form_urlencoded", 1229 | "idna", 1230 | "percent-encoding", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "want" 1235 | version = "0.3.1" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1238 | dependencies = [ 1239 | "try-lock", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "wasi" 1244 | version = "0.11.0+wasi-snapshot-preview1" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1247 | 1248 | [[package]] 1249 | name = "wasm-bindgen" 1250 | version = "0.2.95" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 1253 | dependencies = [ 1254 | "cfg-if", 1255 | "once_cell", 1256 | "wasm-bindgen-macro", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "wasm-bindgen-backend" 1261 | version = "0.2.95" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 1264 | dependencies = [ 1265 | "bumpalo", 1266 | "log", 1267 | "once_cell", 1268 | "proc-macro2", 1269 | "quote", 1270 | "syn", 1271 | "wasm-bindgen-shared", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "wasm-bindgen-futures" 1276 | version = "0.4.45" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" 1279 | dependencies = [ 1280 | "cfg-if", 1281 | "js-sys", 1282 | "wasm-bindgen", 1283 | "web-sys", 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "wasm-bindgen-macro" 1288 | version = "0.2.95" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 1291 | dependencies = [ 1292 | "quote", 1293 | "wasm-bindgen-macro-support", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "wasm-bindgen-macro-support" 1298 | version = "0.2.95" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 1301 | dependencies = [ 1302 | "proc-macro2", 1303 | "quote", 1304 | "syn", 1305 | "wasm-bindgen-backend", 1306 | "wasm-bindgen-shared", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "wasm-bindgen-shared" 1311 | version = "0.2.95" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 1314 | 1315 | [[package]] 1316 | name = "web-sys" 1317 | version = "0.3.72" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" 1320 | dependencies = [ 1321 | "js-sys", 1322 | "wasm-bindgen", 1323 | ] 1324 | 1325 | [[package]] 1326 | name = "webpki-roots" 1327 | version = "0.25.4" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" 1330 | 1331 | [[package]] 1332 | name = "windows-sys" 1333 | version = "0.48.0" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1336 | dependencies = [ 1337 | "windows-targets 0.48.5", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "windows-sys" 1342 | version = "0.52.0" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1345 | dependencies = [ 1346 | "windows-targets 0.52.6", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "windows-sys" 1351 | version = "0.59.0" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1354 | dependencies = [ 1355 | "windows-targets 0.52.6", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "windows-targets" 1360 | version = "0.48.5" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1363 | dependencies = [ 1364 | "windows_aarch64_gnullvm 0.48.5", 1365 | "windows_aarch64_msvc 0.48.5", 1366 | "windows_i686_gnu 0.48.5", 1367 | "windows_i686_msvc 0.48.5", 1368 | "windows_x86_64_gnu 0.48.5", 1369 | "windows_x86_64_gnullvm 0.48.5", 1370 | "windows_x86_64_msvc 0.48.5", 1371 | ] 1372 | 1373 | [[package]] 1374 | name = "windows-targets" 1375 | version = "0.52.6" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1378 | dependencies = [ 1379 | "windows_aarch64_gnullvm 0.52.6", 1380 | "windows_aarch64_msvc 0.52.6", 1381 | "windows_i686_gnu 0.52.6", 1382 | "windows_i686_gnullvm", 1383 | "windows_i686_msvc 0.52.6", 1384 | "windows_x86_64_gnu 0.52.6", 1385 | "windows_x86_64_gnullvm 0.52.6", 1386 | "windows_x86_64_msvc 0.52.6", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "windows_aarch64_gnullvm" 1391 | version = "0.48.5" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1394 | 1395 | [[package]] 1396 | name = "windows_aarch64_gnullvm" 1397 | version = "0.52.6" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1400 | 1401 | [[package]] 1402 | name = "windows_aarch64_msvc" 1403 | version = "0.48.5" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1406 | 1407 | [[package]] 1408 | name = "windows_aarch64_msvc" 1409 | version = "0.52.6" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1412 | 1413 | [[package]] 1414 | name = "windows_i686_gnu" 1415 | version = "0.48.5" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1418 | 1419 | [[package]] 1420 | name = "windows_i686_gnu" 1421 | version = "0.52.6" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1424 | 1425 | [[package]] 1426 | name = "windows_i686_gnullvm" 1427 | version = "0.52.6" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1430 | 1431 | [[package]] 1432 | name = "windows_i686_msvc" 1433 | version = "0.48.5" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1436 | 1437 | [[package]] 1438 | name = "windows_i686_msvc" 1439 | version = "0.52.6" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1442 | 1443 | [[package]] 1444 | name = "windows_x86_64_gnu" 1445 | version = "0.48.5" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1448 | 1449 | [[package]] 1450 | name = "windows_x86_64_gnu" 1451 | version = "0.52.6" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1454 | 1455 | [[package]] 1456 | name = "windows_x86_64_gnullvm" 1457 | version = "0.48.5" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1460 | 1461 | [[package]] 1462 | name = "windows_x86_64_gnullvm" 1463 | version = "0.52.6" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1466 | 1467 | [[package]] 1468 | name = "windows_x86_64_msvc" 1469 | version = "0.48.5" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1472 | 1473 | [[package]] 1474 | name = "windows_x86_64_msvc" 1475 | version = "0.52.6" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1478 | 1479 | [[package]] 1480 | name = "winnow" 1481 | version = "0.6.20" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 1484 | dependencies = [ 1485 | "memchr", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "winreg" 1490 | version = "0.50.0" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1493 | dependencies = [ 1494 | "cfg-if", 1495 | "windows-sys 0.48.0", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "wlr-assets" 1500 | version = "0.2.0" 1501 | source = "git+https://github.com/vmware-labs/webassembly-language-runtimes.git#6e7674cf52edb8299bf34d4f7cb0a385c6ff728d" 1502 | dependencies = [ 1503 | "bytes", 1504 | "flate2", 1505 | "path-absolutize", 1506 | "reqwest", 1507 | "tar", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "wlr-libpy" 1512 | version = "0.2.0" 1513 | source = "git+https://github.com/vmware-labs/webassembly-language-runtimes.git#6e7674cf52edb8299bf34d4f7cb0a385c6ff728d" 1514 | dependencies = [ 1515 | "wlr-assets", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "xattr" 1520 | version = "1.3.1" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 1523 | dependencies = [ 1524 | "libc", 1525 | "linux-raw-sys", 1526 | "rustix", 1527 | ] 1528 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "extism-python-pdk" 3 | version = "0.1.5" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "core" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | anyhow = "1.0.86" 12 | extism-pdk = "1.3.0" 13 | pyo3 = { version = "0.22.0", features = ["abi3-py311"] } 14 | wlr-libpy = { git = "https://github.com/vmware-labs/webassembly-language-runtimes.git", features = [ 15 | "py_main", 16 | ] } 17 | 18 | [build-dependencies] 19 | wlr-libpy = { git = "https://github.com/vmware-labs/webassembly-language-runtimes.git", features = [ 20 | "build", 21 | ] } 22 | -------------------------------------------------------------------------------- /lib/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | use wlr_libpy::bld_cfg::configure_static_libs; 3 | configure_static_libs().unwrap().emit_link_flags(); 4 | println!("cargo::rerun-if-changed=src/prelude.py"); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyModule, PyTuple, PyTracebackMethods}; 2 | use pyo3::{append_to_inittab, conversion::ToPyObject, prelude::*, Py, PyAny, PyResult, Python}; 3 | 4 | mod py_module; 5 | use py_module::make_extism_ffi_module; 6 | 7 | use std::io::Read; 8 | 9 | const PRELUDE: &str = include_str!("prelude.py"); 10 | 11 | fn convert_arg(py: Python, arg: Arg) -> PyObject { 12 | match arg { 13 | Arg::Int(x) => x.to_object(py), 14 | Arg::Float(f) => f.to_object(py), 15 | } 16 | } 17 | 18 | fn wrap_gil PyResult>(err: T, f: F) -> T { 19 | let result = Python::with_gil(|py| { 20 | f(py).map_err(|err| { 21 | let tb = err.traceback_bound(py).and_then(|x|{ 22 | if let Ok(x) = x.format() { 23 | Some(x) 24 | } else { 25 | None 26 | } 27 | }); 28 | let mut s = err.into_value(py).to_string(); 29 | if let Some(tb) = tb { 30 | s += "\n"; 31 | s += &tb; 32 | } 33 | s 34 | }) 35 | }); 36 | match result { 37 | Ok(x) => x, 38 | Err(error) => { 39 | let mem = extism_pdk::Memory::from_bytes(&error) 40 | .expect("Load Python error message into Extism memory"); 41 | unsafe { 42 | extism_pdk::extism::error_set(mem.offset()); 43 | } 44 | err 45 | } 46 | } 47 | } 48 | 49 | #[no_mangle] 50 | pub extern "C" fn __invoke(index: u32, shared: bool) { 51 | wrap_gil((), |py| { 52 | let call_args = unsafe { CALL_ARGS.pop() }; 53 | let mut args: Vec = call_args 54 | .unwrap() 55 | .into_iter() 56 | .map(|x| convert_arg(py, x)) 57 | .collect(); 58 | args.insert(0, shared.to_object(py)); 59 | args.insert(0, index.to_object(py)); 60 | let args = PyTuple::new_bound(py, args); 61 | let m = PyModule::import_bound(py, "extism_plugin")?; 62 | let fun: Py = m.getattr("__invoke")?.into(); 63 | fun.call1(py, args)?; 64 | Ok(()) 65 | }); 66 | } 67 | 68 | #[no_mangle] 69 | pub extern "C" fn __invoke_i32(index: u32, shared: bool) -> i32 { 70 | wrap_gil(-1, |py| -> PyResult { 71 | let call_args = unsafe { CALL_ARGS.pop() }; 72 | let mut args: Vec = call_args 73 | .unwrap() 74 | .into_iter() 75 | .map(|x| convert_arg(py, x)) 76 | .collect(); 77 | args.insert(0, shared.to_object(py)); 78 | args.insert(0, index.to_object(py)); 79 | let args = PyTuple::new_bound(py, args); 80 | let m = PyModule::import_bound(py, "extism_plugin")?; 81 | let fun: Py = m.getattr("__invoke")?.into(); 82 | let res = fun.call1(py, args)?; 83 | if let Ok(res) = res.extract(py) { 84 | return Ok(res); 85 | } 86 | Ok(0) 87 | }) 88 | } 89 | 90 | #[no_mangle] 91 | pub extern "C" fn __invoke_i64(index: u32, shared: bool) -> i64 { 92 | wrap_gil(-1, |py| -> PyResult { 93 | let call_args = unsafe { CALL_ARGS.pop() }; 94 | let mut args: Vec = call_args 95 | .unwrap() 96 | .into_iter() 97 | .map(|x| convert_arg(py, x)) 98 | .collect(); 99 | args.insert(0, shared.to_object(py)); 100 | args.insert(0, index.to_object(py)); 101 | let args = PyTuple::new_bound(py, args); 102 | let m = PyModule::import_bound(py, "extism_plugin")?; 103 | let fun: Py = m.getattr("__invoke")?.into(); 104 | let res = fun.call1(py, args)?; 105 | if let Ok(res) = res.extract(py) { 106 | return Ok(res); 107 | } 108 | Ok(0) 109 | }).into() 110 | } 111 | 112 | enum Arg { 113 | Int(i64), 114 | Float(f64), 115 | } 116 | 117 | static mut CALL_ARGS: Vec> = vec![]; 118 | 119 | #[no_mangle] 120 | pub extern "C" fn __arg_start() { 121 | unsafe { 122 | CALL_ARGS.push(vec![]); 123 | } 124 | } 125 | 126 | #[no_mangle] 127 | pub extern "C" fn __arg_i32(arg: i32) { 128 | unsafe { 129 | CALL_ARGS.last_mut().unwrap().push(Arg::Int(arg as i64)); 130 | } 131 | } 132 | 133 | #[no_mangle] 134 | pub extern "C" fn __arg_i64(arg: i64) { 135 | unsafe { 136 | CALL_ARGS.last_mut().unwrap().push(Arg::Int(arg)); 137 | } 138 | } 139 | 140 | #[no_mangle] 141 | pub extern "C" fn __arg_f32(arg: f32) { 142 | unsafe { 143 | CALL_ARGS.last_mut().unwrap().push(Arg::Float(arg as f64)); 144 | } 145 | } 146 | 147 | #[no_mangle] 148 | pub extern "C" fn __arg_f64(arg: f64) { 149 | unsafe { 150 | CALL_ARGS.last_mut().unwrap().push(Arg::Float(arg)); 151 | } 152 | } 153 | 154 | #[export_name = "wizer.initialize"] 155 | extern "C" fn init() { 156 | append_to_inittab!(make_extism_ffi_module); 157 | pyo3::prepare_freethreaded_python(); 158 | let mut code = String::new(); 159 | std::io::stdin().read_to_string(&mut code).unwrap(); 160 | Python::with_gil(|py| -> PyResult<()> { 161 | PyModule::from_code_bound(py, PRELUDE, "", "extism")?; 162 | PyModule::from_code_bound(py, &code, "", "extism_plugin")?; 163 | Ok(()) 164 | }) 165 | .expect("initialize python code") 166 | } 167 | -------------------------------------------------------------------------------- /lib/src/prelude.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Optional 2 | import json 3 | from enum import Enum 4 | 5 | import extism_ffi as ffi 6 | 7 | LogLevel = ffi.LogLevel 8 | input_str = ffi.input_str 9 | input_bytes = ffi.input_bytes 10 | output_str = ffi.output_str 11 | output_bytes = ffi.output_bytes 12 | memory = ffi.memory 13 | 14 | def log(level, msg): 15 | if isinstance(msg, bytes): 16 | msg = msg.decode() 17 | elif not isinstance(msg, str): 18 | msg = str(msg) 19 | ffi.log(level, msg) 20 | 21 | HttpRequest = ffi.HttpRequest 22 | 23 | __exports = [] 24 | 25 | IMPORT_INDEX = 0 26 | 27 | def _store(x) -> int: 28 | if isinstance(x, str): 29 | return ffi.memory.alloc(x.encode()).offset 30 | elif isinstance(x, bytes): 31 | return ffi.memory.alloc(x).offset 32 | elif isinstance(x, dict) or isinstance(x, list): 33 | return ffi.memory.alloc(json.dumps(x).encode()).offset 34 | elif isinstance(x, Enum): 35 | return ffi.memory.alloc(str(x.value).encode()).offset 36 | elif isinstance(x, ffi.memory.MemoryHandle): 37 | return x.offset 38 | elif isinstance(x, int): 39 | return x 40 | elif x is None: 41 | return 0 42 | else: 43 | raise Exception(f"Unsupported python type: {type(x)}") 44 | 45 | 46 | def _load(t, x): 47 | if t is int: 48 | return x 49 | 50 | mem = ffi.memory.find(x) 51 | if mem is None: 52 | return None 53 | 54 | if t is str: 55 | return ffi.memory.string(mem) 56 | elif t is bytes: 57 | return ffi.memory.bytes(mem) 58 | elif t is dict or t is list: 59 | return json.loads(ffi.memory.string(mem)) 60 | elif issubclass(t, Enum): 61 | return t(ffi.memory.string(mem)) 62 | elif t is ffi.memory.MemoryHandle: 63 | return mem 64 | elif t is type(None): 65 | return None 66 | else: 67 | raise Exception(f"Unsupported python type: {t}") 68 | 69 | 70 | def import_fn(module, name): 71 | """Annotate an import function""" 72 | global IMPORT_INDEX 73 | idx = IMPORT_INDEX 74 | 75 | def inner(func): 76 | def wrapper(*args): 77 | args = [_store(a) for a in args] 78 | if "return" in func.__annotations__: 79 | ret = func.__annotations__["return"] 80 | res = ffi.__invoke_host_func(idx, *args) 81 | return _load(ret, res) 82 | else: 83 | ffi.__invoke_host_func0(idx, *args) 84 | 85 | return wrapper 86 | 87 | IMPORT_INDEX += 1 88 | return inner 89 | 90 | 91 | def plugin_fn(func): 92 | """Annotate a function that will be called by Extism""" 93 | global __exports 94 | __exports.append(func) 95 | 96 | def inner(): 97 | return func() 98 | 99 | return inner 100 | 101 | 102 | def shared_fn(f): 103 | """Annotate a an export that won't be called directly by Extism""" 104 | global __exports 105 | __exports.append(f) 106 | 107 | def inner(*args): 108 | return f(*args) 109 | 110 | return inner 111 | 112 | 113 | def input_json(t: Optional[type] = None): 114 | """Get input as JSON""" 115 | if t is int or t is float: 116 | return t(json.loads(input_str())) 117 | return json.loads(input_str()) 118 | 119 | 120 | def output_json(x): 121 | """Set JSON output""" 122 | if isinstance(x, int) or isinstance(x, float): 123 | output_str(json.dumps(str(x))) 124 | return 125 | 126 | if hasattr(x, "__dict__"): 127 | x = x.__dict__ 128 | output_str(json.dumps(x)) 129 | 130 | 131 | def input(t: type = None): 132 | if t is None: 133 | return None 134 | if t is str: 135 | return input_str() 136 | elif t is bytes: 137 | return input_bytes() 138 | elif t is dict or t is list: 139 | return json.loads(input_str()) 140 | elif issubclass(t, Enum): 141 | return t(input_str()) 142 | else: 143 | raise Exception(f"Unsupported type for input: {t}") 144 | 145 | 146 | def output(x=None): 147 | if x is None: 148 | return 149 | if isinstance(x, str): 150 | output_str(x) 151 | elif isinstance(x, bytes): 152 | output_bytes(x) 153 | elif isinstance(x, dict) or isinstance(x, list): 154 | output_json(x) 155 | elif isinstance(x, Enum): 156 | output_str(x.value) 157 | else: 158 | raise Exception(f"Unsupported type for output: {type(x)}") 159 | 160 | 161 | class Var: 162 | @staticmethod 163 | def get_bytes(key: str) -> Optional[bytes]: 164 | """Get variable as bytes""" 165 | return ffi.var_get(key) 166 | 167 | @staticmethod 168 | def get_str(key: str) -> Optional[str]: 169 | """Get variable as string""" 170 | x = ffi.var_get(key) 171 | if x is None: 172 | return None 173 | return x.decode() 174 | 175 | @staticmethod 176 | def get_json(key: str): 177 | """Get variable as JSON""" 178 | x = Var.get_str(key) 179 | if x is None: 180 | return x 181 | return json.loads(x) 182 | 183 | @staticmethod 184 | def set(key: str, value: Union[bytes, str]): 185 | """Set a variable with a string or bytes value""" 186 | if isinstance(value, str): 187 | value = value.encode() 188 | return ffi.var_set(key, value) 189 | 190 | 191 | class Config: 192 | @staticmethod 193 | def get_str(key: str) -> Optional[str]: 194 | """Get a config value as string""" 195 | return ffi.config_get(key) 196 | 197 | @staticmethod 198 | def get_json(key: str): 199 | """Get a config vakye as JSON""" 200 | x = ffi.config_get(key) 201 | if x is None: 202 | return None 203 | return json.loads(x) 204 | 205 | 206 | class HttpResponse: 207 | _inner: ffi.HttpResponse 208 | 209 | def __init__(self, res: ffi.HttpResponse): 210 | self._inner = res 211 | 212 | @property 213 | def status_code(self): 214 | """Get HTTP status code""" 215 | return self._inner.status_code() 216 | 217 | def data_bytes(self): 218 | """Get response body bytes""" 219 | return self._inner.data() 220 | 221 | def data_str(self): 222 | """Get response body string""" 223 | return self.data_bytes().decode() 224 | 225 | def data_json(self): 226 | """Get response body JSON""" 227 | return json.loads(self.data_str()) 228 | 229 | def headers(self): 230 | """Get HTTP response headers""" 231 | return self._inner.headers or {} 232 | 233 | 234 | class Http: 235 | @staticmethod 236 | def request( 237 | url: str, 238 | meth: str = "GET", 239 | body: Optional[Union[bytes, str]] = None, 240 | headers: Optional[dict] = None, 241 | ) -> HttpResponse: 242 | """Make an HTTP request""" 243 | req = HttpRequest(url, meth, headers or {}) 244 | if body is not None and isinstance(body, str): 245 | body = body.encode() 246 | return HttpResponse(ffi.http_request(req, body)) 247 | -------------------------------------------------------------------------------- /lib/src/py_module.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{ 2 | exceptions::PyException, 3 | prelude::*, 4 | types::{PyBytes, PyInt, PyModule, PyTuple}, 5 | PyErr, PyResult, 6 | }; 7 | 8 | use std::collections::HashMap; 9 | 10 | fn error(x: extism_pdk::Error) -> PyErr { 11 | PyException::new_err(format!("{:?}", x)) 12 | } 13 | 14 | #[pyo3::pyfunction] 15 | pub fn input_bytes(py: Python<'_>) -> PyResult> { 16 | let input = extism_pdk::input::>().map_err(error)?; 17 | Ok(PyBytes::new_bound(py, &input)) 18 | } 19 | 20 | #[pyo3::pyfunction] 21 | pub fn output_bytes(result: &[u8]) -> PyResult<()> { 22 | extism_pdk::output(result).map_err(error)?; 23 | Ok(()) 24 | } 25 | 26 | #[pyo3::pyfunction] 27 | pub fn input_str() -> PyResult { 28 | let input = extism_pdk::input::().map_err(error)?; 29 | Ok(input) 30 | } 31 | 32 | #[pyo3::pyfunction] 33 | pub fn output_str(result: &str) -> PyResult<()> { 34 | extism_pdk::output(result).map_err(error)?; 35 | Ok(()) 36 | } 37 | 38 | #[pyo3::pyfunction] 39 | pub fn set_error(msg: &str) -> PyResult<()> { 40 | let mem = extism_pdk::Memory::from_bytes(msg).map_err(error)?; 41 | unsafe { 42 | extism_pdk::extism::error_set(mem.offset()); 43 | } 44 | Ok(()) 45 | } 46 | 47 | #[pyo3::pyfunction] 48 | pub fn config_get(key: &str) -> PyResult> { 49 | let r = extism_pdk::config::get(key).map_err(error)?; 50 | Ok(r) 51 | } 52 | 53 | #[pyo3::pyfunction] 54 | pub fn var_get<'a>(py: Python<'a>, key: &'a str) -> PyResult>> { 55 | let r: Option> = extism_pdk::var::get(key).map_err(error)?; 56 | if let Some(r) = r { 57 | Ok(Some(PyBytes::new_bound(py, &r))) 58 | } else { 59 | Ok(None) 60 | } 61 | } 62 | 63 | #[pyo3::pyfunction] 64 | pub fn var_set(key: String, value: &[u8]) -> PyResult<()> { 65 | extism_pdk::var::set(key, value).map_err(error)?; 66 | Ok(()) 67 | } 68 | 69 | #[pyo3::pyclass(eq, eq_int)] 70 | #[derive(Debug, PartialEq, Clone, Copy)] 71 | pub enum LogLevel { 72 | Trace, 73 | Debug, 74 | Info, 75 | Warn, 76 | Error, 77 | } 78 | 79 | #[pyo3::pyfunction] 80 | pub fn log(level: LogLevel, msg: &str) -> PyResult<()> { 81 | match level { 82 | LogLevel::Error => extism_pdk::log!(extism_pdk::LogLevel::Error, "{}", msg), 83 | LogLevel::Debug => extism_pdk::log!(extism_pdk::LogLevel::Debug, "{}", msg), 84 | LogLevel::Warn => extism_pdk::log!(extism_pdk::LogLevel::Warn, "{}", msg), 85 | LogLevel::Info => extism_pdk::log!(extism_pdk::LogLevel::Info, "{}", msg), 86 | LogLevel::Trace => extism_pdk::log!(extism_pdk::LogLevel::Trace, "{}", msg), 87 | } 88 | 89 | Ok(()) 90 | } 91 | 92 | #[pyo3::pyclass(eq)] 93 | #[derive(Debug, PartialEq, Clone)] 94 | pub struct HttpRequest { 95 | #[pyo3(get)] 96 | pub url: String, 97 | #[pyo3(get)] 98 | pub method: Option, 99 | #[pyo3(get)] 100 | pub headers: Option>, 101 | } 102 | 103 | #[pymethods] 104 | impl HttpRequest { 105 | #[new] 106 | #[pyo3(signature = (url, method=None, headers=None))] 107 | pub fn new( 108 | url: String, 109 | method: Option, 110 | headers: Option>, 111 | ) -> Self { 112 | HttpRequest { 113 | url, 114 | method, 115 | headers, 116 | } 117 | } 118 | } 119 | 120 | #[pyo3::pyclass(eq)] 121 | #[derive(Debug, Clone, PartialEq)] 122 | pub struct HttpResponse { 123 | pub data: Vec, 124 | pub status: u16, 125 | pub headers: HashMap, 126 | } 127 | 128 | #[pymethods] 129 | impl HttpResponse { 130 | pub fn data<'a>(&self, py: Python<'a>) -> Bound<'a, PyBytes> { 131 | let bytes = PyBytes::new_bound(py, &self.data); 132 | bytes 133 | } 134 | 135 | pub fn status_code(&self) -> u16 { 136 | self.status 137 | } 138 | } 139 | 140 | #[pyo3::pyfunction] 141 | #[pyo3(signature = (req, body=None))] 142 | pub fn http_request(req: HttpRequest, body: Option<&[u8]>) -> PyResult { 143 | let req = extism_pdk::HttpRequest { 144 | url: req.url, 145 | headers: req.headers.unwrap_or_default().into_iter().collect(), 146 | method: req.method, 147 | }; 148 | let res = extism_pdk::http::request(&req, body).map_err(error)?; 149 | let x = HttpResponse { 150 | data: res.body(), 151 | status: res.status_code(), 152 | headers: res.headers().clone(), 153 | }; 154 | res.into_memory().free(); 155 | Ok(x) 156 | } 157 | 158 | #[pyo3::pyclass(eq)] 159 | #[derive(Debug, PartialEq, Clone, Copy)] 160 | pub struct MemoryHandle { 161 | #[pyo3(get)] 162 | pub offset: u64, 163 | #[pyo3(get)] 164 | pub length: u64, 165 | } 166 | 167 | #[pymethods] 168 | impl MemoryHandle { 169 | #[new] 170 | pub fn new(offset: u64, length: u64) -> Self { 171 | MemoryHandle { offset, length } 172 | } 173 | } 174 | 175 | #[pyo3::pyfunction] 176 | #[pyo3(name = "find")] 177 | pub fn memory_find(offs: u64) -> PyResult> { 178 | if let Some(mem) = extism_pdk::Memory::find(offs) { 179 | Ok(Some(MemoryHandle { 180 | offset: mem.offset(), 181 | length: mem.len() as u64, 182 | })) 183 | } else { 184 | Ok(None) 185 | } 186 | } 187 | 188 | #[pyo3::pyfunction] 189 | #[pyo3(name = "bytes")] 190 | pub fn memory_bytes(py: Python<'_>, mem: MemoryHandle) -> PyResult> { 191 | let mem = extism_pdk::Memory(extism_pdk::MemoryHandle { 192 | offset: mem.offset, 193 | length: mem.length, 194 | }); 195 | 196 | Ok(PyBytes::new_bound(py, &mem.to_vec())) 197 | } 198 | 199 | #[pyo3::pyfunction] 200 | #[pyo3(name = "string")] 201 | pub fn memory_string(mem: MemoryHandle) -> PyResult { 202 | let mem = extism_pdk::Memory(extism_pdk::MemoryHandle { 203 | offset: mem.offset, 204 | length: mem.length, 205 | }); 206 | 207 | mem.to_string().map_err(error) 208 | } 209 | 210 | #[pyo3::pyfunction] 211 | #[pyo3(name = "free")] 212 | pub fn memory_free(mem: MemoryHandle) -> PyResult<()> { 213 | let mem = extism_pdk::Memory(extism_pdk::MemoryHandle { 214 | offset: mem.offset, 215 | length: mem.length, 216 | }); 217 | 218 | mem.free(); 219 | Ok(()) 220 | } 221 | 222 | #[pyo3::pyfunction] 223 | #[pyo3(name = "alloc")] 224 | pub fn memory_alloc(data: &[u8]) -> PyResult { 225 | let mem = extism_pdk::Memory::from_bytes(data).map_err(error)?; 226 | Ok(MemoryHandle { 227 | offset: mem.offset(), 228 | length: mem.len() as u64, 229 | }) 230 | } 231 | 232 | #[pyfunction] 233 | #[pyo3(signature = (i, *args))] 234 | #[pyo3(name = "__invoke_host_func")] 235 | fn invoke_host_func(i: &Bound<'_, PyInt>, args: &Bound<'_, PyTuple>) -> PyResult { 236 | let length = args.len(); 237 | let index = i.extract::<'_, u32>()?; 238 | let offs = unsafe { 239 | match length { 240 | 0 => __invokeHostFunc_0_1(index), 241 | 1 => __invokeHostFunc_1_1(index, args.get_item(0)?.extract::<'_, u64>()?), 242 | 2 => __invokeHostFunc_2_1( 243 | index, 244 | args.get_item(0)?.extract::<'_, u64>()?, 245 | args.get_item(1)?.extract::<'_, u64>()?, 246 | ), 247 | 3 => __invokeHostFunc_3_1( 248 | index, 249 | args.get_item(0)?.extract::<'_, u64>()?, 250 | args.get_item(1)?.extract::<'_, u64>()?, 251 | args.get_item(2)?.extract::<'_, u64>()?, 252 | ), 253 | 4 => __invokeHostFunc_4_1( 254 | index, 255 | args.get_item(0)?.extract::<'_, u64>()?, 256 | args.get_item(1)?.extract::<'_, u64>()?, 257 | args.get_item(2)?.extract::<'_, u64>()?, 258 | args.get_item(3)?.extract::<'_, u64>()?, 259 | ), 260 | 5 => __invokeHostFunc_5_1( 261 | index, 262 | args.get_item(0)?.extract::<'_, u64>()?, 263 | args.get_item(1)?.extract::<'_, u64>()?, 264 | args.get_item(2)?.extract::<'_, u64>()?, 265 | args.get_item(3)?.extract::<'_, u64>()?, 266 | args.get_item(4)?.extract::<'_, u64>()?, 267 | ), 268 | _ => { 269 | return Err(error(extism_pdk::Error::msg( 270 | "Host functions with more than 5 parameters are not supported", 271 | ))); 272 | } 273 | } 274 | }; 275 | 276 | Ok(offs) 277 | } 278 | 279 | #[pyfunction] 280 | #[pyo3(signature = (index, *args))] 281 | #[pyo3(name = "__invoke_host_func0")] 282 | fn invoke_host_func0(index: &Bound<'_, PyInt>, args: &Bound<'_, PyTuple>) -> PyResult<()> { 283 | let length = args.len(); 284 | let index = index.extract::<'_, u32>()?; 285 | 286 | unsafe { 287 | match length { 288 | 0 => __invokeHostFunc_0_0(index), 289 | 1 => __invokeHostFunc_1_0(index, args.get_item(0)?.extract::<'_, u64>()?), 290 | 2 => __invokeHostFunc_2_0( 291 | index, 292 | args.get_item(0)?.extract::<'_, u64>()?, 293 | args.get_item(1)?.extract::<'_, u64>()?, 294 | ), 295 | 3 => __invokeHostFunc_3_0( 296 | index, 297 | args.get_item(0)?.extract::<'_, u64>()?, 298 | args.get_item(1)?.extract::<'_, u64>()?, 299 | args.get_item(2)?.extract::<'_, u64>()?, 300 | ), 301 | 4 => __invokeHostFunc_4_0( 302 | index, 303 | args.get_item(0)?.extract::<'_, u64>()?, 304 | args.get_item(1)?.extract::<'_, u64>()?, 305 | args.get_item(2)?.extract::<'_, u64>()?, 306 | args.get_item(3)?.extract::<'_, u64>()?, 307 | ), 308 | 5 => __invokeHostFunc_5_0( 309 | index, 310 | args.get_item(0)?.extract::<'_, u64>()?, 311 | args.get_item(1)?.extract::<'_, u64>()?, 312 | args.get_item(2)?.extract::<'_, u64>()?, 313 | args.get_item(3)?.extract::<'_, u64>()?, 314 | args.get_item(4)?.extract::<'_, u64>()?, 315 | ), 316 | _ => { 317 | return Err(error(extism_pdk::Error::msg( 318 | "Host functions with more than 5 parameters are not supported", 319 | ))); 320 | } 321 | } 322 | } 323 | 324 | Ok(()) 325 | } 326 | 327 | #[pyo3::pymodule] 328 | #[pyo3(name = "extism_ffi")] 329 | pub fn make_extism_ffi_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { 330 | let memory_module = PyModule::new_bound(py, "memory")?; 331 | memory_module.add_class::()?; 332 | memory_module.add_function(pyo3::wrap_pyfunction!(memory_find, &memory_module)?)?; 333 | memory_module.add_function(pyo3::wrap_pyfunction!(memory_bytes, &memory_module)?)?; 334 | memory_module.add_function(pyo3::wrap_pyfunction!(memory_string, &memory_module)?)?; 335 | memory_module.add_function(pyo3::wrap_pyfunction!(memory_free, &memory_module)?)?; 336 | memory_module.add_function(pyo3::wrap_pyfunction!(memory_alloc, &memory_module)?)?; 337 | 338 | module.add_class::()?; 339 | module.add_class::()?; 340 | module.add_class::()?; 341 | module.add_function(pyo3::wrap_pyfunction!(input_bytes, module)?)?; 342 | module.add_function(pyo3::wrap_pyfunction!(output_bytes, module)?)?; 343 | module.add_function(pyo3::wrap_pyfunction!(input_str, module)?)?; 344 | module.add_function(pyo3::wrap_pyfunction!(output_str, module)?)?; 345 | module.add_function(pyo3::wrap_pyfunction!(config_get, module)?)?; 346 | module.add_function(pyo3::wrap_pyfunction!(var_get, module)?)?; 347 | module.add_function(pyo3::wrap_pyfunction!(var_set, module)?)?; 348 | module.add_function(pyo3::wrap_pyfunction!(log, module)?)?; 349 | module.add_function(pyo3::wrap_pyfunction!(set_error, module)?)?; 350 | module.add_function(pyo3::wrap_pyfunction!(http_request, module)?)?; 351 | module.add_function(pyo3::wrap_pyfunction!(invoke_host_func, module)?)?; 352 | module.add_function(pyo3::wrap_pyfunction!(invoke_host_func0, module)?)?; 353 | module.add_submodule(&memory_module)?; 354 | Ok(()) 355 | } 356 | 357 | #[link(wasm_import_module = "shim")] 358 | extern "C" { 359 | // this import will get satisified by the import shim 360 | fn __invokeHostFunc_0_0(func_idx: u32); 361 | fn __invokeHostFunc_1_0(func_idx: u32, ptr: u64); 362 | fn __invokeHostFunc_2_0(func_idx: u32, ptr: u64, ptr2: u64); 363 | fn __invokeHostFunc_3_0(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64); 364 | fn __invokeHostFunc_4_0(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64, ptr4: u64); 365 | fn __invokeHostFunc_5_0(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64, ptr4: u64, ptr5: u64); 366 | fn __invokeHostFunc_0_1(func_idx: u32) -> u64; 367 | fn __invokeHostFunc_1_1(func_idx: u32, ptr: u64) -> u64; 368 | fn __invokeHostFunc_2_1(func_idx: u32, ptr: u64, ptr2: u64) -> u64; 369 | fn __invokeHostFunc_3_1(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64) -> u64; 370 | fn __invokeHostFunc_4_1(func_idx: u32, ptr: u64, ptr2: u64, ptr3: u64, ptr4: u64) -> u64; 371 | fn __invokeHostFunc_5_1( 372 | func_idx: u32, 373 | ptr: u64, 374 | ptr2: u64, 375 | ptr3: u64, 376 | ptr4: u64, 377 | ptr5: u64, 378 | ) -> u64; 379 | } 380 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "python-pdk" 3 | version = "0.1.5" 4 | description = "Extism Python PDK" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [] 8 | 9 | [tool.uv] 10 | dev-dependencies = [ 11 | "ruff>=0.6.3", 12 | ] 13 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81.0" 3 | targets = ["wasm32-wasip1"] -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.11" 4 | 5 | [[package]] 6 | name = "python-pdk" 7 | version = "0.1.5" 8 | source = { virtual = "." } 9 | 10 | [package.dev-dependencies] 11 | dev = [ 12 | { name = "ruff" }, 13 | ] 14 | 15 | [package.metadata] 16 | 17 | [package.metadata.requires-dev] 18 | dev = [{ name = "ruff", specifier = ">=0.6.3" }] 19 | 20 | [[package]] 21 | name = "ruff" 22 | version = "0.6.3" 23 | source = { registry = "https://pypi.org/simple" } 24 | sdist = { url = "https://files.pythonhosted.org/packages/5d/f9/0b32e5d1c6f957df49398cd882a011e9488fcbca0d6acfeeea50ccd37a4d/ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983", size = 2463514 } 25 | wheels = [ 26 | { url = "https://files.pythonhosted.org/packages/72/68/1da6a1e39a03a229ea57c511691d6225072759cc7764206c3f0989521194/ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3", size = 9696928 }, 27 | { url = "https://files.pythonhosted.org/packages/6e/59/3b8b1d3a4271c6eb6ceecd3cef19a6d881639a0f18ad651563d6f619aaae/ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc", size = 9448462 }, 28 | { url = "https://files.pythonhosted.org/packages/35/4f/b942ecb8bbebe53aa9b33e9b96df88acd50b70adaaed3070f1d92131a1cb/ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1", size = 9176190 }, 29 | { url = "https://files.pythonhosted.org/packages/a0/20/b0bcb29d4ee437f3567b73b6905c034e2e94d29b9b826c66daecc1cf6388/ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1", size = 10108892 }, 30 | { url = "https://files.pythonhosted.org/packages/9c/e3/211bc759f424e8823a9937e0f678695ca02113c621dfde1fa756f9f26f6d/ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672", size = 9476471 }, 31 | { url = "https://files.pythonhosted.org/packages/b2/a3/2ec35a2d7a554364864206f0e46812b92a074ad8a014b923d821ead532aa/ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1", size = 10294802 }, 32 | { url = "https://files.pythonhosted.org/packages/03/8b/56ef687b3489c88886dea48c78fb4969b6b65f18007d0ac450070edd1f58/ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384", size = 11022372 }, 33 | { url = "https://files.pythonhosted.org/packages/a5/21/327d147feb442adb88975e81e2263102789eba9ad2afa102c661912a482f/ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a", size = 10596596 }, 34 | { url = "https://files.pythonhosted.org/packages/6c/86/ff386de63729da3e08c8099c57f577a00ec9f3eea711b23ac07cf3588dc5/ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500", size = 11572830 }, 35 | { url = "https://files.pythonhosted.org/packages/38/5d/b33284c108e3f315ddd09b70296fd76bd28ecf8965a520bc93f3bbd8ac40/ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470", size = 10262577 }, 36 | { url = "https://files.pythonhosted.org/packages/29/99/9cdfad0d7f460e66567236eddc691473791afd9aff93a0dfcdef0462a6c7/ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f", size = 10098751 }, 37 | { url = "https://files.pythonhosted.org/packages/a8/9f/f801a1619f5549e552f1f722f1db57eb39e7e1d83d482133142781d450de/ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5", size = 9563859 }, 38 | { url = "https://files.pythonhosted.org/packages/0b/4d/fb2424faf04ffdb960ae2b3a1d991c5183dd981003de727d2d5cc38abc98/ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351", size = 9914291 }, 39 | { url = "https://files.pythonhosted.org/packages/2e/dd/94fddf002a8f6152e8ebfbb51d3f93febc415c1fe694345623c31ce8b33b/ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8", size = 10331549 }, 40 | { url = "https://files.pythonhosted.org/packages/b4/73/ca9c2f9237a430ca423b6dca83b77e9a428afeb7aec80596e86c369123fe/ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521", size = 7962163 }, 41 | { url = "https://files.pythonhosted.org/packages/55/ce/061c605b1dfb52748d59bc0c7a8507546c178801156415773d18febfd71d/ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb", size = 8800901 }, 42 | { url = "https://files.pythonhosted.org/packages/63/28/ae4ffe7d3b6134ca6d31ebef07447ef70097c4a9e8fbbc519b374c5c1559/ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82", size = 8229171 }, 43 | ] 44 | --------------------------------------------------------------------------------