├── tests ├── __init__.py └── test_extism.py ├── .github ├── CODEOWNERS └── workflows │ ├── release.yaml │ ├── ci.yaml │ └── build.yaml ├── wasm ├── code.wasm ├── loop.wasm ├── globals.wasm ├── kitchensink.wasm └── code-functions.wasm ├── .gitignore ├── docs ├── source │ ├── api.rst │ ├── index.rst │ ├── conf.py │ └── usage.rst ├── Makefile └── make.bat ├── setup.cfg ├── Makefile ├── .readthedocs.yaml ├── extism ├── __init__.py └── extism.py ├── pyproject.toml ├── LICENSE ├── justfile ├── example.py ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @chrisdickinson @zshipko 2 | -------------------------------------------------------------------------------- /wasm/code.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/python-sdk/HEAD/wasm/code.wasm -------------------------------------------------------------------------------- /wasm/loop.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/python-sdk/HEAD/wasm/loop.wasm -------------------------------------------------------------------------------- /wasm/globals.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/python-sdk/HEAD/wasm/globals.wasm -------------------------------------------------------------------------------- /wasm/kitchensink.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/python-sdk/HEAD/wasm/kitchensink.wasm -------------------------------------------------------------------------------- /wasm/code-functions.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/python-sdk/HEAD/wasm/code-functions.wasm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | html 2 | __pycache__ 3 | poetry-installer-error-*.log 4 | docs/_build 5 | .DS_Store 6 | dist/ 7 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | .. currentmodule:: extism 7 | 8 | .. automodule:: extism 9 | :members: 10 | :member-order: bysource 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = extism 3 | description = Extism Host SDK for Rust 4 | long_description_content_type = text/markdown 5 | url = https://github.com/extism/python-sdk 6 | project_urls = 7 | Bug Tracker = https://github.com/extism/python-sdk/issues 8 | Changelog = https://github.com/extism/python-sdk/releases 9 | classifiers = 10 | Programming Language :: Python :: 3 11 | License :: OSI Approved :: BSD License 12 | Intended Audience :: Developers 13 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Extism Python Host SDK 2 | ====================== 3 | 4 | This documentation is split into "usage" and "reference" documentation. If 5 | you're just getting started with Extism, check out the :ref:`getting started 6 | ` docs. On the other hand, if you've got a specific question 7 | about a specific API, check out the :ref:`api ` docs. 8 | 9 | Contents 10 | -------- 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | usage 16 | api 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | prepare: 4 | poetry install 5 | 6 | test: prepare 7 | poetry run python -m unittest discover 8 | 9 | clean: 10 | rm -rf dist/* 11 | 12 | publish: clean prepare 13 | poetry build 14 | poetry run twine upload dist/extism-*.tar.gz 15 | 16 | format: 17 | poetry run black extism/ tests/ example.py 18 | 19 | lint: 20 | poetry run black --check extism/ tests/ example.py 21 | 22 | docs: 23 | poetry run pycco extism/*.py 24 | 25 | show-docs: docs 26 | open docs/extism.html 27 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.11" 7 | jobs: 8 | post_create_environment: 9 | # Install poetry 10 | # https://python-poetry.org/docs/#installing-manually 11 | - pip install poetry 12 | # Tell poetry to not use a virtual environment 13 | - poetry config virtualenvs.create false 14 | post_install: 15 | # Install dependencies with 'docs' dependency group 16 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 17 | - poetry install --with dev 18 | 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /extism/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The extism python SDK, used for embedding guest Wasm programs into python 3 | hosts. 4 | """ 5 | 6 | from .extism import ( 7 | Error, 8 | Plugin, 9 | set_log_file, 10 | set_log_custom, 11 | extism_version, 12 | host_fn, 13 | Function, 14 | Memory, 15 | ValType, 16 | Val, 17 | CurrentPlugin, 18 | Codec, 19 | Json, 20 | Pickle, 21 | ) 22 | 23 | __all__ = [ 24 | "Plugin", 25 | "Error", 26 | "CurrentPlugin", 27 | "set_log_file", 28 | "set_log_custom", 29 | "extism_version", 30 | "Memory", 31 | "host_fn", 32 | "CurrentPlugin", 33 | "Function", 34 | "ValType", 35 | "Val", 36 | "Codec", 37 | "Json", 38 | "Pickle", 39 | ] 40 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | release: 4 | types: [published, edited] 5 | jobs: 6 | pypi: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: extractions/setup-just@v1 11 | - name: Install poetry 12 | run: pipx install poetry 13 | - uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.10' 16 | cache: 'poetry' 17 | 18 | - run: poetry config pypi-token.pypi "${{ secrets.PYPI_API_KEY }}" 19 | - name: install twine 20 | run: | 21 | pip install twine 22 | 23 | - name: download release 24 | run: | 25 | tag='${{ github.ref }}' 26 | tag="${tag/refs\/tags\//}" 27 | mkdir dist 28 | cd dist 29 | gh release download "$tag" -p 'extism-*' 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: upload release 34 | run: | 35 | twine upload dist/* 36 | env: 37 | TWINE_USERNAME: __token__ 38 | TWINE_PASSWORD: ${{ secrets.PYPI_API_KEY }} 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | python: 11 | name: Python Test 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | rust: 17 | - stable 18 | python-version: ['3.9', '3.10'] 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v3 22 | 23 | - uses: extractions/setup-just@v1 24 | 25 | - name: Install poetry 26 | run: pipx install poetry 27 | 28 | - uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | cache: 'poetry' 32 | 33 | - run: poetry install 34 | 35 | - run: | 36 | poetry run mypy --install-types -m extism --non-interactive 37 | 38 | - name: Run Python lint 39 | run: | 40 | just lint 41 | 42 | - name: Run Python tests 43 | run: | 44 | just test 45 | 46 | - name: Run Python docs 47 | run: | 48 | just docs 49 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'Extism' 10 | copyright = '2023, The Extism Maintainers' 11 | author = 'The Extism Maintainers' 12 | release = '0.1' 13 | 14 | # -- General configuration 15 | 16 | extensions = [ 17 | 'sphinx.ext.duration', 18 | 'sphinx.ext.doctest', 19 | 'sphinx.ext.autodoc', 20 | 'sphinx.ext.autosummary', 21 | 'sphinx.ext.intersphinx', 22 | 'sphinx_autodoc_typehints', 23 | ] 24 | 25 | intersphinx_mapping = { 26 | 'python': ('https://docs.python.org/3/', None), 27 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 28 | } 29 | intersphinx_disabled_domains = ['std'] 30 | 31 | templates_path = ['_templates'] 32 | 33 | # -- Options for HTML output 34 | 35 | html_theme = 'sphinx_rtd_theme' 36 | 37 | # -- Options for EPUB output 38 | epub_show_urls = 'footnote' 39 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [{ email = "oss@extism.org", name = "The Extism Authors" }] 3 | name = "extism" 4 | version = "0.0.0+replaced-by-ci" 5 | description = "Extism Host SDK for python" 6 | readme = "README.md" 7 | requires-python = ">=3.9" 8 | dependencies = ["extism-sys>=1.9.1"] 9 | 10 | [tool.poetry] 11 | name = "extism" 12 | version = "0.0.0+replaced-by-ci" 13 | description = "Extism Host SDK for python" 14 | authors = ["The Extism Authors "] 15 | license = "BSD-3-Clause" 16 | readme = "README.md" 17 | 18 | [tool.poetry.dependencies] 19 | python = "^3.9" 20 | cffi = "^1.10.0" 21 | extism-sys = { version = ">=1.9.1" } 22 | annotated-types = "^0.6.0" 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | python = "^3.8" 26 | black = "^23.1.0" 27 | sphinx = { version = "^7.2.6", python = "^3.9" } 28 | sphinx-rtd-theme = { version = "^1.3.0", python = "^3.9" } 29 | sphinx-autodoc-typehints = { version = "^1.24.0", python = "^3.9" } 30 | mypy = "^1.5.1" 31 | 32 | [build-system] 33 | requires = ["poetry-core>=1.0.0"] 34 | build-backend = "poetry.core.masonry.api" 35 | 36 | [dependency-groups] 37 | dev = [ 38 | "black>=24.10.0", 39 | "mypy>=1.13.0", 40 | "sphinx-autodoc-typehints>=2.5.0", 41 | "sphinx-rtd-theme>=3.0.2", 42 | "sphinx>=8.1.3", 43 | ] 44 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | export PATH := env_var("PATH") + ":" + env_var_or_default("HOME", env_var_or_default("APPDATA", "")) + "/.local/bin" 2 | 3 | # Make just arguments available as env vars; useful for preserving 4 | # quotes. 5 | set export 6 | 7 | _help: 8 | @just --list 9 | 10 | prepare: 11 | #!/bin/bash 12 | if ! &>/dev/null which poetry; then 13 | curl -sSL https://install.python-poetry.org | sed -e 's|symlinks=False|symlinks=True|' | python3 - 14 | fi 15 | 16 | envs="$(poetry env list || true)" 17 | if [ ! $? ] || [ -z "$envs" ]; then 18 | poetry install --no-cache 19 | fi 20 | 21 | test: prepare 22 | #!/bin/bash 23 | set -eou pipefail 24 | poetry run python -m unittest discover 25 | 26 | set +e 27 | msg=$(2>&1 poetry run python example.py) 28 | if [ $? != 0 ]; then 29 | >&2 echo "$msg" 30 | exit 1 31 | else 32 | echo -e 'poetry run python example.py... \x1b[32mok\x1b[0m' 33 | fi 34 | 35 | poetry *args: prepare 36 | #!/bin/bash 37 | poetry $args 38 | 39 | clean: 40 | rm -rf dist/* 41 | 42 | build: clean prepare 43 | poetry build 44 | 45 | publish: clean prepare 46 | poetry build 47 | poetry run twine upload dist/extism-*.tar.gz 48 | 49 | format: prepare 50 | poetry run black extism/ tests/ example.py 51 | 52 | lint: prepare 53 | poetry run mypy --check extism/ tests/ example.py 54 | poetry run black --check extism/ tests/ example.py 55 | 56 | docs: prepare 57 | poetry run sphinx-build -b html docs/source docs/_build 58 | 59 | serve-docs: docs 60 | poetry run python -m http.server 8000 -d docs/_build 61 | 62 | watch-docs: prepare 63 | watchexec -r -w docs/source -w extism just serve-docs 64 | 65 | show-docs: docs 66 | open docs/extism.html 67 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # run this using `poetry run python example.py`! 2 | import sys 3 | 4 | import json 5 | import hashlib 6 | import pathlib 7 | 8 | from extism import Function, host_fn, ValType, Plugin, set_log_custom, Json 9 | from typing import Annotated 10 | 11 | 12 | @host_fn(user_data=b"Hello again!") 13 | def hello_world(inp: Annotated[dict, Json], *a_string) -> Annotated[dict, Json]: 14 | print("Hello from Python!") 15 | print(a_string) 16 | inp["roundtrip"] = 1 17 | return inp 18 | 19 | 20 | # Compare against Python implementation. 21 | def count_vowels(data): 22 | return sum(letter in b"AaEeIiOoUu" for letter in data) 23 | 24 | 25 | def main(args): 26 | logs = [] 27 | logBuffer = set_log_custom(lambda s: logs.append(s.strip()), "trace") 28 | if len(args) > 1: 29 | data = args[1].encode() 30 | else: 31 | data = b"a" * 1024 32 | 33 | wasm_file_path = pathlib.Path(__file__).parent / "wasm" / "code-functions.wasm" 34 | wasm = wasm_file_path.read_bytes() 35 | hash = hashlib.sha256(wasm).hexdigest() 36 | manifest = {"wasm": [{"data": wasm, "hash": hash}]} 37 | 38 | plugin = Plugin(manifest, wasi=True) 39 | print(plugin.id) 40 | # Call `count_vowels` 41 | wasm_vowel_count = plugin.call("count_vowels", data) 42 | print(wasm_vowel_count) 43 | j = json.loads(wasm_vowel_count) 44 | 45 | print("Number of vowels:", j["count"]) 46 | 47 | assert j["count"] == count_vowels(data) 48 | assert j["roundtrip"] == 1 49 | 50 | # Drain logs and print 51 | logBuffer.drain() 52 | print("Dumping logs", len(logs)) 53 | for line in logs: 54 | print(line) 55 | 56 | 57 | if __name__ == "__main__": 58 | main(sys.argv) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism Python Host SDK 2 | 3 | This repo contains the Python package for integrating with the 4 | [Extism](https://extism.org/) Webassembly framework. Install this library into 5 | your Python application host to run Extism WebAssembly guest plug-ins. 6 | 7 | ```python 8 | import extism 9 | import json 10 | 11 | manifest = {"wasm": [{"url": "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"}]} 12 | with extism.Plugin(manifest, wasi=True) as plugin: 13 | wasm_vowel_count = plugin.call( 14 | "count_vowels", 15 | "hello world", 16 | parse = lambda output: json.loads(bytes(output).decode('utf-8')) 17 | ) 18 | print(wasm_vowel_count) # {'count': 3, 'total': 3, 'vowels': 'aeiouAEIOU'} 19 | ``` 20 | 21 | ## Installation 22 | 23 | Install this package from [PyPI](https://pypi.org/project/extism/): 24 | 25 | ```shell 26 | # using pip 27 | $ pip install extism==1.0.0 28 | 29 | # using poetry 30 | $ poetry add extism=^1.0.0 31 | ``` 32 | 33 | The `extism` package should install an appropriate `extism_sys` dependency 34 | containing a prebuilt shared object for your system. We support the following 35 | targets: 36 | 37 | - MacOS 11.0+, `arm64` 38 | - MacOS 10.7+, `x86_64` 39 | - Manylinux 2.17+, `aarch64` 40 | - Manylinux 2.17+, `x86_64` 41 | - MUSL Linux 1.2+, `aarch64` 42 | - Windows (MSVC), `x86_64` 43 | 44 | If you need support for a different platform or architecture, please [let us know][let-us-know]! 45 | 46 | [let-us-know]: https://github.com/extism/extism/issues/new?title=Please+support+extism_sys+on+my+platform&labels=python&body=Hey%21+Could+you+support+%24PLATFORM+and%2For+%24ARCH%3F 47 | 48 | ## Documentation 49 | 50 | Check out the docs: 51 | 52 | - [Getting Started](https://extism.readthedocs.io/en/latest/usage.html) 53 | - [API docs](https://extism.readthedocs.io/en/latest/api.html) 54 | 55 | ## Development 56 | 57 | ### Local dev 58 | 59 | Install [just](https://just.systems). Running `just test` should install all 60 | other prerequisites. 61 | 62 | ### Release workflow 63 | 64 | 1. Create a semver-formatted git tag (`git tag v1.0.0`). 65 | 2. Push that tag to the repository (`git push origin v1.0.0`.) 66 | 3. Wait for [the Build workflow to run](https://github.com/extism/python-sdk/actions/workflows/build.yaml). 67 | 4. Once the build workflow finishes, go to the [releases](https://github.com/extism/python-sdk/releases) page. You should 68 | see a draft release. 69 | 5. Edit the draft release. Publish the release. 70 | 6. Wait for [the Release workflow to run](https://github.com/extism/python-sdk/actions/workflows/release.yaml). 71 | 7. Once the release workflow completes, you should be able to `pip install extism==${YOUR_TAG}` from PyPI. 72 | 73 | ## LICENSE 74 | 75 | [BSD-3-Clause](./LICENSE) 76 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Python Release Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | tags: 8 | - 'v*' 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | docs: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: extractions/setup-just@v1 20 | - name: Install poetry 21 | run: pipx install poetry 22 | - uses: actions/setup-python@v4 23 | with: 24 | python-version: '3.10' 25 | cache: 'poetry' 26 | - run: poetry install 27 | - name: set version 28 | shell: bash 29 | run: | 30 | pyproject="$(cat pyproject.toml)" 31 | version="${{ github.ref }}" 32 | if [[ "$version" = "refs/heads/main" ]]; then 33 | version="0.0.0-dev" 34 | else 35 | version="${version/refs\/tags\/v/}" 36 | fi 37 | <<<"$pyproject" >pyproject.toml sed -e 's/^version = "0.0.0.replaced-by-ci"/version = "'"$version"'"/g' 38 | 39 | - name: Run Python docs 40 | run: | 41 | just docs 42 | - name: Upload docs artifact 43 | uses: actions/upload-artifact@v3 44 | with: 45 | name: docs 46 | path: docs/_build 47 | 48 | build: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v3 52 | - uses: extractions/setup-just@v1 53 | - name: Install poetry 54 | run: pipx install poetry 55 | - name: set version 56 | shell: bash 57 | run: | 58 | pyproject="$(cat pyproject.toml)" 59 | version="${{ github.ref }}" 60 | if [[ "$version" = "refs/heads/main" ]]; then 61 | version="0.0.0-dev" 62 | else 63 | version="${version/refs\/tags\/v/}" 64 | fi 65 | <<<"$pyproject" >pyproject.toml sed -e 's/^version = "0.0.0.replaced-by-ci"/version = "'"$version"'"/g' 66 | 67 | - uses: actions/setup-python@v4 68 | with: 69 | python-version: '3.10' 70 | cache: 'poetry' 71 | - run: poetry install 72 | - name: build releases 73 | run: | 74 | just build 75 | 76 | - name: upload artifact 77 | uses: actions/upload-artifact@v3 78 | with: 79 | name: dist 80 | path: dist/* 81 | 82 | release: 83 | needs: [docs, build] 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/download-artifact@v3 87 | with: 88 | name: dist 89 | path: dist 90 | - uses: actions/download-artifact@v3 91 | with: 92 | name: docs 93 | path: docs 94 | 95 | - run: | 96 | tar zcvf docs.tar.gz docs --strip-components 1 97 | 98 | - name: Upload Artifact to Draft Release 99 | uses: softprops/action-gh-release@v1 100 | with: 101 | draft: true 102 | files: | 103 | docs 104 | dist/* 105 | if: startsWith(github.ref, 'refs/tags/') 106 | 107 | - uses: "marvinpinto/action-automatic-releases@latest" 108 | with: 109 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 110 | automatic_release_tag: "latest" 111 | prerelease: true 112 | title: "Development Build" 113 | files: | 114 | docs.tar.gz 115 | dist/* 116 | if: github.ref == 'refs/heads/main' 117 | -------------------------------------------------------------------------------- /tests/test_extism.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import unittest 3 | import extism 4 | import hashlib 5 | import json 6 | import time 7 | from threading import Thread 8 | from datetime import datetime, timedelta 9 | from os.path import join, dirname 10 | import typing 11 | import pickle 12 | 13 | 14 | # A pickle-able object. 15 | class Gribble: 16 | def __init__(self, v): 17 | self.v = v 18 | 19 | def frobbitz(self): 20 | return "gromble %s" % self.v 21 | 22 | 23 | class TestExtism(unittest.TestCase): 24 | def test_call_plugin(self): 25 | plugin = extism.Plugin(self._manifest(), functions=[]) 26 | j = json.loads(plugin.call("count_vowels", "this is a test")) 27 | self.assertEqual(j["count"], 4) 28 | j = json.loads(plugin.call("count_vowels", "this is a test again")) 29 | self.assertEqual(j["count"], 7) 30 | j = json.loads(plugin.call("count_vowels", "this is a test thrice")) 31 | self.assertEqual(j["count"], 6) 32 | j = json.loads(plugin.call("count_vowels", "🌎hello🌎world🌎")) 33 | self.assertEqual(j["count"], 3) 34 | 35 | def test_function_exists(self): 36 | plugin = extism.Plugin(self._manifest(), functions=[]) 37 | self.assertTrue(plugin.function_exists("count_vowels")) 38 | self.assertFalse(plugin.function_exists("i_dont_exist")) 39 | 40 | def test_errors_on_unknown_function(self): 41 | plugin = extism.Plugin(self._manifest()) 42 | self.assertRaises( 43 | extism.Error, lambda: plugin.call("i_dont_exist", "someinput") 44 | ) 45 | 46 | def test_can_free_plugin(self): 47 | plugin = extism.Plugin(self._manifest()) 48 | del plugin 49 | 50 | def test_errors_on_bad_manifest(self): 51 | self.assertRaises( 52 | extism.Error, lambda: extism.Plugin({"invalid_manifest": True}) 53 | ) 54 | 55 | def test_extism_version(self): 56 | self.assertIsNotNone(extism.extism_version()) 57 | 58 | def test_extism_plugin_timeout(self): 59 | plugin = extism.Plugin(self._loop_manifest()) 60 | start = datetime.now() 61 | self.assertRaises(extism.Error, lambda: plugin.call("infinite_loop", b"")) 62 | end = datetime.now() 63 | self.assertLess( 64 | end, 65 | start + timedelta(seconds=1.1), 66 | "plugin timeout exceeded 1000ms expectation", 67 | ) 68 | 69 | def test_extism_host_function(self): 70 | @extism.host_fn( 71 | signature=([extism.ValType.I64], [extism.ValType.I64]), user_data=b"test" 72 | ) 73 | def hello_world(plugin, params, results, user_data): 74 | offs = plugin.alloc(len(user_data)) 75 | mem = plugin.memory(offs) 76 | mem[:] = user_data 77 | results[0].value = offs.offset 78 | 79 | plugin = extism.Plugin( 80 | self._manifest(functions=True), functions=[hello_world], wasi=True 81 | ) 82 | res = plugin.call("count_vowels", "aaa") 83 | self.assertEqual(res, b"test") 84 | 85 | def test_inferred_extism_host_function(self): 86 | @extism.host_fn(user_data=b"test") 87 | def hello_world(inp: str, *user_data) -> str: 88 | return "hello world: %s %s" % (inp, user_data[0].decode("utf-8")) 89 | 90 | plugin = extism.Plugin( 91 | self._manifest(functions=True), functions=[hello_world], wasi=True 92 | ) 93 | res = plugin.call("count_vowels", "aaa") 94 | self.assertEqual(res, b'hello world: {"count": 3} test') 95 | 96 | def test_inferred_json_param_extism_host_function(self): 97 | if not hasattr(typing, "Annotated"): 98 | return 99 | 100 | @extism.host_fn(user_data=b"test") 101 | def hello_world(inp: typing.Annotated[dict, extism.Json], *user_data) -> str: 102 | return "hello world: %s %s" % (inp["count"], user_data[0].decode("utf-8")) 103 | 104 | plugin = extism.Plugin( 105 | self._manifest(functions=True), functions=[hello_world], wasi=True 106 | ) 107 | res = plugin.call("count_vowels", "aaa") 108 | self.assertEqual(res, b"hello world: 3 test") 109 | 110 | def test_codecs(self): 111 | if not hasattr(typing, "Annotated"): 112 | return 113 | 114 | @extism.host_fn(user_data=b"test") 115 | def hello_world( 116 | inp: typing.Annotated[ 117 | str, extism.Codec(lambda xs: xs.decode().replace("o", "u")) 118 | ], 119 | *user_data, 120 | ) -> typing.Annotated[ 121 | str, extism.Codec(lambda xs: xs.replace("u", "a").encode()) 122 | ]: 123 | return inp 124 | 125 | foo = b"bar" 126 | plugin = extism.Plugin( 127 | self._manifest(functions=True), functions=[hello_world], wasi=True 128 | ) 129 | res = plugin.call("count_vowels", "aaa") 130 | # Iiiiiii 131 | self.assertEqual(res, b'{"caant": 3}') # stand it, I know you planned it 132 | 133 | def test_inferred_pickle_return_param_extism_host_function(self): 134 | if not hasattr(typing, "Annotated"): 135 | return 136 | 137 | @extism.host_fn(user_data=b"test") 138 | def hello_world( 139 | inp: typing.Annotated[dict, extism.Json], *user_data 140 | ) -> typing.Annotated[Gribble, extism.Pickle]: 141 | return Gribble("robble") 142 | 143 | plugin = extism.Plugin( 144 | self._manifest(functions=True), functions=[hello_world], wasi=True 145 | ) 146 | res = plugin.call("count_vowels", "aaa") 147 | 148 | result = pickle.loads(res) 149 | self.assertIsInstance(result, Gribble) 150 | self.assertEqual(result.frobbitz(), "gromble robble") 151 | 152 | def test_host_context(self): 153 | if not hasattr(typing, "Annotated"): 154 | return 155 | 156 | # Testing two things here: one, if we see CurrentPlugin as the first arg, we pass it through. 157 | # Two, it's possible to refer to fetch the host context from the current plugin. 158 | @extism.host_fn(user_data=b"test") 159 | def hello_world( 160 | current_plugin: extism.CurrentPlugin, 161 | inp: typing.Annotated[dict, extism.Json], 162 | *user_data, 163 | ) -> typing.Annotated[Gribble, extism.Pickle]: 164 | ctx = current_plugin.host_context() 165 | ctx.x = 1000 166 | return Gribble("robble") 167 | 168 | plugin = extism.Plugin( 169 | self._manifest(functions=True), functions=[hello_world], wasi=True 170 | ) 171 | 172 | class Foo: 173 | x = 100 174 | y = 200 175 | 176 | foo = Foo() 177 | 178 | res = plugin.call("count_vowels", "aaa", host_context=foo) 179 | 180 | self.assertEqual(foo.x, 1000) 181 | self.assertEqual(foo.y, 200) 182 | result = pickle.loads(res) 183 | self.assertIsInstance(result, Gribble) 184 | self.assertEqual(result.frobbitz(), "gromble robble") 185 | 186 | def test_extism_plugin_cancel(self): 187 | plugin = extism.Plugin(self._loop_manifest()) 188 | cancel_handle = plugin.cancel_handle() 189 | 190 | def cancel(handle): 191 | time.sleep(0.5) 192 | handle.cancel() 193 | 194 | Thread(target=cancel, args=[cancel_handle]).run() 195 | self.assertRaises(extism.Error, lambda: plugin.call("infinite_loop", b"")) 196 | 197 | def _manifest(self, functions=False): 198 | wasm = self._count_vowels_wasm(functions) 199 | hash = hashlib.sha256(wasm).hexdigest() 200 | return {"wasm": [{"data": wasm, "hash": hash}]} 201 | 202 | def _loop_manifest(self): 203 | wasm = self._infinite_loop_wasm() 204 | hash = hashlib.sha256(wasm).hexdigest() 205 | return { 206 | "wasm": [{"data": wasm, "hash": hash}], 207 | "timeout_ms": 1000, 208 | } 209 | 210 | def _count_vowels_wasm(self, functions=False): 211 | return read_test_wasm("code.wasm" if not functions else "code-functions.wasm") 212 | 213 | def _infinite_loop_wasm(self): 214 | return read_test_wasm("loop.wasm") 215 | 216 | 217 | def read_test_wasm(p): 218 | path = join(dirname(__file__), "..", "wasm", p) 219 | with open(path, "rb") as wasm_file: 220 | return wasm_file.read() 221 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. currentmodule:: extism 5 | 6 | This guide will walk you through Extism concepts using the Python SDK. 7 | 8 | .. _installation: 9 | 10 | Installation 11 | ------------ 12 | 13 | To use extism, first install it using pip: 14 | 15 | .. code-block:: console 16 | 17 | (.venv) $ pip install extism 18 | 19 | .. _getting_started: 20 | 21 | Getting Started 22 | --------------- 23 | 24 | To start, let's use a pre-existing WebAssembly module. We'll define a `Manifest 25 | `_ that pulls a pre-compiled 26 | Wasm module down from the web. In this case, we'll start with a vowel counting 27 | module: 28 | 29 | .. sourcecode:: python 30 | :linenos: 31 | 32 | import extism 33 | import json 34 | 35 | manifest = { 36 | "wasm": [ 37 | { 38 | "url": "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm", 39 | } 40 | ] 41 | } 42 | with extism.Plugin(manifest, wasi=True) as plugin: 43 | ... 44 | 45 | Let's call a Wasm function from the module. Our example module exports a single 46 | guest function, ``count_vowels``: 47 | 48 | .. sourcecode:: python 49 | :linenos: 50 | :lineno-start: 15 51 | 52 | with extism.Plugin(manifest, wasi=True) as plugin: 53 | wasm_vowel_count = plugin.call( 54 | "count_vowels", 55 | "hello world", 56 | ) 57 | print(wasm_vowel_count) # b'{"count": 3, "total": 3, "vowels": "aeiouAEIOU"}' 58 | 59 | All Extism guest functions have a simple "bytes-in", "bytes-out" interface. In this case, 60 | the input is a utf-8 encoded string, and the output is a utf-8 encoded string containing 61 | JSON. We can pass a ``parse`` parameter to ``call`` to automatically decode the output for 62 | us: 63 | 64 | .. sourcecode:: python 65 | :linenos: 66 | :lineno-start: 15 67 | 68 | with extism.Plugin(manifest, wasi=True) as plugin: 69 | 70 | wasm_vowel_count = plugin.call( 71 | "count_vowels", 72 | "hello world", 73 | parse = lambda output: json.loads( 74 | bytes(output).decode('utf-8') 75 | ) 76 | ) 77 | print(wasm_vowel_count) # {'count': 3, 'total': 3, 'vowels': 'aeiouAEIOU'} 78 | 79 | 80 | State and Configuration 81 | ~~~~~~~~~~~~~~~~~~~~~~~ 82 | 83 | Plugins may keep track of their own state: 84 | 85 | .. sourcecode:: python 86 | :linenos: 87 | :lineno-start: 15 88 | 89 | with extism.Plugin(manifest, wasi=True) as plugin: 90 | [print(plugin.call( 91 | "count_vowels", 92 | "hello world", 93 | parse = lambda output: json.loads( 94 | bytes(output).decode('utf-8') 95 | ) 96 | )['total']) for _ in range(0, 3)] # prints 3, 6, 9 97 | 98 | # if we re-instantiate the plugin, however, we reset the count: 99 | with extism.Plugin(manifest, wasi=True) as plugin: 100 | [print(plugin.call( 101 | "count_vowels", 102 | "hello world", 103 | parse = lambda output: json.loads( 104 | bytes(output).decode('utf-8') 105 | ) 106 | )['total']) for _ in range(0, 3)] # prints 3, 6, 9 all over again 107 | 108 | We can also statically configure values for use by the guest: 109 | 110 | .. sourcecode:: python 111 | :linenos: 112 | :lineno-start: 15 113 | 114 | with extism.Plugin(manifest, wasi=True, config = {'vowels': 'h'}) as plugin: 115 | print(plugin.call( 116 | "count_vowels", 117 | "hello world", 118 | parse = lambda output: json.loads( 119 | bytes(output).decode('utf-8') 120 | ) 121 | )['count']) # 1 122 | 123 | 124 | 125 | This example: 126 | 127 | 1. Downloads Wasm from the web, 128 | 2. Verifies that the Wasm matches a hash before running it, 129 | 3. And executes a function exported from that module from Python. 130 | 131 | Host Functions 132 | -------------- 133 | 134 | What if you want your guest plugin to communicate with your host? **Extism has 135 | your back.** 136 | 137 | Let's take a look at a slightly more advanced example. This time, we're going 138 | to call a guest function in a *locally* defined module in WebAssembly Text 139 | Format (AKA "WAT" format), that *guest* function is going to call a *host* function, 140 | and then the *guest* function will return the result. 141 | 142 | Lets take a look at a new example. 143 | 144 | .. sourcecode:: python 145 | :linenos: 146 | :caption: ``host.py`` 147 | 148 | from extism import host_fn, Plugin 149 | 150 | # Our host function. Note the type annotations -- these allow extism to 151 | # automatically generate Wasm type bindings for the function! 152 | @host_fn() 153 | def hello_world(input: str) -> str: 154 | print(input) 155 | return "Hello from Python!" 156 | 157 | with Plugin(open("guest.wat", "rb").read()) as plugin: 158 | plugin.call("my_func", b"") 159 | 160 | We've defined a host function that takes a string and returns a string. If 161 | you're interested in the guest plugin code, check it out below. 162 | 163 | The :func:`.extism.host_fn` decorator registers our Python function with 164 | ``extism``, inferring the name we want to use from the function name. Because 165 | ``hello_world`` has type annotations, :func:`.extism.host_fn` automatically 166 | created appropriate low-level Wasm bindings for us. We can take this a step 167 | further, though: if we expect to be called with JSON data, we can indicate 168 | as much: 169 | 170 | .. sourcecode:: python 171 | :linenos: 172 | :caption: ``host.py`` 173 | 174 | from extism import host_fn, Plugin, Json 175 | from typing import Annotated 176 | 177 | # Our host function. Note the type annotations -- these allow extism to 178 | # automatically generate Wasm type bindings for the function! 179 | @host_fn() 180 | def hello_world(input: Annotated[dict, Json]) -> str: 181 | print(input['message']) 182 | return "Hello from Python!" 183 | 184 | with Plugin(open("guest.wat", "rb").read()) as plugin: 185 | plugin.call("my_func", b"") 186 | 187 | .. _guest_code: 188 | 189 | Guest Code 190 | ~~~~~~~~~~ 191 | 192 | .. tip:: If the WebAssembly here looks intimidating, check out Extism Plugin Dev Kits 193 | (PDKs), which allow you to write code in your language of choice. 194 | 195 | .. sourcecode:: lisp 196 | :linenos: 197 | :caption: guest.wat 198 | 199 | (module 200 | (; code between winking-emoji parenthesis is a WAT comment! ;) 201 | 202 | (import "extism:host/user" "hello_world" (func $hello (param i64) (result i64))) 203 | (import "extism:host/env" "alloc" (func $extism_alloc (param i64) (result i64))) 204 | (import "extism:host/env" "store_u8" (func $extism_store_u8 (param i64 i32))) 205 | (import "extism:host/env" "output_set" (func $extism_output_set (param i64 i64))) 206 | (import "extism:host/env" "length" (func $extism_length (param i64) (result i64))) 207 | 208 | (; store a string to send to the host. ;) 209 | (memory $memory (export "mem") 210 | (data "{\"message\": \"Hello from WAT!\"}\00") 211 | ) 212 | (func $my_func (result i32) 213 | (local $result i64) 214 | (local $offset i64) 215 | (local $i i32) 216 | (local.set $offset (call $extism_alloc (i64.const 31))) 217 | 218 | (; 219 | You can read this as: 220 | 221 | for(i=0; memory[i] != 0; ++i) { 222 | extism_store_u8(offset + i, memory[i]) 223 | } 224 | 225 | We're copying our local string into extism memory to transport it to the host. 226 | ;) 227 | (block $end 228 | (loop $loop 229 | (br_if $end (i32.eq (i32.const 0) (i32.load8_u (local.get $i)))) 230 | 231 | (call $extism_store_u8 232 | (i64.add 233 | (local.get $offset) 234 | (i64.extend_i32_u (local.get $i)) 235 | ) 236 | (i32.load8_u (local.get $i)) 237 | ) 238 | 239 | (local.set $i (i32.add (i32.const 1) (local.get $i))) 240 | br $loop 241 | ) 242 | ) 243 | 244 | (; call the host and store the resulting offset into extism memory in a local variable. ;) 245 | (local.set $result (call $hello (local.get $offset))) 246 | 247 | (; 248 | now tell extism we want to use that offset as our output memory. extism tracks the extent 249 | of that memory, so we can call extism_length() to get that data. 250 | ;) 251 | (call $extism_output_set 252 | (local.get $result) 253 | (call $extism_length (local.get $result)) 254 | ) 255 | 256 | (i32.const 0) 257 | ) 258 | (export "my_func" (func $my_func)) 259 | ) 260 | -------------------------------------------------------------------------------- /extism/extism.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from base64 import b64encode 4 | import typing 5 | from typing import ( 6 | get_args, 7 | get_origin, 8 | get_type_hints, 9 | Any, 10 | Callable, 11 | List, 12 | Union, 13 | Literal, 14 | Optional, 15 | Tuple, 16 | ) 17 | from enum import Enum 18 | from uuid import UUID 19 | from extism_sys import lib as _lib, ffi as _ffi # type: ignore 20 | import functools 21 | import pickle 22 | 23 | 24 | HOST_FN_REGISTRY: List[Any] = [] 25 | 26 | 27 | class Json: 28 | """ 29 | Typing metadata: indicates that an extism host function parameter (or return value) 30 | should be encoded (or decoded) using ``json``. 31 | 32 | .. sourcecode:: python 33 | 34 | @extism.host_fn() 35 | def load(input: typing.Annotated[dict, extism.Json]): 36 | # input will be a dictionary decoded from json input. 37 | input.get("hello", None) 38 | 39 | @extism.host_fn() 40 | def load(input: int) -> typing.Annotated[dict, extism.Json]: 41 | return { 42 | 'hello': 3 43 | } 44 | 45 | """ 46 | 47 | ... 48 | 49 | 50 | class Pickle: 51 | """ 52 | Typing metadata: indicates that an extism host function parameter (or return value) 53 | should be encoded (or decoded) using ``pickle``. 54 | 55 | .. sourcecode:: python 56 | 57 | class Grimace: 58 | ... 59 | 60 | @extism.host_fn() 61 | def load(input: typing.Annotated[Grimace, extism.Pickle]): 62 | # input will be an instance of Grimace! 63 | ... 64 | 65 | @extism.host_fn() 66 | def load(input: int) -> typing.Annotated[Grimace, extism.Pickle]: 67 | return Grimace() 68 | 69 | """ 70 | 71 | ... 72 | 73 | 74 | class Codec: 75 | """ 76 | Typing metadata: indicates that an extism host function parameter (or return value) 77 | should be transformed with the provided function. 78 | 79 | .. sourcecode:: python 80 | 81 | import json 82 | 83 | @extism.host_fn() 84 | def load(input: typing.Annotated[str, extism.Codec(lambda inp: inp.decode(encoding = 'shift_jis'))]): 85 | # you can accept shift-jis bytes as input! 86 | ... 87 | 88 | mojibake_factory = lambda out: out.encode(encoding='utf8').decode(encoding='latin1').encode() 89 | 90 | @extism.host_fn() 91 | def load(input: int) -> typing.Annotated[str, extism.Codec(mojibake_factory)]: 92 | return "get ready for some mojibake 🎉" 93 | """ 94 | 95 | def __init__(self, codec): 96 | self.codec = codec 97 | 98 | 99 | class Error(Exception): 100 | """ 101 | A subclass of :py:class:`Exception ` representing a failed guest function call. 102 | Contains one argument: the error output. 103 | """ 104 | 105 | ... 106 | 107 | 108 | class ValType(Enum): 109 | """ 110 | An enumeration of all available `Wasm value types `. 111 | 112 | `PTR` is an alias for `I64` to make typing a little less confusing when writing host function definitions 113 | """ 114 | 115 | I32 = 0 116 | PTR = 1 117 | I64 = 1 118 | F32 = 2 119 | F64 = 3 120 | V128 = 4 121 | FUNC_REF = 5 122 | EXTERN_REF = 6 123 | 124 | 125 | class Val: 126 | """ 127 | Low-level WebAssembly value with associated :py:class:`ValType`. 128 | """ 129 | 130 | def __init__(self, t: ValType, v): 131 | self.t = t 132 | self.value = v 133 | 134 | def __repr__(self): 135 | return f"Val({self.t}, {self.value})" 136 | 137 | def _assign(self, v): 138 | self.value = v 139 | 140 | 141 | class _Base64Encoder(json.JSONEncoder): 142 | # pylint: disable=method-hidden 143 | def default(self, o): 144 | if isinstance(o, bytes): 145 | return b64encode(o).decode() 146 | return json.JSONEncoder.default(self, o) 147 | 148 | 149 | def set_log_file( 150 | file: str, level: Optional[Literal["debug", "error", "trace", "warn"]] = None 151 | ): 152 | """ 153 | Sets the log file and level, this is a global configuration 154 | 155 | :param file: The path to the logfile 156 | :param level: The debug level. 157 | """ 158 | c_level = level or _ffi.NULL 159 | if isinstance(level, str): 160 | c_level = level.encode() 161 | _lib.extism_log_file(file.encode(), c_level) 162 | 163 | 164 | class CustomLogger: 165 | def __init__(self, f): 166 | self.callback = None 167 | self.set_callback(f) 168 | 169 | def set_callback(self, f): 170 | @_ffi.callback("void(char*, ExtismSize)") 171 | def callback(ptr, len): 172 | f(_ffi.string(ptr, len).decode()) 173 | 174 | self.callback = callback 175 | 176 | def drain(self): 177 | if self.callback is not None: 178 | _lib.extism_log_drain(self.callback) 179 | 180 | def __del__(self): 181 | self.drain() 182 | 183 | 184 | def set_log_custom( 185 | f, level: Optional[Literal["debug", "error", "trace", "warn"]] = None 186 | ): 187 | """ 188 | Enables buffered logging, this is a global configuration 189 | 190 | :param f: The callback function, takes a string argument and no return value. 191 | :param level: The debug level. 192 | 193 | :returns: a CustomLogger with a `drain` method that can be used to handle the buffered logs. 194 | """ 195 | c_level = level or _ffi.NULL 196 | if isinstance(level, str): 197 | c_level = level.encode() 198 | 199 | _lib.extism_log_custom(c_level) 200 | return CustomLogger(f) 201 | 202 | 203 | def extism_version() -> str: 204 | """ 205 | Gets the Extism version string 206 | 207 | :returns: The Extism runtime version string 208 | """ 209 | return _ffi.string(_lib.extism_version()).decode() 210 | 211 | 212 | def _wasm(plugin): 213 | if isinstance(plugin, str) and os.path.exists(plugin): 214 | with open(plugin, "rb") as f: 215 | wasm = f.read() 216 | elif isinstance(plugin, str): 217 | wasm = plugin.encode() 218 | elif isinstance(plugin, dict): 219 | wasm = json.dumps(plugin, cls=_Base64Encoder).encode() 220 | else: 221 | wasm = plugin 222 | return wasm 223 | 224 | 225 | class Memory: 226 | """ 227 | A reference to plugin memory. 228 | """ 229 | 230 | def __init__(self, offs, length): 231 | self.offset = offs 232 | self.length = length 233 | 234 | def __len__(self): 235 | return self.length 236 | 237 | 238 | class Function: 239 | """ 240 | A host function. 241 | """ 242 | 243 | def __init__( 244 | self, namespace: Optional[str], name: str, args, returns, f, *user_data 245 | ): 246 | self.namespace = namespace 247 | self.name = name 248 | self.args = [a.value for a in args] 249 | self.returns = [r.value for r in returns] 250 | if len(user_data) > 0: 251 | self.user_data = _ffi.new_handle(user_data) 252 | else: 253 | self.user_data = _ffi.NULL 254 | self.f = f 255 | 256 | 257 | class _ExtismFunctionMetadata: 258 | def __init__(self, f: Function): 259 | self.pointer = _lib.extism_function_new( 260 | f.name.encode(), 261 | f.args, 262 | len(f.args), 263 | f.returns, 264 | len(f.returns), 265 | f.f, 266 | f.user_data, 267 | _ffi.NULL, 268 | ) 269 | if f.namespace is not None: 270 | _lib.extism_function_set_namespace(self.pointer, f.namespace.encode()) 271 | 272 | def __del__(self): 273 | if not hasattr(self, "pointer"): 274 | return 275 | if self.pointer is not None: 276 | _lib.extism_function_free(self.pointer) 277 | 278 | 279 | def _map_arg(arg_name, xs) -> Tuple[ValType, Callable[[Any, Any], Any]]: 280 | if xs == str: 281 | return (ValType.I64, lambda plugin, slot: plugin.input_string(slot)) 282 | 283 | if xs == bytes: 284 | return (ValType.I64, lambda plugin, slot: plugin.input_bytes(slot)) 285 | 286 | if xs == int: 287 | return (ValType.I64, lambda _, slot: slot.value) 288 | 289 | if xs == float: 290 | return (ValType.F64, lambda _, slot: slot.value) 291 | 292 | if xs == bool: 293 | return (ValType.I32, lambda _, slot: slot.value) 294 | 295 | metadata = getattr(xs, "__metadata__", ()) 296 | for item in metadata: 297 | if item == Json: 298 | return ( 299 | ValType.I64, 300 | lambda plugin, slot: json.loads(plugin.input_string(slot)), 301 | ) 302 | 303 | if item == Pickle: 304 | return ( 305 | ValType.I64, 306 | lambda plugin, slot: pickle.loads(plugin.input_bytes(slot)), 307 | ) 308 | 309 | if isinstance(item, Codec): 310 | return ( 311 | ValType.I64, 312 | lambda plugin, slot: item.codec(plugin.input_bytes(slot)), 313 | ) 314 | 315 | raise TypeError("Could not infer input type for argument %s" % arg_name) 316 | 317 | 318 | def _map_ret(xs) -> List[Tuple[ValType, Callable[[Any, Any, Any], Any]]]: 319 | if xs == str: 320 | return [ 321 | (ValType.I64, lambda plugin, slot, value: plugin.return_string(slot, value)) 322 | ] 323 | 324 | if xs == bytes: 325 | return [ 326 | (ValType.I64, lambda plugin, slot, value: plugin.return_bytes(slot, value)) 327 | ] 328 | 329 | if xs == int: 330 | return [ 331 | ( 332 | ValType.I64, 333 | lambda plugin, slot, value: plugin.return_bytes( 334 | slot, value.to_bytes(length=8, byteorder="little") 335 | ), 336 | ) 337 | ] 338 | 339 | if xs == float: 340 | return [ 341 | ( 342 | ValType.F64, 343 | lambda plugin, slot, value: plugin.return_bytes( 344 | slot, value.to_bytes(length=8, byteorder="little") 345 | ), 346 | ) 347 | ] 348 | 349 | if xs == bool: 350 | return [ 351 | ( 352 | ValType.I32, 353 | lambda plugin, slot, value: plugin.return_bytes( 354 | slot, value.to_bytes(length=4, byteorder="little") 355 | ), 356 | ) 357 | ] 358 | 359 | if get_origin(xs) == tuple: 360 | return functools.reduce(lambda lhs, rhs: lhs + _map_ret(rhs), get_args(xs), []) 361 | 362 | metadata = getattr(xs, "__metadata__", ()) 363 | for item in metadata: 364 | if item == Json: 365 | return [ 366 | ( 367 | ValType.I64, 368 | lambda plugin, slot, value: plugin.return_string( 369 | slot, json.dumps(value) 370 | ), 371 | ) 372 | ] 373 | 374 | if item == Pickle: 375 | return [ 376 | ( 377 | ValType.I64, 378 | lambda plugin, slot, value: plugin.return_bytes( 379 | slot, pickle.dumps(value) 380 | ), 381 | ) 382 | ] 383 | 384 | if isinstance(item, Codec): 385 | return [ 386 | ( 387 | ValType.I64, 388 | lambda plugin, slot, value: plugin.return_bytes( 389 | slot, item.codec(value) 390 | ), 391 | ) 392 | ] 393 | 394 | raise TypeError("Could not infer return type") 395 | 396 | 397 | class ExplicitFunction(Function): 398 | def __init__(self, namespace, name, args, returns, func, user_data): 399 | self.func = func 400 | 401 | super().__init__(namespace, name, args, returns, handle_args, *user_data) 402 | 403 | functools.update_wrapper(self, func) 404 | 405 | def __call__(self, *args, **kwargs): 406 | return self.func(*args, **kwargs) 407 | 408 | 409 | class TypeInferredFunction(ExplicitFunction): 410 | def __init__(self, namespace, name, func, user_data): 411 | kwargs: dict[str, Any] = {} 412 | if hasattr(typing, "Annotated"): 413 | kwargs["include_extras"] = True 414 | 415 | hints = get_type_hints(func, **kwargs) 416 | if len(hints) == 0: 417 | raise TypeError( 418 | "Host function must include Python type annotations or explicitly list arguments." 419 | ) 420 | 421 | arg_names = [arg for arg in hints.keys() if arg != "return"] 422 | returns = hints.pop("return", None) 423 | 424 | uses_current_plugin = False 425 | if len(arg_names) > 0 and hints.get(arg_names[0], None) == CurrentPlugin: 426 | uses_current_plugin = True 427 | arg_names = arg_names[1:] 428 | 429 | args = [_map_arg(arg, hints[arg]) for arg in arg_names] 430 | 431 | returns = [] if returns is None else _map_ret(returns) 432 | 433 | def inner_func(plugin, inputs, outputs, *user_data): 434 | first_arg = [plugin] if uses_current_plugin else [] 435 | inner_args = first_arg + [ 436 | extract(plugin, slot) for ((_, extract), slot) in zip(args, inputs) 437 | ] 438 | 439 | if user_data is not None: 440 | inner_args += list(user_data) 441 | 442 | result = func(*inner_args) 443 | for (_, emplace), slot in zip(returns, outputs): 444 | emplace(plugin, slot, result) 445 | 446 | super().__init__( 447 | namespace, 448 | name, 449 | [typ for (typ, _) in args], 450 | [typ for (typ, _) in returns], 451 | inner_func, 452 | user_data, 453 | ) 454 | 455 | 456 | class CancelHandle: 457 | def __init__(self, ptr): 458 | self.pointer = ptr 459 | 460 | def cancel(self) -> bool: 461 | return _lib.extism_plugin_cancel(self.pointer) 462 | 463 | 464 | class CompiledPlugin: 465 | """ 466 | A ``CompiledPlugin`` represents a parsed Wasm module and associated Extism 467 | kernel. It can be used to rapidly instantiate plugin instances. A ``Plugin`` 468 | instantiated with a ``CompiledPlugin`` inherits the functions and WASI settings 469 | of the compiled plugin. 470 | 471 | .. sourcecode:: python 472 | 473 | import extism 474 | 475 | compiled = extism.CompiledPlugin({ 476 | wasm: [ 477 | { 'url': 'https://example.com/path/to/module.wasm' } 478 | ], 479 | }) 480 | 481 | plugin = extism.Plugin(compiled) 482 | plugin.call("example-function") 483 | """ 484 | 485 | def __init__( 486 | self, 487 | plugin: Union[str, bytes, dict], 488 | wasi: bool = False, 489 | functions: Optional[List[Function]] = HOST_FN_REGISTRY, 490 | ): 491 | wasm = _wasm(plugin) 492 | self.functions = functions 493 | as_extism_functions = [_ExtismFunctionMetadata(f) for f in functions or []] 494 | as_ptrs = [f.pointer for f in as_extism_functions] 495 | # Register plugin 496 | errmsg = _ffi.new("char**") 497 | function_ptrs = ( 498 | _ffi.NULL if len(as_ptrs) == 0 else _ffi.new("ExtismFunction*[]", as_ptrs) 499 | ) 500 | self.pointer = _lib.extism_compiled_plugin_new( 501 | wasm, len(wasm), function_ptrs, len(as_ptrs), wasi, errmsg 502 | ) 503 | 504 | if self.pointer == _ffi.NULL: 505 | msg = _ffi.string(errmsg[0]) 506 | _lib.extism_plugin_new_error_free(errmsg[0]) 507 | raise Error(msg.decode()) 508 | 509 | def __del__(self): 510 | if not hasattr(self, "pointer"): 511 | return 512 | _lib.extism_compiled_plugin_free(self.pointer) 513 | self.pointer = -1 514 | 515 | 516 | class Plugin: 517 | """ 518 | Plugins are used to call WASM functions. Plugins can kept in a context for 519 | as long as you need. They may be freed with the ``del`` keyword. 520 | 521 | .. sourcecode:: python 522 | 523 | import extism 524 | 525 | with extism.Plugin({ 526 | # all three of these wasm modules will be instantiated and their exported functions 527 | # made available to the `plugin` variable below. 528 | wasm: [ 529 | { 'url': 'https://example.com/path/to/module.wasm' } 530 | { 'data': b'\\xde\\xad\\xbe\\xef' } # byte content representing a wasm module 531 | { 'url': 'https://example.com/path/to/module.wasm', hash: 'cafebeef' } # check that the downloaded module matches a specified sha256 hash. 532 | ], 533 | }) as plugin: 534 | plugin.call("example-function") 535 | 536 | :param plugin: Plugin data, passed as bytes representing a single WebAssembly module, 537 | a string representing a serialized JSON `Manifest `_, or 538 | a dict respresenting a deserialized `Manifest `_. 539 | :param wasi: Indicates whether WASI preview 1 support will be enabled for this plugin. 540 | :param config: An optional JSON-serializable object holding a map of configuration keys 541 | and values. 542 | :param functions: An optional list of host :py:class:`functions <.extism.Function>` to 543 | expose to the guest program. Defaults to all registered ``@host_fn()``'s 544 | if not given. 545 | """ 546 | 547 | def __init__( 548 | self, 549 | plugin: Union[str, bytes, CompiledPlugin, dict], 550 | wasi: bool = False, 551 | config: Optional[Any] = None, 552 | functions: Optional[List[Function]] = HOST_FN_REGISTRY, 553 | ): 554 | if not isinstance(plugin, CompiledPlugin): 555 | plugin = CompiledPlugin(plugin, wasi, functions) 556 | 557 | self.compiled_plugin = plugin 558 | errmsg = _ffi.new("char**") 559 | 560 | self.plugin = _lib.extism_plugin_new_from_compiled( 561 | self.compiled_plugin.pointer, errmsg 562 | ) 563 | if self.plugin == _ffi.NULL: 564 | msg = _ffi.string(errmsg[0]) 565 | _lib.extism_plugin_new_error_free(errmsg[0]) 566 | raise Error(msg.decode()) 567 | 568 | if config is not None: 569 | s = json.dumps(config).encode() 570 | _lib.extism_plugin_config(self.plugin, s, len(s)) 571 | 572 | @property 573 | def id(self) -> UUID: 574 | b = bytes(_ffi.unpack(_lib.extism_plugin_id(self.plugin), 16)) 575 | return UUID(bytes=b) 576 | 577 | def cancel_handle(self): 578 | return CancelHandle(_lib.extism_plugin_cancel_handle(self.plugin)) 579 | 580 | def _check_error(self, rc): 581 | error = _lib.extism_plugin_error(self.plugin) 582 | if error != _ffi.NULL: 583 | raise Error(_ffi.string(error).decode()) 584 | if rc != 0: 585 | raise Error(f"Error code: {rc}") 586 | 587 | def function_exists(self, name: str) -> bool: 588 | """ 589 | Given a function name, check whether the name is exported from this function. 590 | 591 | :param name: The function name to query. 592 | """ 593 | return _lib.extism_plugin_function_exists(self.plugin, name.encode()) 594 | 595 | def call( 596 | self, 597 | function_name: str, 598 | data: Union[str, bytes], 599 | parse: Callable[[Any], Any] = lambda xs: bytes(xs), 600 | host_context: Any = None, 601 | ) -> Any: 602 | """ 603 | Call a function by name with the provided input data 604 | 605 | :param function_name: The name of the function to invoke 606 | :param data: The input data to the function. 607 | :param parse: The function used to parse the buffer returned by the guest function call. Defaults to :py:class:`bytes `. 608 | :raises: An :py:class:`extism.Error <.extism.Error>` if the guest function call was unsuccessful. 609 | :returns: The returned bytes from the guest function as interpreted by the ``parse`` parameter. 610 | """ 611 | 612 | host_context = _ffi.new_handle(host_context) 613 | if isinstance(data, str): 614 | data = data.encode() 615 | self._check_error( 616 | _lib.extism_plugin_call_with_host_context( 617 | self.plugin, function_name.encode(), data, len(data), host_context 618 | ) 619 | ) 620 | out_len = _lib.extism_plugin_output_length(self.plugin) 621 | out_buf = _lib.extism_plugin_output_data(self.plugin) 622 | buf = _ffi.buffer(out_buf, out_len) 623 | if parse is None: 624 | return buf 625 | return parse(buf) 626 | 627 | def __del__(self): 628 | if not hasattr(self, "pointer"): 629 | return 630 | _lib.extism_plugin_free(self.plugin) 631 | self.plugin = -1 632 | 633 | def __enter__(self): 634 | return self 635 | 636 | def __exit__(self, type, exc, traceback): 637 | self.__del__() 638 | 639 | 640 | def _convert_value(x): 641 | if x.t == 0: 642 | return Val(ValType.I32, x.v.i32) 643 | elif x.t == 1: 644 | return Val(ValType.I64, x.v.i64) 645 | elif x.t == 2: 646 | return Val(ValType.F32, x.v.f32) 647 | elif x.y == 3: 648 | return Val(ValType.F64, x.v.f64) 649 | return None 650 | 651 | 652 | def _convert_output(x, v): 653 | if v.t.value != x.t: 654 | raise Error(f"Output type mismatch, got {v.t} but expected {x.t}") 655 | 656 | if v.t == ValType.I32: 657 | x.v.i32 = int(v.value) 658 | elif v.t == ValType.I64: 659 | x.v.i64 = int(v.value) 660 | elif x.t == ValType.F32: 661 | x.v.f32 = float(v.value) 662 | elif x.t == ValType.F64: 663 | x.v.f64 = float(v.value) 664 | else: 665 | raise Error("Unsupported return type: " + str(x.t)) 666 | 667 | 668 | class CurrentPlugin: 669 | """ 670 | This object is accessible when calling from the guest :py:class:`Plugin` into the host via 671 | a host :py:class:`Function`. It provides plugin memory access to host functions. 672 | """ 673 | 674 | def __init__(self, p): 675 | self.pointer = p 676 | 677 | def memory(self, mem: Memory) -> _ffi.buffer: 678 | """ 679 | Given a reference to plugin memory, return an FFI buffer 680 | 681 | :param mem: A memory reference to be accessed 682 | """ 683 | p = _lib.extism_current_plugin_memory(self.pointer) 684 | if p == 0: 685 | return None 686 | return _ffi.buffer(p + mem.offset, mem.length) 687 | 688 | def host_context(self) -> Any: 689 | result = _lib.extism_current_plugin_host_context(self.pointer) 690 | if result == 0: 691 | return None 692 | return _ffi.from_handle(result) 693 | 694 | def alloc(self, size: int) -> Memory: 695 | """ 696 | Allocate a new block of memory. 697 | 698 | :param size: The number of bytes to allocate. Must be a positive integer. 699 | """ 700 | offs = _lib.extism_current_plugin_memory_alloc(self.pointer, size) 701 | return Memory(offs, size) 702 | 703 | def free(self, mem: Memory): 704 | """Free a block of memory by :py:class:`Memory <.Memory>` reference""" 705 | return _lib.extism_current_plugin_memory_free(self.pointer, mem.offset) 706 | 707 | def memory_at_offset(self, offs: Union[int, Val]) -> Memory: 708 | """Given a memory offset, return the corresponding a :py:class:`Memory <.Memory>` reference.""" 709 | if isinstance(offs, Val): 710 | offs = offs.value 711 | len = _lib.extism_current_plugin_memory_length(self.pointer, offs) 712 | return Memory(offs, len) 713 | 714 | def return_bytes(self, output: Val, b: bytes): 715 | """ 716 | A shortcut for returning :py:class:`bytes` from a host function. 717 | 718 | :param output: This argument will be **mutated** and made to hold the memory address of ``b``. 719 | :param b: The bytes to return. 720 | """ 721 | mem = self.alloc(len(b)) 722 | self.memory(mem)[:] = b 723 | output.value = mem.offset 724 | 725 | def return_string(self, output: Val, s: str): 726 | """ 727 | A shortcut for returning :py:class:`str` from a host function. 728 | 729 | :param output: This argument will be **mutated** and made to hold the memory address of ``s``. 730 | :param s: The string to return. 731 | """ 732 | self.return_bytes(output, s.encode()) 733 | 734 | def input_buffer(self, input: Val): 735 | mem = self.memory_at_offset(input) 736 | return self.memory(mem) 737 | 738 | def input_bytes(self, input: Val) -> bytes: 739 | """ 740 | A shortcut for accessing :py:class:`bytes` passed at the :py:class:`input <.Val>` parameter. 741 | 742 | :param input: The input value that references bytes. 743 | """ 744 | return self.input_buffer(input)[:] 745 | 746 | def input_string(self, input: Val) -> str: 747 | """ 748 | A shortcut for accessing :py:class:`str` passed at the :py:class:`input <.Val>` parameter. 749 | 750 | .. sourcecode:: python 751 | 752 | @extism.host_fn(signature=([extism.ValType.PTR], [])) 753 | def hello_world(plugin, params, results): 754 | my_str = plugin.input_string(params[0]) 755 | print(my_str) 756 | 757 | # assume the following wasm file exists: 758 | # example.wat = \""" 759 | # (module 760 | # (import "example" "hello_world" (func $hello (param i64))) 761 | # (import "extism:host/env" "alloc" (func $extism_alloc (param i64) (result i64))) 762 | # (import "extism:host/env" "store_u8" (func $extism_store_u8 (;6;) (param i64 i32))) 763 | # 764 | # (memory $memory (export "mem") 765 | # (data "Hello from WAT!\00") 766 | # ) 767 | # (func $my_func (result i64) 768 | # 769 | # ;; allocate extism memory and copy our message into it 770 | # (local $offset i64) (local $i i32) 771 | # (local.set $offset (call $extism_alloc (i64.const 15))) 772 | # (block $end 773 | # (loop $loop 774 | # (br_if $end (i32.eq (i32.const 0) (i32.load8_u (local.get $i)))) 775 | # (call $extism_store_u8 (i64.add (local.get $offset) (i64.extend_i32_u (local.get $i))) (i32.load8_u (local.get $i))) 776 | # (local.set $i (i32.add (i32.const 1) (local.get $i))) 777 | # br $loop 778 | # ) 779 | # ) 780 | # 781 | # ;; call our hello_world function with our extism memory offset. 782 | # local.get $offset 783 | # call $hello 784 | # i64.const 0 785 | # ) 786 | # (export "my_func" (func $my_func)) 787 | # ) 788 | # \""" 789 | # ... and we've compiled it using "wasm-tools parse example.wat -o example.wasm" 790 | with open("example.wasm", "rb") as wasm_file: 791 | data = wasm_file.read() 792 | 793 | with extism.Plugin(data, functions=[hello_world]) as plugin: 794 | plugin.call("my_func", "") 795 | 796 | :param input: The input value that references a string. 797 | """ 798 | return self.input_bytes(input).decode() 799 | 800 | 801 | def host_fn( 802 | name: Optional[str] = None, 803 | namespace: Optional[str] = None, 804 | signature: Optional[Tuple[List[ValType], List[ValType]]] = None, 805 | user_data: Optional[Union[bytes, List[bytes]]] = None, 806 | ): 807 | """ 808 | A decorator for creating host functions. Host functions are installed into a thread-local 809 | registry. 810 | 811 | :param name: The function name to expose to the guest plugin. If not given, inferred from the 812 | wrapped function name. 813 | :param namespace: The namespace to install the function into; defaults to "extism:host/user" if not given. 814 | :param signature: A tuple of two arrays representing the function parameter types and return value types. 815 | If not given, types will be inferred from ``typing`` annotations. 816 | :param userdata: Any custom userdata to associate with the function. 817 | 818 | Supported Inferred Types 819 | ------------------------ 820 | 821 | - ``typing.Annotated[Any, extism.Json]``: In both parameter and return 822 | positions. Written to extism memory; offset encoded in return value as 823 | ``I64``. 824 | - ``typing.Annotated[Any, extism.Pickle]``: In both parameter and return 825 | positions. Written to extism memory; offset encoded in return value as 826 | ``I64``. 827 | - ``str``, ``bytes``: In both parameter and return 828 | positions. Written to extism memory; offset encoded in return value as 829 | ``I64``. 830 | - ``int``: In both parameter and return positions. Encoded as ``I64``. 831 | - ``float``: In both parameter and return positions. Encoded as ``F64``. 832 | - ``bool``: In both parameter and return positions. Encoded as ``I32``. 833 | - ``typing.Tuple[]``: In return position; expands 834 | return list to include all member type encodings. 835 | 836 | .. sourcecode:: python 837 | 838 | import typing 839 | import extism 840 | 841 | @extism.host_fn() 842 | def greet(who: str) -> str: 843 | return "hello %s" % who 844 | 845 | @extism.host_fn() 846 | def load(input: typing.Annotated[dict, extism.Json]) -> typing.Tuple[int, int]: 847 | # input will be a dictionary decoded from json input. The tuple will be returned 848 | # two I64 values. 849 | return (3, 4) 850 | 851 | @extism.host_fn() 852 | def return_many_encoded() -> typing.Tuple(int, typing.Annotated[dict, extism.Json]): 853 | # we auto-encoded any Json-annotated return values, even in a tuple 854 | return (32, {"hello": "world"}) 855 | 856 | class Gromble: 857 | ... 858 | 859 | @extism.host_fn() 860 | def everyone_loves_a_pickle(grumble: typing.Annotated[Gromble, extism.Pickle]) -> typing.Annotated[Gromble, extism.Pickle]: 861 | # you can pass pickled objects in and out of host funcs 862 | return Gromble() 863 | 864 | @extism.host_fn(signature=([extism.ValType.PTR], [])) 865 | def more_control( 866 | current_plugin: extism.CurrentPlugin, 867 | params: typing.List[extism.Val], 868 | results: typing.List[extism.Val], 869 | *user_data 870 | ): 871 | # if you need more control, you can specify the wasm-level input 872 | # and output types explicitly. 873 | ... 874 | 875 | """ 876 | if user_data is None: 877 | user_data = [] 878 | elif isinstance(user_data, bytes): 879 | user_data = [user_data] 880 | 881 | def outer(func): 882 | n = name or func.__name__ 883 | 884 | idx = len(HOST_FN_REGISTRY).to_bytes(length=4, byteorder="big") 885 | user_data.append(idx) 886 | fn = ( 887 | TypeInferredFunction(namespace, n, func, user_data) 888 | if signature is None 889 | else ExplicitFunction( 890 | namespace, n, signature[0], signature[1], func, user_data 891 | ) 892 | ) 893 | HOST_FN_REGISTRY.append(fn) 894 | return fn 895 | 896 | return outer 897 | 898 | 899 | @_ffi.callback( 900 | "void(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)" 901 | ) 902 | def handle_args(current, inputs, n_inputs, outputs, n_outputs, user_data): 903 | inp = [] 904 | outp = [] 905 | 906 | for i in range(n_inputs): 907 | inp.append(_convert_value(inputs[i])) 908 | 909 | for i in range(n_outputs): 910 | outp.append(_convert_value(outputs[i])) 911 | 912 | if user_data == _ffi.NULL: 913 | udata = [] 914 | else: 915 | udata = list(_ffi.from_handle(user_data)) 916 | 917 | idx = int.from_bytes(udata.pop(), byteorder="big") 918 | 919 | HOST_FN_REGISTRY[idx](CurrentPlugin(current), inp, outp, *udata) 920 | 921 | for i in range(n_outputs): 922 | _convert_output(outputs[i], outp[i]) 923 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alabaster" 5 | version = "0.7.16" 6 | description = "A light, configurable Sphinx theme" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, 12 | {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, 13 | ] 14 | 15 | [[package]] 16 | name = "babel" 17 | version = "2.16.0" 18 | description = "Internationalization utilities" 19 | optional = false 20 | python-versions = ">=3.8" 21 | groups = ["dev"] 22 | files = [ 23 | {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, 24 | {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, 25 | ] 26 | 27 | [package.extras] 28 | dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] 29 | 30 | [[package]] 31 | name = "black" 32 | version = "23.12.1" 33 | description = "The uncompromising code formatter." 34 | optional = false 35 | python-versions = ">=3.8" 36 | groups = ["dev"] 37 | files = [ 38 | {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, 39 | {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, 40 | {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, 41 | {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, 42 | {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, 43 | {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, 44 | {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, 45 | {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, 46 | {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, 47 | {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, 48 | {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, 49 | {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, 50 | {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, 51 | {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, 52 | {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, 53 | {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, 54 | {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, 55 | {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, 56 | {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, 57 | {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, 58 | {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, 59 | {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, 60 | ] 61 | 62 | [package.dependencies] 63 | click = ">=8.0.0" 64 | mypy-extensions = ">=0.4.3" 65 | packaging = ">=22.0" 66 | pathspec = ">=0.9.0" 67 | platformdirs = ">=2" 68 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 69 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 70 | 71 | [package.extras] 72 | colorama = ["colorama (>=0.4.3)"] 73 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 74 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 75 | uvloop = ["uvloop (>=0.15.2)"] 76 | 77 | [[package]] 78 | name = "certifi" 79 | version = "2024.8.30" 80 | description = "Python package for providing Mozilla's CA Bundle." 81 | optional = false 82 | python-versions = ">=3.6" 83 | groups = ["dev"] 84 | files = [ 85 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 86 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 87 | ] 88 | 89 | [[package]] 90 | name = "cffi" 91 | version = "1.17.1" 92 | description = "Foreign Function Interface for Python calling C code." 93 | optional = false 94 | python-versions = ">=3.8" 95 | groups = ["main"] 96 | files = [ 97 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 98 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 99 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 100 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 101 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 102 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 103 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 104 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 105 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 106 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 107 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 108 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 109 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 110 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 111 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 112 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 113 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 114 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 115 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 116 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 117 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 118 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 119 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 120 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 121 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 122 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 123 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 124 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 125 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 126 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 127 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 128 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 129 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 130 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 131 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 132 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 133 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 134 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 135 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 136 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 137 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 138 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 139 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 140 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 141 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 142 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 143 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 144 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 145 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 146 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 147 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 148 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 149 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 150 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 151 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 152 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 153 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 154 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 155 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 156 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 157 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 158 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 159 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 160 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 161 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 162 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 163 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 164 | ] 165 | 166 | [package.dependencies] 167 | pycparser = "*" 168 | 169 | [[package]] 170 | name = "charset-normalizer" 171 | version = "3.4.0" 172 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 173 | optional = false 174 | python-versions = ">=3.7.0" 175 | groups = ["dev"] 176 | files = [ 177 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, 178 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, 179 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, 180 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, 181 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, 182 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, 183 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, 184 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, 185 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, 186 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, 187 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, 188 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, 189 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, 190 | {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, 191 | {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, 192 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, 193 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, 194 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, 195 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, 196 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, 197 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, 198 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, 199 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, 200 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, 201 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, 202 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, 203 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, 204 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, 205 | {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, 206 | {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, 207 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, 208 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, 209 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, 210 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, 211 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, 212 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, 213 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, 214 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, 215 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, 216 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, 217 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, 218 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, 219 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, 220 | {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, 221 | {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, 222 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, 223 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, 224 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, 225 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, 226 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, 227 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, 228 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, 229 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, 230 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, 231 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, 232 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, 233 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, 234 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, 235 | {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, 236 | {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, 237 | {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, 238 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, 239 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, 240 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, 241 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, 242 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, 243 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, 244 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, 245 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, 246 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, 247 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, 248 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, 249 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, 250 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, 251 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, 252 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, 253 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, 254 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, 255 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, 256 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, 257 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, 258 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, 259 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, 260 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, 261 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, 262 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, 263 | {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, 264 | {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, 265 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, 266 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, 267 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, 268 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, 269 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, 270 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, 271 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, 272 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, 273 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, 274 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, 275 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, 276 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, 277 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, 278 | {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, 279 | {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, 280 | {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, 281 | {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, 282 | ] 283 | 284 | [[package]] 285 | name = "click" 286 | version = "8.1.7" 287 | description = "Composable command line interface toolkit" 288 | optional = false 289 | python-versions = ">=3.7" 290 | groups = ["dev"] 291 | files = [ 292 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 293 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 294 | ] 295 | 296 | [package.dependencies] 297 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 298 | 299 | [[package]] 300 | name = "colorama" 301 | version = "0.4.6" 302 | description = "Cross-platform colored terminal text." 303 | optional = false 304 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 305 | groups = ["dev"] 306 | markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" 307 | files = [ 308 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 309 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 310 | ] 311 | 312 | [[package]] 313 | name = "docutils" 314 | version = "0.18.1" 315 | description = "Docutils -- Python Documentation Utilities" 316 | optional = false 317 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 318 | groups = ["dev"] 319 | files = [ 320 | {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, 321 | {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, 322 | ] 323 | 324 | [[package]] 325 | name = "extism-sys" 326 | version = "1.9.1" 327 | description = "" 328 | optional = false 329 | python-versions = ">=3.7" 330 | groups = ["main"] 331 | files = [ 332 | {file = "extism_sys-1.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c1a9627f4ae616f5baeeecb04c935bd0bc011d521fc2c3922c65cb08e0ecdd8f"}, 333 | {file = "extism_sys-1.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b02ccedf1a55c5967a302e6d3d893f26510488a4bded0564b41b85c66aad6764"}, 334 | {file = "extism_sys-1.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69d11c7b32a879a8f6f717d5a146c1742c16d030a521ccf57981ddfbf85ffdec"}, 335 | {file = "extism_sys-1.9.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:eada236d0b1a6cbb8c07a8b205edbabe86d238cad95e81d0889e2cfb5033bb94"}, 336 | {file = "extism_sys-1.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:34a07f2dcac470d872de771f685b0f79750c3d1a5c62b93a21cf7b5f14afe373"}, 337 | {file = "extism_sys-1.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e405026f21b85525001e47ca0d64ec0b278b526cb7e15685aea2d2e7da288b31"}, 338 | {file = "extism_sys-1.9.1-py3-none-win_amd64.whl", hash = "sha256:997f09fc0fa76b652e7fd3a0403626b0a520d4fb4d4d8ccf6cb7026ac8210bf8"}, 339 | ] 340 | 341 | [package.dependencies] 342 | cffi = "*" 343 | 344 | [[package]] 345 | name = "idna" 346 | version = "3.10" 347 | description = "Internationalized Domain Names in Applications (IDNA)" 348 | optional = false 349 | python-versions = ">=3.6" 350 | groups = ["dev"] 351 | files = [ 352 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 353 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 354 | ] 355 | 356 | [package.extras] 357 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 358 | 359 | [[package]] 360 | name = "imagesize" 361 | version = "1.4.1" 362 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 363 | optional = false 364 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 365 | groups = ["dev"] 366 | files = [ 367 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 368 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 369 | ] 370 | 371 | [[package]] 372 | name = "importlib-metadata" 373 | version = "8.5.0" 374 | description = "Read metadata from Python packages" 375 | optional = false 376 | python-versions = ">=3.8" 377 | groups = ["dev"] 378 | markers = "python_version < \"3.10\"" 379 | files = [ 380 | {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, 381 | {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, 382 | ] 383 | 384 | [package.dependencies] 385 | zipp = ">=3.20" 386 | 387 | [package.extras] 388 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 389 | cover = ["pytest-cov"] 390 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 391 | enabler = ["pytest-enabler (>=2.2)"] 392 | perf = ["ipython"] 393 | test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] 394 | type = ["pytest-mypy"] 395 | 396 | [[package]] 397 | name = "jinja2" 398 | version = "3.1.4" 399 | description = "A very fast and expressive template engine." 400 | optional = false 401 | python-versions = ">=3.7" 402 | groups = ["dev"] 403 | files = [ 404 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 405 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 406 | ] 407 | 408 | [package.dependencies] 409 | MarkupSafe = ">=2.0" 410 | 411 | [package.extras] 412 | i18n = ["Babel (>=2.7)"] 413 | 414 | [[package]] 415 | name = "markupsafe" 416 | version = "3.0.2" 417 | description = "Safely add untrusted strings to HTML/XML markup." 418 | optional = false 419 | python-versions = ">=3.9" 420 | groups = ["dev"] 421 | files = [ 422 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 423 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 424 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 425 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 426 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 427 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 428 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 429 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 430 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 431 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 432 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 433 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 434 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 435 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 436 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 437 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 438 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 439 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 440 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 441 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 442 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 443 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 444 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 445 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 446 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 447 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 448 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 449 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 450 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 451 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 452 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 453 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 454 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 455 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 456 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 457 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 458 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 459 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 460 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 461 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 462 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 463 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 464 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 465 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 466 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 467 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 468 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 469 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 470 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 471 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 472 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 473 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 474 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 475 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 476 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 477 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 478 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 479 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 480 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 481 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 482 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 483 | ] 484 | 485 | [[package]] 486 | name = "mypy" 487 | version = "1.13.0" 488 | description = "Optional static typing for Python" 489 | optional = false 490 | python-versions = ">=3.8" 491 | groups = ["dev"] 492 | files = [ 493 | {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, 494 | {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, 495 | {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, 496 | {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, 497 | {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, 498 | {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, 499 | {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, 500 | {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, 501 | {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, 502 | {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, 503 | {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, 504 | {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, 505 | {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, 506 | {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, 507 | {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, 508 | {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, 509 | {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, 510 | {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, 511 | {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, 512 | {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, 513 | {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, 514 | {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, 515 | {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, 516 | {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, 517 | {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, 518 | {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, 519 | {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, 520 | {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, 521 | {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, 522 | {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, 523 | {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, 524 | {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, 525 | ] 526 | 527 | [package.dependencies] 528 | mypy-extensions = ">=1.0.0" 529 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 530 | typing-extensions = ">=4.6.0" 531 | 532 | [package.extras] 533 | dmypy = ["psutil (>=4.0)"] 534 | faster-cache = ["orjson"] 535 | install-types = ["pip"] 536 | mypyc = ["setuptools (>=50)"] 537 | reports = ["lxml"] 538 | 539 | [[package]] 540 | name = "mypy-extensions" 541 | version = "1.0.0" 542 | description = "Type system extensions for programs checked with the mypy type checker." 543 | optional = false 544 | python-versions = ">=3.5" 545 | groups = ["dev"] 546 | files = [ 547 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 548 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 549 | ] 550 | 551 | [[package]] 552 | name = "packaging" 553 | version = "24.2" 554 | description = "Core utilities for Python packages" 555 | optional = false 556 | python-versions = ">=3.8" 557 | groups = ["dev"] 558 | files = [ 559 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 560 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 561 | ] 562 | 563 | [[package]] 564 | name = "pathspec" 565 | version = "0.12.1" 566 | description = "Utility library for gitignore style pattern matching of file paths." 567 | optional = false 568 | python-versions = ">=3.8" 569 | groups = ["dev"] 570 | files = [ 571 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 572 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 573 | ] 574 | 575 | [[package]] 576 | name = "platformdirs" 577 | version = "4.3.6" 578 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 579 | optional = false 580 | python-versions = ">=3.8" 581 | groups = ["dev"] 582 | files = [ 583 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 584 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 585 | ] 586 | 587 | [package.extras] 588 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 589 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 590 | type = ["mypy (>=1.11.2)"] 591 | 592 | [[package]] 593 | name = "pycparser" 594 | version = "2.22" 595 | description = "C parser in Python" 596 | optional = false 597 | python-versions = ">=3.8" 598 | groups = ["main"] 599 | files = [ 600 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 601 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 602 | ] 603 | 604 | [[package]] 605 | name = "pygments" 606 | version = "2.18.0" 607 | description = "Pygments is a syntax highlighting package written in Python." 608 | optional = false 609 | python-versions = ">=3.8" 610 | groups = ["dev"] 611 | files = [ 612 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 613 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 614 | ] 615 | 616 | [package.extras] 617 | windows-terminal = ["colorama (>=0.4.6)"] 618 | 619 | [[package]] 620 | name = "requests" 621 | version = "2.32.3" 622 | description = "Python HTTP for Humans." 623 | optional = false 624 | python-versions = ">=3.8" 625 | groups = ["dev"] 626 | files = [ 627 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 628 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 629 | ] 630 | 631 | [package.dependencies] 632 | certifi = ">=2017.4.17" 633 | charset-normalizer = ">=2,<4" 634 | idna = ">=2.5,<4" 635 | urllib3 = ">=1.21.1,<3" 636 | 637 | [package.extras] 638 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 639 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 640 | 641 | [[package]] 642 | name = "snowballstemmer" 643 | version = "2.2.0" 644 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 645 | optional = false 646 | python-versions = "*" 647 | groups = ["dev"] 648 | files = [ 649 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 650 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 651 | ] 652 | 653 | [[package]] 654 | name = "sphinx" 655 | version = "7.3.7" 656 | description = "Python documentation generator" 657 | optional = false 658 | python-versions = ">=3.9" 659 | groups = ["dev"] 660 | files = [ 661 | {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, 662 | {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, 663 | ] 664 | 665 | [package.dependencies] 666 | alabaster = ">=0.7.14,<0.8.0" 667 | babel = ">=2.9" 668 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 669 | docutils = ">=0.18.1,<0.22" 670 | imagesize = ">=1.3" 671 | importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} 672 | Jinja2 = ">=3.0" 673 | packaging = ">=21.0" 674 | Pygments = ">=2.14" 675 | requests = ">=2.25.0" 676 | snowballstemmer = ">=2.0" 677 | sphinxcontrib-applehelp = "*" 678 | sphinxcontrib-devhelp = "*" 679 | sphinxcontrib-htmlhelp = ">=2.0.0" 680 | sphinxcontrib-jsmath = "*" 681 | sphinxcontrib-qthelp = "*" 682 | sphinxcontrib-serializinghtml = ">=1.1.9" 683 | tomli = {version = ">=2", markers = "python_version < \"3.11\""} 684 | 685 | [package.extras] 686 | docs = ["sphinxcontrib-websupport"] 687 | lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] 688 | test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] 689 | 690 | [[package]] 691 | name = "sphinx-autodoc-typehints" 692 | version = "1.25.3" 693 | description = "Type hints (PEP 484) support for the Sphinx autodoc extension" 694 | optional = false 695 | python-versions = ">=3.8" 696 | groups = ["dev"] 697 | files = [ 698 | {file = "sphinx_autodoc_typehints-1.25.3-py3-none-any.whl", hash = "sha256:d3da7fa9a9761eff6ff09f8b1956ae3090a2d4f4ad54aebcade8e458d6340835"}, 699 | {file = "sphinx_autodoc_typehints-1.25.3.tar.gz", hash = "sha256:70db10b391acf4e772019765991d2de0ff30ec0899b9ba137706dc0b3c4835e0"}, 700 | ] 701 | 702 | [package.dependencies] 703 | sphinx = ">=7.1.2" 704 | 705 | [package.extras] 706 | docs = ["furo (>=2023.9.10)"] 707 | numpy = ["nptyping (>=2.5)"] 708 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.8)"] 709 | 710 | [[package]] 711 | name = "sphinx-rtd-theme" 712 | version = "1.3.0" 713 | description = "Read the Docs theme for Sphinx" 714 | optional = false 715 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 716 | groups = ["dev"] 717 | files = [ 718 | {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, 719 | {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, 720 | ] 721 | 722 | [package.dependencies] 723 | docutils = "<0.19" 724 | sphinx = ">=1.6,<8" 725 | sphinxcontrib-jquery = ">=4,<5" 726 | 727 | [package.extras] 728 | dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] 729 | 730 | [[package]] 731 | name = "sphinxcontrib-applehelp" 732 | version = "2.0.0" 733 | description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 734 | optional = false 735 | python-versions = ">=3.9" 736 | groups = ["dev"] 737 | files = [ 738 | {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, 739 | {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, 740 | ] 741 | 742 | [package.extras] 743 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 744 | standalone = ["Sphinx (>=5)"] 745 | test = ["pytest"] 746 | 747 | [[package]] 748 | name = "sphinxcontrib-devhelp" 749 | version = "2.0.0" 750 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" 751 | optional = false 752 | python-versions = ">=3.9" 753 | groups = ["dev"] 754 | files = [ 755 | {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, 756 | {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, 757 | ] 758 | 759 | [package.extras] 760 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 761 | standalone = ["Sphinx (>=5)"] 762 | test = ["pytest"] 763 | 764 | [[package]] 765 | name = "sphinxcontrib-htmlhelp" 766 | version = "2.1.0" 767 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 768 | optional = false 769 | python-versions = ">=3.9" 770 | groups = ["dev"] 771 | files = [ 772 | {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, 773 | {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, 774 | ] 775 | 776 | [package.extras] 777 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 778 | standalone = ["Sphinx (>=5)"] 779 | test = ["html5lib", "pytest"] 780 | 781 | [[package]] 782 | name = "sphinxcontrib-jquery" 783 | version = "4.1" 784 | description = "Extension to include jQuery on newer Sphinx releases" 785 | optional = false 786 | python-versions = ">=2.7" 787 | groups = ["dev"] 788 | files = [ 789 | {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, 790 | {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, 791 | ] 792 | 793 | [package.dependencies] 794 | Sphinx = ">=1.8" 795 | 796 | [[package]] 797 | name = "sphinxcontrib-jsmath" 798 | version = "1.0.1" 799 | description = "A sphinx extension which renders display math in HTML via JavaScript" 800 | optional = false 801 | python-versions = ">=3.5" 802 | groups = ["dev"] 803 | files = [ 804 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 805 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 806 | ] 807 | 808 | [package.extras] 809 | test = ["flake8", "mypy", "pytest"] 810 | 811 | [[package]] 812 | name = "sphinxcontrib-qthelp" 813 | version = "2.0.0" 814 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" 815 | optional = false 816 | python-versions = ">=3.9" 817 | groups = ["dev"] 818 | files = [ 819 | {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, 820 | {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, 821 | ] 822 | 823 | [package.extras] 824 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 825 | standalone = ["Sphinx (>=5)"] 826 | test = ["defusedxml (>=0.7.1)", "pytest"] 827 | 828 | [[package]] 829 | name = "sphinxcontrib-serializinghtml" 830 | version = "2.0.0" 831 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" 832 | optional = false 833 | python-versions = ">=3.9" 834 | groups = ["dev"] 835 | files = [ 836 | {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, 837 | {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, 838 | ] 839 | 840 | [package.extras] 841 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 842 | standalone = ["Sphinx (>=5)"] 843 | test = ["pytest"] 844 | 845 | [[package]] 846 | name = "tomli" 847 | version = "2.1.0" 848 | description = "A lil' TOML parser" 849 | optional = false 850 | python-versions = ">=3.8" 851 | groups = ["dev"] 852 | markers = "python_version < \"3.11\"" 853 | files = [ 854 | {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, 855 | {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, 856 | ] 857 | 858 | [[package]] 859 | name = "typing-extensions" 860 | version = "4.12.2" 861 | description = "Backported and Experimental Type Hints for Python 3.8+" 862 | optional = false 863 | python-versions = ">=3.8" 864 | groups = ["dev"] 865 | files = [ 866 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 867 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 868 | ] 869 | 870 | [[package]] 871 | name = "urllib3" 872 | version = "2.2.3" 873 | description = "HTTP library with thread-safe connection pooling, file post, and more." 874 | optional = false 875 | python-versions = ">=3.8" 876 | groups = ["dev"] 877 | files = [ 878 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 879 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 880 | ] 881 | 882 | [package.extras] 883 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 884 | h2 = ["h2 (>=4,<5)"] 885 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 886 | zstd = ["zstandard (>=0.18.0)"] 887 | 888 | [[package]] 889 | name = "zipp" 890 | version = "3.21.0" 891 | description = "Backport of pathlib-compatible object wrapper for zip files" 892 | optional = false 893 | python-versions = ">=3.9" 894 | groups = ["dev"] 895 | markers = "python_version < \"3.10\"" 896 | files = [ 897 | {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, 898 | {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, 899 | ] 900 | 901 | [package.extras] 902 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 903 | cover = ["pytest-cov"] 904 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 905 | enabler = ["pytest-enabler (>=2.2)"] 906 | test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 907 | type = ["pytest-mypy"] 908 | 909 | [metadata] 910 | lock-version = "2.1" 911 | python-versions = "^3.9" 912 | content-hash = "8a1d515417d917914f5b0d3cec6af3d04d1f5ff5bfd24f0ee7d95fd448972eec" 913 | --------------------------------------------------------------------------------