├── .flake8 ├── .github └── workflows │ └── python-client-build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── PUBLISHING.md ├── README.md ├── poetry.lock ├── pyproject.toml ├── python ├── _convex │ ├── __init__.py │ ├── _convex.pyi │ ├── int64.py │ └── py.typed └── convex │ ├── __init__.py │ ├── http_client.py │ ├── py.typed │ └── values.py ├── rust-toolchain ├── simple_example.py ├── src ├── client │ └── mod.rs ├── lib.rs ├── query_result.rs └── subscription.rs └── tests ├── __init__.py ├── test_simple.py ├── test_values.py └── test_version.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | extend_ignore = D105 4 | per_file_ignores = 5 | tests/*.py:D100,D103,D104 6 | *example*.py:D100,D103 7 | exclude = .venv 8 | -------------------------------------------------------------------------------- /.github/workflows/python-client-build.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.7.8 2 | # To update, run 3 | # 4 | # maturin generate-ci github -m crates/py_client/Cargo.toml 5 | # 6 | name: Python Client Build 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - "convex-py/*" 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | linux: 23 | runs-on: ${{ matrix.platform.runner }} 24 | strategy: 25 | matrix: 26 | platform: 27 | - runner: ubuntu-22.04 28 | target: x86_64 29 | - runner: ubuntu-22.04 30 | target: x86 31 | - runner: ubuntu-22.04 32 | target: aarch64 33 | - runner: ubuntu-22.04 34 | target: armv7 35 | - runner: ubuntu-22.04 36 | target: s390x 37 | - runner: ubuntu-22.04 38 | target: ppc64le 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-python@v5 42 | with: 43 | python-version: 3.x 44 | - name: Build wheels 45 | uses: PyO3/maturin-action@v1 46 | with: 47 | target: ${{ matrix.platform.target }} 48 | args: --release --out dist 49 | sccache: "true" 50 | manylinux: auto 51 | before-script-linux: | 52 | # see https://github.com/sfackler/rust-openssl/issues/2036#issuecomment-1724324145 for discussion of this script 53 | # If we're running on rhel centos, install needed packages. 54 | if command -v yum &> /dev/null; then 55 | yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic 56 | # If we're running on i686 we need to symlink libatomic 57 | # in order to build openssl with -latomic flag. 58 | if [[ ! -d "/usr/lib64" ]]; then 59 | ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so 60 | fi 61 | else 62 | # If we're running on debian-based system. 63 | apt update -y && apt-get install -y libssl-dev openssl pkg-config 64 | fi 65 | - name: Build free-threaded wheels 66 | uses: PyO3/maturin-action@v1 67 | with: 68 | target: ${{ matrix.platform.target }} 69 | args: --release --out dist -i python3.13t 70 | sccache: "true" 71 | manylinux: auto 72 | before-script-linux: | 73 | if command -v yum &> /dev/null; then 74 | yum update -y && yum install -y perl-core openssl openssl-devel pkgconfig libatomic 75 | # If we're running on i686 we need to symlink libatomic 76 | # in order to build openssl with -latomic flag. 77 | if [[ ! -d "/usr/lib64" ]]; then 78 | ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so 79 | fi 80 | else 81 | # If we're running on debian-based system. 82 | apt update -y && apt-get install -y libssl-dev openssl pkg-config 83 | fi 84 | - name: Upload wheels 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: wheels-linux-${{ matrix.platform.target }} 88 | path: dist 89 | 90 | musllinux: 91 | runs-on: ${{ matrix.platform.runner }} 92 | strategy: 93 | matrix: 94 | platform: 95 | - runner: ubuntu-22.04 96 | target: x86_64 97 | - runner: ubuntu-22.04 98 | target: x86 99 | - runner: ubuntu-22.04 100 | target: aarch64 101 | - runner: ubuntu-22.04 102 | target: armv7 103 | steps: 104 | - uses: actions/checkout@v4 105 | - uses: actions/setup-python@v5 106 | with: 107 | python-version: 3.x 108 | - name: Build wheels 109 | uses: PyO3/maturin-action@v1 110 | with: 111 | target: ${{ matrix.platform.target }} 112 | args: --release --out dist 113 | sccache: "true" 114 | manylinux: musllinux_1_2 115 | - name: Build free-threaded wheels 116 | uses: PyO3/maturin-action@v1 117 | with: 118 | target: ${{ matrix.platform.target }} 119 | args: --release --out dist -i python3.13t 120 | sccache: "true" 121 | manylinux: musllinux_1_2 122 | - name: Upload wheels 123 | uses: actions/upload-artifact@v4 124 | with: 125 | name: wheels-musllinux-${{ matrix.platform.target }} 126 | path: dist 127 | 128 | windows: 129 | runs-on: ${{ matrix.platform.runner }} 130 | strategy: 131 | matrix: 132 | platform: 133 | - runner: windows-latest 134 | target: x64 135 | - runner: windows-latest 136 | target: x86 137 | steps: 138 | - uses: actions/checkout@v4 139 | - uses: actions/setup-python@v5 140 | with: 141 | python-version: 3.x 142 | architecture: ${{ matrix.platform.target }} 143 | - name: Build wheels 144 | uses: PyO3/maturin-action@v1 145 | with: 146 | target: ${{ matrix.platform.target }} 147 | args: --release --out dist 148 | sccache: "true" 149 | - name: Build free-threaded wheels 150 | uses: PyO3/maturin-action@v1 151 | with: 152 | target: ${{ matrix.platform.target }} 153 | args: --release --out dist --find-interpreter 154 | sccache: "true" 155 | - name: Upload wheels 156 | uses: actions/upload-artifact@v4 157 | with: 158 | name: wheels-windows-${{ matrix.platform.target }} 159 | path: dist 160 | 161 | macos: 162 | runs-on: ${{ matrix.platform.runner }} 163 | strategy: 164 | matrix: 165 | platform: 166 | - runner: macos-13 167 | target: x86_64 168 | - runner: macos-14 169 | target: aarch64 170 | steps: 171 | - uses: actions/checkout@v4 172 | - uses: actions/setup-python@v5 173 | with: 174 | python-version: 3.x 175 | - name: Build wheels 176 | uses: PyO3/maturin-action@v1 177 | with: 178 | target: ${{ matrix.platform.target }} 179 | args: --release --out dist 180 | sccache: "true" 181 | - name: Build free-threaded wheels 182 | uses: PyO3/maturin-action@v1 183 | with: 184 | target: ${{ matrix.platform.target }} 185 | args: --release --out dist -i python3.13t 186 | sccache: "true" 187 | - name: Upload wheels 188 | uses: actions/upload-artifact@v4 189 | with: 190 | name: wheels-macos-${{ matrix.platform.target }} 191 | path: dist 192 | 193 | sdist: 194 | runs-on: ubuntu-latest 195 | steps: 196 | - uses: actions/checkout@v4 197 | - name: Build sdist 198 | uses: PyO3/maturin-action@v1 199 | with: 200 | command: sdist 201 | args: --out dist 202 | - name: Upload sdist 203 | uses: actions/upload-artifact@v4 204 | with: 205 | name: wheels-sdist 206 | path: dist 207 | 208 | release: 209 | name: Release 210 | runs-on: ubuntu-latest 211 | if: 212 | ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 213 | 'workflow_dispatch' }} 214 | needs: [linux, musllinux, windows, macos, sdist] 215 | permissions: 216 | # Use to sign the release artifacts 217 | id-token: write 218 | # Used to upload release artifacts 219 | contents: write 220 | # Used to generate artifact attestation 221 | attestations: write 222 | steps: 223 | - uses: actions/download-artifact@v4 224 | - name: Generate artifact attestation 225 | uses: actions/attest-build-provenance@v1 226 | with: 227 | subject-path: "wheels-*/*" 228 | - name: Publish to PyPI 229 | if: ${{ startsWith(github.ref, 'refs/tags/convex-py/') }} 230 | uses: PyO3/maturin-action@v1 231 | with: 232 | command: upload 233 | args: --non-interactive --skip-existing wheels-*/* 234 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | # dev-specific path to virtualenv, useful for the pyright language server 3 | # unless you are using VS Code which handles it for you. 4 | pyrightconfig.json 5 | 6 | # binary output 7 | python/_convex/_convex.*.so 8 | 9 | target/ 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .mypy_cache/ 2 | .pytest_cache/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | proseWrap: "always" 2 | arrowParens: "avoid" 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Upcoming 2 | 3 | # 0.7.0 4 | 5 | - Add async iterator support to query subscription (Thanks byin) 6 | - Don't set global log level 7 | - Bump pyo3, url, convex-rs, and other dependencies 8 | - Switch to a default native-tls-vendored 9 | - Configure Convex-Client header 10 | 11 | # 0.6.0 12 | 13 | Version 0.6.0 is a rewrite which adds the ability to subscribe to live query 14 | updates. The Python client now wraps the 15 | [Convex Rust client](https://docs.rs/convex) using the PyO3[https://pyo3.rs/]. 16 | 17 | The API is a superset of the API in 0.5.1 but there are some behavior 18 | differences. If you encounter unexpected incompatibilities consider the old 19 | client, moved to `convex.http_client`. 20 | 21 | One big change is that running a `client.query()` or mutation or action will 22 | retry on network errors instead of throwing. If you were catching network errors 23 | to implement retries in your code you should be able to get rid of this code. 24 | The Convex Python client will retry indefinitely. 25 | 26 | # 0.5.1 27 | 28 | - Expose ConvexInt64 from the top level `convex` module. 29 | 30 | # 0.5.0 31 | 32 | - Remove ConvexMap and ConvexSet: these types are no longer allowed as arguments 33 | or returned by Convex functions. 34 | 35 | See the [NPM version 0.19.0](https://news.convex.dev/announcing-convex-0-19-0) 36 | release notes for more. 37 | 38 | If you need to communicate with a backend with functions that accept or return 39 | these types, _stay on version 0.4.0_. 40 | 41 | - `ConvexClient.set_debug()` no longer applies to production deployments: logs 42 | from production deployments are no longer sent to clients, only appearing on 43 | the Convex dashboard. 44 | 45 | - Add Support for `ConvexError`. 46 | 47 | # 0.4.0 48 | 49 | - Remove the `Id` class since document IDs are strings for Convex functions 50 | starting from 51 | [NPM version 0.17](https://news.convex.dev/announcing-convex-0-17-0/) 52 | - Add warnings when calling functions on deprecated versions of Convex 53 | 54 | # 0.3.0 55 | 56 | This release corresponds with Convex npm release 0.13.0. 57 | 58 | - `mutation()`, `action()`, and `query()` now take two arguments: the function 59 | name and an (optional) arguments dictionary. See 60 | https://news.convex.dev/announcing-convex-0-13-0/ for more about this change. 61 | 62 | If you need to communicate with a Convex backend with functions with 63 | positional arguments instead of a single arguments object, _stay on version 64 | 0.2.0_. 65 | 66 | # 0.2.0 67 | 68 | - Both python `int` and `float` will coerce to a `number` type in Convex's JS 69 | environment. If you need a JS `bigint`, use a python `ConvexInt64`. Convex 70 | functions are written in JavaScript where `number` is used pervasively, so 71 | this change generally simplifies using Convex functions from python. 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! 4 | 5 | Please share any general questions, feature requests, or product feedback in our 6 | [Convex Discord Community](https://convex.dev/community). We're particularly 7 | excited to see what you build on Convex! 8 | 9 | Please ensure that python code is formatted with 10 | [black](https://pypi.org/project/black/) and markdown files are formatted with 11 | [prettier](https://prettier.io/). 12 | 13 | Run tests with 14 | 15 | ``` 16 | poetry install 17 | poetry run pytest 18 | ``` 19 | 20 | Convex is a fast moving project developed by a dedicated team. We're excited to 21 | contribute to the community by releasing this code, but we want to manage 22 | expectations as well. 23 | 24 | - We are a small company with a lot of product surface area. 25 | - We value a cohesive developer experience for folks building applications 26 | across all of our languages and platforms. 27 | - We value transparency in how we operate. 28 | 29 | We're excited for community PRs. Be aware we may not get to it for a while. 30 | Smaller PRs that only affect documentation/comments are easier to review and 31 | integrate. For any larger or more fundamental changes, get in touch with us on 32 | Discord before you put in too much work to see if it's consistent with our short 33 | term plan. We think carefully about how our APIs contribute to a cohesive 34 | product, so chatting up front goes a long way. 35 | 36 | # Development notes 37 | 38 | You'll need a Rust toolchain installed (e.g. through https://rustup.rs/) to 39 | build this library when developing locally. 40 | 41 | ```sh 42 | # install dependencies 43 | poetry install --no-root 44 | 45 | # build _convex 46 | # This is requred to run tests and to use from other local installations (like smoke tests) 47 | poetry run maturin dev 48 | 49 | # run a test script 50 | poetry run python simple_example.py 51 | 52 | # interactive shell 53 | poetry run python 54 | >>> from convex import ConvexClient 55 | >>> client = ConvexClint("https://flippant-cardinal-923.convex.cloud") 56 | >>> print(client.query("users:list")) 57 | 58 | # The wrapped Rust client can also be run in an interactive shell 59 | >>> from _convex import PyConvexClient 60 | >>> client = PyConvexClient("https://flippant-cardinal-923.convex.cloud") 61 | >>> print(client.query("users:list")) 62 | 63 | ``` 64 | 65 | This package depends on the [convex](https://crates.io/crates/convex) package on 66 | crates.io. Updating behavior defined in that package requires publishing an 67 | update to that crate and updating the version required in this project. 68 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "_convex" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "convex", 10 | "futures", 11 | "parking_lot", 12 | "pyo3", 13 | "pyo3-async-runtimes", 14 | "pyo3-build-config", 15 | "tokio", 16 | "tracing", 17 | "tracing-subscriber", 18 | ] 19 | 20 | [[package]] 21 | name = "addr2line" 22 | version = "0.21.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 25 | dependencies = [ 26 | "gimli", 27 | ] 28 | 29 | [[package]] 30 | name = "adler" 31 | version = "1.0.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 34 | 35 | [[package]] 36 | name = "aho-corasick" 37 | version = "1.1.3" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 40 | dependencies = [ 41 | "memchr", 42 | ] 43 | 44 | [[package]] 45 | name = "anyhow" 46 | version = "1.0.97" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 49 | 50 | [[package]] 51 | name = "archery" 52 | version = "1.2.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" 55 | 56 | [[package]] 57 | name = "async-trait" 58 | version = "0.1.87" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" 61 | dependencies = [ 62 | "proc-macro2", 63 | "quote", 64 | "syn 2.0.95", 65 | ] 66 | 67 | [[package]] 68 | name = "autocfg" 69 | version = "1.1.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 72 | 73 | [[package]] 74 | name = "backtrace" 75 | version = "0.3.71" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 78 | dependencies = [ 79 | "addr2line", 80 | "cc", 81 | "cfg-if", 82 | "libc", 83 | "miniz_oxide", 84 | "object", 85 | "rustc-demangle", 86 | ] 87 | 88 | [[package]] 89 | name = "base64" 90 | version = "0.13.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 93 | 94 | [[package]] 95 | name = "base64" 96 | version = "0.21.7" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 99 | 100 | [[package]] 101 | name = "bit-set" 102 | version = "0.5.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 105 | dependencies = [ 106 | "bit-vec", 107 | ] 108 | 109 | [[package]] 110 | name = "bit-vec" 111 | version = "0.6.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 114 | 115 | [[package]] 116 | name = "bitflags" 117 | version = "1.3.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 120 | 121 | [[package]] 122 | name = "bitflags" 123 | version = "2.6.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 126 | 127 | [[package]] 128 | name = "bitmaps" 129 | version = "3.2.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" 132 | 133 | [[package]] 134 | name = "block-buffer" 135 | version = "0.10.4" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 138 | dependencies = [ 139 | "generic-array", 140 | ] 141 | 142 | [[package]] 143 | name = "bytes" 144 | version = "1.10.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 147 | 148 | [[package]] 149 | name = "cc" 150 | version = "1.2.16" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" 153 | dependencies = [ 154 | "shlex", 155 | ] 156 | 157 | [[package]] 158 | name = "cfg-if" 159 | version = "1.0.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 162 | 163 | [[package]] 164 | name = "convert_case" 165 | version = "0.7.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 168 | dependencies = [ 169 | "unicode-segmentation", 170 | ] 171 | 172 | [[package]] 173 | name = "convex" 174 | version = "0.9.0" 175 | dependencies = [ 176 | "anyhow", 177 | "async-trait", 178 | "base64 0.13.1", 179 | "bytes", 180 | "convex_sync_types", 181 | "futures", 182 | "imbl", 183 | "parking_lot", 184 | "proptest", 185 | "proptest-derive", 186 | "rand 0.9.0", 187 | "serde_json", 188 | "thiserror", 189 | "tokio", 190 | "tokio-stream", 191 | "tokio-tungstenite", 192 | "tracing", 193 | "url", 194 | "uuid", 195 | ] 196 | 197 | [[package]] 198 | name = "convex_sync_types" 199 | version = "0.9.0" 200 | dependencies = [ 201 | "anyhow", 202 | "base64 0.13.1", 203 | "derive_more", 204 | "headers", 205 | "pretty_assertions", 206 | "proptest", 207 | "proptest-derive", 208 | "rand 0.9.0", 209 | "serde", 210 | "serde_json", 211 | "uuid", 212 | ] 213 | 214 | [[package]] 215 | name = "core-foundation" 216 | version = "0.9.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 219 | dependencies = [ 220 | "core-foundation-sys", 221 | "libc", 222 | ] 223 | 224 | [[package]] 225 | name = "core-foundation" 226 | version = "0.10.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" 229 | dependencies = [ 230 | "core-foundation-sys", 231 | "libc", 232 | ] 233 | 234 | [[package]] 235 | name = "core-foundation-sys" 236 | version = "0.8.7" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 239 | 240 | [[package]] 241 | name = "cpufeatures" 242 | version = "0.2.6" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" 245 | dependencies = [ 246 | "libc", 247 | ] 248 | 249 | [[package]] 250 | name = "crypto-common" 251 | version = "0.1.6" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 254 | dependencies = [ 255 | "generic-array", 256 | "typenum", 257 | ] 258 | 259 | [[package]] 260 | name = "ctor" 261 | version = "0.1.26" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" 264 | dependencies = [ 265 | "quote", 266 | "syn 1.0.109", 267 | ] 268 | 269 | [[package]] 270 | name = "data-encoding" 271 | version = "2.3.3" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" 274 | 275 | [[package]] 276 | name = "derive_more" 277 | version = "2.0.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 280 | dependencies = [ 281 | "derive_more-impl", 282 | ] 283 | 284 | [[package]] 285 | name = "derive_more-impl" 286 | version = "2.0.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 289 | dependencies = [ 290 | "convert_case", 291 | "proc-macro2", 292 | "quote", 293 | "syn 2.0.95", 294 | "unicode-xid", 295 | ] 296 | 297 | [[package]] 298 | name = "diff" 299 | version = "0.1.13" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 302 | 303 | [[package]] 304 | name = "digest" 305 | version = "0.10.7" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 308 | dependencies = [ 309 | "block-buffer", 310 | "crypto-common", 311 | ] 312 | 313 | [[package]] 314 | name = "displaydoc" 315 | version = "0.2.5" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 318 | dependencies = [ 319 | "proc-macro2", 320 | "quote", 321 | "syn 2.0.95", 322 | ] 323 | 324 | [[package]] 325 | name = "equivalent" 326 | version = "1.0.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 329 | 330 | [[package]] 331 | name = "errno" 332 | version = "0.3.10" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 335 | dependencies = [ 336 | "libc", 337 | "windows-sys 0.59.0", 338 | ] 339 | 340 | [[package]] 341 | name = "fastrand" 342 | version = "2.3.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 345 | 346 | [[package]] 347 | name = "fnv" 348 | version = "1.0.7" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 351 | 352 | [[package]] 353 | name = "foreign-types" 354 | version = "0.3.2" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 357 | dependencies = [ 358 | "foreign-types-shared", 359 | ] 360 | 361 | [[package]] 362 | name = "foreign-types-shared" 363 | version = "0.1.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 366 | 367 | [[package]] 368 | name = "form_urlencoded" 369 | version = "1.2.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 372 | dependencies = [ 373 | "percent-encoding", 374 | ] 375 | 376 | [[package]] 377 | name = "futures" 378 | version = "0.3.31" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 381 | dependencies = [ 382 | "futures-channel", 383 | "futures-core", 384 | "futures-executor", 385 | "futures-io", 386 | "futures-sink", 387 | "futures-task", 388 | "futures-util", 389 | ] 390 | 391 | [[package]] 392 | name = "futures-channel" 393 | version = "0.3.31" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 396 | dependencies = [ 397 | "futures-core", 398 | "futures-sink", 399 | ] 400 | 401 | [[package]] 402 | name = "futures-core" 403 | version = "0.3.31" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 406 | 407 | [[package]] 408 | name = "futures-executor" 409 | version = "0.3.31" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 412 | dependencies = [ 413 | "futures-core", 414 | "futures-task", 415 | "futures-util", 416 | ] 417 | 418 | [[package]] 419 | name = "futures-io" 420 | version = "0.3.31" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 423 | 424 | [[package]] 425 | name = "futures-macro" 426 | version = "0.3.31" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn 2.0.95", 433 | ] 434 | 435 | [[package]] 436 | name = "futures-sink" 437 | version = "0.3.31" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 440 | 441 | [[package]] 442 | name = "futures-task" 443 | version = "0.3.31" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 446 | 447 | [[package]] 448 | name = "futures-util" 449 | version = "0.3.31" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 452 | dependencies = [ 453 | "futures-channel", 454 | "futures-core", 455 | "futures-io", 456 | "futures-macro", 457 | "futures-sink", 458 | "futures-task", 459 | "memchr", 460 | "pin-project-lite", 461 | "pin-utils", 462 | "slab", 463 | ] 464 | 465 | [[package]] 466 | name = "generic-array" 467 | version = "0.14.7" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 470 | dependencies = [ 471 | "typenum", 472 | "version_check", 473 | ] 474 | 475 | [[package]] 476 | name = "getrandom" 477 | version = "0.2.15" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 480 | dependencies = [ 481 | "cfg-if", 482 | "libc", 483 | "wasi 0.11.0+wasi-snapshot-preview1", 484 | ] 485 | 486 | [[package]] 487 | name = "getrandom" 488 | version = "0.3.1" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 491 | dependencies = [ 492 | "cfg-if", 493 | "libc", 494 | "wasi 0.13.3+wasi-0.2.2", 495 | "windows-targets", 496 | ] 497 | 498 | [[package]] 499 | name = "gimli" 500 | version = "0.28.1" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 503 | 504 | [[package]] 505 | name = "hashbrown" 506 | version = "0.15.2" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 509 | 510 | [[package]] 511 | name = "headers" 512 | version = "0.4.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" 515 | dependencies = [ 516 | "base64 0.21.7", 517 | "bytes", 518 | "headers-core", 519 | "http", 520 | "httpdate", 521 | "mime", 522 | "sha1", 523 | ] 524 | 525 | [[package]] 526 | name = "headers-core" 527 | version = "0.3.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" 530 | dependencies = [ 531 | "http", 532 | ] 533 | 534 | [[package]] 535 | name = "heck" 536 | version = "0.5.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 539 | 540 | [[package]] 541 | name = "http" 542 | version = "1.3.1" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 545 | dependencies = [ 546 | "bytes", 547 | "fnv", 548 | "itoa", 549 | ] 550 | 551 | [[package]] 552 | name = "httparse" 553 | version = "1.10.1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 556 | 557 | [[package]] 558 | name = "httpdate" 559 | version = "1.0.3" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 562 | 563 | [[package]] 564 | name = "icu_collections" 565 | version = "1.5.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 568 | dependencies = [ 569 | "displaydoc", 570 | "yoke", 571 | "zerofrom", 572 | "zerovec", 573 | ] 574 | 575 | [[package]] 576 | name = "icu_locid" 577 | version = "1.5.0" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 580 | dependencies = [ 581 | "displaydoc", 582 | "litemap", 583 | "tinystr", 584 | "writeable", 585 | "zerovec", 586 | ] 587 | 588 | [[package]] 589 | name = "icu_locid_transform" 590 | version = "1.5.0" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 593 | dependencies = [ 594 | "displaydoc", 595 | "icu_locid", 596 | "icu_locid_transform_data", 597 | "icu_provider", 598 | "tinystr", 599 | "zerovec", 600 | ] 601 | 602 | [[package]] 603 | name = "icu_locid_transform_data" 604 | version = "1.5.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 607 | 608 | [[package]] 609 | name = "icu_normalizer" 610 | version = "1.5.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 613 | dependencies = [ 614 | "displaydoc", 615 | "icu_collections", 616 | "icu_normalizer_data", 617 | "icu_properties", 618 | "icu_provider", 619 | "smallvec", 620 | "utf16_iter", 621 | "utf8_iter", 622 | "write16", 623 | "zerovec", 624 | ] 625 | 626 | [[package]] 627 | name = "icu_normalizer_data" 628 | version = "1.5.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 631 | 632 | [[package]] 633 | name = "icu_properties" 634 | version = "1.5.1" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 637 | dependencies = [ 638 | "displaydoc", 639 | "icu_collections", 640 | "icu_locid_transform", 641 | "icu_properties_data", 642 | "icu_provider", 643 | "tinystr", 644 | "zerovec", 645 | ] 646 | 647 | [[package]] 648 | name = "icu_properties_data" 649 | version = "1.5.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 652 | 653 | [[package]] 654 | name = "icu_provider" 655 | version = "1.5.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 658 | dependencies = [ 659 | "displaydoc", 660 | "icu_locid", 661 | "icu_provider_macros", 662 | "stable_deref_trait", 663 | "tinystr", 664 | "writeable", 665 | "yoke", 666 | "zerofrom", 667 | "zerovec", 668 | ] 669 | 670 | [[package]] 671 | name = "icu_provider_macros" 672 | version = "1.5.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 675 | dependencies = [ 676 | "proc-macro2", 677 | "quote", 678 | "syn 2.0.95", 679 | ] 680 | 681 | [[package]] 682 | name = "idna" 683 | version = "1.0.3" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 686 | dependencies = [ 687 | "idna_adapter", 688 | "smallvec", 689 | "utf8_iter", 690 | ] 691 | 692 | [[package]] 693 | name = "idna_adapter" 694 | version = "1.2.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 697 | dependencies = [ 698 | "icu_normalizer", 699 | "icu_properties", 700 | ] 701 | 702 | [[package]] 703 | name = "imbl" 704 | version = "5.0.0" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "e4308a675e4cfc1920f36a8f4d8fb62d5533b7da106844bd1ec51c6f1fa94a0c" 707 | dependencies = [ 708 | "archery", 709 | "bitmaps", 710 | "imbl-sized-chunks", 711 | "rand_core 0.9.1", 712 | "rand_xoshiro", 713 | "version_check", 714 | ] 715 | 716 | [[package]] 717 | name = "imbl-sized-chunks" 718 | version = "0.1.3" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" 721 | dependencies = [ 722 | "bitmaps", 723 | ] 724 | 725 | [[package]] 726 | name = "indexmap" 727 | version = "2.9.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 730 | dependencies = [ 731 | "equivalent", 732 | "hashbrown", 733 | ] 734 | 735 | [[package]] 736 | name = "indoc" 737 | version = "2.0.6" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 740 | 741 | [[package]] 742 | name = "itoa" 743 | version = "1.0.15" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 746 | 747 | [[package]] 748 | name = "lazy_static" 749 | version = "1.5.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 752 | 753 | [[package]] 754 | name = "libc" 755 | version = "0.2.172" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 758 | 759 | [[package]] 760 | name = "libm" 761 | version = "0.2.6" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" 764 | 765 | [[package]] 766 | name = "linux-raw-sys" 767 | version = "0.4.15" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 770 | 771 | [[package]] 772 | name = "litemap" 773 | version = "0.7.3" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" 776 | 777 | [[package]] 778 | name = "lock_api" 779 | version = "0.4.13" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 782 | dependencies = [ 783 | "autocfg", 784 | "scopeguard", 785 | ] 786 | 787 | [[package]] 788 | name = "log" 789 | version = "0.4.25" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 792 | 793 | [[package]] 794 | name = "matchers" 795 | version = "0.1.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 798 | dependencies = [ 799 | "regex-automata 0.1.10", 800 | ] 801 | 802 | [[package]] 803 | name = "memchr" 804 | version = "2.7.2" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 807 | 808 | [[package]] 809 | name = "memoffset" 810 | version = "0.9.1" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 813 | dependencies = [ 814 | "autocfg", 815 | ] 816 | 817 | [[package]] 818 | name = "mime" 819 | version = "0.3.17" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 822 | 823 | [[package]] 824 | name = "miniz_oxide" 825 | version = "0.7.3" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" 828 | dependencies = [ 829 | "adler", 830 | ] 831 | 832 | [[package]] 833 | name = "mio" 834 | version = "1.0.3" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 837 | dependencies = [ 838 | "libc", 839 | "wasi 0.11.0+wasi-snapshot-preview1", 840 | "windows-sys 0.52.0", 841 | ] 842 | 843 | [[package]] 844 | name = "native-tls" 845 | version = "0.2.11" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 848 | dependencies = [ 849 | "lazy_static", 850 | "libc", 851 | "log", 852 | "openssl", 853 | "openssl-probe", 854 | "openssl-sys", 855 | "schannel", 856 | "security-framework 2.8.2", 857 | "security-framework-sys", 858 | "tempfile", 859 | ] 860 | 861 | [[package]] 862 | name = "nu-ansi-term" 863 | version = "0.46.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 866 | dependencies = [ 867 | "overload", 868 | "winapi", 869 | ] 870 | 871 | [[package]] 872 | name = "num-traits" 873 | version = "0.2.19" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 876 | dependencies = [ 877 | "autocfg", 878 | "libm", 879 | ] 880 | 881 | [[package]] 882 | name = "object" 883 | version = "0.32.2" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 886 | dependencies = [ 887 | "memchr", 888 | ] 889 | 890 | [[package]] 891 | name = "once_cell" 892 | version = "1.20.2" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 895 | 896 | [[package]] 897 | name = "openssl" 898 | version = "0.10.72" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" 901 | dependencies = [ 902 | "bitflags 2.6.0", 903 | "cfg-if", 904 | "foreign-types", 905 | "libc", 906 | "once_cell", 907 | "openssl-macros", 908 | "openssl-sys", 909 | ] 910 | 911 | [[package]] 912 | name = "openssl-macros" 913 | version = "0.1.1" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 916 | dependencies = [ 917 | "proc-macro2", 918 | "quote", 919 | "syn 2.0.95", 920 | ] 921 | 922 | [[package]] 923 | name = "openssl-probe" 924 | version = "0.1.5" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 927 | 928 | [[package]] 929 | name = "openssl-src" 930 | version = "300.2.3+3.2.1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" 933 | dependencies = [ 934 | "cc", 935 | ] 936 | 937 | [[package]] 938 | name = "openssl-sys" 939 | version = "0.9.107" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" 942 | dependencies = [ 943 | "cc", 944 | "libc", 945 | "openssl-src", 946 | "pkg-config", 947 | "vcpkg", 948 | ] 949 | 950 | [[package]] 951 | name = "output_vt100" 952 | version = "0.1.3" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" 955 | dependencies = [ 956 | "winapi", 957 | ] 958 | 959 | [[package]] 960 | name = "overload" 961 | version = "0.1.1" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 964 | 965 | [[package]] 966 | name = "parking_lot" 967 | version = "0.12.4" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 970 | dependencies = [ 971 | "lock_api", 972 | "parking_lot_core", 973 | ] 974 | 975 | [[package]] 976 | name = "parking_lot_core" 977 | version = "0.9.11" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 980 | dependencies = [ 981 | "cfg-if", 982 | "libc", 983 | "redox_syscall", 984 | "smallvec", 985 | "windows-targets", 986 | ] 987 | 988 | [[package]] 989 | name = "percent-encoding" 990 | version = "2.3.1" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 993 | 994 | [[package]] 995 | name = "pin-project-lite" 996 | version = "0.2.16" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 999 | 1000 | [[package]] 1001 | name = "pin-utils" 1002 | version = "0.1.0" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1005 | 1006 | [[package]] 1007 | name = "pkg-config" 1008 | version = "0.3.26" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 1011 | 1012 | [[package]] 1013 | name = "portable-atomic" 1014 | version = "1.11.0" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1017 | 1018 | [[package]] 1019 | name = "ppv-lite86" 1020 | version = "0.2.17" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1023 | 1024 | [[package]] 1025 | name = "pretty_assertions" 1026 | version = "1.3.0" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" 1029 | dependencies = [ 1030 | "ctor", 1031 | "diff", 1032 | "output_vt100", 1033 | "yansi", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "proc-macro2" 1038 | version = "1.0.92" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 1041 | dependencies = [ 1042 | "unicode-ident", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "proptest" 1047 | version = "1.5.0" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" 1050 | dependencies = [ 1051 | "bit-set", 1052 | "bit-vec", 1053 | "bitflags 2.6.0", 1054 | "lazy_static", 1055 | "num-traits", 1056 | "rand 0.8.5", 1057 | "rand_chacha 0.3.1", 1058 | "rand_xorshift", 1059 | "regex-syntax 0.8.5", 1060 | "rusty-fork", 1061 | "tempfile", 1062 | "unarray", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "proptest-derive" 1067 | version = "0.5.0" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" 1070 | dependencies = [ 1071 | "proc-macro2", 1072 | "quote", 1073 | "syn 2.0.95", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "pyo3" 1078 | version = "0.24.2" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" 1081 | dependencies = [ 1082 | "cfg-if", 1083 | "indoc", 1084 | "libc", 1085 | "memoffset", 1086 | "once_cell", 1087 | "portable-atomic", 1088 | "pyo3-build-config", 1089 | "pyo3-ffi", 1090 | "pyo3-macros", 1091 | "unindent", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "pyo3-async-runtimes" 1096 | version = "0.24.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "dd0b83dc42f9d41f50d38180dad65f0c99763b65a3ff2a81bf351dd35a1df8bf" 1099 | dependencies = [ 1100 | "futures", 1101 | "once_cell", 1102 | "pin-project-lite", 1103 | "pyo3", 1104 | "tokio", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "pyo3-build-config" 1109 | version = "0.24.2" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" 1112 | dependencies = [ 1113 | "once_cell", 1114 | "target-lexicon", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "pyo3-ffi" 1119 | version = "0.24.2" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" 1122 | dependencies = [ 1123 | "libc", 1124 | "pyo3-build-config", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "pyo3-macros" 1129 | version = "0.24.2" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" 1132 | dependencies = [ 1133 | "proc-macro2", 1134 | "pyo3-macros-backend", 1135 | "quote", 1136 | "syn 2.0.95", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "pyo3-macros-backend" 1141 | version = "0.24.2" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" 1144 | dependencies = [ 1145 | "heck", 1146 | "proc-macro2", 1147 | "pyo3-build-config", 1148 | "quote", 1149 | "syn 2.0.95", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "quick-error" 1154 | version = "1.2.3" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1157 | 1158 | [[package]] 1159 | name = "quote" 1160 | version = "1.0.38" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1163 | dependencies = [ 1164 | "proc-macro2", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "rand" 1169 | version = "0.8.5" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1172 | dependencies = [ 1173 | "libc", 1174 | "rand_chacha 0.3.1", 1175 | "rand_core 0.6.4", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "rand" 1180 | version = "0.9.0" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 1183 | dependencies = [ 1184 | "rand_chacha 0.9.0", 1185 | "rand_core 0.9.1", 1186 | "zerocopy", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "rand_chacha" 1191 | version = "0.3.1" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1194 | dependencies = [ 1195 | "ppv-lite86", 1196 | "rand_core 0.6.4", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "rand_chacha" 1201 | version = "0.9.0" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1204 | dependencies = [ 1205 | "ppv-lite86", 1206 | "rand_core 0.9.1", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "rand_core" 1211 | version = "0.6.4" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1214 | dependencies = [ 1215 | "getrandom 0.2.15", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "rand_core" 1220 | version = "0.9.1" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" 1223 | dependencies = [ 1224 | "getrandom 0.3.1", 1225 | "zerocopy", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "rand_xorshift" 1230 | version = "0.3.0" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 1233 | dependencies = [ 1234 | "rand_core 0.6.4", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "rand_xoshiro" 1239 | version = "0.7.0" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" 1242 | dependencies = [ 1243 | "rand_core 0.9.1", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "redox_syscall" 1248 | version = "0.5.10" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 1251 | dependencies = [ 1252 | "bitflags 2.6.0", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "regex" 1257 | version = "1.10.4" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 1260 | dependencies = [ 1261 | "aho-corasick", 1262 | "memchr", 1263 | "regex-automata 0.4.6", 1264 | "regex-syntax 0.8.5", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "regex-automata" 1269 | version = "0.1.10" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1272 | dependencies = [ 1273 | "regex-syntax 0.6.29", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "regex-automata" 1278 | version = "0.4.6" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 1281 | dependencies = [ 1282 | "aho-corasick", 1283 | "memchr", 1284 | "regex-syntax 0.8.5", 1285 | ] 1286 | 1287 | [[package]] 1288 | name = "regex-syntax" 1289 | version = "0.6.29" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1292 | 1293 | [[package]] 1294 | name = "regex-syntax" 1295 | version = "0.8.5" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1298 | 1299 | [[package]] 1300 | name = "ring" 1301 | version = "0.17.14" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1304 | dependencies = [ 1305 | "cc", 1306 | "cfg-if", 1307 | "getrandom 0.2.15", 1308 | "libc", 1309 | "untrusted", 1310 | "windows-sys 0.52.0", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "rustc-demangle" 1315 | version = "0.1.22" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" 1318 | 1319 | [[package]] 1320 | name = "rustix" 1321 | version = "0.38.43" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" 1324 | dependencies = [ 1325 | "bitflags 2.6.0", 1326 | "errno", 1327 | "libc", 1328 | "linux-raw-sys", 1329 | "windows-sys 0.59.0", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "rustls" 1334 | version = "0.23.23" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" 1337 | dependencies = [ 1338 | "once_cell", 1339 | "rustls-pki-types", 1340 | "rustls-webpki", 1341 | "subtle", 1342 | "zeroize", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "rustls-native-certs" 1347 | version = "0.8.1" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 1350 | dependencies = [ 1351 | "openssl-probe", 1352 | "rustls-pki-types", 1353 | "schannel", 1354 | "security-framework 3.2.0", 1355 | ] 1356 | 1357 | [[package]] 1358 | name = "rustls-pki-types" 1359 | version = "1.11.0" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1362 | 1363 | [[package]] 1364 | name = "rustls-webpki" 1365 | version = "0.102.8" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1368 | dependencies = [ 1369 | "ring", 1370 | "rustls-pki-types", 1371 | "untrusted", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "rusty-fork" 1376 | version = "0.3.0" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 1379 | dependencies = [ 1380 | "fnv", 1381 | "quick-error", 1382 | "tempfile", 1383 | "wait-timeout", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "ryu" 1388 | version = "1.0.13" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 1391 | 1392 | [[package]] 1393 | name = "schannel" 1394 | version = "0.1.21" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 1397 | dependencies = [ 1398 | "windows-sys 0.42.0", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "scopeguard" 1403 | version = "1.1.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1406 | 1407 | [[package]] 1408 | name = "security-framework" 1409 | version = "2.8.2" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 1412 | dependencies = [ 1413 | "bitflags 1.3.2", 1414 | "core-foundation 0.9.3", 1415 | "core-foundation-sys", 1416 | "libc", 1417 | "security-framework-sys", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "security-framework" 1422 | version = "3.2.0" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 1425 | dependencies = [ 1426 | "bitflags 2.6.0", 1427 | "core-foundation 0.10.0", 1428 | "core-foundation-sys", 1429 | "libc", 1430 | "security-framework-sys", 1431 | ] 1432 | 1433 | [[package]] 1434 | name = "security-framework-sys" 1435 | version = "2.14.0" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1438 | dependencies = [ 1439 | "core-foundation-sys", 1440 | "libc", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "serde" 1445 | version = "1.0.219" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1448 | dependencies = [ 1449 | "serde_derive", 1450 | ] 1451 | 1452 | [[package]] 1453 | name = "serde_derive" 1454 | version = "1.0.219" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1457 | dependencies = [ 1458 | "proc-macro2", 1459 | "quote", 1460 | "syn 2.0.95", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "serde_json" 1465 | version = "1.0.132" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 1468 | dependencies = [ 1469 | "indexmap", 1470 | "itoa", 1471 | "memchr", 1472 | "ryu", 1473 | "serde", 1474 | ] 1475 | 1476 | [[package]] 1477 | name = "sha1" 1478 | version = "0.10.5" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 1481 | dependencies = [ 1482 | "cfg-if", 1483 | "cpufeatures", 1484 | "digest", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "sharded-slab" 1489 | version = "0.1.4" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1492 | dependencies = [ 1493 | "lazy_static", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "shlex" 1498 | version = "1.3.0" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1501 | 1502 | [[package]] 1503 | name = "signal-hook-registry" 1504 | version = "1.4.1" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1507 | dependencies = [ 1508 | "libc", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "slab" 1513 | version = "0.4.9" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1516 | dependencies = [ 1517 | "autocfg", 1518 | ] 1519 | 1520 | [[package]] 1521 | name = "smallvec" 1522 | version = "1.15.0" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1525 | 1526 | [[package]] 1527 | name = "socket2" 1528 | version = "0.5.7" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1531 | dependencies = [ 1532 | "libc", 1533 | "windows-sys 0.52.0", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "stable_deref_trait" 1538 | version = "1.2.0" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1541 | 1542 | [[package]] 1543 | name = "subtle" 1544 | version = "2.5.0" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 1547 | 1548 | [[package]] 1549 | name = "syn" 1550 | version = "1.0.109" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1553 | dependencies = [ 1554 | "proc-macro2", 1555 | "quote", 1556 | "unicode-ident", 1557 | ] 1558 | 1559 | [[package]] 1560 | name = "syn" 1561 | version = "2.0.95" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" 1564 | dependencies = [ 1565 | "proc-macro2", 1566 | "quote", 1567 | "unicode-ident", 1568 | ] 1569 | 1570 | [[package]] 1571 | name = "synstructure" 1572 | version = "0.13.1" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1575 | dependencies = [ 1576 | "proc-macro2", 1577 | "quote", 1578 | "syn 2.0.95", 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "target-lexicon" 1583 | version = "0.13.2" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" 1586 | 1587 | [[package]] 1588 | name = "tempfile" 1589 | version = "3.15.0" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" 1592 | dependencies = [ 1593 | "cfg-if", 1594 | "fastrand", 1595 | "getrandom 0.2.15", 1596 | "once_cell", 1597 | "rustix", 1598 | "windows-sys 0.59.0", 1599 | ] 1600 | 1601 | [[package]] 1602 | name = "thiserror" 1603 | version = "2.0.11" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1606 | dependencies = [ 1607 | "thiserror-impl", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "thiserror-impl" 1612 | version = "2.0.11" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1615 | dependencies = [ 1616 | "proc-macro2", 1617 | "quote", 1618 | "syn 2.0.95", 1619 | ] 1620 | 1621 | [[package]] 1622 | name = "thread_local" 1623 | version = "1.1.7" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 1626 | dependencies = [ 1627 | "cfg-if", 1628 | "once_cell", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "tinystr" 1633 | version = "0.7.6" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1636 | dependencies = [ 1637 | "displaydoc", 1638 | "zerovec", 1639 | ] 1640 | 1641 | [[package]] 1642 | name = "tokio" 1643 | version = "1.43.1" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c" 1646 | dependencies = [ 1647 | "backtrace", 1648 | "bytes", 1649 | "libc", 1650 | "mio", 1651 | "parking_lot", 1652 | "pin-project-lite", 1653 | "signal-hook-registry", 1654 | "socket2", 1655 | "tokio-macros", 1656 | "windows-sys 0.52.0", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "tokio-macros" 1661 | version = "2.5.0" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1664 | dependencies = [ 1665 | "proc-macro2", 1666 | "quote", 1667 | "syn 2.0.95", 1668 | ] 1669 | 1670 | [[package]] 1671 | name = "tokio-native-tls" 1672 | version = "0.3.1" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1675 | dependencies = [ 1676 | "native-tls", 1677 | "tokio", 1678 | ] 1679 | 1680 | [[package]] 1681 | name = "tokio-rustls" 1682 | version = "0.26.2" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1685 | dependencies = [ 1686 | "rustls", 1687 | "tokio", 1688 | ] 1689 | 1690 | [[package]] 1691 | name = "tokio-stream" 1692 | version = "0.1.16" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" 1695 | dependencies = [ 1696 | "futures-core", 1697 | "pin-project-lite", 1698 | "tokio", 1699 | "tokio-util", 1700 | ] 1701 | 1702 | [[package]] 1703 | name = "tokio-tungstenite" 1704 | version = "0.26.2" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" 1707 | dependencies = [ 1708 | "futures-util", 1709 | "log", 1710 | "native-tls", 1711 | "rustls", 1712 | "rustls-native-certs", 1713 | "rustls-pki-types", 1714 | "tokio", 1715 | "tokio-native-tls", 1716 | "tokio-rustls", 1717 | "tungstenite", 1718 | "webpki-roots", 1719 | ] 1720 | 1721 | [[package]] 1722 | name = "tokio-util" 1723 | version = "0.7.14" 1724 | source = "registry+https://github.com/rust-lang/crates.io-index" 1725 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 1726 | dependencies = [ 1727 | "bytes", 1728 | "futures-core", 1729 | "futures-sink", 1730 | "pin-project-lite", 1731 | "tokio", 1732 | ] 1733 | 1734 | [[package]] 1735 | name = "tracing" 1736 | version = "0.1.40" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1739 | dependencies = [ 1740 | "pin-project-lite", 1741 | "tracing-attributes", 1742 | "tracing-core", 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "tracing-attributes" 1747 | version = "0.1.27" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1750 | dependencies = [ 1751 | "proc-macro2", 1752 | "quote", 1753 | "syn 2.0.95", 1754 | ] 1755 | 1756 | [[package]] 1757 | name = "tracing-core" 1758 | version = "0.1.32" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1761 | dependencies = [ 1762 | "once_cell", 1763 | "valuable", 1764 | ] 1765 | 1766 | [[package]] 1767 | name = "tracing-log" 1768 | version = "0.1.3" 1769 | source = "registry+https://github.com/rust-lang/crates.io-index" 1770 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1771 | dependencies = [ 1772 | "lazy_static", 1773 | "log", 1774 | "tracing-core", 1775 | ] 1776 | 1777 | [[package]] 1778 | name = "tracing-subscriber" 1779 | version = "0.3.17" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 1782 | dependencies = [ 1783 | "matchers", 1784 | "nu-ansi-term", 1785 | "once_cell", 1786 | "regex", 1787 | "sharded-slab", 1788 | "smallvec", 1789 | "thread_local", 1790 | "tracing", 1791 | "tracing-core", 1792 | "tracing-log", 1793 | ] 1794 | 1795 | [[package]] 1796 | name = "tungstenite" 1797 | version = "0.26.2" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" 1800 | dependencies = [ 1801 | "bytes", 1802 | "data-encoding", 1803 | "http", 1804 | "httparse", 1805 | "log", 1806 | "native-tls", 1807 | "rand 0.9.0", 1808 | "rustls", 1809 | "rustls-pki-types", 1810 | "sha1", 1811 | "thiserror", 1812 | "url", 1813 | "utf-8", 1814 | ] 1815 | 1816 | [[package]] 1817 | name = "typenum" 1818 | version = "1.17.0" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1821 | 1822 | [[package]] 1823 | name = "unarray" 1824 | version = "0.1.4" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 1827 | 1828 | [[package]] 1829 | name = "unicode-ident" 1830 | version = "1.0.8" 1831 | source = "registry+https://github.com/rust-lang/crates.io-index" 1832 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 1833 | 1834 | [[package]] 1835 | name = "unicode-segmentation" 1836 | version = "1.12.0" 1837 | source = "registry+https://github.com/rust-lang/crates.io-index" 1838 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1839 | 1840 | [[package]] 1841 | name = "unicode-xid" 1842 | version = "0.2.6" 1843 | source = "registry+https://github.com/rust-lang/crates.io-index" 1844 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1845 | 1846 | [[package]] 1847 | name = "unindent" 1848 | version = "0.2.4" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" 1851 | 1852 | [[package]] 1853 | name = "untrusted" 1854 | version = "0.9.0" 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" 1856 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1857 | 1858 | [[package]] 1859 | name = "url" 1860 | version = "2.5.4" 1861 | source = "registry+https://github.com/rust-lang/crates.io-index" 1862 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1863 | dependencies = [ 1864 | "form_urlencoded", 1865 | "idna", 1866 | "percent-encoding", 1867 | ] 1868 | 1869 | [[package]] 1870 | name = "utf-8" 1871 | version = "0.7.6" 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" 1873 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1874 | 1875 | [[package]] 1876 | name = "utf16_iter" 1877 | version = "1.0.5" 1878 | source = "registry+https://github.com/rust-lang/crates.io-index" 1879 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1880 | 1881 | [[package]] 1882 | name = "utf8_iter" 1883 | version = "1.0.4" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1886 | 1887 | [[package]] 1888 | name = "uuid" 1889 | version = "1.8.0" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" 1892 | dependencies = [ 1893 | "getrandom 0.2.15", 1894 | "serde", 1895 | ] 1896 | 1897 | [[package]] 1898 | name = "valuable" 1899 | version = "0.1.0" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1902 | 1903 | [[package]] 1904 | name = "vcpkg" 1905 | version = "0.2.15" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1908 | 1909 | [[package]] 1910 | name = "version_check" 1911 | version = "0.9.4" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1914 | 1915 | [[package]] 1916 | name = "wait-timeout" 1917 | version = "0.2.0" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1920 | dependencies = [ 1921 | "libc", 1922 | ] 1923 | 1924 | [[package]] 1925 | name = "wasi" 1926 | version = "0.11.0+wasi-snapshot-preview1" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1929 | 1930 | [[package]] 1931 | name = "wasi" 1932 | version = "0.13.3+wasi-0.2.2" 1933 | source = "registry+https://github.com/rust-lang/crates.io-index" 1934 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1935 | dependencies = [ 1936 | "wit-bindgen-rt", 1937 | ] 1938 | 1939 | [[package]] 1940 | name = "webpki-roots" 1941 | version = "0.26.8" 1942 | source = "registry+https://github.com/rust-lang/crates.io-index" 1943 | checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" 1944 | dependencies = [ 1945 | "rustls-pki-types", 1946 | ] 1947 | 1948 | [[package]] 1949 | name = "winapi" 1950 | version = "0.3.9" 1951 | source = "registry+https://github.com/rust-lang/crates.io-index" 1952 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1953 | dependencies = [ 1954 | "winapi-i686-pc-windows-gnu", 1955 | "winapi-x86_64-pc-windows-gnu", 1956 | ] 1957 | 1958 | [[package]] 1959 | name = "winapi-i686-pc-windows-gnu" 1960 | version = "0.4.0" 1961 | source = "registry+https://github.com/rust-lang/crates.io-index" 1962 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1963 | 1964 | [[package]] 1965 | name = "winapi-x86_64-pc-windows-gnu" 1966 | version = "0.4.0" 1967 | source = "registry+https://github.com/rust-lang/crates.io-index" 1968 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1969 | 1970 | [[package]] 1971 | name = "windows-sys" 1972 | version = "0.42.0" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1975 | dependencies = [ 1976 | "windows_aarch64_gnullvm 0.42.2", 1977 | "windows_aarch64_msvc 0.42.2", 1978 | "windows_i686_gnu 0.42.2", 1979 | "windows_i686_msvc 0.42.2", 1980 | "windows_x86_64_gnu 0.42.2", 1981 | "windows_x86_64_gnullvm 0.42.2", 1982 | "windows_x86_64_msvc 0.42.2", 1983 | ] 1984 | 1985 | [[package]] 1986 | name = "windows-sys" 1987 | version = "0.52.0" 1988 | source = "registry+https://github.com/rust-lang/crates.io-index" 1989 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1990 | dependencies = [ 1991 | "windows-targets", 1992 | ] 1993 | 1994 | [[package]] 1995 | name = "windows-sys" 1996 | version = "0.59.0" 1997 | source = "registry+https://github.com/rust-lang/crates.io-index" 1998 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1999 | dependencies = [ 2000 | "windows-targets", 2001 | ] 2002 | 2003 | [[package]] 2004 | name = "windows-targets" 2005 | version = "0.52.6" 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" 2007 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2008 | dependencies = [ 2009 | "windows_aarch64_gnullvm 0.52.6", 2010 | "windows_aarch64_msvc 0.52.6", 2011 | "windows_i686_gnu 0.52.6", 2012 | "windows_i686_gnullvm", 2013 | "windows_i686_msvc 0.52.6", 2014 | "windows_x86_64_gnu 0.52.6", 2015 | "windows_x86_64_gnullvm 0.52.6", 2016 | "windows_x86_64_msvc 0.52.6", 2017 | ] 2018 | 2019 | [[package]] 2020 | name = "windows_aarch64_gnullvm" 2021 | version = "0.42.2" 2022 | source = "registry+https://github.com/rust-lang/crates.io-index" 2023 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 2024 | 2025 | [[package]] 2026 | name = "windows_aarch64_gnullvm" 2027 | version = "0.52.6" 2028 | source = "registry+https://github.com/rust-lang/crates.io-index" 2029 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2030 | 2031 | [[package]] 2032 | name = "windows_aarch64_msvc" 2033 | version = "0.42.2" 2034 | source = "registry+https://github.com/rust-lang/crates.io-index" 2035 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 2036 | 2037 | [[package]] 2038 | name = "windows_aarch64_msvc" 2039 | version = "0.52.6" 2040 | source = "registry+https://github.com/rust-lang/crates.io-index" 2041 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2042 | 2043 | [[package]] 2044 | name = "windows_i686_gnu" 2045 | version = "0.42.2" 2046 | source = "registry+https://github.com/rust-lang/crates.io-index" 2047 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 2048 | 2049 | [[package]] 2050 | name = "windows_i686_gnu" 2051 | version = "0.52.6" 2052 | source = "registry+https://github.com/rust-lang/crates.io-index" 2053 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2054 | 2055 | [[package]] 2056 | name = "windows_i686_gnullvm" 2057 | version = "0.52.6" 2058 | source = "registry+https://github.com/rust-lang/crates.io-index" 2059 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2060 | 2061 | [[package]] 2062 | name = "windows_i686_msvc" 2063 | version = "0.42.2" 2064 | source = "registry+https://github.com/rust-lang/crates.io-index" 2065 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 2066 | 2067 | [[package]] 2068 | name = "windows_i686_msvc" 2069 | version = "0.52.6" 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" 2071 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2072 | 2073 | [[package]] 2074 | name = "windows_x86_64_gnu" 2075 | version = "0.42.2" 2076 | source = "registry+https://github.com/rust-lang/crates.io-index" 2077 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 2078 | 2079 | [[package]] 2080 | name = "windows_x86_64_gnu" 2081 | version = "0.52.6" 2082 | source = "registry+https://github.com/rust-lang/crates.io-index" 2083 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2084 | 2085 | [[package]] 2086 | name = "windows_x86_64_gnullvm" 2087 | version = "0.42.2" 2088 | source = "registry+https://github.com/rust-lang/crates.io-index" 2089 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 2090 | 2091 | [[package]] 2092 | name = "windows_x86_64_gnullvm" 2093 | version = "0.52.6" 2094 | source = "registry+https://github.com/rust-lang/crates.io-index" 2095 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2096 | 2097 | [[package]] 2098 | name = "windows_x86_64_msvc" 2099 | version = "0.42.2" 2100 | source = "registry+https://github.com/rust-lang/crates.io-index" 2101 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 2102 | 2103 | [[package]] 2104 | name = "windows_x86_64_msvc" 2105 | version = "0.52.6" 2106 | source = "registry+https://github.com/rust-lang/crates.io-index" 2107 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2108 | 2109 | [[package]] 2110 | name = "wit-bindgen-rt" 2111 | version = "0.33.0" 2112 | source = "registry+https://github.com/rust-lang/crates.io-index" 2113 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 2114 | dependencies = [ 2115 | "bitflags 2.6.0", 2116 | ] 2117 | 2118 | [[package]] 2119 | name = "write16" 2120 | version = "1.0.0" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2123 | 2124 | [[package]] 2125 | name = "writeable" 2126 | version = "0.5.5" 2127 | source = "registry+https://github.com/rust-lang/crates.io-index" 2128 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2129 | 2130 | [[package]] 2131 | name = "yansi" 2132 | version = "0.5.1" 2133 | source = "registry+https://github.com/rust-lang/crates.io-index" 2134 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 2135 | 2136 | [[package]] 2137 | name = "yoke" 2138 | version = "0.7.5" 2139 | source = "registry+https://github.com/rust-lang/crates.io-index" 2140 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2141 | dependencies = [ 2142 | "serde", 2143 | "stable_deref_trait", 2144 | "yoke-derive", 2145 | "zerofrom", 2146 | ] 2147 | 2148 | [[package]] 2149 | name = "yoke-derive" 2150 | version = "0.7.5" 2151 | source = "registry+https://github.com/rust-lang/crates.io-index" 2152 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2153 | dependencies = [ 2154 | "proc-macro2", 2155 | "quote", 2156 | "syn 2.0.95", 2157 | "synstructure", 2158 | ] 2159 | 2160 | [[package]] 2161 | name = "zerocopy" 2162 | version = "0.8.20" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" 2165 | dependencies = [ 2166 | "zerocopy-derive", 2167 | ] 2168 | 2169 | [[package]] 2170 | name = "zerocopy-derive" 2171 | version = "0.8.20" 2172 | source = "registry+https://github.com/rust-lang/crates.io-index" 2173 | checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" 2174 | dependencies = [ 2175 | "proc-macro2", 2176 | "quote", 2177 | "syn 2.0.95", 2178 | ] 2179 | 2180 | [[package]] 2181 | name = "zerofrom" 2182 | version = "0.1.4" 2183 | source = "registry+https://github.com/rust-lang/crates.io-index" 2184 | checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" 2185 | dependencies = [ 2186 | "zerofrom-derive", 2187 | ] 2188 | 2189 | [[package]] 2190 | name = "zerofrom-derive" 2191 | version = "0.1.5" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 2194 | dependencies = [ 2195 | "proc-macro2", 2196 | "quote", 2197 | "syn 2.0.95", 2198 | "synstructure", 2199 | ] 2200 | 2201 | [[package]] 2202 | name = "zeroize" 2203 | version = "1.8.1" 2204 | source = "registry+https://github.com/rust-lang/crates.io-index" 2205 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2206 | 2207 | [[package]] 2208 | name = "zerovec" 2209 | version = "0.10.4" 2210 | source = "registry+https://github.com/rust-lang/crates.io-index" 2211 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2212 | dependencies = [ 2213 | "yoke", 2214 | "zerofrom", 2215 | "zerovec-derive", 2216 | ] 2217 | 2218 | [[package]] 2219 | name = "zerovec-derive" 2220 | version = "0.10.3" 2221 | source = "registry+https://github.com/rust-lang/crates.io-index" 2222 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2223 | dependencies = [ 2224 | "proc-macro2", 2225 | "quote", 2226 | "syn 2.0.95", 2227 | ] 2228 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "_convex" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | 7 | [lib] 8 | name = "_convex" 9 | crate-type = [ "cdylib" ] 10 | 11 | [dependencies] 12 | convex = { version = "=0.9.0", default-features = false } 13 | futures = { version = "0.3" } 14 | parking_lot = { version = "0.12" } 15 | pyo3 = { features = [ "abi3", "abi3-py39" ], version = "0.24" } 16 | pyo3-async-runtimes = { version = "0.24", features = [ "tokio-runtime" ] } 17 | tokio = { features = [ "full" ], version = "1" } 18 | tracing = { version = "0.1" } 19 | tracing-subscriber = { features = [ "env-filter" ], version = "0.3.17" } 20 | 21 | [dev-dependencies] 22 | convex = { version = "=0.9.0", default-features = false, features = [ "testing" ] } 23 | tracing-subscriber = { features = [ "env-filter" ], version = "0.3.17" } 24 | 25 | [build-dependencies] 26 | pyo3-build-config = { version = "0.24" } 27 | 28 | [features] 29 | default = [ "native-tls-vendored" ] 30 | native-tls = [ "convex/native-tls" ] 31 | native-tls-vendored = [ "convex/native-tls-vendored" ] 32 | rustls-tls-native-roots = [ "convex/rustls-tls-native-roots" ] 33 | rustls-tls-webpki-roots = [ "convex/rustls-tls-webpki-roots" ] 34 | 35 | [lints.rust] 36 | unused_extern_crates = "warn" 37 | 38 | [lints.clippy] 39 | await_holding_lock = "warn" 40 | await_holding_refcell_ref = "warn" 41 | large_enum_variant = "allow" 42 | manual_map = "allow" 43 | new_without_default = "allow" 44 | op_ref = "allow" 45 | ptr_arg = "allow" 46 | single_match = "allow" 47 | too_many_arguments = "allow" 48 | type_complexity = "allow" 49 | upper_case_acronyms = "allow" 50 | useless_format = "allow" 51 | useless_vec = "allow" 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2025 Convex, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Deploy Process 2 | 3 | To manually deploy properly to pypi, use the `python-client-build` job whose 4 | instructions are at `go/push/open-source` 5 | 6 | ## Test publish 7 | 8 | This is useful to test out stuff against testpypi. You probably won't need it 9 | much, but if you do - here's how it works. 10 | 11 | First set up a section of your ~/.pypirc with a token 12 | 13 | ``` 14 | [testpypi] 15 | username = __token__ 16 | password = 17 | ``` 18 | 19 | Then increment the version number in pyproject.yaml and open that PR. Download 20 | the wheels artifact (aprox. 100MB) from the GitHub CI workflow 21 | python-client-build.yml and copy it into the dist directory. 22 | 23 | ``` 24 | # This is only required for distribution 25 | rm -r dist python/_convex/_convex.*.so 26 | poetry install 27 | poetry run maturin build --out dist 28 | # test publish 29 | MATURIN_REPOSITORY=testpypi maturin upload dist/* 30 | # Now you can download thei convex package from test pypi 31 | python -m pip install --index-url https://test.pypi.org/simple/ convex 32 | ``` 33 | 34 | Navigate https://pypi.org/project/convex/ and double check things look good. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convex 2 | 3 | The official Python client for [Convex](https://convex.dev/). 4 | 5 | ![PyPI](https://img.shields.io/pypi/v/convex?label=convex&logo=pypi) 6 | ![GitHub](https://img.shields.io/github/license/get-convex/convex-py) 7 | 8 | Write and read data from a Convex backend with queries, mutations, and actions. 9 | Get up and running at [docs.convex.dev](https://docs.convex.dev/home). 10 | 11 | Installation: 12 | 13 | pip install convex 14 | 15 | Basic usage: 16 | 17 | ```python 18 | >>> from convex import ConvexClient 19 | >>> client = ConvexClient('https://example-lion-123.convex.cloud') 20 | >>> messages = client.query("messages:list") 21 | >>> from pprint import pprint 22 | >>> pprint(messages) 23 | [{'_creationTime': 1668107495676.2854, 24 | '_id': '2sh2c7pn6nyvkexbdsfj66vd9h5q3hg', 25 | 'author': 'Tom', 26 | 'body': 'Have you tried Convex?'}, 27 | {'_creationTime': 1668107497732.2295, 28 | '_id': '1f053fgh2tt2fc93mw3sn2x09h5bj08', 29 | 'author': 'Sarah', 30 | 'body': "Yeah, it's working pretty well for me."}] 31 | >>> client.mutation("messages:send", dict(author="Me", body="Hello!")) 32 | >>> for mesages client.subscribe("messages:list", {}): 33 | ... print(len(messages)) 34 | ... 35 | 3 36 | 37 | ``` 38 | 39 | To find the url of your convex backend, open the deployment you want to work 40 | with in the appropriate project in the 41 | [Convex dashboard](https://dashboard.convex.dev) and click "Settings" where the 42 | Deployment URL should be visible. To find out which queries, mutations, and 43 | actions are available check the Functions pane in the dashboard. 44 | 45 | To see logs emitted from Convex functions, set the debug mode to True. 46 | 47 | ```python 48 | >>> client.set_debug(True) 49 | ``` 50 | 51 | To provide authentication for function execution, call `set_auth()`. 52 | 53 | ```python 54 | >>> client.set_auth("token-from-authetication-flow") 55 | ``` 56 | 57 | [Join us on Discord](https://www.convex.dev/community) to get your questions 58 | answered or share what you're doing with Convex. If you're just getting started, 59 | see https://docs.convex.dev to see how to quickly spin up a backend that does 60 | everything you need in the Convex cloud. 61 | 62 | # Convex types 63 | 64 | Convex backend functions are written in JavaScript, so arguments passed to 65 | Convex RPC functions in Python are serialized, sent over the network, and 66 | deserialized into JavaScript objects. To learn about Convex's supported types 67 | see https://docs.convex.dev/using/types. 68 | 69 | In order to call a function that expects a JavaScript type, use the 70 | corresponding Python type or any other type that coerces to it. Values returned 71 | from Convex will be of the corresponding Python type. 72 | 73 | | JavaScript Type | Python Type | Example | Other Python Types that Convert | 74 | | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------- | 75 | | [null](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#null_type) | [None](https://docs.python.org/3/library/stdtypes.html#the-null-object) | `None` | | 76 | | [bigint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#bigint_type) | ConvexInt64 (see below) | `ConvexInt64(2**60)` | | 77 | | [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type) | [float](https://docs.python.org/3/library/functions.html#float) or [int](https://docs.python.org/3/library/functions.html#int) | `3.1`, `10` | | 78 | | [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type) | [bool](https://docs.python.org/3/library/functions.html#bool) | `True`, `False` | | 79 | | [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | [str](https://docs.python.org/3/library/stdtypes.html#str) | `'abc'` | | 80 | | [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) | [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) | `b'abc'` | ArrayBuffer | 81 | | [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) | [list](https://docs.python.org/3/library/stdtypes.html#list) | `[1, 3.2, "abc"]` | tuple, collections.abc.Sequence | 82 | | [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#objects) | [dict](https://docs.python.org/3/library/stdtypes.html#dict) | `{a: "abc"}` | collections.abc.Mapping | 83 | 84 | ### Ints and Floats 85 | 86 | While 87 | [Convex supports storing Int64s and Float64s](https://docs.convex.dev/using/types#convex-types), 88 | idiomatic JavaScript pervasively uses the (floating point) `Number` type. In 89 | Python `float`s are often understood to contain the `int`s: the `float` type 90 | annotation is 91 | [generally understood as `Union[int, float]`](https://peps.python.org/pep-0484/#the-numeric-tower). 92 | 93 | Therefore, the Python Convex client converts Python's `float`s and `int`s to a 94 | `Float64` in Convex. 95 | 96 | To specify a JavaScript BigInt, use the ConvexInt64 class. Functions which 97 | return JavaScript BigInts will return ConvexInt64 instances. 98 | 99 | # Convex Errors 100 | 101 | The Python client supports the `ConvexError` type to hold application errors 102 | that are propagated from your Convex functions. To learn about how to throw 103 | `ConvexError`s see 104 | https://docs.convex.dev/functions/error-handling/application-errors. 105 | 106 | On the Python client, `ConvexError`s are Exceptions with a `data` field that 107 | contains some `ConvexValue`. Handling application errors from the Python client 108 | might look something like this: 109 | 110 | ```python 111 | import convex 112 | client = convex.ConvexClient('https://happy-animal-123.convex.cloud') 113 | 114 | try: 115 | client.mutation("messages:sendMessage", {body: "hi", author: "anjan"}) 116 | except convex.ConvexError as err: 117 | if isinstance(err.data, dict): 118 | if "code" in err.data and err.data["code"] == 1: 119 | # do something 120 | else: 121 | # do something else 122 | elif isinstance(err.data, str): 123 | print(err.data) 124 | except Exception as err: 125 | # log internally 126 | ``` 127 | 128 | # Pagination 129 | 130 | [Paginated queries](https://docs.convex.dev/database/pagination) are queries 131 | that accept pagination options as an argument and can be called repeatedly to 132 | produce additional "pages" of results. 133 | 134 | For a paginated query like this: 135 | 136 | ```javascript 137 | import { query } from "./_generated/server"; 138 | 139 | export default query({ 140 | handler: async ({ db }, { paginationOpts }) => { 141 | return await db.query("messages").order("desc").paginate(paginationOpts); 142 | }, 143 | }); 144 | ``` 145 | 146 | and returning all results 5 at a time in Python looks like this: 147 | 148 | ```python 149 | import convex 150 | client = convex.ConvexClient('https://happy-animal-123.convex.cloud') 151 | 152 | done = False 153 | cursor = None 154 | data = [] 155 | 156 | while not done: 157 | result = client.query('listMessages', {"paginationOpts": {"numItems": 5, "cursor": cursor}}) 158 | cursor = result['continueCursor'] 159 | done = result["isDone"] 160 | data.extend(result['page']) 161 | print('got', len(result['page']), 'results') 162 | 163 | print('collected', len(data), 'results') 164 | ``` 165 | 166 | # Versioning 167 | 168 | While we are pre-1.0.0, we'll update the minor version for large changes, and 169 | the patch version for small bugfixes. We may make backwards incompatible changes 170 | to the python client's API, but we will limit those to minor version bumps. 171 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.2,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "convex" 7 | version = "0.7.0" # Also update in __init__.py 8 | description = "Python client for the reactive backend-as-a-service Convex." 9 | authors = [ 10 | { name = "Convex, Inc", email = "support@convex.dev" }, 11 | ] 12 | requires-python = ">=3.9" 13 | classifiers = [ 14 | "Development Status :: 3 - Alpha", 15 | "Programming Language :: JavaScript", 16 | "Programming Language :: Python :: Implementation :: CPython", 17 | "Programming Language :: Rust", 18 | "Topic :: Database :: Front-Ends", 19 | "Topic :: Software Development :: Libraries :: Python Modules" 20 | ] 21 | license = "Apache-2.0" 22 | 23 | [project.urls] 24 | homepage = "https://convex.dev" 25 | repository = "https://github.com/get-convex/convex-py" 26 | documentation = "https://docs.convex.dev" 27 | 28 | [tool.maturin] 29 | python-source = "python" 30 | python-packages = ["convex", "_convex"] 31 | bindings = "pyo3" 32 | features = ["pyo3/extension-module"] 33 | 34 | [tool.poetry] 35 | # We publish with maturin which uses the [project] metadata above 36 | # but these three fields are required just to be able to use poetry. 37 | name = "convex" 38 | version = "0.7.0" # Also update in __init__.py 39 | description = "Python client for the reactive backend-as-a-service Convex." 40 | authors = ["Convex, Inc. "] 41 | license = "Apache-2.0" 42 | 43 | [tool.poetry.dependencies] 44 | python = ">=3.9" 45 | python-dotenv = "^1.0.0" 46 | requests = "^2.32.0" 47 | 48 | [tool.poetry.group.dev.dependencies] 49 | autoflake = "^2.0.0" 50 | black = ">=23,<25" 51 | flake8 = { version = "^7.0.0", python = "^3.9" } 52 | flake8-bugbear = { version = "^24.0.0", python = "^3.9" } 53 | flake8-docstrings = { version = "^1.7.0", python = "^3.9" } 54 | flake8-noqa = { version = "^1.3.0", python = "^3.9" } 55 | isort = "^5.10.1" 56 | maturin = ">=1.2,<2.0" 57 | mypy = "^1.0" 58 | pytest = "^8.0.0" 59 | pytest-profiling = "^1.7.0" 60 | twine = "^6.0.0" 61 | types-requests = "^2.27.30" 62 | typing-extensions = "^4.4.0" 63 | 64 | [tool.pytest.ini_options] 65 | addopts = "--doctest-modules" 66 | testpaths = [ 67 | "tests", 68 | "integration", 69 | "convex" 70 | ] 71 | 72 | [tool.mypy] 73 | strict = true 74 | 75 | [tool.isort] 76 | skip = [".venv"] 77 | -------------------------------------------------------------------------------- /python/_convex/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "PyQuerySubscription", 3 | "PyQuerySetSubscription", 4 | "PyConvexClient", 5 | "init_logging", 6 | "py_to_rust_to_py", 7 | "ConvexInt64", 8 | ] 9 | from ._convex import ( 10 | PyConvexClient, 11 | PyQuerySetSubscription, 12 | PyQuerySubscription, 13 | init_logging, 14 | py_to_rust_to_py, 15 | ) 16 | from .int64 import ConvexInt64 17 | -------------------------------------------------------------------------------- /python/_convex/_convex.pyi: -------------------------------------------------------------------------------- 1 | # _convex.pyi 2 | 3 | # These types are defined in `crates/py_client/src/client/mod.rs` and `crates/py_client/src/client/subscription.rs`. 4 | # Types in this file will need to be manually updated when these pyo3-annotated structs change. 5 | 6 | from typing import Any, Awaitable, Dict, Literal, Optional, Union 7 | 8 | from typing_extensions import TypedDict 9 | 10 | class ValueResult(TypedDict): 11 | type: Literal["value"] 12 | value: Any 13 | 14 | class ConvexErrorResult(TypedDict): 15 | type: Literal["convexerror"] 16 | message: str 17 | data: Any 18 | 19 | Result = Union[ValueResult, ConvexErrorResult] 20 | 21 | class PyQuerySubscription: 22 | def exists(self) -> bool: ... 23 | @property 24 | def id(self) -> Any: ... 25 | def unsubscribe(self) -> None: ... 26 | def next(self) -> Result: ... 27 | def anext(self) -> Awaitable[Result]: ... 28 | 29 | class PyQuerySetSubscription: 30 | def exists(self) -> bool: ... 31 | def next(self) -> Dict[Any, Any]: ... 32 | def anext(self) -> Awaitable[Dict[Any, Any]]: ... 33 | 34 | class PyConvexClient: 35 | def __new__(cls, deployment_url: str, version: str) -> "PyConvexClient": ... 36 | def subscribe( 37 | self, name: str, args: Optional[Dict[str, Any]] = None 38 | ) -> PyQuerySubscription: ... 39 | def query(self, name: str, args: Optional[Dict[str, Any]] = None) -> Result: ... 40 | def mutation(self, name: str, args: Optional[Dict[str, Any]] = None) -> Result: ... 41 | def action(self, name: str, args: Optional[Dict[str, Any]] = None) -> Result: ... 42 | def watch_all(self) -> PyQuerySetSubscription: ... 43 | def set_auth(self, token: Optional[str]) -> None: ... 44 | def set_admin_auth(self, token: str) -> None: ... 45 | 46 | def init_logging() -> None: 47 | """ 48 | Configure all tracing events with target "convex logs" to be written to 49 | stdout. 50 | """ 51 | 52 | def py_to_rust_to_py(value: Any) -> Any: 53 | """Convert a Python value to Rust and bring it back to test conversions.""" 54 | -------------------------------------------------------------------------------- /python/_convex/int64.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import struct 3 | from typing import Any, Dict, cast 4 | 5 | MIN_INT64 = -(2**63) 6 | MAX_INT64 = 2**63 - 1 7 | 8 | 9 | # Don't inherit from int to keep this as explicit as possible. 10 | class ConvexInt64: 11 | """ 12 | A wrapper around a Python int to representing a Convex Int64. 13 | 14 | >>> ConvexInt64(123) 15 | ConvexInt64(123) 16 | """ 17 | 18 | def __init__(self, value: int): 19 | """Create a wrapper around a Python int to representing a Convex Int64.""" 20 | if not isinstance(value, int): 21 | raise TypeError(f"{value} is not an int") 22 | if not MIN_INT64 <= value <= MAX_INT64: 23 | raise ValueError(f"{value} is outside allowed range for an int64") 24 | self.value = value 25 | 26 | def __repr__(self) -> str: 27 | return f"ConvexInt64({self.value})" 28 | 29 | def to_json(self) -> Dict[str, Any]: 30 | """Convert this Int64 to its wrapped Convex representation.""" 31 | if not MIN_INT64 <= self.value <= MAX_INT64: 32 | raise ValueError(f"{self.value} does not fit in an Int64") 33 | data = struct.pack(" bool: 37 | if type(other) is not ConvexInt64: 38 | return cast(bool, other == self.value) 39 | return other.value == self.value 40 | -------------------------------------------------------------------------------- /python/_convex/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-py/6f4da34b98473215af91c8332d234d40fe11b280/python/_convex/py.typed -------------------------------------------------------------------------------- /python/convex/__init__.py: -------------------------------------------------------------------------------- 1 | """Python client for [Convex](https://convex.dev/) backed by a WebSocket.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | from typing import Any, Dict, Optional 7 | 8 | from _convex import ( 9 | PyConvexClient, 10 | PyQuerySetSubscription, 11 | PyQuerySubscription, 12 | init_logging, 13 | ) 14 | 15 | from .values import ( 16 | CoercibleToConvexValue, 17 | ConvexInt64, 18 | ConvexValue, 19 | JsonValue, 20 | coerce_args_to_convex, 21 | convex_to_json, 22 | json_to_convex, 23 | ) 24 | 25 | # Initialize Rust's logging system 26 | init_logging() 27 | 28 | 29 | __all__ = [ 30 | "JsonValue", 31 | "ConvexValue", 32 | "convex_to_json", 33 | "json_to_convex", 34 | "ConvexError", 35 | "ConvexClient", 36 | "ConvexInt64", 37 | ] 38 | 39 | __version__ = "0.7.0" # Also update in pyproject.toml 40 | 41 | 42 | class ConvexError(Exception): 43 | """Represents a ConvexError thrown on a Convex server. 44 | 45 | Unlike other errors thrown in Convex functions, ConvexErrors 46 | propagate to clients where they can be caught and inspected. 47 | """ 48 | 49 | def __init__(self, message: str, data: Any): 50 | """Construct a ConvexError with given message (usually derived from data) and data. 51 | 52 | Unlike other errors thrown in Convex functions, ConvexErrors 53 | propagate to clients where they can be caught and inspected. 54 | """ 55 | super().__init__(message) 56 | self.data = data 57 | 58 | 59 | class ConvexExecutionError(Exception): 60 | """Convex execution error on server.""" 61 | 62 | 63 | FunctionArgs = Optional[Dict[str, CoercibleToConvexValue]] 64 | SubscriberId = Any 65 | 66 | 67 | class QuerySubscription: 68 | """This structure represents a single subscription to a query with args. 69 | 70 | It is returned by `ConvexClient::subscribe`. The subscription lives in the active 71 | query set for as long as this token stays in scope and is not unsubscribed. 72 | 73 | QuerySubscription provides a iterator of results to the query. 74 | """ 75 | 76 | def __init__( 77 | self, 78 | inner: PyQuerySubscription, 79 | client: PyConvexClient, 80 | name: str, 81 | args: FunctionArgs = None, 82 | ) -> None: 83 | self.inner: PyQuerySubscription = inner 84 | self.client: PyConvexClient = client 85 | self.name: str = name 86 | self.args: FunctionArgs = args 87 | self.invalidated: bool = False 88 | 89 | # This addresses the "hitting ctrl-c while subscribed" issue 90 | # described in https://github.com/get-convex/convex/pull/18559. 91 | # If our program is interrupted/crashes while we're awaiting on a function 92 | # result from our backend, we might end up with a lost subscription. 93 | # This safeguard checks to make sure the inner subscription on the 94 | # Rust side exists. If not, we first re-subscribe. 95 | def safe_inner_sub(self) -> PyQuerySubscription: 96 | # Check if the subscription was unsubscribed. 97 | if self.invalidated: 98 | raise Exception("This subscription has been dropped") 99 | # In what situation would this occur? Why resubscribe? 100 | if not self.inner.exists(): 101 | logging.warn("Retarting subscription dropped during possible ctrl-c") 102 | self.inner = self.client.subscribe(self.name, self.args) 103 | return self.inner 104 | 105 | @property 106 | def id(self) -> SubscriberId: 107 | """Returns an identifier for this subscription based on its query and args. 108 | 109 | This identifier can be used to find the result within a QuerySetSubscription 110 | as returned by `ConvexClient::watch_all()` 111 | """ 112 | return self.safe_inner_sub().id 113 | 114 | def __iter__(self) -> QuerySubscription: 115 | return self 116 | 117 | def __next__(self) -> ConvexValue: 118 | result = self.safe_inner_sub().next() 119 | if result["type"] == "convexerror": 120 | raise ConvexError(result["message"], result["data"]) 121 | return result["value"] 122 | 123 | def __aiter__(self) -> QuerySubscription: 124 | return self 125 | 126 | async def __anext__(self) -> ConvexValue: 127 | result = await self.safe_inner_sub().anext() 128 | if result["type"] == "convexerror": 129 | raise ConvexError(result["message"], result["data"]) 130 | return result["value"] 131 | 132 | def unsubscribe(self) -> None: 133 | """Unsubscribe from the query and drop this subscription from the active query set. 134 | 135 | Other subscriptions to the same query will be unaffected. 136 | """ 137 | self.safe_inner_sub().unsubscribe() 138 | self.invalidated = True 139 | 140 | 141 | class QuerySetSubscription: 142 | """A subscription to a consistent view of multiple queries. 143 | 144 | Provides an iterator, where each item contains a consistent view of the results 145 | of all the queries in the query set. Queries can be added to the query set via 146 | `ConvexClient::subscribe`. 147 | Queries can be removed from the query set via dropping the `QuerySubscription` 148 | token returned by `ConvexClient::subscribe`. 149 | 150 | Each item maps from `SubscriberId` to its latest result `ConvexValue`, 151 | or an error message/`ConvexError` if execution did not succeed. 152 | """ 153 | 154 | def __init__(self, inner: PyQuerySetSubscription, client: PyConvexClient): 155 | self.inner: PyQuerySetSubscription = inner 156 | self.client: PyConvexClient = client 157 | 158 | def safe_inner_sub(self) -> PyQuerySetSubscription: 159 | if not self.inner.exists(): 160 | self.inner = self.client.watch_all() 161 | return self.inner 162 | 163 | def __iter__(self) -> QuerySetSubscription: 164 | return self 165 | 166 | def __next__(self) -> Optional[Dict[SubscriberId, ConvexValue]]: 167 | result = self.safe_inner_sub().next() 168 | if not result: 169 | return result 170 | for k in result: 171 | result[k] = result[k] 172 | return result 173 | 174 | def __aiter__(self) -> QuerySetSubscription: 175 | return self 176 | 177 | async def __anext__(self) -> Optional[Dict[SubscriberId, ConvexValue]]: 178 | result = await self.safe_inner_sub().anext() 179 | if not result: 180 | return result 181 | for k in result: 182 | result[k] = result[k] 183 | return result 184 | 185 | 186 | class ConvexClient: 187 | """WebSocket-based Convex Client. 188 | 189 | A client to interact with a Convex deployment to perform 190 | queries/mutations/actions and manage query subscriptions. 191 | """ 192 | 193 | # This client wraps PyConvexClient by 194 | # - implementing additional type convertions (e.g. tuples to arrays) 195 | # - making arguments dicts optional 196 | 197 | def __init__(self, deployment_url: str): 198 | """Construct a WebSocket-based client given the URL of a Convex deployment.""" 199 | self.client: PyConvexClient = PyConvexClient(deployment_url, __version__) 200 | 201 | def subscribe(self, name: str, args: FunctionArgs = None) -> QuerySubscription: 202 | """Return a to subscription to the query `name` with optional `args`.""" 203 | subscription = self.client.subscribe(name, args if args else {}) 204 | return QuerySubscription(subscription, self.client, name, args if args else {}) 205 | 206 | # Return Any because its more useful than the big union type ConvexValue. 207 | def query(self, name: str, args: FunctionArgs = None) -> Any: 208 | """Perform the query `name` with `args` returning the result.""" 209 | result = self.client.query(name, coerce_args_to_convex(args)) 210 | if result["type"] == "convexerror": 211 | raise ConvexError(result["message"], result["data"]) 212 | return result["value"] 213 | 214 | def mutation(self, name: str, args: FunctionArgs = None) -> Any: 215 | """Perform the mutation `name` with `args` returning the result.""" 216 | result = self.client.mutation(name, coerce_args_to_convex(args)) 217 | if result["type"] == "convexerror": 218 | raise ConvexError(result["message"], result["data"]) 219 | return result["value"] 220 | 221 | def action(self, name: str, args: FunctionArgs = None) -> Any: 222 | """Perform the action `name` with `args` returning the result.""" 223 | result = self.client.action(name, coerce_args_to_convex(args)) 224 | if result["type"] == "convexerror": 225 | raise ConvexError(result["message"], result["data"]) 226 | return result["value"] 227 | 228 | def watch_all(self) -> QuerySetSubscription: 229 | """Return a QuerySetSubscription of all currently subscribed queries. 230 | 231 | This set changes over time as subscriptions are added and dropped. 232 | """ 233 | set_subscription: PyQuerySetSubscription = self.client.watch_all() 234 | return QuerySetSubscription(set_subscription, self.client) 235 | 236 | def set_auth(self, token: str) -> None: 237 | """Set auth for use when calling Convex functions. 238 | 239 | Set it with a token that you get from your auth provider via their login 240 | flow. If `None` is passed as the token, then auth is unset (logging out). 241 | """ 242 | self.client.set_auth(token) 243 | 244 | def clear_auth(self) -> None: 245 | """Clear any auth previously set.""" 246 | self.client.set_auth(None) 247 | 248 | def set_admin_auth(self, admin_key: str) -> None: 249 | """Set admin auth for the deployment. Not typically required.""" 250 | self.client.set_admin_auth(admin_key) 251 | -------------------------------------------------------------------------------- /python/convex/http_client.py: -------------------------------------------------------------------------------- 1 | """Legacy HTTP Python client for [Convex](https://convex.dev/). 2 | 3 | For general use the WebSocket based ConvexClient is better. 4 | """ 5 | 6 | import warnings 7 | from typing import Any, Dict, Optional 8 | 9 | import requests 10 | from requests.exceptions import HTTPError 11 | 12 | from . import __version__ # Also update in pyproject.toml 13 | from . import ConvexError 14 | from .values import ( 15 | CoercibleToConvexValue, 16 | ConvexValue, 17 | JsonValue, 18 | convex_to_json, 19 | json_to_convex, 20 | ) 21 | 22 | __all__ = [ 23 | "ConvexHttpClient", 24 | ] 25 | 26 | 27 | class ConvexExecutionError(Exception): 28 | """Convex execution error on server.""" 29 | 30 | 31 | FunctionArgs = Optional[Dict[str, CoercibleToConvexValue]] 32 | 33 | 34 | class ConvexHttpClient: 35 | """Client for communicating with convex.""" 36 | 37 | def __init__(self, address: str) -> None: 38 | """Instantiate a `ConvexClient` that speaks to a deployment at `address`.""" 39 | self.address: str = address 40 | self.auth: Optional[str] = None 41 | self.debug: bool = False 42 | # format prerelease with a dash in headers 43 | version = __version__.replace("a", "-a") 44 | self.headers = {"Convex-Client": f"python-{version}"} 45 | 46 | def set_auth(self, value: str) -> None: 47 | """Set auth for use when calling Convex functions.""" 48 | self.auth = f"Bearer {value}" 49 | 50 | def set_admin_auth(self, admin_key: str) -> None: 51 | """Set admin auth for the deployment. Not typically required.""" 52 | self.auth = f"Convex {admin_key}" 53 | 54 | def clear_auth(self) -> None: 55 | """Clear any auth previously set.""" 56 | self.auth = None 57 | 58 | def set_debug(self, value: bool) -> None: 59 | """Set whether the result log lines should be printed on the console. 60 | 61 | Log lines are only delivered when accessing a development deployment, 62 | in production they are not delivered. 63 | """ 64 | self.debug = value 65 | 66 | def _request(self, url: str, name: str, args: FunctionArgs) -> ConvexValue: 67 | if args is None: 68 | args = {} 69 | if not type(args) is dict: 70 | raise Exception( 71 | f"Arguments to a Convex function must be a dictionary. Received: {args}" 72 | ) 73 | 74 | data: Dict[str, JsonValue] = { 75 | "path": name, 76 | "format": "convex_encoded_json", 77 | "args": convex_to_json(args), 78 | } 79 | 80 | headers = self.headers.copy() 81 | if self.auth is not None: 82 | headers["Authorization"] = self.auth 83 | 84 | r = requests.post(url, json=data, headers=headers) 85 | 86 | # If we re-raise in except, Python will confusingly print out all of the previous 87 | # exceptions. To avoid this, set a response variable and raise outside of the except block 88 | # if necessary. 89 | try: 90 | response = r.json() 91 | except requests.exceptions.JSONDecodeError: 92 | response = None 93 | 94 | if not response: 95 | # If it's not json, it's probably a connectivity error or an error issued by 96 | # convex infrastructure. 97 | r.raise_for_status() 98 | # If it's not an error, then we've unexpectedly gotten some valid response we can't 99 | # parse. 100 | raise ConvexExecutionError(f"Unexpected response format: {r.text}") 101 | 102 | if self.debug and "logLines" in response: 103 | for line in response["logLines"]: 104 | print(line) 105 | 106 | deprecation_state = r.headers.get("x-convex-deprecation-state") 107 | deprecation_msg = r.headers.get("x-convex-deprecation-message") 108 | if deprecation_state and deprecation_msg: 109 | warnings.warn(f"{deprecation_state}: {deprecation_msg}", stacklevel=1) 110 | 111 | # If it was valid json, but an error, provide a little more info from the json. 112 | try: 113 | r.raise_for_status() 114 | except HTTPError: 115 | raise Exception( 116 | f"{r.status_code} {response['code']}: {response['message']}" 117 | ) 118 | 119 | if response["status"] == "success": 120 | return json_to_convex(response["value"]) 121 | if response["status"] == "error": 122 | if "errorData" in response: 123 | if isinstance(response["errorData"], str): 124 | raise ConvexError(response["errorData"], response["errorData"]) 125 | elif hasattr(response["errorData"], "message"): 126 | raise ConvexError( 127 | response["errorData"]["message"], response["errorData"] 128 | ) 129 | else: 130 | raise ConvexError("Convex error", response["errorData"]) 131 | raise ConvexExecutionError(response["errorMessage"]) 132 | raise Exception("Received unexpected response from Convex server.") 133 | 134 | def query(self, name: str, args: FunctionArgs = None) -> Any: 135 | """Run a query on Convex.""" 136 | url = f"{self.address}/api/query" 137 | return self._request(url, name, args) 138 | 139 | def mutation(self, name: str, args: FunctionArgs = None) -> Any: 140 | """Run a mutation on Convex.""" 141 | url = f"{self.address}/api/mutation" 142 | return self._request(url, name, args) 143 | 144 | def action(self, name: str, args: FunctionArgs = None) -> Any: 145 | """Run an action on Convex.""" 146 | url = f"{self.address}/api/action" 147 | return self._request(url, name, args) 148 | -------------------------------------------------------------------------------- /python/convex/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-py/6f4da34b98473215af91c8332d234d40fe11b280/python/convex/py.typed -------------------------------------------------------------------------------- /python/convex/values.py: -------------------------------------------------------------------------------- 1 | """Value types supported by Convex.""" 2 | 3 | import base64 4 | import math 5 | import struct 6 | import sys 7 | from collections import abc 8 | from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union, cast 9 | 10 | from _convex import ConvexInt64 11 | 12 | if not TYPE_CHECKING: 13 | if sys.version_info[1] < 9: 14 | # In Python 3.8, abc.* types don't implement __getitem__ 15 | # We don't typecheck in 3.8, so we shim just needs work at runtime. 16 | # Use abc when indexing types and collections.abc otherwise. 17 | orig_abc = abc 18 | 19 | class ReplacementAbc: 20 | def __getattr__(self, attr): 21 | return Indexable(getattr(orig_abc, attr)) 22 | 23 | class Indexable: 24 | def __init__(self, target): 25 | self.target = target 26 | 27 | def __getitem__(self, _key): 28 | return self.target 29 | 30 | abc = ReplacementAbc() 31 | 32 | if TYPE_CHECKING: 33 | from typing_extensions import TypeGuard 34 | 35 | 36 | __all__ = [ 37 | "JsonValue", 38 | "ConvexValue", 39 | "convex_to_json", 40 | "strict_convex_to_json", 41 | "json_to_convex", 42 | "ConvexInt64", 43 | ] 44 | 45 | JsonValue = Union[ 46 | None, bool, str, float, int, List["JsonValue"], Dict[str, "JsonValue"] 47 | ] 48 | 49 | ConvexValue = Union[ 50 | None, 51 | bool, 52 | str, 53 | float, 54 | List["ConvexValue"], 55 | Dict[str, "ConvexValue"], 56 | bytes, 57 | "ConvexInt64", 58 | ] 59 | 60 | # Currently in the normal WebSocket client: 61 | # Many Python values --coerce_to_convex()--> Specific Python values, e.g. converting tuples to lists 62 | # Specific Python values --py_to_value()--> Rust values, converting ConvxInt64 -> i64 and int -> f64 63 | 64 | # Better future state for the WebSocket client: no coersion takes place in Python, it's all in Rust. 65 | 66 | # This should be a wider type: it also includes objects that implement the buffer protocol. 67 | CoercibleToConvexValue = Union[ 68 | ConvexValue, 69 | int, 70 | abc.Mapping["CoercibleToConvexValue", "CoercibleToConvexValue"], 71 | abc.Sequence["CoercibleToConvexValue"], 72 | # pyright typechecking requires spelling Dict this way 73 | Dict[str, "CoercibleToConvexValue"], 74 | ] 75 | 76 | MAX_IDENTIFIER_LEN = 1024 77 | 78 | MIN_INT64 = -(2**63) 79 | MAX_INT64 = 2**63 - 1 80 | 81 | # This is the range of integers safely representable in a float64. 82 | # `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` in JavaScript are one 83 | # closer to zero, but these 2 extra values can be safely serialized. 84 | MIN_SAFE_INTEGER = -(2**53) 85 | MAX_SAFE_INTEGER = 2**53 86 | 87 | 88 | def int_to_float(v: int) -> float: 89 | if not MIN_SAFE_INTEGER <= v <= MAX_SAFE_INTEGER: 90 | raise ValueError( 91 | f"Integer {v} is outside the range of a Convex `Float64` (-2^53 to 2^53). " 92 | "Consider using a `ConvexInt64`, which corresponds to a `BigInt` in " 93 | "JavaScript Convex functions." 94 | ) 95 | return float(v) 96 | 97 | 98 | def _validate_object_field(k: str) -> None: 99 | if len(k) > MAX_IDENTIFIER_LEN: 100 | raise ValueError( 101 | f"Field name {k} exceeds maximum field name length {MAX_IDENTIFIER_LEN}." 102 | ) 103 | if k.startswith("$"): 104 | raise ValueError(f"Field name {k} starts with a '$', which is reserved.") 105 | 106 | for char in k: 107 | # Non-control ASCII characters 108 | if ord(char) < 32 or ord(char) >= 127: 109 | raise ValueError( 110 | f"Field name '{k}' has invalid character '{char}': " 111 | "Field names can only contain non-control ASCII characters" 112 | ) 113 | 114 | 115 | def is_special_float(v: float) -> bool: 116 | """Return True if value cannot be serialized to JSON.""" 117 | return ( 118 | math.isnan(v) or not math.isfinite(v) or (v == 0 and math.copysign(1, v) == -1) 119 | ) 120 | 121 | 122 | def float_to_json(v: float) -> JsonValue: 123 | if is_special_float(v): 124 | data = struct.pack(" JsonValue: 130 | d: Dict[str, JsonValue] = {} 131 | for key, val in v.items(): 132 | if not isinstance(key, str): 133 | raise ValueError(f"Convex object keys must be strings, found {key}") 134 | _validate_object_field(key) 135 | obj_val: JsonValue = _convex_to_json(val, coerce) 136 | d[key] = obj_val 137 | return d 138 | 139 | 140 | def mapping_to_convex(v: abc.Mapping[Any, Any], coerce: bool) -> Dict[str, ConvexValue]: 141 | d: Dict[str, ConvexValue] = {} 142 | for key, val in v.items(): 143 | if not isinstance(key, str): 144 | raise ValueError(f"Convex object keys must be strings, found {key}") 145 | _validate_object_field(key) 146 | obj_val: ConvexValue = _to_convex(val, coerce) 147 | d[key] = obj_val 148 | return d 149 | 150 | 151 | def buffer_to_json(v: Any) -> JsonValue: 152 | return {"$bytes": base64.standard_b64encode(v).decode("ascii")} 153 | 154 | 155 | def iterable_to_array_json(v: abc.Iterable[Any], coerce: bool) -> JsonValue: 156 | # Convex arrays can have 8192 items maximum. 157 | # Let the server check this for now. 158 | return [_convex_to_json(x, coerce) for x in v] 159 | 160 | 161 | def iterable_to_convex(v: abc.Iterable[Any], coerce: bool) -> ConvexValue: 162 | # Convex arrays can have 8192 items maximum. 163 | # Let the server check this for now. 164 | return [_to_convex(x, coerce) for x in v] 165 | 166 | 167 | def convex_to_json(v: CoercibleToConvexValue) -> JsonValue: 168 | """Convert Convex-serializable values to JSON-serializable objects. 169 | 170 | Convex types are described at https://docs.convex.dev/using/types and 171 | include Python builtin types str, int, float, bool, bytes, None, list, and 172 | dict, as well as instances of the ConvexInt64 class. 173 | 174 | >>> convex_to_json({'a': 1.0}) 175 | {'a': 1.0} 176 | 177 | In addition to these basic Convex values, many Python types can be coerced 178 | to Convex values: for example, tuples: 179 | 180 | >>> convex_to_json((1.0, 2.0, 3.0)) 181 | [1.0, 2.0, 3.0] 182 | 183 | Python plays fast and loose with ints and floats (divide one int by another, 184 | get a float!), which makes treating these as two separate types in Convex 185 | functions awkward. Convex functions run in JavaScript, where `number` (an 186 | IEEE 754 double-precision float) is frequently used for both ints and 187 | floats. 188 | To ensure Convex functions receive floats, both ints and floats in Python are 189 | converted to floats. 190 | 191 | >>> json_to_convex(convex_to_json(1.23)) 192 | 1.23 193 | >>> json_to_convex(convex_to_json(17)) 194 | 17.0 195 | 196 | Convex supports storing Int64s in the database, represented in JavaScript as 197 | BigInts. To specify an Int64, use the ConvexInt64 wrapper type. 198 | 199 | >>> json_to_convex(convex_to_json(ConvexInt64(17))) 200 | ConvexInt64(17) 201 | """ 202 | return _convex_to_json(v, coerce=True) 203 | 204 | 205 | def strict_convex_to_json(v: ConvexValue) -> JsonValue: 206 | """Convert Convex round-trippable values to JSON-serializable objects.""" 207 | return _convex_to_json(v, coerce=False) 208 | 209 | 210 | def coerce_args_to_convex( 211 | v: Optional[Mapping[str, CoercibleToConvexValue]] = None 212 | ) -> Dict[str, ConvexValue]: 213 | """Convert a mapping of strings to objects convertable to Convex values into a dict.""" 214 | if v is None: 215 | return {} 216 | if not isinstance(v, dict) and not isinstance(v, abc.Mapping): 217 | raise TypeError(f"Args are not a mapping: {v}") 218 | return mapping_to_convex(v, True) 219 | 220 | 221 | def coerce_to_convex(v: CoercibleToConvexValue) -> ConvexValue: 222 | return _to_convex(v, True) 223 | 224 | 225 | # This coercion logic would be better to have in Rust 226 | def _to_convex(v: CoercibleToConvexValue, coerce: bool) -> ConvexValue: 227 | """ 228 | Convert to the types expected by the wrapped Rust client. 229 | 230 | Ints become floats, ConvexInt64s become ints, and possibly more 231 | if coerce is true. 232 | 233 | Creates a copy of all mutable objects. 234 | """ 235 | # 1. values which roundtrip 236 | if v is None: 237 | return None 238 | if v is True or v is False: 239 | return v 240 | if type(v) is float: 241 | return v 242 | if type(v) is str: 243 | return v 244 | if type(v) is bytes: 245 | return v 246 | if type(v) is dict: 247 | return mapping_to_convex(v, coerce) 248 | if type(v) is list: 249 | return iterable_to_convex(v, coerce) 250 | if type(v) is ConvexInt64: 251 | return v 252 | 253 | if not coerce: 254 | raise TypeError( 255 | f"{v} is not a supported Convex type. " 256 | "To learn about Convex's supported types " 257 | "see https://docs.convex.dev/using/types." 258 | ) 259 | 260 | # 2. common types that don't roundtrip but have clear representations in Convex 261 | if isinstance(v, int): 262 | return int_to_float(v) 263 | if isinstance(v, tuple): 264 | return iterable_to_convex(v, coerce) 265 | 266 | # 3. allow subclasses (which will not round-trip) 267 | if isinstance(v, float): 268 | return float(v) 269 | if isinstance(v, str): 270 | return v 271 | if isinstance(v, bytes): 272 | return bytes(v) 273 | if isinstance(v, dict): 274 | return mapping_to_convex(v, coerce) 275 | if isinstance(v, list): 276 | return iterable_to_convex(v, coerce) 277 | 278 | # 4. check for implementing abstract classes and protocols 279 | try: 280 | # Does this object conform to the buffer protocol? 281 | memoryview(v) # type: ignore 282 | except TypeError: 283 | pass 284 | else: 285 | return bytes(v) # type: ignore 286 | 287 | if isinstance(v, abc.Mapping): 288 | return mapping_to_convex(v, coerce) 289 | if isinstance(v, abc.Sequence): 290 | return iterable_to_convex(v, coerce) 291 | 292 | raise TypeError( 293 | f"{v} is not a supported Convex type. " 294 | "To learn about Convex's supported types, see https://docs.convex.dev/using/types." 295 | ) 296 | 297 | 298 | # There's a server-enforced limit on total document size of 1MB. This could be 299 | # enforced for each field individually, but it could still exceed the document 300 | # size limit when combined, so let the server enforce this. 301 | def _convex_to_json(v: CoercibleToConvexValue, coerce: bool) -> JsonValue: 302 | # 1. values which roundtrip 303 | if v is None: 304 | return None 305 | if v is True or v is False: 306 | return v 307 | if type(v) is float: 308 | return float_to_json(v) 309 | if type(v) is str: 310 | return v 311 | if type(v) is bytes: 312 | return buffer_to_json(v) 313 | if type(v) is dict: 314 | return mapping_to_object_json(v, coerce) 315 | if type(v) is list: 316 | return iterable_to_array_json(v, coerce) 317 | if type(v) is ConvexInt64: 318 | return v.to_json() 319 | 320 | if isinstance(v, int): 321 | return int_to_float(v) 322 | 323 | if not coerce: 324 | raise TypeError( 325 | f"{v} of type {type(v)} is not a supported Convex type. " 326 | "To learn about Convex's supported types " 327 | "see https://docs.convex.dev/using/types." 328 | ) 329 | 330 | # 2. common types that don't roundtrip but have clear representations in Convex 331 | if isinstance(v, tuple): 332 | return iterable_to_array_json(v, coerce) 333 | 334 | # 3. allow subclasses (which will not round-trip) 335 | if isinstance(v, float): 336 | return float_to_json(v) 337 | if isinstance(v, str): 338 | return v 339 | if isinstance(v, bytes): 340 | return buffer_to_json(v) 341 | if isinstance(v, dict): 342 | return mapping_to_object_json(v, coerce) 343 | if isinstance(v, list): 344 | return iterable_to_array_json(v, coerce) 345 | 346 | # 4. check for implementing abstract classes and protocols 347 | try: 348 | # Does this object conform to the buffer protocol? 349 | memoryview(v) # type: ignore 350 | except TypeError: 351 | pass 352 | else: 353 | return buffer_to_json(v) 354 | 355 | if isinstance(v, abc.Mapping): 356 | return mapping_to_object_json(v, coerce) 357 | if isinstance(v, abc.Sequence): 358 | return iterable_to_array_json(v, coerce) 359 | 360 | raise TypeError( 361 | f"{v} is not a supported Convex type. " 362 | "To learn about Convex's supported types, see https://docs.convex.dev/using/types." 363 | ) 364 | 365 | 366 | def json_to_convex(v: JsonValue) -> ConvexValue: 367 | """Convert from simple Python JSON objects to richer types.""" 368 | if isinstance(v, (bool, float, str)): 369 | return v 370 | if v is None: 371 | return None 372 | if isinstance(v, list): 373 | convex_values: ConvexValue = [json_to_convex(x) for x in v] 374 | return convex_values 375 | if isinstance(v, dict) and len(v) == 1: 376 | attr = list(v.keys())[0] 377 | if attr == "$bytes": 378 | data_str = cast(str, v["$bytes"]) 379 | return base64.standard_b64decode(data_str) 380 | if attr == "$integer": 381 | data_str = cast(str, v["$integer"]) 382 | (i,) = struct.unpack(" "TypeGuard[CoercibleToConvexValue]": 404 | """Return True if value is coercible to a convex value. 405 | 406 | >>> is_coercible_to_convex_value((1,2,3)) 407 | True 408 | """ 409 | try: 410 | convex_to_json(v) 411 | except (TypeError, ValueError): 412 | return False 413 | return True 414 | 415 | 416 | def is_convex_value(v: Any) -> "TypeGuard[ConvexValue]": 417 | """Return True if value is a convex value. 418 | 419 | >>> is_convex_value((1,2,3)) 420 | False 421 | """ 422 | try: 423 | strict_convex_to_json(v) 424 | except (TypeError, ValueError): 425 | return False 426 | return True 427 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-12-16" 3 | -------------------------------------------------------------------------------- /simple_example.py: -------------------------------------------------------------------------------- 1 | from convex import ConvexClient 2 | 3 | client = ConvexClient("https://flippant-cardinal-923.convex.cloud") 4 | mutation_result = client.mutation("sample_mutation:sample", {}) 5 | print(client.query("users:list")) 6 | sub = client.subscribe("users:list", {}) 7 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | future::Future, 4 | io::{ 5 | self, 6 | Write, 7 | }, 8 | }; 9 | 10 | use convex::{ 11 | ConvexClient, 12 | ConvexClientBuilder, 13 | FunctionResult, 14 | Value, 15 | }; 16 | use pyo3::{ 17 | exceptions::PyException, 18 | prelude::*, 19 | pyclass, 20 | types::PyDict, 21 | }; 22 | use tokio::{ 23 | runtime, 24 | time::{ 25 | sleep, 26 | Duration, 27 | }, 28 | }; 29 | use tracing::{ 30 | field::{ 31 | Field, 32 | Visit, 33 | }, 34 | subscriber::set_global_default, 35 | Event, 36 | Level, 37 | Subscriber, 38 | }; 39 | use tracing_subscriber::{ 40 | layer::Context, 41 | prelude::__tracing_subscriber_SubscriberExt, 42 | Layer, 43 | Registry, 44 | }; 45 | 46 | use crate::{ 47 | query_result::{ 48 | convex_error_to_py_wrapped, 49 | py_to_value, 50 | value_to_py, 51 | value_to_py_wrapped, 52 | }, 53 | subscription::{ 54 | PyQuerySetSubscription, 55 | PyQuerySubscription, 56 | }, 57 | }; 58 | 59 | /// A wrapper type that can accept a Python `Dict[str, CoercibleToConvexValue]` 60 | #[derive(Default)] 61 | pub struct FunctionArgsWrapper(BTreeMap); 62 | impl<'py> FromPyObject<'py> for FunctionArgsWrapper { 63 | fn extract_bound(d: &Bound<'py, PyAny>) -> PyResult { 64 | let map = d 65 | .downcast::()? 66 | .iter() 67 | .map(|(key, value)| { 68 | let k = key.extract::()?; 69 | let v = py_to_value(value.as_borrowed())?; 70 | Ok((k, v)) 71 | }) 72 | .collect::>()?; 73 | 74 | Ok(FunctionArgsWrapper(map)) 75 | } 76 | } 77 | 78 | async fn check_python_signals_periodically() -> PyErr { 79 | loop { 80 | sleep(Duration::from_secs(1)).await; 81 | if let Err(e) = Python::with_gil(|py| py.check_signals()) { 82 | return e; 83 | } 84 | } 85 | } 86 | /// An asynchronous client to interact with a specific project to perform 87 | /// queries/mutations/actions and manage query subscriptions. 88 | #[pyclass] 89 | pub struct PyConvexClient { 90 | rt: tokio::runtime::Runtime, 91 | client: ConvexClient, 92 | } 93 | 94 | impl PyConvexClient { 95 | fn function_result_to_py_result( 96 | &mut self, 97 | py: Python<'_>, 98 | result: FunctionResult, 99 | ) -> PyResult { 100 | match result { 101 | FunctionResult::Value(v) => Ok(value_to_py_wrapped(py, v)), 102 | FunctionResult::ErrorMessage(e) => Err(PyException::new_err(e)), 103 | FunctionResult::ConvexError(v) => { 104 | // pyo3 can't defined new custom exceptions when using the common abi 105 | // `features = ["abi3"]` https://github.com/PyO3/pyo3/issues/1344 106 | // so we define this error in Python. So just return a wrapped one. 107 | Ok(convex_error_to_py_wrapped(py, v)) 108 | }, 109 | } 110 | } 111 | 112 | fn block_on_and_check_signals<'a, T, E: ToString, F: Future>>( 113 | &'a mut self, 114 | f: impl FnOnce(&'a mut ConvexClient) -> F, 115 | ) -> PyResult { 116 | self.rt.block_on(async { 117 | tokio::select!( 118 | res1 = f(&mut self.client) => res1.map_err(|e| PyException::new_err(e.to_string())), 119 | res2 = check_python_signals_periodically() => Err(res2), 120 | ) 121 | }) 122 | } 123 | } 124 | 125 | #[pymethods] 126 | impl PyConvexClient { 127 | /// Note that the WebSocket is not connected yet and therefore the 128 | /// connection url is not validated to be accepting connections. 129 | #[new] 130 | fn py_new(deployment_url: &str, version: &str) -> PyResult { 131 | // The ConvexClient is instantiated in the context of a tokio Runtime, and 132 | // needs to run its worker in the background so that it can constantly 133 | // listen for new messages from the server. Here, we choose to build a 134 | // multi-thread scheduler to make that possible. 135 | let rt = runtime::Builder::new_multi_thread() 136 | .enable_all() 137 | .worker_threads(1) 138 | .build() 139 | .unwrap(); 140 | 141 | // Block on the async function using the Tokio runtime. 142 | let client_id = format!("python-{}", version); 143 | let instance = rt.block_on( 144 | ConvexClientBuilder::new(deployment_url) 145 | .with_client_id(&client_id) 146 | .build(), 147 | ); 148 | match instance { 149 | Ok(instance) => Ok(PyConvexClient { 150 | rt, 151 | client: instance, 152 | }), 153 | Err(e) => Err(PyException::new_err(format!( 154 | "{}: {}", 155 | "Failed to create PyConvexClient", 156 | &e.to_string() 157 | ))), 158 | } 159 | } 160 | 161 | /// Creates a single subscription to a query, with optional args. 162 | #[pyo3(signature = (name, args=None))] 163 | pub fn subscribe( 164 | &mut self, 165 | name: &str, 166 | args: Option, 167 | ) -> PyResult { 168 | let args: BTreeMap = args.unwrap_or_default().0; 169 | let res = self.block_on_and_check_signals(|client| client.subscribe(name, args))?; 170 | Ok(PyQuerySubscription::new(res, self.rt.handle().clone())) 171 | } 172 | 173 | /// Make a oneshot request to a query `name` with `args`. 174 | /// 175 | /// Returns a `convex::Value` representing the result of the query. 176 | #[pyo3(signature = (name, args=None))] 177 | pub fn query( 178 | &mut self, 179 | py: Python<'_>, 180 | name: &str, 181 | args: Option, 182 | ) -> PyResult { 183 | let args: BTreeMap = args.unwrap_or_default().0; 184 | let res = self.block_on_and_check_signals(|client| client.query(name, args))?; 185 | self.function_result_to_py_result(py, res) 186 | } 187 | 188 | /// Perform a mutation `name` with `args` and return a future 189 | /// containing the return value of the mutation once it completes. 190 | #[pyo3(signature = (name, args=None))] 191 | pub fn mutation( 192 | &mut self, 193 | py: Python<'_>, 194 | name: &str, 195 | args: Option, 196 | ) -> PyResult { 197 | let args: BTreeMap = args.unwrap_or_default().0; 198 | let res = self.block_on_and_check_signals(|client| client.mutation(name, args))?; 199 | self.function_result_to_py_result(py, res) 200 | } 201 | 202 | /// Perform an action `name` with `args` and return a future 203 | /// containing the return value of the action once it completes. 204 | #[pyo3(signature = (name, args=None))] 205 | pub fn action( 206 | &mut self, 207 | py: Python<'_>, 208 | name: &str, 209 | args: Option, 210 | ) -> PyResult { 211 | let args: BTreeMap = args.unwrap_or_default().0; 212 | let res = self.block_on_and_check_signals(|client| client.action(name, args))?; 213 | self.function_result_to_py_result(py, res) 214 | } 215 | 216 | /// Get a consistent view of the results of every query the client is 217 | /// currently subscribed to. This set changes over time as subscriptions 218 | /// are added and dropped. 219 | pub fn watch_all(&mut self, _py: Python<'_>) -> PyQuerySetSubscription { 220 | let mut py_res: PyQuerySetSubscription = self.client.watch_all().into(); 221 | py_res.rt_handle = Some(self.rt.handle().clone()); 222 | py_res 223 | } 224 | 225 | /// Set auth for use when calling Convex functions. 226 | /// 227 | /// Set it with a token that you get from your auth provider via their login 228 | /// flow. If `None` is passed as the token, then auth is unset (logging 229 | /// out). 230 | #[pyo3(signature = (token=None))] 231 | pub fn set_auth(&mut self, token: Option) -> PyResult<()> { 232 | self.rt.block_on(async { 233 | tokio::select!( 234 | () = self.client.set_auth(token) => Ok(()), 235 | err = check_python_signals_periodically() => Err(err), 236 | ) 237 | }) 238 | } 239 | 240 | /// Set auth which allows access to system resources. 241 | /// 242 | /// Set it with a deploy key obtained from the convex dashboard of a 243 | /// deployment you control. This auth cannot be unset. 244 | pub fn set_admin_auth(&mut self, token: String) -> PyResult<()> { 245 | self.rt.block_on(async { 246 | tokio::select!( 247 | () = self.client.set_admin_auth(token, None) => Ok(()), 248 | err = check_python_signals_periodically() => Err(err), 249 | ) 250 | }) 251 | } 252 | } 253 | 254 | struct UDFLogVisitor { 255 | fields: BTreeMap, 256 | } 257 | 258 | impl UDFLogVisitor { 259 | fn new() -> Self { 260 | UDFLogVisitor { 261 | fields: BTreeMap::new(), 262 | } 263 | } 264 | } 265 | 266 | // Extracts a BTreeMap from our log line 267 | impl Visit for UDFLogVisitor { 268 | fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { 269 | let s = format!("{:?}", value); 270 | self.fields.insert(field.name().to_string(), s); 271 | } 272 | } 273 | 274 | struct ConvexLoggingLayer; 275 | 276 | impl Layer for ConvexLoggingLayer { 277 | fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { 278 | let mut visitor = UDFLogVisitor::new(); 279 | event.record(&mut visitor); 280 | let mut log_writer = io::stdout(); 281 | if let Some(message) = visitor.fields.get("message") { 282 | writeln!(log_writer, "{}", message).unwrap(); 283 | } 284 | } 285 | } 286 | 287 | #[pyfunction] 288 | fn init_logging() { 289 | let subscriber = Registry::default().with(ConvexLoggingLayer.with_filter( 290 | tracing_subscriber::filter::Targets::new().with_target("convex_logs", Level::DEBUG), 291 | )); 292 | 293 | set_global_default(subscriber).expect("Failed to set up custom logging subscriber"); 294 | } 295 | 296 | // Exposed for testing 297 | #[pyfunction] 298 | fn py_to_rust_to_py(py: Python<'_>, py_val: Bound<'_, PyAny>) -> PyResult { 299 | // this is just a map 300 | match py_to_value(py_val.as_borrowed()) { 301 | Ok(val) => Ok(value_to_py(py, val)), 302 | Err(err) => Err(err), 303 | } 304 | } 305 | 306 | #[pymodule] 307 | #[pyo3(name = "_convex")] 308 | fn _convex(_py: Python, m: Bound<'_, PyModule>) -> PyResult<()> { 309 | m.add_class::()?; 310 | m.add_class::()?; 311 | m.add_class::()?; 312 | m.add_function(wrap_pyfunction!(init_logging, &m)?)?; 313 | m.add_function(wrap_pyfunction!(py_to_rust_to_py, &m)?)?; 314 | Ok(()) 315 | } 316 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Convex Client 2 | //! The wrapped Python client for [Convex](https://convex.dev). 3 | //! 4 | //! Convex is the backend application platform with everything you need to build 5 | //! your product. Convex clients can subscribe to queries and perform mutations 6 | //! and actions. Check out the [Convex Documentation](https://docs.convex.dev) for more information. 7 | //! 8 | //! There is a Python layer above this package which re-exposes some of these 9 | //! pyo3 structs in a more Pythonic way. Please refer to https://pypi.org/project/convex/ 10 | //! for official Python client documentation. 11 | 12 | #![warn(missing_docs)] 13 | #![warn(rustdoc::missing_crate_level_docs)] 14 | 15 | mod client; 16 | pub use client::PyConvexClient; 17 | 18 | mod query_result; 19 | mod subscription; 20 | -------------------------------------------------------------------------------- /src/query_result.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use convex::ConvexError; 4 | use pyo3::{ 5 | exceptions::PyException, 6 | types::{ 7 | PyAnyMethods, 8 | PyBool, 9 | PyBytes, 10 | PyDict, 11 | PyDictMethods, 12 | PyFloat, 13 | PyInt, 14 | PyList, 15 | PyListMethods, 16 | PyString, 17 | }, 18 | Borrowed, 19 | PyAny, 20 | PyObject, 21 | PyResult, 22 | Python, 23 | }; 24 | 25 | // TODO using an enum would be cleaner here 26 | pub fn value_to_py_wrapped(py: Python<'_>, v: convex::Value) -> PyObject { 27 | let py_dict = PyDict::new(py); 28 | py_dict 29 | .set_item("type", PyString::new(py, "value")) 30 | .unwrap(); 31 | py_dict.set_item("value", value_to_py(py, v)).unwrap(); 32 | py_dict.into() 33 | } 34 | 35 | pub fn convex_error_to_py_wrapped(py: Python<'_>, err: ConvexError) -> PyObject { 36 | let py_dict = PyDict::new(py); 37 | py_dict 38 | .set_item("type", PyString::new(py, "convexerror")) 39 | .unwrap(); 40 | py_dict.set_item("message", err.message).unwrap(); 41 | py_dict.set_item("data", value_to_py(py, err.data)).unwrap(); 42 | py_dict.into() 43 | } 44 | 45 | pub fn value_to_py(py: Python<'_>, v: convex::Value) -> PyObject { 46 | match v { 47 | convex::Value::Null => py.None(), 48 | convex::Value::Int64(val) => { 49 | let int64_module = py 50 | .import("_convex.int64") 51 | .expect("Couldn't import _convex.int64"); 52 | let int_64_class = int64_module 53 | .getattr("ConvexInt64") 54 | .expect("Couldn't import ConvexInt64 from _convex.int64"); 55 | let obj: PyObject = int_64_class 56 | .call((val,), None) 57 | .unwrap_or_else(|_| panic!("Couldn't construct ConvexInt64() from {:?}", val)) 58 | .into(); 59 | obj 60 | }, 61 | 62 | convex::Value::Float64(val) => PyFloat::new(py, val).into(), 63 | convex::Value::Boolean(val) => PyBool::new(py, val).as_any().clone().unbind(), 64 | convex::Value::String(val) => PyString::new(py, &val).into(), 65 | convex::Value::Bytes(val) => PyBytes::new(py, &val).into(), 66 | convex::Value::Array(arr) => { 67 | let py_list = PyList::empty(py); 68 | for item in arr { 69 | py_list.append(value_to_py(py, item)).unwrap(); 70 | } 71 | py_list.into() 72 | }, 73 | convex::Value::Object(obj) => { 74 | let py_dict = PyDict::new(py); 75 | for (key, value) in obj { 76 | py_dict.set_item(key, value_to_py(py, value)).unwrap(); 77 | } 78 | py_dict.into() 79 | }, 80 | } 81 | } 82 | 83 | // TODO Implement all or most of the coercions from the Python client. 84 | /// Translate a Python value to Rust, doing isinstance coersion (e.g. subclasses 85 | /// of list will be interpreted as lists) but not other conversions (e.g. tuple 86 | /// to list). 87 | pub fn py_to_value(py_val: Borrowed<'_, '_, PyAny>) -> PyResult { 88 | let py = py_val.py(); 89 | let int64_module = py.import("_convex.int64")?; 90 | let int_64_class = int64_module.getattr("ConvexInt64")?; 91 | 92 | // check boolean first, since it's a subclass of int 93 | if py_val.is_instance_of::() { 94 | let val: bool = py_val.extract::()?; 95 | return Ok(convex::Value::Boolean(val)); 96 | } 97 | if py_val.is_instance_of::() { 98 | // Note conversion from int to float 99 | let val: f64 = py_val.extract()?; 100 | return Ok(convex::Value::Float64(val)); 101 | } 102 | if py_val.is_instance_of::() { 103 | let val: f64 = py_val.extract::()?; 104 | return Ok(convex::Value::Float64(val)); 105 | } 106 | if py_val.is_instance(&int_64_class)? { 107 | let value = py_val.getattr("value")?; 108 | let val: i64 = value.extract()?; 109 | return Ok(convex::Value::Int64(val)); 110 | } 111 | if py_val.is_instance_of::() { 112 | let val: String = py_val.extract::()?; 113 | return Ok(convex::Value::String(val)); 114 | } 115 | if py_val.is_instance_of::() { 116 | let val: Vec = py_val.extract::>()?; 117 | return Ok(convex::Value::Bytes(val)); 118 | } 119 | if py_val.is_instance_of::() { 120 | let py_list = py_val.downcast::()?; 121 | let mut vec: Vec = Vec::new(); 122 | for item in py_list { 123 | let inner_value: convex::Value = py_to_value(item.as_borrowed())?; 124 | vec.push(inner_value); 125 | } 126 | return Ok(convex::Value::Array(vec)); 127 | } 128 | if py_val.is_instance_of::() { 129 | let py_dict = py_val.downcast::()?; 130 | let mut map: BTreeMap = BTreeMap::new(); 131 | for (key, value) in py_dict.iter() { 132 | let inner_value: convex::Value = py_to_value(value.as_borrowed())?; 133 | let inner_key: convex::Value = py_to_value(key.as_borrowed())?; 134 | match inner_key { 135 | convex::Value::String(s) => map.insert(s, inner_value), 136 | _ => { 137 | return Err(PyException::new_err(format!( 138 | "Bad key for Convex object: {:?}", 139 | key 140 | ))) 141 | }, 142 | }; 143 | } 144 | return Ok(convex::Value::Object(map)); 145 | } 146 | if py_val.is_none() { 147 | return Ok(convex::Value::Null); 148 | } 149 | 150 | Err(PyException::new_err(format!( 151 | "Failed to serialize to Convex value {:?} of type {:?}", 152 | py_val, 153 | py_val.get_type() 154 | ))) 155 | } 156 | -------------------------------------------------------------------------------- /src/subscription.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::hash_map::DefaultHasher, 3 | hash::{ 4 | Hash, 5 | Hasher, 6 | }, 7 | sync::Arc, 8 | }; 9 | 10 | use convex::{ 11 | FunctionResult, 12 | SubscriberId, 13 | }; 14 | use futures::StreamExt; 15 | use parking_lot::Mutex; 16 | use pyo3::{ 17 | exceptions::{ 18 | PyException, 19 | PyNotImplementedError, 20 | PyStopAsyncIteration, 21 | PyStopIteration, 22 | }, 23 | prelude::*, 24 | pyclass::CompareOp, 25 | types::PyDict, 26 | }; 27 | use tokio::time::{ 28 | sleep, 29 | Duration, 30 | }; 31 | 32 | use crate::query_result::{ 33 | convex_error_to_py_wrapped, 34 | value_to_py, 35 | value_to_py_wrapped, 36 | }; 37 | 38 | #[pyclass(frozen)] 39 | pub struct PyQuerySubscription { 40 | // TODO document here why this needs to be an Arc>> 41 | inner: Arc>>, 42 | pub rt_handle: tokio::runtime::Handle, 43 | } 44 | 45 | impl PyQuerySubscription { 46 | pub fn new(query_sub: convex::QuerySubscription, rt_handle: tokio::runtime::Handle) -> Self { 47 | PyQuerySubscription { 48 | inner: Arc::new(Mutex::new(Some(query_sub))), 49 | rt_handle, 50 | } 51 | } 52 | } 53 | 54 | #[pyclass(frozen)] 55 | pub struct PySubscriberId { 56 | inner: convex::SubscriberId, 57 | } 58 | 59 | impl From for PySubscriberId { 60 | fn from(sub_id: convex::SubscriberId) -> Self { 61 | PySubscriberId { inner: sub_id } 62 | } 63 | } 64 | 65 | #[pymethods] 66 | impl PySubscriberId { 67 | fn __str__(&self) -> String { 68 | format!("{:#?}", self.inner) 69 | } 70 | 71 | fn __repr__(&self) -> String { 72 | format!("{:#?}", self.inner) 73 | } 74 | 75 | fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { 76 | match op { 77 | CompareOp::Eq => Ok(self.inner == other.inner), 78 | CompareOp::Ne => Ok(self.inner != other.inner), 79 | _ => Err(PyNotImplementedError::new_err( 80 | "Can't compare SubscriberIds in the requested way.", 81 | )), 82 | } 83 | } 84 | 85 | fn __hash__(&self) -> u64 { 86 | let mut hasher = DefaultHasher::new(); 87 | self.inner.hash(&mut hasher); 88 | hasher.finish() 89 | } 90 | } 91 | 92 | async fn check_python_signals_periodically() -> PyErr { 93 | loop { 94 | sleep(Duration::from_secs(1)).await; 95 | if let Err(e) = Python::with_gil(|py| py.check_signals()) { 96 | return e; 97 | } 98 | } 99 | } 100 | 101 | #[pymethods] 102 | impl PyQuerySubscription { 103 | fn exists(&self) -> bool { 104 | self.inner.lock().is_some() 105 | } 106 | 107 | #[getter] 108 | fn id(&self) -> PySubscriberId { 109 | let query_sub = self.inner.clone(); 110 | let query_sub_inner = query_sub.lock().take().unwrap(); 111 | let sub_id: SubscriberId = *query_sub_inner.id(); 112 | let _ = query_sub.lock().insert(query_sub_inner); 113 | PySubscriberId::from(sub_id) 114 | } 115 | 116 | // Drops the inner subscription object, which causes a 117 | // downstream unsubscription event. 118 | fn unsubscribe(&self) { 119 | self.inner.lock().take(); 120 | } 121 | 122 | fn next(&self, py: Python) -> PyResult { 123 | let query_sub = self.inner.clone(); 124 | let res = self.rt_handle.block_on(async { 125 | tokio::select!( 126 | res1 = async move { 127 | let query_sub_inner = query_sub.lock().take(); 128 | if query_sub_inner.is_none() { 129 | return Err(PyStopIteration::new_err("Stream requires reset")); 130 | } 131 | let mut query_sub_inner = query_sub_inner.unwrap(); 132 | let res = query_sub_inner.next().await; 133 | let _ = query_sub.lock().insert(query_sub_inner); 134 | Ok(res) 135 | } => res1, 136 | res2 = check_python_signals_periodically() => Err(res2) 137 | ) 138 | })?; 139 | match res.unwrap() { 140 | FunctionResult::Value(v) => Ok(value_to_py_wrapped(py, v)), 141 | FunctionResult::ErrorMessage(e) => Err(PyException::new_err(e)), 142 | FunctionResult::ConvexError(v) => { 143 | // pyo3 can't defined new custom exceptions when using the common abi 144 | // `features = ["abi3"]` https://github.com/PyO3/pyo3/issues/1344 145 | // so we define this error in Python. So just return a wrapped one. 146 | Ok(convex_error_to_py_wrapped(py, v)) 147 | }, 148 | } 149 | } 150 | 151 | fn anext(&self, py: Python<'_>) -> PyResult { 152 | let query_sub = self.inner.clone(); 153 | let fut = pyo3_async_runtimes::tokio::future_into_py(py, async move { 154 | let query_sub_inner = query_sub.lock().take(); 155 | if query_sub_inner.is_none() { 156 | return Err(PyStopAsyncIteration::new_err("Stream requires reset")); 157 | } 158 | let mut query_sub_inner = query_sub_inner.unwrap(); 159 | let res = query_sub_inner.next().await; 160 | let _ = query_sub.lock().insert(query_sub_inner); 161 | Python::with_gil(|py| match res.unwrap() { 162 | FunctionResult::Value(v) => Ok(value_to_py_wrapped(py, v)), 163 | FunctionResult::ErrorMessage(e) => Err(PyException::new_err(e)), 164 | FunctionResult::ConvexError(v) => { 165 | // pyo3 can't defined new custom exceptions when using the common abi 166 | // `features = ["abi3"]` https://github.com/PyO3/pyo3/issues/1344 167 | // so we define this error in Python. So just return a wrapped one. 168 | Ok(convex_error_to_py_wrapped(py, v)) 169 | }, 170 | }) 171 | })?; 172 | Ok(fut.unbind()) 173 | } 174 | } 175 | 176 | #[pyclass(frozen)] 177 | pub struct PyQuerySetSubscription { 178 | inner: Arc>>, 179 | pub rt_handle: Option, 180 | } 181 | 182 | impl From for PyQuerySetSubscription { 183 | fn from(query_set_sub: convex::QuerySetSubscription) -> Self { 184 | PyQuerySetSubscription { 185 | inner: Arc::new(Mutex::new(Some(query_set_sub))), 186 | rt_handle: None, 187 | } 188 | } 189 | } 190 | 191 | #[pymethods] 192 | impl PyQuerySetSubscription { 193 | fn exists(&self) -> bool { 194 | self.inner.lock().is_some() 195 | } 196 | 197 | fn next(&self, py: Python) -> PyResult { 198 | let query_sub = self.inner.clone(); 199 | let res = self.rt_handle.as_ref().unwrap().block_on(async { 200 | tokio::select!( 201 | res1 = async move { 202 | let query_sub_inner = query_sub.lock().take(); 203 | if query_sub_inner.is_none() { 204 | return Err(PyStopIteration::new_err("Stream requires reset")); 205 | } 206 | let mut query_sub_inner = query_sub_inner.unwrap(); 207 | let res = query_sub_inner.next().await; 208 | let _ = query_sub.lock().insert(query_sub_inner); 209 | Ok(res) 210 | } => res1, 211 | res2 = check_python_signals_periodically() => Err(res2) 212 | ) 213 | })?; 214 | let query_results = res.unwrap(); 215 | let py_dict = PyDict::new(py); 216 | for (sub_id, function_result) in query_results.iter() { 217 | if function_result.is_none() { 218 | continue; 219 | } 220 | let py_sub_id: PySubscriberId = (*sub_id).into(); 221 | 222 | let sub_value: PyObject = match function_result.unwrap() { 223 | FunctionResult::Value(v) => value_to_py_wrapped(py, v.clone()), 224 | FunctionResult::ErrorMessage(e) => { 225 | // TODO this is wrong! 226 | value_to_py(py, convex::Value::String(e.clone())) 227 | }, 228 | FunctionResult::ConvexError(v) => { 229 | // pyo3 can't defined new custom exceptions when using the common abi 230 | // `features = ["abi3"]` https://github.com/PyO3/pyo3/issues/1344 231 | // so we define this error in Python. So just return a wrapped one. 232 | convex_error_to_py_wrapped(py, v.clone()) 233 | .into_pyobject(py)? 234 | .unbind() 235 | }, 236 | }; 237 | py_dict 238 | .set_item(py_sub_id.into_pyobject(py)?, sub_value) 239 | .unwrap(); 240 | } 241 | Ok(py_dict.into_any().unbind()) 242 | } 243 | 244 | fn anext(&self, py: Python<'_>) -> PyResult { 245 | let query_sub = self.inner.clone(); 246 | let fut = pyo3_async_runtimes::tokio::future_into_py(py, async move { 247 | let query_sub_inner = query_sub.lock().take(); 248 | if query_sub_inner.is_none() { 249 | return Err(PyStopAsyncIteration::new_err("Stream requires reset")); 250 | } 251 | let mut query_sub_inner = query_sub_inner.unwrap(); 252 | let res = query_sub_inner.next().await; 253 | let _ = query_sub.lock().insert(query_sub_inner); 254 | 255 | Python::with_gil(|py| -> PyResult { 256 | let query_results = res.unwrap(); 257 | let py_dict = PyDict::new(py); 258 | for (sub_id, function_result) in query_results.iter() { 259 | if function_result.is_none() { 260 | continue; 261 | } 262 | let py_sub_id: PySubscriberId = (*sub_id).into(); 263 | let sub_value: PyObject = match function_result.unwrap() { 264 | FunctionResult::Value(v) => value_to_py(py, v.clone()), 265 | // TODO: this conflates errors with genuine values 266 | FunctionResult::ErrorMessage(e) => { 267 | value_to_py(py, convex::Value::String(e.to_string())) 268 | }, 269 | FunctionResult::ConvexError(e) => { 270 | let e = e.clone(); 271 | ( 272 | value_to_py(py, convex::Value::String(e.message)), 273 | value_to_py(py, e.data), 274 | ) 275 | .into_pyobject(py)? 276 | .into_any() 277 | .unbind() 278 | }, 279 | }; 280 | py_dict.set_item(py_sub_id, sub_value).unwrap(); 281 | } 282 | Ok(py_dict.into()) 283 | }) 284 | })?; 285 | Ok(fut.unbind()) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-py/6f4da34b98473215af91c8332d234d40fe11b280/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | from _convex import PyConvexClient 2 | from convex import ConvexClient, __version__ 3 | 4 | 5 | def test_rust_instantiation() -> None: 6 | PyConvexClient("https://made-up-animal.convex.cloud", __version__) 7 | 8 | 9 | def test_instantiation() -> None: 10 | # this is currently completely different code (HTTP client) 11 | ConvexClient("https://made-up-animal.convex.cloud") 12 | -------------------------------------------------------------------------------- /tests/test_values.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | from typing import Any, Optional 4 | 5 | import _convex 6 | import pytest 7 | from convex.values import ( 8 | CoercibleToConvexValue, 9 | ConvexInt64, 10 | ConvexValue, 11 | convex_to_json, 12 | json_to_convex, 13 | strict_convex_to_json, 14 | ) 15 | 16 | 17 | # These tests check that values produced by this library roundtrip. 18 | # It's even more important to check that values from the backend roundtrip, 19 | # which is tested elsewhere. 20 | def coerced_roundtrip(original: CoercibleToConvexValue) -> None: 21 | """Assert that a coercible to Python value roundtrips to Convex types. 22 | 23 | - can be coerced to a Convex value (this may NOT roundtrip) 24 | - has a json representation that roundtrips through a Convex value 25 | - has a coerced Convex value that roundtrips through JSON representation 26 | """ 27 | # this coerced value may not be equal to the original 28 | coerced = json_to_convex(json.loads(json.dumps(convex_to_json(original)))) 29 | strict_roundtrip(coerced) 30 | 31 | 32 | def strict_roundtrip(original: ConvexValue) -> None: 33 | """Assert that a Python value roundtrips to Convex types. 34 | 35 | - roundtrips through json, and 36 | - has a json object representation that also roundtrips 37 | """ 38 | json_object = strict_convex_to_json(original) 39 | 40 | # the json itself should roundtrip through serialization 41 | assert json_object == json.loads(json.dumps(json_object)) 42 | 43 | roundtripped_object = json_to_convex(json_object) 44 | 45 | # the original object should roundtrip 46 | assert roundtripped_object == original 47 | assert type(roundtripped_object) is type(original) 48 | 49 | roundtripped_json_object = strict_convex_to_json(roundtripped_object) 50 | 51 | # the json object representation of it should also roundtrip 52 | assert roundtripped_json_object == json_object 53 | # ...even when serialized 54 | assert json.dumps(roundtripped_json_object) == json.dumps(json_object) 55 | 56 | # Now let's roundtrip to and from Rust! 57 | # This codepath is the future, but we're keeping values.py around 58 | # for a pure-Python HTTP-based Convex client. 59 | 60 | roundtripped_from_rust = _convex.py_to_rust_to_py(original) 61 | assert roundtripped_from_rust == original 62 | assert type(roundtripped_from_rust) is type(original) 63 | 64 | 65 | def coerced_roundtrip_raises( 66 | original: Any, message: Optional[str] = None 67 | ) -> pytest.ExceptionInfo[Exception]: 68 | with pytest.raises((ValueError, TypeError)) as e: 69 | convex_to_json(original) 70 | if message: 71 | assert message in e.value.args[0] 72 | return e 73 | 74 | 75 | def test_strict_values() -> None: 76 | strict_roundtrip(None) 77 | strict_roundtrip(True) 78 | strict_roundtrip(False) 79 | strict_roundtrip(0.123) 80 | strict_roundtrip("abc") 81 | strict_roundtrip({"a": 0.123}) 82 | strict_roundtrip({}) 83 | strict_roundtrip({"a": 1.0, "b": 2.0}) 84 | strict_roundtrip(b"abc") 85 | 86 | # special values 87 | strict_roundtrip(float("inf")) 88 | strict_roundtrip(float("-0.0")) 89 | 90 | 91 | def test_ConvexInt64() -> None: 92 | strict_roundtrip(ConvexInt64(123)) 93 | roundtripped_from_rust = _convex.py_to_rust_to_py(ConvexInt64(123)) 94 | assert type(roundtripped_from_rust) is ConvexInt64 95 | assert roundtripped_from_rust.value == 123 96 | 97 | 98 | def test_floats_get_treated_as_ints() -> None: 99 | original = 123 100 | roundtripped = json_to_convex(convex_to_json(original)) 101 | assert type(roundtripped) is float 102 | 103 | roundtripped_rust = _convex.py_to_rust_to_py(original) 104 | assert type(roundtripped_rust) is float 105 | 106 | 107 | def test_subclassed_values() -> None: 108 | class IntSubclass(int): 109 | pass 110 | 111 | coerced_roundtrip(IntSubclass(0)) 112 | 113 | class FloatSubclass(float): 114 | pass 115 | 116 | coerced_roundtrip(FloatSubclass(123.0)) 117 | 118 | class StrSubclass(str): 119 | pass 120 | 121 | coerced_roundtrip(StrSubclass("asdf")) 122 | 123 | class BytesSubclass(bytes): 124 | pass 125 | 126 | coerced_roundtrip(BytesSubclass(b"adsf")) 127 | 128 | class DictSubclass(dict): # type: ignore 129 | pass 130 | 131 | coerced_roundtrip(DictSubclass(a=1)) 132 | 133 | class ListSubclass(list): # type: ignore 134 | pass 135 | 136 | coerced_roundtrip(ListSubclass("abc")) 137 | 138 | 139 | def test_coercion() -> None: 140 | # integers are coerced to floats 141 | coerced_roundtrip(0) 142 | coerced_roundtrip(1) 143 | 144 | coerced_roundtrip((1, 2)) 145 | coerced_roundtrip(range(10)) 146 | coerced_roundtrip(bytearray(b"abc")) 147 | coerced_roundtrip(collections.Counter("asdf")) 148 | 149 | 150 | def test_non_values() -> None: 151 | coerced_roundtrip_raises(object()) 152 | coerced_roundtrip_raises(object) 153 | coerced_roundtrip_raises(list) 154 | coerced_roundtrip_raises(pytest) 155 | coerced_roundtrip_raises(Any) 156 | coerced_roundtrip_raises(convex_to_json) 157 | coerced_roundtrip_raises({1: 1}) 158 | 159 | # value errors 160 | coerced_roundtrip(2**53) 161 | coerced_roundtrip(-(2**53)) 162 | coerced_roundtrip_raises(2**53 + 1) 163 | coerced_roundtrip_raises(-(2**53 + 1)) 164 | 165 | 166 | def test_context_errors() -> None: 167 | coerced_roundtrip_raises({"$a": 1}, "starts with a '$'") 168 | coerced_roundtrip_raises({"b": {2: 1}}, "must be strings") 169 | 170 | 171 | def test_decode_json() -> None: 172 | # TODO prevent top-level _a 173 | with pytest.raises(ValueError) as e: 174 | json_to_convex({"$a": 1}) 175 | assert "$" in e.value.args[0] 176 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | import convex 4 | import convex.http_client 5 | 6 | 7 | def test_version() -> None: 8 | # provisionally exists in >=3.8, no longer provisional in 3.10 9 | version: str = importlib.metadata.version("convex") # pyright: ignore 10 | assert convex.__version__ == version 11 | 12 | http_client = convex.http_client.ConvexHttpClient("https://www.example.com") 13 | 14 | # Convex backend expects a dash in 0.1.6-a0, PyPI doesn't. 15 | if "a" in version: 16 | version = version.replace("a", "-a") 17 | assert http_client.headers["Convex-Client"] == f"python-{version}" 18 | --------------------------------------------------------------------------------