diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 597a7e1e8..f773845de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: build: strategy: matrix: - device: [maixpy_m5stickv, maixpy_amigo, maixpy_dock, maixpy_yahboom, maixpy_cube, maixpy_wonder_mv, maixpy_tzt] + device: [maixpy_m5stickv, maixpy_amigo, maixpy_dock, maixpy_yahboom, maixpy_cube, maixpy_wonder_mv, maixpy_tzt, maixpy_embed_fire, maixpy_wonder_k] runs-on: ubuntu-latest services: registry: @@ -209,6 +209,54 @@ jobs: path: ${{ steps.extract-tzt.outputs.destination }} name: build-tzt + - name: Build for Embed Fire + if: matrix.device == 'maixpy_embed_fire' + uses: docker/build-push-action@v6 + with: + context: . + build-args: DEVICE=maixpy_embed_fire + push: true + tags: localhost:5000/selfcustody/krux-builder-embed-fire:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + - name: Extract firmware for Embed Fire + id: extract-embed-fire + if: matrix.device == 'maixpy_embed_fire' + uses: shrink/actions-docker-extract@v3 + with: + image: localhost:5000/selfcustody/krux-builder-embed-fire:latest + path: /src/firmware/Kboot/build/. + - name: Upload firmware for Embed Fire + if: matrix.device == 'maixpy_embed_fire' + uses: actions/upload-artifact@v4 + with: + path: ${{ steps.extract-embed-fire.outputs.destination }} + name: build-embed-fire + + - name: Build for WonderK + if: matrix.device == 'maixpy_wonder_k' + uses: docker/build-push-action@v6 + with: + context: . + build-args: DEVICE=maixpy_wonder_k + push: true + tags: localhost:5000/selfcustody/krux-builder-wonder-k:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + - name: Extract firmware for WonderK + id: extract-wonder-k + if: matrix.device == 'maixpy_wonder_k' + uses: shrink/actions-docker-extract@v3 + with: + image: localhost:5000/selfcustody/krux-builder-wonder-k:latest + path: /src/firmware/Kboot/build/. + - name: Upload firmware for WonderK + if: matrix.device == 'maixpy_wonder_k' + uses: actions/upload-artifact@v4 + with: + path: ${{ steps.extract-wonder-k.outputs.destination }} + name: build-wonder-k + - name: Move cache run: | rm -rf /tmp/.buildx-cache diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 000000000..46b2a02f5 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,13 @@ +name: Conventional Commits + +on: + pull_request: # We only need to check commit messages in PRs + +jobs: + conventional-commits: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: webiny/action-conventional-commits@v1.3.0 + with: + allowed-commit-types: "feat,fix,docs,style,refactor,test,i18n,ci,chore,git" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ed5edf19..a202a66d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -105,6 +105,6 @@ jobs: - name: Build coverage file run: poetry run pytest --cache-clear --cov src/krux --cov-report xml tests - name: Upload coverage reports to Codecov with GitHub Action - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 78c0ad7ed..004cf71f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +# Changelog 26.03.0 - March 2025 + +### New Device Support: Embed Fire +This device shares similarities with the WonderMV but stands out with its larger 2.4" touchscreen. + +### New Device Support: WonderK PRO +From the wonderful land of Korea, a new creation arrives: the WonderK PRO. Created by an entrepreneur who loves the Krux project, the WonderK follows in the footsteps of the WonderMV, but boasts a larger 2.8" display! Computer simulator for the WonderK device is also included. + +### Loading a Descriptor Sets Wallet Attributes +Loading a descriptor is now a quick way to set wallet attributes, as all necessary information is extracted automatically. This eliminates the need for manual configuration and ensures consistency with your coordinator wallet. + +### Changed BIP39 Passphrase Validation +Krux now displays a warning instead of blocking QR-encoded passphrases that contain non-ASCII characters. Users are encouraged to use QR codes containing only ASCII passphrases or non-ASCII that have already been normalized to NFKD. + +### Easier to Scan UR QR Codes +Exported Uniform Resource (UR) QR codes, a widely adopted standard for exchanging PSBTs, now use uppercase data to reduce QR density, improving scan reliability without increasing the number of frames. + +### Other Bug Fixes and Improvements +- Settings: Reduced default _Buttons Debounce_ value (with an even lower default on _M5StickV_) +- Settings: Expanded value ranges for _Touch Threshold_ and _Buttons Debounce_ +- Swipe handling: Detection threshold has been slightly reduced +- Keypad: Added backtick **`** +- Bugfix: Screensaver not activating in menu pages without statusbar +- Embit: Improved BIP39 mnemonic validation +- Bug Fix: Corrected handling of certain binary-encoded QR codes +- Fix fingerprint unset warn message for rare case +- Improved QR code decoding performance and added inverted color QR code detection +- Allow non deterministic chaincodes to be used with Taproot provably unspendable keys + # Changelog 25.10.1 - October 2025 ### Bugfix: Krux encrypted mnemonic as a passphrase is invalid, but no error was raised @@ -28,6 +57,9 @@ The Maix Bit device has long been discouraged due to its poor-quality camera. St - Optimized Datum show: total pages are no longer visible, and navigation no longer wraps from the first page to the last or vice versa; better memory management and handling for large (~100K) files - Enabled swipe up/down gestures on keypads, menus, and QR transcribe - Fix mnemonic thermal printing when long words span multiple lines +- Tools -> Check SD card is now in Tools -> Device Tests +- All devices scan TinySeed and other binary grids in grayscale mode for better speed. +- Button functionality reestablished on some pages for Yahboom with v1.1 hardware # Changelog 25.09.0 - September 2025 diff --git a/Dockerfile b/Dockerfile index 6785b1637..b361be165 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,7 +59,16 @@ RUN apt-get update -y && \ python3-setuptools RUN mkdir -p /opt && \ - git clone --depth 1 --recurse-submodules --shallow-submodules --branch v8.2.0-20190409 https://github.com/kendryte/kendryte-gnu-toolchain + GIT_TERMINAL_PROMPT=0 \ + git clone --depth 1 --branch v8.2.0-20190409 https://github.com/kendryte/kendryte-gnu-toolchain + +RUN cd kendryte-gnu-toolchain && \ + sed -i 's|https://github.com/bminor/binutils-gdb.git|https://github.com/riscvarchive/riscv-binutils-gdb.git|' .gitmodules && \ + git submodule sync && \ + git submodule update \ + --init \ + --recursive \ + --depth 1 RUN cd kendryte-gnu-toolchain && \ export PATH=$PATH:/opt/kendryte-toolchain/bin && \ diff --git a/docs/getting-started/features/tools.en.md b/docs/getting-started/features/tools.en.md index ca8236abe..48feb8274 100644 --- a/docs/getting-started/features/tools.en.md +++ b/docs/getting-started/features/tools.en.md @@ -59,7 +59,7 @@ A suite of automated tests to verify important features on your device.
-#### Touchscreen (Maix Amigo, Yahboom, WonderMV and TZT only) +#### Touchscreen (Maix Amigo, Yahboom, WonderMV, TZT or Embed Fire only) Confirm that touch input is accurately detected across the entire display surface. diff --git a/docs/getting-started/installing/from-pre-built-release.en.md b/docs/getting-started/installing/from-pre-built-release.en.md index 096d1282a..726902096 100644 --- a/docs/getting-started/installing/from-pre-built-release.en.md +++ b/docs/getting-started/installing/from-pre-built-release.en.md @@ -41,7 +41,7 @@ Connect the device to your computer via USB (for Maix Amigo, make sure you’re ./ktool -B goE -b 1500000 maixpy_DEVICE/kboot.kfpkg ``` -For `dock`, `wonder_mv` or `tzt` use the `-B dan` parameter: +For `dock`, `wonder_mv`, `tzt` or `embed_fire` use the `-B dan` parameter: ```bash ./ktool -B dan -b 1500000 maixpy_DEVICE/kboot.kfpkg ``` diff --git a/docs/getting-started/installing/from-source.en.md b/docs/getting-started/installing/from-source.en.md index b8c055c09..bf16c67bb 100644 --- a/docs/getting-started/installing/from-source.en.md +++ b/docs/getting-started/installing/from-source.en.md @@ -65,7 +65,7 @@ unzip kboot.kfpkg -d ./kboot/ ``` ### Flash the firmware onto the device -Connect the device to your computer via USB (for Maix Amigo, make sure you’re using bottom port), power it on, and run the following, replacing `DEVICE` with either `m5stickv`, `amigo`, `cube`, `dock`, `yahboom`, `wonder_mv` or `tzt`: +Connect the device to your computer via USB (for Maix Amigo, make sure you’re using bottom port), power it on, and run the following, replacing `DEVICE` with either `m5stickv`, `amigo`, `cube`, `dock`, `yahboom`, `wonder_mv`, `tzt` or `embed_fire`: ```bash # flash firmware to DEVICE ./krux flash maixpy_DEVICE diff --git a/docs/getting-started/installing/from-test-release.en.md b/docs/getting-started/installing/from-test-release.en.md index 5397febb5..94b00d662 100644 --- a/docs/getting-started/installing/from-test-release.en.md +++ b/docs/getting-started/installing/from-test-release.en.md @@ -142,3 +142,21 @@ To Flash TZT you need to pass the `-B dan` parameter. ```pwsh .\ktool-win.exe -B dan -b 1500000 maixpy_tzt\kboot.kfpkg ``` + +#### Embed Fire +To Flash Embed Fire you need to pass the `-B dan` parameter and `-b 400000` (maximum supported baud rate). + +##### Linux +```bash +./ktool-linux -B dan -b 400000 maixpy_embed_fire/kboot.kfpkg +``` + +##### Mac +```bash +./ktool-mac -B dan -b 400000 maixpy_embed_fire/kboot.kfpkg +``` + +##### Windows +```pwsh +.\ktool-win.exe -B dan -b 400000 maixpy_embed_fire\kboot.kfpkg +``` diff --git a/docs/getting-started/settings.en.md b/docs/getting-started/settings.en.md index 121a8b8c7..20c317a25 100644 --- a/docs/getting-started/settings.en.md +++ b/docs/getting-started/settings.en.md @@ -119,7 +119,7 @@ Here you choose between `thermal/adafruit`, `cnc/file`, `cnc/grbl` or `none` (de
-#### Touchscreen (Maix Amigo, Yahboom, WonderMV and TZT only) +#### Touchscreen (Maix Amigo, Yahboom, WonderMV, TZT or Embed Fire only) If your device has touchscreen you can change the touch detection threshold. If it is being too sensitive or detecting false or ghost touches, you should increase the threshold value, making it less sensitive. The other way is also valid, reduce the threshold to make the screen more sensitive to touches. diff --git a/docs/getting-started/usage/generating-a-mnemonic.en.md b/docs/getting-started/usage/generating-a-mnemonic.en.md index 5f8cf2381..171434f5a 100644 --- a/docs/getting-started/usage/generating-a-mnemonic.en.md +++ b/docs/getting-started/usage/generating-a-mnemonic.en.md @@ -6,7 +6,7 @@ At the start screen, select **New Mnemonic**, and choose between camera, words, ## Camera -(Experimental!) Choose between 12, 24 words or double mnemonic, then take a random picture and Krux will generate a mnemonic from the hash of the image bytes. +(Experimental!) Choose between 12, 24 words or double mnemonic, then take a random picture and Krux will generate a mnemonic from the hash of the image's raw bytes.[:material-numeric-1-circle:{ title="Conceptually similar to Lavarand" }](#lavarand) @@ -17,9 +17,13 @@ At the start screen, select **New Mnemonic**, and choose between camera, words, -During image capture, entropy quality estimation is displayed to assist you in obtaining a quality image source for your key. After the snapshot is taken, [Shannon's entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) and pixel deviation indices are presented. Minimum thresholds are established to prevent the use of poor-quality images with low entropy for key generation. +During image capture, a real-time entropy quality indicator is displayed to help you select a suitable image. After the photo is taken, [Shannon's entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) and pixel deviation metrics are shown. Minimum thresholds are enforced to prevent using images with insufficient entropy quality. -**Note**: These values ​​serve only as indicators or estimations of the quality of entropy, but are not absolute values ​​of entropy in a cryptographic context. +**Note**: These metrics are *approximations* intended to guide image selection. They do **not** represent absolute cryptographic entropy values. + +:material-numeric-1-circle:{id="lavarand"}: +This approach is conceptually similar to [Lavarand](https://en.wikipedia.org/wiki/Lavarand), which used chaotic visual phenomena as source of entropy for cryptographic random number generators. Likewise, Krux leverages the unpredictable structure of captured images as entropy. +
diff --git a/docs/getting-started/usage/loading-a-mnemonic.en.md b/docs/getting-started/usage/loading-a-mnemonic.en.md index bacd30bf7..cef1e0af6 100644 --- a/docs/getting-started/usage/loading-a-mnemonic.en.md +++ b/docs/getting-started/usage/loading-a-mnemonic.en.md @@ -161,7 +161,11 @@ It is possible to change any of the **wallet's attributes** (it will be possible -You can type or scan a BIP39 passphrase. When typing, swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change keypads if your device has a touchscreen. You can also hold the button `PAGE` or `PREVIOUS` when navigating among letters while typing text to fast forward or backward. For scanning, you can also create a QR code from your offline passphrase using the [Datum tool](../features/tools.md/#datum-tool). +You can type or scan a BIP39 passphrase. When typing, swipe left :material-gesture-swipe-left: or right :material-gesture-swipe-right: to change keypads if your device has a touchscreen. Hold `PAGE` or `PREVIOUS` to move quickly through letters, and hold `ENTER` to jump to the next keypad. + +For scanning, you can generate an offline passphrase QR code using the [Datum tool](../features/tools.md/#datum-tool). + +**Note**: [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed) requires passphrases to be NFKD-normalized. Due to firmware size constraints, Krux cannot perform normalization internally. We therefore recommend using only ASCII QR codes or ensuring any non-ASCII are already normalized to NFKD.
diff --git a/docs/getting-started/usage/navigating-the-main-menu.en.md b/docs/getting-started/usage/navigating-the-main-menu.en.md index 6dc1b6063..3857fbcd9 100644 --- a/docs/getting-started/usage/navigating-the-main-menu.en.md +++ b/docs/getting-started/usage/navigating-the-main-menu.en.md @@ -152,6 +152,13 @@ For multisig and miniscript, loading a wallet descriptor is essential to verify When you select the `Wallet Descriptor` option for the first time, you will be prompted to load via QR code or SD card. After loading, a preview of the descriptor attributes will be displayed for confirmation. We shown each key’s fingerprint, derivation path, and abbreviated XPUB highlighted with a different color. +Loading a descriptor is also a quick way to set wallet attributes, as all necessary information is extracted automatically: +- Network (Mainnet or Testnet) +- Policy type (single-sig, multisig or miniscript) +- Script type (Native Segwit, Taproot, Nested Segwit, Legacy) + +This eliminates the need for manual configuration and ensures consistency with your coordinator wallet. +
**Miniscript Descriptors** present an indented view of the miniscript after the keys. When Taproot is used, Krux checks if the internal key is "provably unspendable", meaning funds can only be moved via Tap tree scripts, in which case the internal key is displayed in a disabled color. diff --git a/docs/img/maixpy_embed_fire/about-303.png b/docs/img/maixpy_embed_fire/about-303.png new file mode 100644 index 000000000..53785b1ee Binary files /dev/null and b/docs/img/maixpy_embed_fire/about-303.png differ diff --git a/docs/img/maixpy_embed_fire/logo-303.en.png b/docs/img/maixpy_embed_fire/logo-303.en.png new file mode 100644 index 000000000..11693b307 Binary files /dev/null and b/docs/img/maixpy_embed_fire/logo-303.en.png differ diff --git a/docs/index.en.md b/docs/index.en.md index 2e37b81a6..13e974cfc 100644 --- a/docs/index.en.md +++ b/docs/index.en.md @@ -14,7 +14,7 @@ hide: Krux is an open-source firmware that transforms off-the-shelf Kendryte K210 devices, such as the Maix Amigo, M5StickV and [more](parts.md), into versatile bitcoin transaction signers. Beyond its core functionality, Krux is a flexible platform that can adapt to devices with different form factors, providing a suite of tools to assist with the creation and recovery of mnemonic backups, some of which include encryption options for enhanced security. -Devices like the Maix Amigo, Yahboom, WonderMV or TZT come ready to use, with large touchscreens that make it easy and user-friendly to operate. These devices are ideal for those looking for a plug-and-play solution. On the other hand, Krux also supports development board kits, which are perfect for DIY enthusiasts who enjoy customizing and building their own hardware setups. +Devices like the Maix Amigo, Yahboom, WonderMV, TZT or Embed Fire come ready to use, with large touchscreens that make it easy and user-friendly to operate. These devices are ideal for those looking for a plug-and-play solution. On the other hand, Krux also supports development board kits, which are perfect for DIY enthusiasts who enjoy customizing and building their own hardware setups. Interacting seamlessly with leading coordinator wallets through QR codes, SD cards, and even thermal printers, the user-friendly firmware offers unique features to support transactions and mnemonic backups in an offline environment. diff --git a/docs/parts.en.md b/docs/parts.en.md index 8dbef3c77..91026f28a 100644 --- a/docs/parts.en.md +++ b/docs/parts.en.md @@ -2,17 +2,17 @@ ### Compatible Devices (comparative table) -| Device | [M5StickV](#m5stickv) | [Maix Amigo](#maix-amigo) | [Maix Dock](#maix-dock) | [Yahboom k210 module](#yahboom-k210-module) | [Maix Cube](#maix-cube) | [WonderMV](#wondermv) | [TZT](#tzt) | -| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | -| Price range | US$ 50-55 | US$ 50-85 | US$ 27-35 | US$ 45-61 | US$ 34-49 | US$ 58-86 | US$ 48 | -| Screen size / resolution | 1.14" / 135*240 | 3.5" / 320*480 | 2.4" / 240*320 | 2" / 240*320 | 1.3" / 240*240 | 2" / 240*320 | 2" / 240*320 | -| Brightness control | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | -| Device size | 48\*24\*22mm | 104\*63\*17mm | 98\*59\*18mm | 57\*41\*17mm | 40\*40\*16mm | 59\*41\*17mm | 57\*41\*16mm | -| Touchscreen | :x: | Capacitive | :x: | Capacitive | :x: | Capacitive | Capacitive | -| Camera | `OV7740` | `OV7740` rear
`GC0328` front | `GC0328` | `OV2640` (VER:1.0) or
`GC2145` (VER:1.1) | `OV7740` | `GC2145` | `GC2145` or
`GC0328` | -| Battery | 200mAh | 520mAh | :x: | :x: | 200mAh | :x: | :x: | -| Requirements | None | None | [Rotary encoder](https://duckduckgo.com/?q=ky-040)
[3D printed case](https://github.com/selfcustody/DockEncoderCase)
Soldering
Assembly | None | None | None | None | -| Warnings | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) | [:material-numeric-2-circle:{ title="Maix Amigo screens" }](#amigo-info) | [:material-numeric-3-circle:{ title="Maix Dock and soldered pin" }](#dock-info) | Micro USB | 3-Way button | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) [:material-numeric-4-circle:{ title="WonderMV and SD card" }](#wondermv-info) | None | +| Device | [M5StickV](#m5stickv) | [Maix Amigo](#maix-amigo) | [Maix Dock](#maix-dock) | [Yahboom k210 module](#yahboom-k210-module) | [Maix Cube](#maix-cube) | [WonderMV](#wondermv) | [TZT](#tzt) | [Embed Fire](#embed-fire) | +| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | +| Price range | US$ 50-55 | US$ 50-85 | US$ 27-35 | US$ 45-61 | US$ 34-49 | US$ 58-86 | US$ 48 | US$ 74 | +| Screen size /
Res. px /
PPI | 1.14" /
135 x 240 /
241 | 3.5" /
320 x 480 /
165 | 2.4" /
240 x 320 /
167 | 2" /
240 x 320 /
200 | 1.3" /
240 x 240 /
261 | 2" /
240 x 320 /
200| 2" /
240 x 320 /
200 | 2.4" /
240 x 320 /
167 | +| Brightness control | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | +| Device size | 48 x 24 x 22mm | 104 x 63 x 17mm | 98 x 59 x 18mm | 57 x 41 x 17mm | 40 x 40 x 16mm | 59 x 41 x 17mm | 57 x 41 x 16mm | 63 x 45 x 14mm | +| Touchscreen | :x: | Capacitive | :x: | Capacitive | :x: | Capacitive | Capacitive | Capacitive | +| Camera | `OV7740` | `OV7740` rear /
`GC0328` front | `GC0328` | `OV2640` (VER:1.0) or
`GC2145` (VER:1.1) | `OV7740` | `GC2145` | `GC2145` or
`GC0328` | `OV2640` or
`GC2145` | +| Battery | 200mAh | 520mAh | :x: | :x: | 200mAh | :x: | :x: | :x: | +| Requirements | None | None | [Rotary encoder](https://duckduckgo.com/?q=ky-040)
[3D printed case](https://github.com/selfcustody/DockEncoderCase)
Soldering
Assembly | None | None | None | None | None | +| Warnings | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) | [:material-numeric-2-circle:{ title="Maix Amigo screens" }](#amigo-info) | [:material-numeric-3-circle:{ title="Maix Dock and soldered pin" }](#dock-info) | Micro USB | 3-Way button | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) [:material-numeric-4-circle:{ title="WonderMV and SD card" }](#wondermv-info) | None | [:material-numeric-5-circle:{ title="Embed Fire loose touchscreen" }](#embedfire-info) | :material-numeric-1-circle:{id="pull-up-resistor-info"}: ----8<---- @@ -34,12 +34,16 @@ Some Maix Dock units are shipped with soldered pin connectors, which prevent the WonderMV may restart when certain SD cards are inserted. See [Why does my WonderMV reboot](troubleshooting.md/#why-does-my-wondermv-reboot-when-i-insert-an-sd-card). +:material-numeric-5-circle:{id="embedfire-info"}: +Embed Fire touchscreen may shift slightly within the case when pressed. Some units does't support high baudrate for flash. + + **All devices feature Kendryte K210 chip:** 28nm process, dual-core RISC-V 64bit @400MHz, 8 MB high-speed SRAM, DVP camera and MCU LCD interface, AES Accelerator, SHA256 Accelerator, FFT Accelerator. ### M5StickV - + Krux was first ported to this device in March 2022. Featuring the smallest screen size and resolution, it also includes a built-in battery. Refer to the [USB-C recognition warning :material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) above for important notes. Below is a list of distributors where it can be found: @@ -57,7 +61,7 @@ Krux was first ported to this device in March 2022. Featuring the smallest scree
### Maix Amigo - + Krux has supported this device since its second version in August 2022. It offers the largest screen size and resolution, a touchscreen, and a built-in battery. Below is a list of distributors where it is available: @@ -71,7 +75,7 @@ Krux has supported this device since its second version in August 2022. It offer
### Yahboom k210 module - + Supported by Krux since March 2024, this device features a touchscreen and comes with a compatible 32 GB card, a USB card reader, one PH2.0 4-pin male-to-male connector, and a PH2.0 female adapter for connecting to a [thermal printer](#optional-ttl-serial-thermal-printer). Below is a list of distributors where it is available: @@ -83,7 +87,7 @@ Supported by Krux since March 2024, this device features a touchscreen and comes
### Maix Cube - + Supported by Krux since July 2024, this device features the second smallest screen size and resolution, a 3-way button, and a built-in battery. Below is a list of distributors where it is available: @@ -97,7 +101,7 @@ Supported by Krux since July 2024, this device features the second smallest scre
### WonderMV - + Supported by Krux since September 2024, this touchscreen device features a metal backplate and includes a compatible 32 GB card, a USB card reader, and two 5264 4-pin connector male-to-male cables for connecting to a [thermal printer](#optional-ttl-serial-thermal-printer). Refer to the [USB-C recognition warning :material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) and the [SD card warning :material-numeric-4-circle:{ title="WonderMV and SD card" }](#wondermv-info) above for important notes. The following distributors offer this device: @@ -111,18 +115,27 @@ Supported by Krux since September 2024, this touchscreen device features a metal ### TZT - + Supported by Krux since October 2025, this touchscreen device comes in a premium milled aluminum housing and features five buttons. Available through the following distributors: -- [AliExpress](https://www.aliexpress.com/w/wholesale-tzt-canmv-k210.html) +- [AliExpress](https://www.aliexpress.com/w/wholesale-canmv-k210-ai-vision.html) + +
+ +### Embed Fire + + +Supported by Krux since November 2025, this touchscreen device features a 2.4" display, a metal backplate and also cables for connecting to a [thermal printer](#optional-ttl-serial-thermal-printer). It is available through the following distributors: + +- [AliExpress](https://www.aliexpress.com/w/wholesale-k210-AI-python.html)
### Maix Dock - + For DIY enthusiasts, Krux has supported the Maix Dock since August 2022. These kits include a board and screen but require you to source a rotary encoder or buttons separately and assemble the device yourself. Some Maix Dock boards also include Wi-Fi. diff --git a/docs/snippets/error-flashing-mac.en.txt b/docs/snippets/error-flashing-mac.en.txt new file mode 100644 index 000000000..766b34907 --- /dev/null +++ b/docs/snippets/error-flashing-mac.en.txt @@ -0,0 +1,6 @@ +**On macOS**, you may also run into flashing errors. This often happens when a proper CH34X USB driver is missing, preventing proper communication with the device. +To fix it, install the CH341 driver. In a terminal, type: +``` +brew install --cask wch-ch34x-usb-serial-driver +``` +After installing, you may also need to approve driver's "extension" in macOS system settings, identified as CH34xVCPDriver.app" \ No newline at end of file diff --git a/docs/troubleshooting.en.md b/docs/troubleshooting.en.md index a159e4e5f..d0b389842 100644 --- a/docs/troubleshooting.en.md +++ b/docs/troubleshooting.en.md @@ -52,6 +52,10 @@ On **Windows**, you may encounter the **flash failed** error when trying to flas +----8<---- +error-flashing-mac.en.txt +----8<---- + ## **After Installing** diff --git a/docs/video-tutorials.en.md b/docs/video-tutorials.en.md index c93bf99a5..e2ac23147 100644 --- a/docs/video-tutorials.en.md +++ b/docs/video-tutorials.en.md @@ -5,24 +5,24 @@ Most people prefer to learn by watching videos, and we are fortunate to have exc
-[![Krux on M5StickV + Sparrow Wallet](https://img.youtube.com/vi/rsdvmIcucwQ/0.jpg)](https://www.youtube.com/watch?v=rsdvmIcucwQ1) +[![Krux on Maix Amigo + Blue Wallet](https://img.youtube.com/vi/EtV_Iw6a0O0/0.jpg)](https://www.youtube.com/watch?v=EtV_Iw6a0O0)
-Krux on M5StickV + Sparrow Wallet - DIY Bitcoin Hardware Wallet (Similar to SeedSigner) +Krux on Maix Amigo + Blue Wallet - DIY Bitcoin Hardware Wallet (Similar to SeedSigner) -Crypto Guide (Jan 2023) +Crypto Guide (Dec 2022)
-[![Krux on Maix Amigo + Blue Wallet](https://img.youtube.com/vi/EtV_Iw6a0O0/0.jpg)](https://www.youtube.com/watch?v=EtV_Iw6a0O0) +[![Krux on M5StickV + Sparrow Wallet](https://img.youtube.com/vi/rsdvmIcucwQ/0.jpg)](https://www.youtube.com/watch?v=rsdvmIcucwQ1)
-Krux on Maix Amigo + Blue Wallet - DIY Bitcoin Hardware Wallet (Similar to SeedSigner) +Krux on M5StickV + Sparrow Wallet - DIY Bitcoin Hardware Wallet (Similar to SeedSigner) -Crypto Guide (Dec 2022) +Crypto Guide (Jan 2023)
@@ -127,37 +127,37 @@ Usando a Krux com impressora térmica
-[![Carteira Bitcoin com celular OFFLINE](https://img.youtube.com/vi/CPEnTP7l_9Y/0.jpg)](https://www.youtube.com/watch?v=CPEnTP7l_9Y) +[![Como instalar a carteira Krux no celular](https://img.youtube.com/vi/hhMYq75pAOc/0.jpg)](https://www.youtube.com/watch?v=hhMYq75pAOc)
-Carteira Bitcoin com celular OFFLINE - Krux mobile APK +Como instalar a carteira Krux no celular -DIG P2P - Bitcoin Para Iniciantes! (Aug 2023) +João Trein (Feb 2024)
-[![Como instalar a carteira Krux no celular](https://img.youtube.com/vi/hhMYq75pAOc/0.jpg)](https://www.youtube.com/watch?v=hhMYq75pAOc) +[![Faça sua hardware wallet em casa com a KRUX!](https://img.youtube.com/vi/1V6Lp0m8esc/0.jpg)](https://www.youtube.com/watch?v=1V6Lp0m8esc)
-Como instalar a carteira Krux no celular +Faça sua hardware wallet em casa com a KRUX! -João Trein (Feb 2024) +CAIOVSKI (Jun 2024)
-[![Faça sua hardware wallet em casa com a KRUX!](https://img.youtube.com/vi/1V6Lp0m8esc/0.jpg)](https://www.youtube.com/watch?v=1V6Lp0m8esc) +[![MAIX CUBE + KRUX É UMA BOA CARTEIRA?](https://img.youtube.com/vi/8k2RivwnHUc/0.jpg)](https://www.youtube.com/watch?v=8k2RivwnHUc)
-Faça sua hardware wallet em casa com a KRUX! +MAIX CUBE + KRUX É UMA BOA CARTEIRA? -CAIOVSKI (Jun 2024) +DIG P2P - Bitcoin Para Iniciantes! (Nov 2025)
diff --git a/firmware/Kboot b/firmware/Kboot index cb53021a4..66d7848d1 160000 --- a/firmware/Kboot +++ b/firmware/Kboot @@ -1 +1 @@ -Subproject commit cb53021a4591400377bbd56a50da821cbd513bb7 +Subproject commit 66d7848d1c31f5defc9197fadb7eca503d7f9b98 diff --git a/firmware/MaixPy b/firmware/MaixPy index ba5bc7e2d..9ae463fe5 160000 --- a/firmware/MaixPy +++ b/firmware/MaixPy @@ -1 +1 @@ -Subproject commit ba5bc7e2ddfcd5013195118eb4dcf406a786091c +Subproject commit 9ae463fe553ff135b36b21e02c330f4ba8648c42 diff --git a/firmware/font/hextokff.py b/firmware/font/hextokff.py index 87fcea906..0ac69d923 100644 --- a/firmware/font/hextokff.py +++ b/firmware/font/hextokff.py @@ -28,9 +28,8 @@ import os import json - BYTE_LEN = 2 -CHAR_LIST_EXCEPT_ASIAN = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !#$%&'()*+,-./:;<=>?@[\\]^_\"{|}~█₿ ⊚↳«»…" +CHAR_LIST_EXCEPT_ASIAN = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !#$%&'()*+,-./:;<=>?@`[\\]^_\"{|}~█₿ ⊚↳«»…" DEFAULT_CODEPOINTS = [ord(char) for char in CHAR_LIST_EXCEPT_ASIAN] TRANSLATIONS_DIR = "../../i18n/translations" diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index 76ddc6c66..9efac85e2 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -2,19 +2,18 @@ "% of the amount.": "% des Betrags.", "%d of %d multisig": "%d von %d Multisig", "%d to %d": "%d bis %d ", - "%s bits (%s bits/px)": "%s Bits (%s Bits/px)", + "%s bits": "%s Bits", "%s bytes": "%s Bytes", "%s chars": "%s Zchn.", "%s removed.": "%s wurde entfernt.", "%s: loaded!": "%s: geladen!", + "(%s bits/px)": "(%s Bits/px)", "(Experimental)": "(Experimental)", "(watch-only)": "(nur zur Betrachtung)", "12 words": "12 Wörter", "24 words": "24 Wörter", "About": "Über", - "Accept assumption?": "Annahme akzeptieren?", "Account": "Konto", - "Account #0 would be assumed": "Konto #0 würde angenommen", "Account Index": "Kontoindex", "Add or change wallet passphrase?": "Wallet-Passphrase hinzufügen oder ändern?", "Additional entropy from camera required for %s": "Zusätzliche Entropie von der Kamera erforderlich für %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Aufnahme abgebrochen", "Change": "Change Adresse", "Change theme and reboot?": "Thema ändern und neu starten?", + "Change wallet?": "Geldbörse ändern?", "Change:": "Change:", "Changes will last until shutdown.": "Änderungen bleiben bis zum Herunterfahren bestehen.", "Check SD Card": "Prüfe SD-Karte", @@ -175,12 +175,12 @@ "Mnemonic XOR": "Mnemonisches XOR", "Mnemonic and passphrase will be kept.": "Mnemotechnik und Passphrase werden beibehalten.", "Modified:": "Geändert:", - "Native Segwit - 84 would be assumed": "Native Segwit - 84 würde angenommen", "Network": "Netzwerk", "New Mnemonic": "Neue Mnemonic", "New firmware detected.": "Neue Firmware erkannt.", "No": "Nein", "No Passphrase": "Keine Passphrase", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Nicht-ASCII-Zeichen wurden in Ihrer Passphrase erkannt. Krux kann nicht garantieren, dass andere Wallets den gleichen Schlüssel ableiten.", "Not enough rolls!": "Nicht genug Würfe!", "Numbers": "Zahlen", "Octal": "Oktal", @@ -339,6 +339,7 @@ "Wait for the capture": "Warte auf die Erfassung", "Wallet": "Wallet", "Wallet Descriptor": "Wallet-Deskriptor", + "Wallet mismatch:": "Geldbörse passt nicht:", "Wallet output descriptor": "Wallet Ausgabedeskriptor", "Wallet output descriptor loaded!": "Wallet Ausgabedeskriptor geladen!", "Wallet output descriptor not found.": "Wallet Ausgabedeskriptor nicht gefunden.", diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 594155858..ad181ab6a 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -2,19 +2,18 @@ "% of the amount.": "% del monto.", "%d of %d multisig": "%d de %d multisig", "%d to %d": "%d a %d", - "%s bits (%s bits/px)": "%s bits (%s bits/px)", + "%s bits": "%s bits", "%s bytes": "%s bytes", "%s chars": "%s carac.", "%s removed.": "%s eliminado.", "%s: loaded!": "%s: ¡cargado!", + "(%s bits/px)": "(%s bits/px)", "(Experimental)": "(Experimental)", "(watch-only)": "(Solo para observación)", "12 words": "12 palabras", "24 words": "24 palabras", "About": "Nosotros", - "Accept assumption?": "¿Aceptar la suposición?", "Account": "Cuenta", - "Account #0 would be assumed": "Se supondría que la cuenta #0", "Account Index": "Índice de la cuenta", "Add or change wallet passphrase?": "¿Añadir o cambiar passphrase de la cartera?", "Additional entropy from camera required for %s": "Se requiere entropía adicional de la cámara para %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Captura cancelada", "Change": "Cambio", "Change theme and reboot?": "¿Cambiar de tema y reiniciar?", + "Change wallet?": "¿Cambiar cartera?", "Change:": "Cambio:", "Changes will last until shutdown.": "Los cambios durarán hasta que el dispositivo se apague.", "Check SD Card": "Revisar Tarjeta SD", @@ -175,12 +175,12 @@ "Mnemonic XOR": "XOR mnemónico", "Mnemonic and passphrase will be kept.": "Mnemónico y passphrase se mantendrán.", "Modified:": "Modificado:", - "Native Segwit - 84 would be assumed": "Segwit nativo - 84 se supondría", "Network": "Red", "New Mnemonic": "Nuevo Mnemónico", "New firmware detected.": "Nuevo firmware detectado.", "No": "No", "No Passphrase": "Sin Passphrase", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Se detectaron caracteres no ASCII en su frase de contraseña. Krux no puede garantizar que otros monederos obtengan la misma clave.", "Not enough rolls!": "¡No hay suficientes tiradas!", "Numbers": "Números", "Octal": "Octales", @@ -339,6 +339,7 @@ "Wait for the capture": "Espera la captura", "Wallet": "Cartera", "Wallet Descriptor": "Descriptor de Cartera", + "Wallet mismatch:": "Cartera no coincide:", "Wallet output descriptor": "Descriptor de salida de cartera", "Wallet output descriptor loaded!": "¡Se ha cargado el descriptor de salida de la cartera!", "Wallet output descriptor not found.": "No se encontró el descriptor de salida de la cartera.", diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index a0a36f04b..e9be61249 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -2,19 +2,18 @@ "% of the amount.": "% du montant.", "%d of %d multisig": "%d de %d multisignature", "%d to %d": "%d à %d", - "%s bits (%s bits/px)": "%s bits (%s bits/px)", + "%s bits": "%s bits", "%s bytes": "%s octets", "%s chars": "%s car.", "%s removed.": "%s supprimé.", "%s: loaded!": "%s : chargé !", + "(%s bits/px)": "(%s bits/px)", "(Experimental)": "(Expérimental)", "(watch-only)": "(consultation)", "12 words": "12 mots", "24 words": "24 mots", "About": "À propos", - "Accept assumption?": "Accepter supposition ?", "Account": "Compte", - "Account #0 would be assumed": "Le compte n °0 serait supposé", "Account Index": "Index du compte", "Add or change wallet passphrase?": "Ajoutez ou modifiez la phrase secrète ?", "Additional entropy from camera required for %s": "Entropie supplémentaire de la caméra requise pour %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Capture annulée", "Change": "Monnaie", "Change theme and reboot?": "Changer de thème et redémarrer ?", + "Change wallet?": "Changer portefeuille?", "Change:": "La monnaie :", "Changes will last until shutdown.": "Les modifications dureront jusqu'à l'arrêt.", "Check SD Card": "Vérifiez la carte SD", @@ -175,12 +175,12 @@ "Mnemonic XOR": "Mnémonique XOR", "Mnemonic and passphrase will be kept.": "Mnémonique et phrase secrète seront conservés.", "Modified:": "Modifié :", - "Native Segwit - 84 would be assumed": "Native Segwit - 84 serait supposé", "Network": "Réseau", "New Mnemonic": "Nouveau Mnémonique", "New firmware detected.": "Nouveau micrologiciel détecté.", "No": "Non", "No Passphrase": "Pas de phrase secrète", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Des caractères non ASCII ont été détectés dans votre phrase secrète. Krux ne peut garantir que d'autres portefeuilles obtiendront la même clé.", "Not enough rolls!": "Pas assez de jets !", "Numbers": "Nombres", "Octal": "Octale", @@ -339,6 +339,7 @@ "Wait for the capture": "Attendez la capture", "Wallet": "Portefeuille", "Wallet Descriptor": "Descripteur de Portefeuille", + "Wallet mismatch:": "Portefeuille différent:", "Wallet output descriptor": "Descripteur de sortie du portefeuille", "Wallet output descriptor loaded!": "Descripteur de sortie du portefeuille chargé !", "Wallet output descriptor not found.": "Descripteur de sortie du portefeuille introuvable.", diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json index 4c2d7b420..a2dfc2969 100644 --- a/i18n/translations/ja-JP.json +++ b/i18n/translations/ja-JP.json @@ -2,19 +2,18 @@ "% of the amount.": "% の金額.", "%d of %d multisig": "%d の%d マルチシグネチャ", "%d to %d": "%d へ %d", - "%s bits (%s bits/px)": "%sビット(%sビット/px)", + "%s bits": "%sビット", "%s bytes": "%s バイト", "%s chars": "%s 文字", "%s removed.": "%s 削除されました.", "%s: loaded!": "%s:読み込まれました!", + "(%s bits/px)": "(%sビット/px)", "(Experimental)": "(実験的)", "(watch-only)": "(閲覧専用)", "12 words": "12の単語", "24 words": "24の単語", "About": "概要", - "Accept assumption?": "仮定を受け入れますか?", "Account": "アカウント", - "Account #0 would be assumed": "アカウント#0は仮定されます", "Account Index": "アカウントインデックス", "Add or change wallet passphrase?": "ウォレットのパスフレーズを追加または変更しますか?", "Additional entropy from camera required for %s": "%sにはカメラからの追加エントロピーが必要です", @@ -39,6 +38,7 @@ "Capture cancelled": "キャプチャがキャンセルされました", "Change": "お釣り", "Change theme and reboot?": "テーマの変更と再起動しますか?", + "Change wallet?": "ウォレット変更?", "Change:": "お釣り:", "Changes will last until shutdown.": "変更はシャットダウンするまで持続します.", "Check SD Card": "SDカードを確認する", @@ -175,12 +175,12 @@ "Mnemonic XOR": "ニーモニックXOR", "Mnemonic and passphrase will be kept.": "Mnemonicとパスフレーズは保持されます.", "Modified:": "修正されました:", - "Native Segwit - 84 would be assumed": "ネイティブSegwit - 84が仮定されます", "Network": "ネットワーク", "New Mnemonic": "新しい Mnemonic", "New firmware detected.": "新しいファームウェアが検出されました.", "No": "いいえ", "No Passphrase": "パスフレーズなし", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "パスフレーズに非ASCII文字が検出されました.Kruxは、他のウォレットが同じキーを取得することを保証することはできません.", "Not enough rolls!": "ロールが足りません!", "Numbers": "数字", "Octal": "Octal", @@ -339,6 +339,7 @@ "Wait for the capture": "キャプチャを待ってください", "Wallet": "ワレット", "Wallet Descriptor": "ウォレットディスクリプター", + "Wallet mismatch:": "ウォレット不一致:", "Wallet output descriptor": "ウォレット出力ディスクリプター", "Wallet output descriptor loaded!": "ウォレット出力ディスクリプターがロードされました!", "Wallet output descriptor not found.": "ウォレット出力ディスクリプターが見つかりません.", diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json index 0cf4250a0..0124099b0 100644 --- a/i18n/translations/ko-KR.json +++ b/i18n/translations/ko-KR.json @@ -2,19 +2,18 @@ "% of the amount.": "수량: %", "%d of %d multisig": "%d의 %d 멀티시그", "%d to %d": "%d 부터 %d", - "%s bits (%s bits/px)": "%s 비트 (%s 비트/픽셀)", + "%s bits": "%s 비트", "%s bytes": "%s 바이트", "%s chars": "%s 자", "%s removed.": "%s 제거되었습니다.", "%s: loaded!": "%s: 로드됨!", + "(%s bits/px)": "(%s 비트/픽셀)", "(Experimental)": "(실험적 기능)", "(watch-only)": "(잔액조회 전용)", "12 words": "12단어", "24 words": "24단어", "About": "기본 정보", - "Accept assumption?": "가정을 수락하시겠습니까?", "Account": "계정", - "Account #0 would be assumed": "계정 #0이 가정됩니다", "Account Index": "계정 인덱스", "Add or change wallet passphrase?": "패스프레이즈를 추가하거나 변경하시겠습니까?", "Additional entropy from camera required for %s": "%s 에 필요한 카메라의 추가 엔트로피", @@ -39,6 +38,7 @@ "Capture cancelled": "캡처 취소됨", "Change": "잔돈", "Change theme and reboot?": "테마를 변경하고 재부팅하시겠습니까?", + "Change wallet?": "지갑 변경?", "Change:": "잔돈:", "Changes will last until shutdown.": "변경 사항은 종료될때까지 유지됩니다.", "Check SD Card": "SD카드 확인", @@ -175,12 +175,12 @@ "Mnemonic XOR": "니모닉 XOR", "Mnemonic and passphrase will be kept.": "니모닉과 암호는 유지됩니다.", "Modified:": "수정되었습니다:", - "Native Segwit - 84 would be assumed": "네이티브 세그윗 - BIP84를 적용합니다", "Network": "네트워크", "New Mnemonic": "새로운 니모닉", "New firmware detected.": "새로운 펌웨어가 감지되었습니다.", "No": "아니요", "No Passphrase": "패스프레이즈 없음", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "암호에서 ASCII가 아닌 문자가 감지되었습니다. Krux는 다른 지갑이 동일한 키를 파생할 것이라고 보장할 수 없습니다.", "Not enough rolls!": "주사위 횟수가 충분하지 않습니다!", "Numbers": "시드문구 순번", "Octal": "8진수", @@ -339,6 +339,7 @@ "Wait for the capture": "캡처될때까지 기다리십시오", "Wallet": "지갑 설정", "Wallet Descriptor": "지갑 디스크립터", + "Wallet mismatch:": "지갑 불일치:", "Wallet output descriptor": "지갑 출력 디스크립터", "Wallet output descriptor loaded!": "지갑 출력 디스크립터가 로드되었습니다!", "Wallet output descriptor not found.": "지갑 출력 디스크립터를 찾을 수 없습니다.", diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 0b997df1e..0595c0435 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -2,19 +2,18 @@ "% of the amount.": "% van het bedrag.", "%d of %d multisig": "%d van %d multisig", "%d to %d": "%d tot %d", - "%s bits (%s bits/px)": "%s bits (%s bits/px)", + "%s bits": "%s bits", "%s bytes": "%s bytes", "%s chars": "%s tek.", "%s removed.": "%s verwijderd.", "%s: loaded!": "%s: geladen!", + "(%s bits/px)": "(%s bits/px)", "(Experimental)": "(Experimenteel)", "(watch-only)": "(alleen inzien)", "12 words": "12 woorden", "24 words": "24 woorden", "About": "Over", - "Accept assumption?": "Aanname accepteren?", "Account": "Account", - "Account #0 would be assumed": "Account #0 zou worden aangenomen", "Account Index": "Accountindex", "Add or change wallet passphrase?": "Wachtwoordzin voor portemonnee toevoegen of wijzigen?", "Additional entropy from camera required for %s": "Extra entropie van camera vereist voor %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Opname geannuleerd", "Change": "Change", "Change theme and reboot?": "Thema veranderen en opnieuw opstarten?", + "Change wallet?": "Portemonnee wijzigen?", "Change:": "Wisselgeld:", "Changes will last until shutdown.": "Wijzigingen blijven van kracht tot afsluiten.", "Check SD Card": "Controleer SD kaart", @@ -175,12 +175,12 @@ "Mnemonic XOR": "Mnemonic XOR", "Mnemonic and passphrase will be kept.": "Geheugensteun en wachtwoord worden bewaard.", "Modified:": "Aangepast:", - "Native Segwit - 84 would be assumed": "Native Segwit - 84 zal worden gebruikt", "Network": "Netwerk", "New Mnemonic": "Geheugensteun aanmaken", "New firmware detected.": "Nieuwe firmware gevonden.", "No": "Nee", "No Passphrase": "Geen wachtwoordzin", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Er zijn niet-ASCII-tekens gedetecteerd in uw wachtwoordzin. Krux kan niet garanderen dat andere portefeuilles dezelfde sleutel zullen afleiden.", "Not enough rolls!": "Niet genoeg gedobbeld!", "Numbers": "Nummers", "Octal": "Octaal", @@ -339,6 +339,7 @@ "Wait for the capture": "Wacht op opname", "Wallet": "Portemonnee", "Wallet Descriptor": "Descriptor", + "Wallet mismatch:": "Portemonnee onjuist:", "Wallet output descriptor": "Portemonnee descriptor", "Wallet output descriptor loaded!": "Portemonnee descriptor geladen!", "Wallet output descriptor not found.": "Portemonnee descriptor niet gevonden.", diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index 09d5e3a61..e506f1ee2 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -2,19 +2,18 @@ "% of the amount.": "% do total.", "%d of %d multisig": "%d de %d multisig", "%d to %d": "%d a %d", - "%s bits (%s bits/px)": "%s bits (%s bits/px)", + "%s bits": "%s bits", "%s bytes": "%s bytes", "%s chars": "%s carac.", "%s removed.": "%s excluído.", "%s: loaded!": "%s: carregado!", + "(%s bits/px)": "(%s bits/px)", "(Experimental)": "(Experimental)", "(watch-only)": "(Somente visualização)", "12 words": "12 palavras", "24 words": "24 palavras", "About": "Sobre", - "Accept assumption?": "Aceitar suposições?", "Account": "Conta", - "Account #0 would be assumed": "A conta #0 seria assumida", "Account Index": "Índice da Conta", "Add or change wallet passphrase?": "Adicionar ou alterar a senha da carteira?", "Additional entropy from camera required for %s": "Entropia adicional da câmera é necessária para %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Captura cancelada", "Change": "Troco", "Change theme and reboot?": "Mudar o tema e reiniciar?", + "Change wallet?": "Mudar carteira?", "Change:": "Troco:", "Changes will last until shutdown.": "As alterações só durarão até o desligamento.", "Check SD Card": "Explorar o cartão SD", @@ -175,12 +175,12 @@ "Mnemonic XOR": "XOR Mnemônico", "Mnemonic and passphrase will be kept.": "Mnemônico e senha serão mantidos.", "Modified:": "Alterado:", - "Native Segwit - 84 would be assumed": "Segwit nativo - 84 seria assumido", "Network": "Rede", "New Mnemonic": "Novo Mnemônico", "New firmware detected.": "Novo firmware detectado.", "No": "Não", "No Passphrase": "Sem Senha", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Caracteres não ASCII foram detectados em sua senha. A Krux não pode garantir que outras carteiras obtenham a mesma chave.", "Not enough rolls!": "Jogadas insuficientes!", "Numbers": "Números", "Octal": "Octal", @@ -339,6 +339,7 @@ "Wait for the capture": "Aguarde a captura", "Wallet": "Carteira", "Wallet Descriptor": "Descritor da Carteira", + "Wallet mismatch:": "Carteira diferente:", "Wallet output descriptor": "Descritor da carteira", "Wallet output descriptor loaded!": "Descritor da carteira carregado!", "Wallet output descriptor not found.": "O descritor da carteira não foi encontrado.", diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json index 6acb2b15c..4b47d0dd9 100644 --- a/i18n/translations/ru-RU.json +++ b/i18n/translations/ru-RU.json @@ -2,19 +2,18 @@ "% of the amount.": "% от суммы.", "%d of %d multisig": "%d из %d мультиподпись", "%d to %d": "%d к %d", - "%s bits (%s bits/px)": "%s бит (%s бит/px)", + "%s bits": "%s бит", "%s bytes": "%s байт", "%s chars": "%s симв.", "%s removed.": "%s удален.", "%s: loaded!": "%s: загружено!", + "(%s bits/px)": "(%s бит/пиксель)", "(Experimental)": "(Эксперементальный)", "(watch-only)": "(Только часы)", "12 words": "12 слов", "24 words": "24 слова", "About": "О Программе", - "Accept assumption?": "Принять предположение?", "Account": "Учетная запись", - "Account #0 would be assumed": "Будет принят счет №0", "Account Index": "Индекс счета", "Add or change wallet passphrase?": "Добавить или изменить пароль кошелька?", "Additional entropy from camera required for %s": "Требуется дополнительная энтропия от камеры для %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Захват отменен", "Change": "Сдача", "Change theme and reboot?": "Сменить тему и перезагрузить?", + "Change wallet?": "Сменить кошелёк?", "Change:": "Сдача:", "Changes will last until shutdown.": "Изменения будут храниться до выключения.", "Check SD Card": "Проверить SD Карту", @@ -175,12 +175,12 @@ "Mnemonic XOR": "Мнемонический XOR", "Mnemonic and passphrase will be kept.": "Мнемоника и парольная фраза будут сохранены.", "Modified:": "Изменено:", - "Native Segwit - 84 would be assumed": "Native Segwit - 84 будет принято", "Network": "Сеть", "New Mnemonic": "Новая Мнемоника", "New firmware detected.": "Обнаружена новая прошивка.", "No": "Нет", "No Passphrase": "Нет Фраза-пароль", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "В вашей парольной фразе обнаружены символы, отличные от ASCII. Krux не может гарантировать, что другие кошельки получат тот же ключ.", "Not enough rolls!": "Недостаточно бросков!", "Numbers": "Числа", "Octal": "Восьмеричный", @@ -339,6 +339,7 @@ "Wait for the capture": "Дождитесь Захвата", "Wallet": "Кошелек", "Wallet Descriptor": "Дескриптор Кошелька", + "Wallet mismatch:": "Кошелёк не совпадает:", "Wallet output descriptor": "Выходной дескриптор кошелька", "Wallet output descriptor loaded!": "Выходной дескриптор кошелька загружен!", "Wallet output descriptor not found.": "Выходной дескриптор кошелька не найден.", diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json index 588ec91df..97747a70a 100644 --- a/i18n/translations/tr-TR.json +++ b/i18n/translations/tr-TR.json @@ -2,19 +2,18 @@ "% of the amount.": "tutarın %'si.", "%d of %d multisig": "%d / %d çoklu imza", "%d to %d": "%d to %d", - "%s bits (%s bits/px)": "%s bit (%s bit/piksel)", + "%s bits": "%s bit", "%s bytes": "%s bayt", "%s chars": "%s kar.", "%s removed.": "%s kaldırıldı.", "%s: loaded!": "%s: yüklendi!", + "(%s bits/px)": "(%s bit/piksel)", "(Experimental)": "(Deneysel)", "(watch-only)": "(sadece izle)", "12 words": "12 kelime", "24 words": "24 kelime", "About": "Hakkında", - "Accept assumption?": "Varsayım kabul edilsin mi?", "Account": "Hesap", - "Account #0 would be assumed": "#0 numaralı hesap varsayılacaktır", "Account Index": "Hesap Endeksi", "Add or change wallet passphrase?": "Cüzdan parolası eklensin mi veya değiştirilsin mi?", "Additional entropy from camera required for %s": "%s için kameradan gelen ek entropi gerekli", @@ -39,6 +38,7 @@ "Capture cancelled": "Yakalama iptal edildi", "Change": "Para Üstü", "Change theme and reboot?": "Temayı değiştir ve yeniden başlat?", + "Change wallet?": "Cüzdan değiştir?", "Change:": "Para Üstü:", "Changes will last until shutdown.": "Değişiklikler kapanışa kadar devam edecek.", "Check SD Card": "SD Kartı Kontrol Et", @@ -175,12 +175,12 @@ "Mnemonic XOR": "Anımsatıcı XOR", "Mnemonic and passphrase will be kept.": "Mnemonik ve parola tutulacaktır.", "Modified:": "Değiştirildi:", - "Native Segwit - 84 would be assumed": "Yerel Segwit - 84 varsayılacaktır", "Network": "Ağ", "New Mnemonic": "Yeni Mnemonic", "New firmware detected.": "Yeni donanım yazılımı tespit edildi.", "No": "Hayır", "No Passphrase": "Parola Yok", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Parolanızda ASCII olmayan karakterler algılandı. Krux, diğer cüzdanların aynı anahtarı türeteceğini garanti edemez.", "Not enough rolls!": "Yeterli zar atışı yok!", "Numbers": "Numaralar", "Octal": "Sekizlik", @@ -339,6 +339,7 @@ "Wait for the capture": "Yakalamanın tamamlanmasını bekleyin", "Wallet": "Cüzdan", "Wallet Descriptor": "Cüzdan Tanımlayıcısı", + "Wallet mismatch:": "Cüzdan uyuşmazlığı:", "Wallet output descriptor": "Cüzdan çıktı tanımlayıcısı", "Wallet output descriptor loaded!": "Cüzdan çıktı tanımlayıcısı yüklendi!", "Wallet output descriptor not found.": "Cüzdan çıktı tanımlayıcısı bulunamadı.", diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 7a86932ce..d901f57c9 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -2,19 +2,18 @@ "% of the amount.": "% của số tiền.", "%d of %d multisig": "%d của %d đa chữ kí", "%d to %d": "%d đến %d", - "%s bits (%s bits/px)": "%s bit (%s bit/px)", + "%s bits": "%s bit", "%s bytes": "%s byte", "%s chars": "%s ký tự", "%s removed.": "%s Đã bỏ.", "%s: loaded!": "%s: đã tải!", + "(%s bits/px)": "(%s bit/px)", "(Experimental)": "(Thực nghiệm)", "(watch-only)": "(chỉ xem)", "12 words": "12 từ", "24 words": "24 từ", "About": "Về chúng tôi", - "Accept assumption?": "Chấp nhận giả định?", "Account": "Tài khoản", - "Account #0 would be assumed": "Tài khoản #0 sẽ được giả định", "Account Index": "Chỉ mục tài khoản", "Add or change wallet passphrase?": "Thêm hoặc thay đổi cụm mật khẩu ví?", "Additional entropy from camera required for %s": "Entropy bổ sung từ máy ảnh cần thiết cho %s", @@ -39,6 +38,7 @@ "Capture cancelled": "Hủy chụp hình", "Change": "Thay đổi", "Change theme and reboot?": "Thay đổi giao diện và khởi động lại?", + "Change wallet?": "Đổi ví?", "Change:": "Thay đổi:", "Changes will last until shutdown.": "Thay đổi sẽ kéo dài cho đến khi tắt máy.", "Check SD Card": "Kiểm tra thẻ SD", @@ -175,12 +175,12 @@ "Mnemonic XOR": "Mnemonic XOR", "Mnemonic and passphrase will be kept.": "Từ gợi nhớ và cụm mật khẩu sẽ được lưu giữ.", "Modified:": "Đã sửa đổi:", - "Native Segwit - 84 would be assumed": "Native Segwit - 84 sẽ được giả định", "Network": "Mạng lưới", "New Mnemonic": "Mnemonic mới", "New firmware detected.": "Phát hiện phần sụn mới.", "No": "Không", "No Passphrase": "Không có cụm mật khẩu", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "Các ký tự không phải ASCII đã được phát hiện trong cụm mật khẩu của bạn. Krux không thể đảm bảo rằng các ví khác sẽ lấy được cùng một khóa.", "Not enough rolls!": "Không đủ số lần quay!", "Numbers": "Số", "Octal": "Bát phân", @@ -339,6 +339,7 @@ "Wait for the capture": "Chờ bắt", "Wallet": "Ví", "Wallet Descriptor": "Trình mô tả ví", + "Wallet mismatch:": "Ví không khớp:", "Wallet output descriptor": "Ví đầu ra mô tả", "Wallet output descriptor loaded!": "Đã tải bộ mô tả đầu ra của ví!", "Wallet output descriptor not found.": "Không tìm thấy bộ mô tả đầu ra ví.", diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json index 212e24e7f..e6deb371e 100644 --- a/i18n/translations/zh-CN.json +++ b/i18n/translations/zh-CN.json @@ -2,19 +2,18 @@ "% of the amount.": "% 的金额.", "%d of %d multisig": "%d/%d 多签", "%d to %d": "%d到%d", - "%s bits (%s bits/px)": "%s位 (%s位/像素)", + "%s bits": "%s位", "%s bytes": "%s 字节", "%s chars": "%s 个字符", "%s removed.": "已移除%s", "%s: loaded!": "%s :已加载!", + "(%s bits/px)": "(%s位/像素)", "(Experimental)": "(实验性)", "(watch-only)": "(仅观察)", "12 words": "12 单词", "24 words": "24 单词", "About": "关于", - "Accept assumption?": "接受假设?", "Account": "账户", - "Account #0 would be assumed": "将假定为账户 #0", "Account Index": "账户索引", "Add or change wallet passphrase?": "添加或更改钱包密码?", "Additional entropy from camera required for %s": "%s需要摄像头的额外熵", @@ -39,6 +38,7 @@ "Capture cancelled": "截取已取消", "Change": "找零", "Change theme and reboot?": "更改主题并重新启动?", + "Change wallet?": "更换钱包?", "Change:": "找零:", "Changes will last until shutdown.": "更改将在关机前保持.", "Check SD Card": "检查 SD 卡", @@ -175,12 +175,12 @@ "Mnemonic XOR": "助记符异或", "Mnemonic and passphrase will be kept.": "助记词和密码将被保留.", "Modified:": "修改时间:", - "Native Segwit - 84 would be assumed": "假定为原生 Segwit - 84", "Network": "网络", "New Mnemonic": "新助记词", "New firmware detected.": "检测到新固件.", "No": "否", "No Passphrase": "无 Passphrase ", + "Non-ASCII characters were detected in your passphrase. Krux cannot guarantee that other wallets will derive the same key.": "在您的密码短语中检测到非ASCII字符.Krux无法保证其他钱包将获得相同的密钥.", "Not enough rolls!": "投掷次数不足!", "Numbers": "数字", "Octal": "八进制", @@ -339,6 +339,7 @@ "Wait for the capture": "等待截取", "Wallet": "钱包", "Wallet Descriptor": "钱包描述", + "Wallet mismatch:": "钱包不匹配:", "Wallet output descriptor": "钱包输出描述符", "Wallet output descriptor loaded!": "钱包输出描述符加载重复!", "Wallet output descriptor not found.": "未找到钱包输出描述符.", diff --git a/krux b/krux index 0cbb2fc22..13dedb84b 100755 --- a/krux +++ b/krux @@ -70,6 +70,7 @@ if [ "$1" == "build" ]; then [ "$device" == "maixpy_yahboom_devkit" ]|| [ "$device" == "maixpy_tzt" ]|| [ "$device" == "maixpy_wonder_k" ]|| + [ "$device" == "maixpy_embed_fire" ]|| [ "$allow_unsupported" ]; then declare $(grep -m 1 version pyproject.toml | sed 's/ *= */=/g' | sed 's/"//g' | sed 's/\r//g') sed -i -- 's/VERSION = ".*"/VERSION = "'"$version"'"/g' src/krux/metadata.py @@ -96,7 +97,7 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == " CMD_SUFFIX="-s firmware.bin" elif [ "$1" == "boot-terminal" ]; then CMD_SUFFIX="-s firmware.bin -t" - else + elif [ "$1" == "erase" ]; then CMD_SUFFIX="-E" fi device="$2" @@ -105,10 +106,15 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == " else device_vendor_product_id="" BAUDRATE=2000000 + if [ "$device" == "maixpy_embed_fire" ]; then + BAUDRATE=400000 + elif [ "$device" == "maixpy_m5stickv" ]|| + [ "$device" == "maixpy_dock" ]; then + BAUDRATE=1500000 + fi if [ "$device" == "maixpy_m5stickv" ]; then # https://devicehunt.com/view/type/usb/vendor/0403/device/6001 device_vendor_product_id="0403:6001" - BAUDRATE=1500000 elif [ "$device" == "maixpy_amigo" ]|| [ "$device" == "maixpy_bit" ]|| [ "$device" == "maixpy_cube" ]; then @@ -118,7 +124,8 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == " [ "$device" == "maixpy_yahboom" ]|| [ "$device" == "maixpy_wonder_mv" ]|| [ "$device" == "maixpy_yahboom_devkit" ]|| - [ "$device" == "maixpy_wonder_k" ]; then + [ "$device" == "maixpy_wonder_k" ]|| + [ "$device" == "maixpy_embed_fire" ]; then # https://devicehunt.com/view/type/usb/vendor/1a86/device/7523 device_vendor_product_id="1a86:7523" elif [ "$device" == "maixpy_tzt" ]; then @@ -138,7 +145,8 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == " if [ "$device" == "maixpy_dock" ]|| [ "$device" == "maixpy_wonder_mv" ]|| [ "$device" == "maixpy_tzt" ]|| - [ "$device" == "maixpy_wonder_k" ]; then + [ "$device" == "maixpy_wonder_k" ]|| + [ "$device" == "maixpy_embed_fire" ]; then BOARD="dan" else BOARD="goE" @@ -283,7 +291,7 @@ elif [ "$1" == "build-release" ]; then cp -f ktool-win.exe ../$release_dir/ktool-win.exe cd .. - devices=("maixpy_m5stickv" "maixpy_amigo" "maixpy_dock" "maixpy_yahboom" "maixpy_cube" "maixpy_wonder_mv" "maixpy_tzt") + devices=("maixpy_m5stickv" "maixpy_amigo" "maixpy_dock" "maixpy_yahboom" "maixpy_cube" "maixpy_wonder_mv" "maixpy_tzt" "maixpy_embed_fire" "maixpy_wonder_k") for device in ${devices[@]}; do ./krux build $device mkdir -p $release_dir/$device diff --git a/mkdocs.yml b/mkdocs.yml index 1b0d7163f..088fe2c93 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,7 +52,7 @@ edit_uri: edit/main/docs docs_dir: docs site_dir: public extra: - latest_krux: krux-v25.10.1 + latest_krux: krux-v26.03.0 latest_installer: v0.0.21 latest_installer_rpm: krux_installer-0.0.21-1.x86_64.rpm latest_installer_deb: krux_installer_0.0.21_amd64.deb diff --git a/poetry.lock b/poetry.lock index dc953c8b9..809c5ef2a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,28 +1,28 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "astroid" -version = "4.0.1" +version = "4.0.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "astroid-4.0.1-py3-none-any.whl", hash = "sha256:37ab2f107d14dc173412327febf6c78d39590fdafcb44868f03b6c03452e3db0"}, - {file = "astroid-4.0.1.tar.gz", hash = "sha256:0d778ec0def05b935e198412e62f9bcca8b3b5c39fdbe50b0ba074005e477aab"}, + {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"}, + {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"}, ] [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] @@ -30,20 +30,20 @@ dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)" [[package]] name = "backrefs" -version = "5.9" +version = "6.2" description = "A wrapper around re and regex that adds additional back references." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f"}, - {file = "backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf"}, - {file = "backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa"}, - {file = "backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b"}, - {file = "backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9"}, - {file = "backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60"}, - {file = "backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59"}, + {file = "backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8"}, + {file = "backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be"}, + {file = "backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90"}, + {file = "backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b"}, + {file = "backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7"}, + {file = "backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7"}, + {file = "backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49"}, ] [package.extras] @@ -51,196 +51,220 @@ extras = ["regex"] [[package]] name = "black" -version = "25.9.0" +version = "26.3.1" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"}, - {file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"}, - {file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"}, - {file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"}, - {file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"}, - {file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"}, - {file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"}, - {file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"}, - {file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"}, - {file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"}, - {file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"}, - {file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"}, - {file = "black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175"}, - {file = "black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f"}, - {file = "black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831"}, - {file = "black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357"}, - {file = "black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47"}, - {file = "black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823"}, - {file = "black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140"}, - {file = "black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933"}, - {file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"}, - {file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"}, + {file = "black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2"}, + {file = "black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b"}, + {file = "black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac"}, + {file = "black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a"}, + {file = "black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a"}, + {file = "black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff"}, + {file = "black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c"}, + {file = "black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5"}, + {file = "black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e"}, + {file = "black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5"}, + {file = "black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1"}, + {file = "black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f"}, + {file = "black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7"}, + {file = "black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983"}, + {file = "black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb"}, + {file = "black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54"}, + {file = "black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f"}, + {file = "black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56"}, + {file = "black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839"}, + {file = "black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2"}, + {file = "black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78"}, + {file = "black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568"}, + {file = "black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f"}, + {file = "black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c"}, + {file = "black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1"}, + {file = "black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b"}, + {file = "black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" -pathspec = ">=0.9.0" +pathspec = ">=1.0.0" platformdirs = ">=2" -pytokens = ">=0.1.10" +pytokens = ">=0.4.0,<0.5.0" [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""] [[package]] name = "certifi" -version = "2025.10.5" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.6" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, + {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, + {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, - {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -256,108 +280,122 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or extra == \"docs\" or extra == \"sign\" and sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "extra == \"sign\" and sys_platform == \"win32\" or extra == \"docs\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coverage" -version = "7.11.0" +version = "7.13.5" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"}, - {file = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"}, - {file = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"}, - {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"}, - {file = "coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"}, - {file = "coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"}, - {file = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"}, - {file = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"}, - {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"}, - {file = "coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"}, - {file = "coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"}, - {file = "coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}, - {file = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}, - {file = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}, - {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}, - {file = "coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}, - {file = "coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}, - {file = "coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, - {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, - {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, - {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, - {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, - {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, - {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, - {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, - {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, - {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, - {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, - {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, - {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, - {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, - {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, - {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, - {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, - {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, - {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, - {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, - {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, - {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, - {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, - {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, - {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, - {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, - {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, ] [package.extras] @@ -365,14 +403,14 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dill" -version = "0.4.0" +version = "0.4.1" description = "serialize all of Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, ] [package.extras] @@ -434,11 +472,12 @@ version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] +markers = {main = "extra == \"docs\""} [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] @@ -457,19 +496,18 @@ files = [ [[package]] name = "isort" -version = "6.1.0" +version = "8.0.1" description = "A Python utility / library to sort Python imports." optional = false -python-versions = ">=3.9.0" +python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, - {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, + {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, + {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, ] [package.extras] colors = ["colorama"] -plugins = ["setuptools"] [[package]] name = "jinja2" @@ -496,7 +534,7 @@ version = "2.1.1" description = "Python bindings for LibreTranslate API" optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] files = [ {file = "libretranslatepy-2.1.1-py3-none-any.whl", hash = "sha256:bf9f8b0003c94f34e141553b7ec0a02876297c06955f5efea49afbc9f85303a9"}, {file = "libretranslatepy-2.1.1.tar.gz", hash = "sha256:3f28e1b990ba5f514ae215c08ace0c4e2327eeccaa356983aefbca3a25ecc568"}, @@ -508,7 +546,7 @@ version = "6.0.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, @@ -660,19 +698,19 @@ htmlsoup = ["BeautifulSoup4"] [[package]] name = "markdown" -version = "3.9" +version = "3.10.2" description = "Python implementation of John Gruber's Markdown." optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, - {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, + {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"}, + {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"}, ] [package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] testing = ["coverage", "pyyaml"] [[package]] @@ -834,15 +872,15 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform [[package]] name = "mkdocs-get-deps" -version = "0.2.0" -description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +version = "0.2.2" +description = "An extra command for MkDocs that infers required PyPI packages from `plugins` in mkdocs.yml" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, + {file = "mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650"}, + {file = "mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1"}, ] [package.dependencies] @@ -852,15 +890,15 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-macros-plugin" -version = "1.4.0" +version = "1.5.0" description = "Unleash the power of MkDocs with macros and variables" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "mkdocs_macros_plugin-1.4.0-py3-none-any.whl", hash = "sha256:630de99fe14d39c26c7915725b39bb7730058c622f547226432eb371cabff33c"}, - {file = "mkdocs_macros_plugin-1.4.0.tar.gz", hash = "sha256:687e710988736731d1a059633fc34cf66d21e57a07801bdd1991419199e0677c"}, + {file = "mkdocs_macros_plugin-1.5.0-py3-none-any.whl", hash = "sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f"}, + {file = "mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f"}, ] [package.dependencies] @@ -871,43 +909,44 @@ packaging = "*" pathspec = "*" python-dateutil = "*" pyyaml = "*" -super-collections = ">=0.5.7" +requests = "*" +super-collections = ">=0.6.2" termcolor = "*" [package.extras] doc = ["mkdocs-mermaid2-plugin"] -test = ["mkdocs-d2-plugin", "mkdocs-include-markdown-plugin", "mkdocs-macros-test", "mkdocs-material (>=6.2)", "mkdocs-test"] +test = ["mkdocs-d2-plugin", "mkdocs-include-markdown-plugin", "mkdocs-macros-test", "mkdocs-material (>=6.2)", "mkdocs-test (>=0.6.0)", "pytest"] [[package]] name = "mkdocs-material" -version = "9.6.22" +version = "9.7.5" description = "Documentation that simply works" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "mkdocs_material-9.6.22-py3-none-any.whl", hash = "sha256:14ac5f72d38898b2f98ac75a5531aaca9366eaa427b0f49fc2ecf04d99b7ad84"}, - {file = "mkdocs_material-9.6.22.tar.gz", hash = "sha256:87c158b0642e1ada6da0cbd798a3389b0bc5516b90e5ece4a0fb939f00bacd1c"}, + {file = "mkdocs_material-9.7.5-py3-none-any.whl", hash = "sha256:7cf9df2ff121fd098ff6e05c732b0be3699afca9642e2dfe4926c40eb5873eec"}, + {file = "mkdocs_material-9.7.5.tar.gz", hash = "sha256:f76bdab532bad1d9c57ca7187b37eccf64dd12e1586909307f8856db3be384ea"}, ] [package.dependencies] -babel = ">=2.10,<3.0" -backrefs = ">=5.7.post1,<6.0" -colorama = ">=0.4,<1.0" -jinja2 = ">=3.1,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.6,<2.0" -mkdocs-material-extensions = ">=1.3,<2.0" -paginate = ">=0.5,<1.0" -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -requests = ">=2.26,<3.0" +babel = ">=2.10" +backrefs = ">=5.7.post1" +colorama = ">=0.4" +jinja2 = ">=3.1" +markdown = ">=3.2" +mkdocs = ">=1.6,<2" +mkdocs-material-extensions = ">=1.3" +paginate = ">=0.5" +pygments = ">=2.16" +pymdown-extensions = ">=10.2" +requests = ">=2.30" [package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<12.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] +git = ["mkdocs-git-committers-plugin-2 (>=1.1)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4)"] +imaging = ["cairosvg (>=2.6)", "pillow (>=10.2)"] +recommended = ["mkdocs-minify-plugin (>=0.7)", "mkdocs-redirects (>=1.2)", "mkdocs-rss-plugin (>=1.6)"] [[package]] name = "mkdocs-material-extensions" @@ -924,22 +963,22 @@ files = [ [[package]] name = "mkdocs-static-i18n" -version = "1.3.0" +version = "1.3.1" description = "MkDocs i18n plugin using static translation markdown files" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "mkdocs_static_i18n-1.3.0-py3-none-any.whl", hash = "sha256:7905d52fff71d2c108b6c344fd223e848ca7e39ddf319b70864dfa47dba85d6b"}, - {file = "mkdocs_static_i18n-1.3.0.tar.gz", hash = "sha256:65731e1e4ec6d719693e24fee9340f5516460b2b7244d2a89bed4ce3cfa6a173"}, + {file = "mkdocs_static_i18n-1.3.1-py3-none-any.whl", hash = "sha256:4036e24795a150c9c4d4b001ed24a43aec01335f76188dbe5a5d8fb4a27eba65"}, + {file = "mkdocs_static_i18n-1.3.1.tar.gz", hash = "sha256:a6125ea7db6cc1a900d76a967f262535af09831160a93c56d7f0d522a79b5faf"}, ] [package.dependencies] mkdocs = ">=1.5.2" [package.extras] -material = ["mkdocs-material (>=9.2.5)"] +material = ["mkdocs-material (<9.7.2)"] [[package]] name = "mypy-extensions" @@ -955,120 +994,119 @@ files = [ [[package]] name = "numpy" -version = "2.3.4" +version = "2.4.3" description = "Fundamental package for array computing in Python" optional = true python-versions = ">=3.11" groups = ["main"] markers = "extra == \"simulator\"" files = [ - {file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a"}, - {file = "numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1"}, - {file = "numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996"}, - {file = "numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786"}, - {file = "numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc"}, - {file = "numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32"}, - {file = "numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd"}, - {file = "numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646"}, - {file = "numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d"}, - {file = "numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64"}, - {file = "numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"}, - {file = "numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c"}, - {file = "numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26"}, - {file = "numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc"}, - {file = "numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9"}, - {file = "numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f"}, - {file = "numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d"}, - {file = "numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6"}, - {file = "numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"}, - {file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920"}, + {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9"}, + {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470"}, + {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71"}, + {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15"}, + {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52"}, + {file = "numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd"}, + {file = "numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec"}, + {file = "numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4"}, + {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18"}, + {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5"}, + {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97"}, + {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c"}, + {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc"}, + {file = "numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9"}, + {file = "numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5"}, + {file = "numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee"}, + {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f"}, + {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f"}, + {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc"}, + {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476"}, + {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92"}, + {file = "numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687"}, + {file = "numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd"}, + {file = "numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d"}, + {file = "numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875"}, + {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070"}, + {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73"}, + {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368"}, + {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22"}, + {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a"}, + {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349"}, + {file = "numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c"}, + {file = "numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26"}, + {file = "numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b"}, + {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950"}, + {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd"}, + {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24"}, + {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0"}, + {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0"}, + {file = "numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a"}, + {file = "numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc"}, + {file = "numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7"}, + {file = "numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657"}, + {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7"}, + {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093"}, + {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a"}, + {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611"}, + {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720"}, + {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5"}, + {file = "numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0"}, + {file = "numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b"}, + {file = "numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857"}, + {file = "numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5"}, + {file = "numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd"}, ] [[package]] name = "opencv-python" -version = "4.11.0.86" +version = "4.13.0.92" description = "Wrapper package for OpenCV python bindings." optional = true python-versions = ">=3.6" groups = ["main"] markers = "extra == \"simulator\"" files = [ - {file = "opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59"}, + {file = "opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5"}, ] [package.dependencies] -numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} +numpy = {version = ">=2", markers = "python_version >= \"3.9\""} [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] markers = {main = "extra == \"docs\""} @@ -1103,161 +1141,146 @@ files = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] markers = {main = "extra == \"docs\""} +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + [[package]] name = "pillow" -version = "11.3.0" -description = "Python Imaging Library (Fork)" +version = "12.1.1" +description = "Python Imaging Library (fork)" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"simulator\"" files = [ - {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, - {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}, - {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}, - {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}, - {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}, - {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}, - {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}, - {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}, - {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}, - {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}, - {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}, - {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}, - {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}, - {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}, - {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}, - {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}, - {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}, - {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}, - {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}, - {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}, - {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}, - {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}, - {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"}, - {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"}, - {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"}, - {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}, - {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -test-arrow = ["pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.9.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, - {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, ] markers = {main = "extra == \"docs\""} -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - [[package]] name = "pluggy" version = "1.6.0" @@ -1276,19 +1299,19 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "poethepoet" -version = "0.37.0" +version = "0.42.1" description = "A task runner that works well with poetry and uv." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "poethepoet-0.37.0-py3-none-any.whl", hash = "sha256:861790276315abcc8df1b4bd60e28c3d48a06db273edd3092f3c94e1a46e5e22"}, - {file = "poethepoet-0.37.0.tar.gz", hash = "sha256:73edf458707c674a079baa46802e21455bda3a7f82a408e58c31b9f4fe8e933d"}, + {file = "poethepoet-0.42.1-py3-none-any.whl", hash = "sha256:d8d1345a5ca521be9255e7c13bc2c4c8698ed5e5ac5e9e94890d239fcd423d0a"}, + {file = "poethepoet-0.42.1.tar.gz", hash = "sha256:205747e276062c2aaba8afd8a98838f8a3a0237b7ab94715fab8d82718aac14f"}, ] [package.dependencies] pastel = ">=0.2.1,<0.3.0" -pyyaml = ">=6.0.2,<7.0" +pyyaml = ">=6.0.3,<7.0" [package.extras] poetry-plugin = ["poetry (>=1.2.0,<3.0.0) ; python_version < \"4.0\""] @@ -1433,21 +1456,21 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "4.0.2" +version = "4.0.5" description = "python code static checker" optional = false python-versions = ">=3.10.0" groups = ["dev"] files = [ - {file = "pylint-4.0.2-py3-none-any.whl", hash = "sha256:9627ccd129893fb8ee8e8010261cb13485daca83e61a6f854a85528ee579502d"}, - {file = "pylint-4.0.2.tar.gz", hash = "sha256:9c22dfa52781d3b79ce86ab2463940f874921a3e5707bcfc98dd0c019945014e"}, + {file = "pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2"}, + {file = "pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"}, ] [package.dependencies] -astroid = ">=4.0.1,<=4.1.dev0" +astroid = ">=4.0.2,<=4.1.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} -isort = ">=5,<5.13 || >5.13,<8" +isort = ">=5,<5.13 || >5.13,<9" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" tomlkit = ">=0.10.1" @@ -1458,15 +1481,15 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.16.1" +version = "10.21" description = "Extension pack for Python Markdown." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, - {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, + {file = "pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f"}, + {file = "pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5"}, ] [package.dependencies] @@ -1509,20 +1532,20 @@ cp2110 = ["hidapi"] [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1" -packaging = ">=20" +iniconfig = ">=1.0.1" +packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" @@ -1531,22 +1554,23 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pytest-cov" -version = "5.0.0" +version = "7.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" @@ -1584,14 +1608,54 @@ six = ">=1.5" [[package]] name = "pytokens" -version = "0.2.0" -description = "A Fast, spec compliant Python 3.13+ tokenizer that runs on older Pythons." +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8"}, - {file = "pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43"}, + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, ] [package.extras] @@ -1741,11 +1805,12 @@ version = "2.32.5" description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] certifi = ">=2017.4.17" @@ -1791,15 +1856,15 @@ test = ["pytest (>=7.0)", "pyyaml", "rich"] [[package]] name = "termcolor" -version = "3.1.0" +version = "3.3.0" description = "ANSI color formatting for output in terminal" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ - {file = "termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa"}, - {file = "termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970"}, + {file = "termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5"}, + {file = "termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5"}, ] [package.extras] @@ -1807,26 +1872,26 @@ tests = ["pytest", "pytest-cov"] [[package]] name = "tomlkit" -version = "0.13.3" +version = "0.14.0" description = "Style preserving TOML library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, ] [[package]] name = "translate" -version = "3.6.1" +version = "3.8.0" description = "This is a simple, yet powerful command line translator with google translate behind it. You can also use it as a Python module in your code." optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] files = [ - {file = "translate-3.6.1-py2.py3-none-any.whl", hash = "sha256:cebfb004989d9a2ab0d24c0c5805783c7f4e07243ea4ed2a8f1809d072bf712b"}, - {file = "translate-3.6.1.tar.gz", hash = "sha256:7e70ffa46f193cc744be7c88b8e1323f10f6b2bb90d24bb5d29fdf1e56618783"}, + {file = "translate-3.8.0-py2.py3-none-any.whl", hash = "sha256:44f85b51698ae1727476367efeb4ff58f863c0fdfc784df19201799a9489269f"}, + {file = "translate-3.8.0.tar.gz", hash = "sha256:b4c1ea5069c57c8dbd3ee3bb92a6d046d66f747171132e3a4639443e5dc190c7"}, ] [package.dependencies] @@ -1851,21 +1916,22 @@ url = "vendor/foundation-ur-py" [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] +markers = {main = "extra == \"docs\""} [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "urtypes" @@ -1883,14 +1949,14 @@ url = "vendor/urtypes" [[package]] name = "vulture" -version = "2.14" +version = "2.15" description = "Find dead code" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"}, - {file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"}, + {file = "vulture-2.15-py3-none-any.whl", hash = "sha256:a3d8ebef918694326620eb128fa783486c8d285b23381c2b457d864ac056ef8d"}, + {file = "vulture-2.15.tar.gz", hash = "sha256:f9d8b4ce29c69950d323f21dceab4a4d6c694403dffbed7713c4691057e561fe"}, ] [[package]] @@ -1946,4 +2012,4 @@ simulator = ["Pillow", "numpy", "opencv-python", "pygame", "pyzbar"] [metadata] lock-version = "2.1" python-versions = "^3.12.3" -content-hash = "c5d0eed771abce5ddb72522617d2d837317555aa5778055d9b95defd65166dcf" +content-hash = "507a7474dc8b120283104f65aa49761764c18b2cb3d156148bdaa9aba05deb2e" diff --git a/pyproject.toml b/pyproject.toml index 8fd159d9f..261272858 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ [tool.poetry] name = "krux" -version = "25.10.1" +version = "26.03.0" description = "Open-source signing device firmware for Bitcoin" authors = ["Jeff S "] readme = "README.md" @@ -30,22 +30,21 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.12.3" -translate = "^3.6.1" embit = { path = "./vendor/embit/", develop = true } ur = { path = "./vendor/foundation-ur-py/", develop = true } urtypes = { path = "./vendor/urtypes/", develop = true } # Docs site dependencies. Optional extras mkdocs = { version = "^1.6.1", optional = true } -mkdocs-material = { version = "^9.6.18", optional = true } -mkdocs-static-i18n = { version = "^1.3.0", optional = true } -pymdown-extensions = { version = "^10.16.1", optional = true } -mkdocs-macros-plugin = { version = "^1.3.9", optional = true } +mkdocs-material = { version = "^9.7.5", optional = true } +mkdocs-static-i18n = { version = "^1.3.1", optional = true } +pymdown-extensions = { version = "^10.21", optional = true } +mkdocs-macros-plugin = { version = "^1.5.0", optional = true } # Simulator dependencies. Optional extras -numpy = { version = "^2.3.2", optional = true } -opencv-python = { version = "^4.11.0.86", optional = true } -Pillow = { version = "^11.3.0", optional = true } +numpy = { version = "^2.4.3", optional = true } +opencv-python = { version = "^4.13.0.92", optional = true } +Pillow = { version = "^12.1.1", optional = true } pygame = { version = "^2.6.1", optional = true } pyzbar = { version = "^0.1.9", optional = true } @@ -56,15 +55,17 @@ pyserial = { version = "^3.5", optional = true } qrcode = { version = "^8.2", optional = true } [tool.poetry.group.dev.dependencies] -black = "^25.1.0" -pylint = "^4.0.2" -pytest = "^8.4.2" -pytest-cov = "^5.0.0" -pytest-mock = "^3.15.0" +black = "^26.3.1" +pylint = "^4.0.5" +pytest = "^9.0.2" +pytest-cov = "^7.0.0" +pytest-mock = "^3.15.1" PyQRCode = "^1.2.1" pycryptodome = "^3.23.0" -poethepoet = "^0.37.0" -vulture = "^2.14" +poethepoet = "^0.42.1" +vulture = "^2.15" +translate = "^3.8.0" +urllib3 = ">=2.6.3" [tool.poetry.extras] docs = ["mkdocs", "mkdocs-material", "mkdocs-static-i18n", "pymdown-extensions", "mkdocs-macros-plugin"] @@ -110,7 +111,7 @@ pre-commit = ["format", "lint", "i18n validate", "vulture", "test-simple"] pre-release = ["format", "lint", "i18n validate", "i18n-build", "update-glyphs", "vulture", "test-simple"] # run docs locally -docs = "poetry run mkdocs serve" +docs = "poetry run mkdocs serve --livereload" # translations tasks i18n = "python i18n/i18n.py" @@ -130,6 +131,7 @@ simulator-cube = "python simulator/simulator.py --device maixpy_cube" simulator-wonder-mv = "python simulator/simulator.py --device maixpy_wonder_mv" simulator-tzt = "python simulator/simulator.py --device maixpy_tzt" simulator-wonder-k = "python simulator/simulator.py --device maixpy_wonder_k" +simulator-embed-fire = "python simulator/simulator.py --device maixpy_embed_fire" # aliases simulator.ref = "simulator-amigo" simulator-m5.ref = "simulator-m5stickv" @@ -138,6 +140,8 @@ simulator-wonder.ref = "simulator-wonder-mv" simulator-wondermv.ref = "simulator-wonder-mv" simulator-wonderk.ref = "simulator-wonder-k" simulator-k.ref = "simulator-wonder-k" +simulator-embed.ref = "simulator-embed-fire" +simulator-embedfire.ref = "simulator-embed-fire" # git tasks git-update = "git submodule update --init --recursive" diff --git a/sign_release.py b/sign_release.py index c37b3819c..c6812e656 100644 --- a/sign_release.py +++ b/sign_release.py @@ -40,6 +40,8 @@ "maixpy_cube", "maixpy_wonder_mv", "maixpy_tzt", + "maixpy_embed_fire", + "maixpy_wonder_k", ] diff --git a/simulator/assets/maixpy_embed_fire.png b/simulator/assets/maixpy_embed_fire.png new file mode 100644 index 000000000..1b2991188 Binary files /dev/null and b/simulator/assets/maixpy_embed_fire.png differ diff --git a/simulator/assets/maixpy_embed_fire_mask.png b/simulator/assets/maixpy_embed_fire_mask.png new file mode 100644 index 000000000..fb42bc7bb Binary files /dev/null and b/simulator/assets/maixpy_embed_fire_mask.png differ diff --git a/simulator/kruxsim/devices.py b/simulator/kruxsim/devices.py index 31c2f6d25..a27b0f658 100644 --- a/simulator/kruxsim/devices.py +++ b/simulator/kruxsim/devices.py @@ -31,6 +31,7 @@ WONDER_MV = "maixpy_wonder_mv" WONDER_K = "maixpy_wonder_k" TZT = "maixpy_tzt" +EMBEDFIRE = "maixpy_embed_fire" WINDOW_SIZES = { M5STICKV: (320, 640), @@ -41,7 +42,8 @@ CUBE: (484, 612), WONDER_MV: (410, 590), WONDER_K: (500, 650), - TZT: (410,580) + TZT: (410,580), + EMBEDFIRE: (500, 671), } @@ -79,7 +81,7 @@ def load_font(device): os.path.join("..", "firmware", "font", "FusionPixel-14.bdf"), ), ] - elif device in (DOCK, YAHBOOM, WONDER_MV, TZT, WONDER_K): + elif device in (DOCK, YAHBOOM, WONDER_MV, TZT, WONDER_K, EMBEDFIRE): fonts[device] = [ pg.freetype.Font( os.path.join("..", "firmware", "font", "ter-u16n.bdf") @@ -156,6 +158,13 @@ def screenshot_rect(device): screen.get_rect().center[0] - 0, screen.get_rect().center[1] + 28, #36 #20 ) + elif device == EMBEDFIRE: + rect.width -= 10 + rect.height -= 30 + rect.center = ( + screen.get_rect().center[0] + 5, + screen.get_rect().center[1] + 15, + ) # TODO: fix after WONDER_K img is ready # elif device == WONDER_K: # rect.width -= 0 diff --git a/simulator/kruxsim/mocks/cst816.py b/simulator/kruxsim/mocks/cst816.py new file mode 100644 index 000000000..5e6fcb2e4 --- /dev/null +++ b/simulator/kruxsim/mocks/cst816.py @@ -0,0 +1,39 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import sys +from unittest import mock +from kruxsim.mocks.touchscreen_common import TCOMMON, register_sequence_executor + + +class CST816(TCOMMON): + def activate(self, irq_pin): + pass + + +touch_control = CST816() + + +if "krux.touchscreens.cst816" not in sys.modules: + sys.modules["krux.touchscreens.cst816"] = mock.MagicMock( + touch_control=touch_control, + register_sequence_executor=register_sequence_executor, + ) diff --git a/simulator/simulator.py b/simulator/simulator.py index 3b5ba25dc..c0c093247 100644 --- a/simulator/simulator.py +++ b/simulator/simulator.py @@ -112,6 +112,7 @@ from kruxsim.mocks import shannon from kruxsim.mocks import ft6x36 from kruxsim.mocks import gt911 +from kruxsim.mocks import cst816 from kruxsim.mocks import buttons from kruxsim.mocks import rotary from kruxsim.sequence import SequenceExecutor @@ -166,6 +167,7 @@ def run_krux(): CUBE_SIZE = (400, 424) WONDER_MV_SIZE = (304, 440) TZT_SIZE = (314, 442) +EMBEDFIRE_SIZE = (303,407) # Handle screenshots scale and alpha bg # When exporting the mask from GIMP uncheck "Save info about transparent pixels color" @@ -204,6 +206,11 @@ def run_krux(): mask_img = pg.image.load( os.path.join("assets", "maixpy_tzt_mask.png") ).convert_alpha() +elif (args.device == devices.EMBEDFIRE): + device_screenshot_size = EMBEDFIRE_SIZE + mask_img = pg.image.load( + os.path.join("assets", "maixpy_embed_fire_mask.png") + ).convert_alpha() # TODO: WONDER_K IMG # elif (args.device == devices.WONDER_K): # device_screenshot_size = WONDER_K_SIZE @@ -300,6 +307,8 @@ def screenshot(filename): if event.type == pg.MOUSEBUTTONDOWN: if args.device == devices.WONDER_K: gt911.touch_control.trigger_event() + elif args.device == devices.EMBEDFIRE: + cst816.touch_control.trigger_event() else: ft6x36.touch_control.trigger_event() if event.type == pg.ACTIVEEVENT and event.gain: diff --git a/src/krux/bip39.py b/src/krux/bip39.py index ba2ffe05f..2b1ccc928 100644 --- a/src/krux/bip39.py +++ b/src/krux/bip39.py @@ -20,7 +20,7 @@ def k_mnemonic_bytes(mnemonic: str, ignore_checksum: bool = False, wordlist=WORD Verifies the mnemonic checksum and returns it in bytes Equivalent to embit.bip39.mnemonic_to_bytes """ - words = mnemonic.strip().split() + words = mnemonic.split(" ") if len(words) % 3 != 0 or not 12 <= len(words) <= 24: raise ValueError("Invalid recovery phrase") diff --git a/src/krux/camera.py b/src/krux/camera.py index 840b88e9b..95088078d 100644 --- a/src/krux/camera.py +++ b/src/krux/camera.py @@ -117,7 +117,10 @@ def initialize_sensor(self, mode=QR_SCAN_MODE): if self.cam_id == OV5642_ID: sensor.set_hmirror(1) if self.cam_id == OV2640_ID: - sensor.set_vflip(1) + if kboard.is_embed_fire: + sensor.set_hmirror(0) + else: + sensor.set_vflip(1) if kboard.is_bit: # CIF mode will use central pixels and discard darker periphery sensor.set_framesize(sensor.CIF) @@ -173,7 +176,7 @@ def config_gc_2145(self): # Center weight mode = 7, default=0x01 (center mode = 0) sensor.__write_reg(0x0C, 0x71) - def has_antiglare(self): + def has_mode_control(self): """Returns whether the camera has anti-glare functionality""" return self.cam_id in (OV7740_ID, OV2640_ID, GC2145_ID, GC0328_ID) diff --git a/src/krux/display.py b/src/krux/display.py index 93e0c4073..fac23bb7f 100644 --- a/src/krux/display.py +++ b/src/krux/display.py @@ -151,6 +151,16 @@ def initialize_lcd(self): ss=board.config["lcd"]["ss"], clk=board.config["lcd"]["clk"], ) + elif kboard.is_embed_fire: + lcd.init( + type=6, # DEV_EMBED_FIRE + invert=False, + rst=board.config["lcd"]["rst"], + dcx=board.config["lcd"]["dcx"], + ss=board.config["lcd"]["ss"], + clk=board.config["lcd"]["clk"], + lcd_type=6, # ST7789V + ) elif kboard.is_cube: lcd.init( invert=True, diff --git a/src/krux/kboard.py b/src/krux/kboard.py index ebe47898b..bf77ad934 100644 --- a/src/krux/kboard.py +++ b/src/krux/kboard.py @@ -30,6 +30,7 @@ def __init__(self): self.is_amigo = board.config["type"] == "amigo" self.is_bit = board.config["type"] == "bit" self.is_cube = board.config["type"] == "cube" + self.is_embed_fire = board.config["type"] == "embed_fire" self.is_yahboom = board.config["type"] == "yahboom" self.is_wonder_mv = board.config["type"] == "wonder_mv" self.is_tzt = board.config["type"] == "tzt" diff --git a/src/krux/kef.py b/src/krux/kef.py index 42aab1a73..d91f0ae08 100644 --- a/src/krux/kef.py +++ b/src/krux/kef.py @@ -23,7 +23,6 @@ import ucryptolib import uhashlib_hw - # KEF: AES, MODEs VERSIONS, MODE_NUMBERS, and MODE_IVS are defined here # to disable a MODE: set its value to None # to disable a VERSION: set its value to None diff --git a/src/krux/key.py b/src/krux/key.py index 73127943d..7559f4e0f 100644 --- a/src/krux/key.py +++ b/src/krux/key.py @@ -131,6 +131,13 @@ NAME_MINISCRIPT: TYPE_MINISCRIPT, } +# Reverse mapping: policy type ID to name +POLICY_TYPE_NAMES_MAP = { + TYPE_SINGLESIG: NAME_SINGLE_SIG, + TYPE_MULTISIG: NAME_MULTISIG, + TYPE_MINISCRIPT: NAME_MINISCRIPT, +} + FINGERPRINT_SYMBOL = "⊚" DERIVATION_PATH_SYMBOL = "↳" @@ -345,3 +352,15 @@ def get_final_word_candidates(words): candidates.append(last_word) return candidates + + +def get_policy_type_name(policy_type): + """Returns human-readable name for a policy type constant""" + return POLICY_TYPE_NAMES_MAP.get(policy_type, "Unknown") + + +def get_network_name(network): + """Returns human-readable name for a network object (Mainnet or Testnet)""" + if not network: + return None + return "Mainnet" if network == NETWORKS["main"] else "Testnet" diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py index b10719f74..c78150501 100644 --- a/src/krux/krux_settings.py +++ b/src/krux/krux_settings.py @@ -300,7 +300,8 @@ class ButtonsSettings(SettingsNamespace): """Buttons debounce settings""" namespace = "settings.buttons" - debounce = NumberSetting(int, "debounce", 100, [100, 500]) + default_deb = 50 if kboard.is_m5stickv else 80 + debounce = NumberSetting(int, "debounce", default_deb, [20, 500]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" @@ -314,7 +315,7 @@ class TouchSettings(SettingsNamespace): namespace = "settings.touchscreen" default_th = 40 if kboard.is_wonder_k else 22 - threshold = NumberSetting(int, "threshold", default_th, [10, 200]) + threshold = NumberSetting(int, "threshold", default_th, [2, 200]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" diff --git a/src/krux/metadata.py b/src/krux/metadata.py index ad56c165e..d4ebc55cf 100644 --- a/src/krux/metadata.py +++ b/src/krux/metadata.py @@ -19,5 +19,5 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -VERSION = "25.10.1" +VERSION = "26.03.0" SIGNER_PUBKEY = "03339e883157e45891e61ca9df4cd3bb895ef32d475b8e793559ea10a36766689b" diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index b3c7f2517..5e954c093 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -67,7 +67,7 @@ LETTERS = "abcdefghijklmnopqrstuvwxyz" UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" NUM_SPECIAL_1 = "1234567890 !#$%()&*'" -NUM_SPECIAL_2 = '<>.,"[]:;/{}^~|-+=_\\?@' +NUM_SPECIAL_2 = '<>.,"[]:;/{}^~|-+=_\\?@`' DIGITS = "1234567890" BATTERY_WIDTH = 22 @@ -344,10 +344,7 @@ def display_mnemonic( display_mnemonic = display_mnemonic or mnemonic words = display_mnemonic.split(" ") - word_list = [ - "{}.{}{}".format(i + 1, " " if i + 1 < 10 else " ", word) - for i, word in enumerate(words) - ] + word_list = ["{:>2}. {}".format(i + 1, word) for i, word in enumerate(words)] if is_double_mnemonic(mnemonic): suffix += "*" if fingerprint: @@ -785,7 +782,7 @@ def run_loop(self, start_from_index=None, swipe_up_fnc=None, swipe_down_fnc=None ) if isinstance(selected_item_index, tuple): return selected_item_index - elif btn is None and self.menu_offset == STATUS_BAR_HEIGHT: + elif btn is None and self.menu_offset <= STATUS_BAR_HEIGHT: # Activates screensaver if there's no info_box(other things draw on the screen) self.screensaver() diff --git a/src/krux/pages/capture_entropy.py b/src/krux/pages/capture_entropy.py index 21e5324dc..3d51d29d3 100644 --- a/src/krux/pages/capture_entropy.py +++ b/src/krux/pages/capture_entropy.py @@ -187,9 +187,11 @@ def capture(self, show_entropy_details=True): shannon_16b_total = shannon_16b * img_pixels entropy_msg = t("Shannon's entropy:") + "\n" - entropy_msg += (t("%s bits (%s bits/px)") + "\n\n") % ( - generate_thousands_separator(int(shannon_16b_total)), - replace_decimal_separator("%.2g" % shannon_16b), + entropy_msg += (t("%s bits") + "\n") % generate_thousands_separator( + int(shannon_16b_total) + ) + entropy_msg += (t("(%s bits/px)") + "\n\n") % replace_decimal_separator( + "%.2g" % shannon_16b ) entropy_msg += "%s %s" % (t("Pixels deviation index:"), str(self.stdev_index)) self.ctx.display.clear() diff --git a/src/krux/pages/datum_tool.py b/src/krux/pages/datum_tool.py index 95663db1a..503d4d935 100644 --- a/src/krux/pages/datum_tool.py +++ b/src/krux/pages/datum_tool.py @@ -78,22 +78,23 @@ def urobj_to_data(ur_obj): """returns flatened data from a UR object. belongs in qr or qr_capture???""" - import urtypes - - if ur_obj.type == "crypto-bip39": - data = urtypes.crypto.BIP39.from_cbor(ur_obj.cbor).words - elif ur_obj.type == "crypto-account": - data = ( - urtypes.crypto.Account.from_cbor(ur_obj.cbor) - .output_descriptors[0] - .descriptor() - ) - elif ur_obj.type == "crypto-output": - data = urtypes.crypto.Output.from_cbor(ur_obj.cbor).descriptor() - elif ur_obj.type == "crypto-psbt": - data = urtypes.crypto.PSBT.from_cbor(ur_obj.cbor).data - elif ur_obj.type == "bytes": - data = urtypes.bytes.Bytes.from_cbor(ur_obj.cbor).data + from urtypes.crypto.bip39 import BIP39 + from urtypes.crypto.account import Account + from urtypes.crypto.output import Output + from urtypes.crypto.psbt import PSBT + from urtypes.bytes import Bytes + + if ur_obj.type.upper() == "CRYPTO-BIP39": + data = BIP39.from_cbor(ur_obj.cbor).words + data = " ".join(data) + elif ur_obj.type.upper() == "CRYPTO-ACCOUNT": + data = Account.from_cbor(ur_obj.cbor).output_descriptors[0].descriptor() + elif ur_obj.type.upper() == "CRYPTO-OUTPUT": + data = Output.from_cbor(ur_obj.cbor).descriptor() + elif ur_obj.type.upper() == "CRYPTO-PSBT": + data = PSBT.from_cbor(ur_obj.cbor).data + elif ur_obj.type.upper() == "BYTES": + data = Bytes.from_cbor(ur_obj.cbor).data else: data = None return data @@ -325,6 +326,12 @@ def scan_qr(self): title += ", UR:" + contents.type contents = urobj_to_data(contents) + if isinstance(contents, bytes): + try: + contents = contents.decode() + except: + pass + page = DatumTool(self.ctx) page.contents, page.title = contents, title return page.view_contents() @@ -415,7 +422,8 @@ def view_qr(self): """Reusable handler for viewing a QR code""" from ..qr import QR_CAPACITY_BYTE, QR_CAPACITY_ALPHANUMERIC, QR_CAPACITY_NUMERIC from ..bbqr import encode_bbqr - import urtypes + from urtypes.bytes import Bytes + from urtypes.crypto.psbt import PSBT from ur.ur import UR # Helper function to check if character is alphanumeric @@ -508,9 +516,9 @@ def is_alnum(c): elif qr_fmt == FORMAT_UR: ur_type = menu_opts[idx][1][1] if ur_type == "bytes": - encoded = UR(ur_type, urtypes.Bytes(encoded).to_cbor()) + encoded = UR(ur_type, Bytes(encoded).to_cbor()) elif ur_type == "crypto-psbt": - encoded = UR(ur_type, urtypes.PSBT(encoded).to_cbor()) + encoded = UR(ur_type, PSBT(encoded).to_cbor()) # TODO: other urtypes try: @@ -628,7 +636,7 @@ def _update_infobox(curr_page): if pages[-1] < endpos < content_len: pages.append(endpos) - offset_y = DEFAULT_PADDING + (info_len) * FONT_HEIGHT + 1 + offset_y = DEFAULT_PADDING + info_len * FONT_HEIGHT + 1 for line in lines: self.ctx.display.draw_string(offset_x, offset_y, line) offset_y += FONT_HEIGHT @@ -822,7 +830,7 @@ def view_contents(self, try_decrypt=True, offer_convert=False): menu = Menu( self.ctx, todo_menu, - offset=(info_len + 1) * FONT_HEIGHT + DEFAULT_PADDING + 2, + offset=info_len * FONT_HEIGHT + DEFAULT_PADDING, **back_status ) _, status = menu.run_loop() diff --git a/src/krux/pages/encryption_ui.py b/src/krux/pages/encryption_ui.py index e0bd4c14d..dd13cb864 100644 --- a/src/krux/pages/encryption_ui.py +++ b/src/krux/pages/encryption_ui.py @@ -525,6 +525,11 @@ def load_qr_encryption_key(self): return None if len(data) > ENCRYPTION_KEY_MAX_LEN: raise ValueError("Maximum length exceeded (%s)" % ENCRYPTION_KEY_MAX_LEN) + if isinstance(data, bytes): + try: + data = data.decode() + except: + pass return data diff --git a/src/krux/pages/home_pages/addresses.py b/src/krux/pages/home_pages/addresses.py index 4b4a59c68..fd1518bd4 100644 --- a/src/krux/pages/home_pages/addresses.py +++ b/src/krux/pages/home_pages/addresses.py @@ -313,6 +313,7 @@ def scan_address(self, addr_type=0): pass addr = None + data = data.decode() if isinstance(data, bytes) else data try: from ...wallet import parse_address diff --git a/src/krux/pages/home_pages/home.py b/src/krux/pages/home_pages/home.py index efacd436a..a4fdced21 100644 --- a/src/krux/pages/home_pages/home.py +++ b/src/krux/pages/home_pages/home.py @@ -400,7 +400,10 @@ def _post_load_psbt_warn(self, signer): return False # Fix zero fingerprint, it is necessary for the signing process on embit in a few cases - if signer.fill_zero_fingerprint(): + if ( + self.ctx.wallet.key.fingerprint != b"\x00\x00\x00\x00" + and signer.fill_zero_fingerprint() + ): self.ctx.display.clear() self.ctx.display.draw_centered_text(t("Fingerprint unset in PSBT")) if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE): diff --git a/src/krux/pages/home_pages/sign_message_ui.py b/src/krux/pages/home_pages/sign_message_ui.py index 5955591df..28e037402 100644 --- a/src/krux/pages/home_pages/sign_message_ui.py +++ b/src/krux/pages/home_pages/sign_message_ui.py @@ -45,7 +45,6 @@ from ...settings import TEST_TXT, MAIN_TXT from ...kboard import kboard - SD_MESSAGE_HEADER = "-----BEGIN BITCOIN SIGNED MESSAGE-----" SD_SIGNATURE_HEADER = "-----BEGIN BITCOIN SIGNATURE-----" SD_SIGNATURE_FOOTER = "-----END BITCOIN SIGNATURE-----" diff --git a/src/krux/pages/home_pages/wallet_descriptor.py b/src/krux/pages/home_pages/wallet_descriptor.py index 93fc90c47..344e82acf 100644 --- a/src/krux/pages/home_pages/wallet_descriptor.py +++ b/src/krux/pages/home_pages/wallet_descriptor.py @@ -122,10 +122,11 @@ def wallet(self): return MENU_CONTINUE - def _load_wallet(self): - """Load a wallet output descriptor from the camera or SD card""" - + def _load_wallet_data(self): + """Loads and decrypts wallet data from camera or SD card""" persisted = False + wallet_data = None + load_method = self.load_method() if load_method == LOAD_FROM_CAMERA: from ..qr_capture import QRCodeCapture @@ -150,15 +151,16 @@ def _load_wallet(self): except OSError: pass else: # Cancel - return MENU_CONTINUE + return None, None, False self.ctx.display.clear() self.ctx.display.draw_centered_text(t("Processing…")) if wallet_data is None: # Camera or SD card loading failed! self.flash_error(t("Failed to load")) - return MENU_CONTINUE + return None, None, False + # Decrypt if needed from ..encryption_ui import decrypt_kef try: @@ -169,31 +171,167 @@ def _load_wallet(self): wallet_data = wallet_data.decode() except: self.flash_error(t("Failed to load")) - return MENU_CONTINUE + return None, None, False except KeyError: self.flash_error(t("Failed to decrypt")) - return MENU_CONTINUE + return None, None, False except ValueError: # ValueError=not KEF or declined to decrypt pass - from ...wallet import Wallet, AssumptionWarning + return wallet_data, qr_format, persisted + + def _build_mismatch_warning( + self, + current_policy_type, + required_policy_type, + current_network, + required_network, + current_script_type=None, + required_script_type=None, + ): + """Builds warning text for policy/network/script type mismatch""" + from ...key import get_policy_type_name, get_network_name + + offset_y = DEFAULT_PADDING + 2 * FONT_HEIGHT + + # Check if policy changed + if current_policy_type != required_policy_type: + self.ctx.display.draw_hcentered_text( + "{}:".format(t("Policy Type")), offset_y, theme.highlight_color + ) + offset_y += FONT_HEIGHT + offset_y += ( + self.ctx.display.draw_hcentered_text( + "{} > {}".format( + get_policy_type_name(current_policy_type), + get_policy_type_name(required_policy_type), + ), + offset_y, + ) + * FONT_HEIGHT + ) + offset_y += 2 * FONT_HEIGHT + + # Check if script type changed + if ( + required_script_type + and current_script_type + and current_script_type != required_script_type + ): + self.ctx.display.draw_hcentered_text( + "{}:".format(t("Script Type")), offset_y, theme.highlight_color + ) + offset_y += FONT_HEIGHT + offset_y += ( + self.ctx.display.draw_hcentered_text( + "{} > {}".format( + current_script_type.upper(), + required_script_type.upper(), + ), + offset_y, + ) + * FONT_HEIGHT + ) + offset_y += 2 * FONT_HEIGHT + + # Check if network changed + if required_network and current_network != required_network: + self.ctx.display.draw_hcentered_text( + "{}:".format(t("Network")), offset_y, theme.highlight_color + ) + offset_y += FONT_HEIGHT + offset_y += ( + self.ctx.display.draw_hcentered_text( + "{} > {}".format( + get_network_name(current_network), + get_network_name(required_network), + ), + offset_y, + ) + * FONT_HEIGHT + ) + + def _handle_policy_mismatch(self, exception, wallet_data, qr_format, persisted): + """Handles policy/network mismatch by prompting user and re-deriving key""" + from ...wallet import Wallet + from ...key import Key + + required_policy_type = exception.args[0] + required_script_type = exception.args[1] + current_policy_type = exception.args[2] + required_network = exception.args[3] if len(exception.args) > 3 else None + current_network = exception.args[4] if len(exception.args) > 4 else None + current_script_type = self.ctx.wallet.key.script_type + + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text( + t("Wallet mismatch:"), DEFAULT_PADDING, theme.error_color + ) + + self._build_mismatch_warning( + current_policy_type, + required_policy_type, + current_network, + required_network, + current_script_type, + required_script_type, + ) + + if not self.prompt(t("Change wallet?"), BOTTOM_PROMPT_LINE): + return None, None + + # Re-derive key with correct policy type, script type, and network + updated_key = Key( + self.ctx.wallet.key.mnemonic, + required_policy_type, + network=( + required_network if required_network else self.ctx.wallet.key.network + ), + passphrase=self.ctx.wallet.key.passphrase, + account_index=self.ctx.wallet.key.account_index, + script_type=required_script_type, + ) + + wallet = Wallet(updated_key) + wallet.persisted = persisted + try: + wallet.load(wallet_data, qr_format) + self.ctx.wallet.key = updated_key + return wallet, None + except Exception as e: + return wallet, e + + def _attempt_wallet_load(self, wallet_data, qr_format, persisted): + """Attempts to load wallet with exception handling""" + from ...wallet import Wallet, PolicyMismatchWarning wallet = Wallet(self.ctx.wallet.key) wallet.persisted = persisted wallet_load_exception = None + try: wallet.load(wallet_data, qr_format) - except AssumptionWarning as e: - self.ctx.display.clear() - self.ctx.display.draw_centered_text(e.args[0], theme.error_color) - if self.prompt(t("Accept assumption?"), BOTTOM_PROMPT_LINE): - try: - wallet.load(wallet_data, qr_format, allow_assumption=e.args[1]) - except Exception as e_again: - wallet_load_exception = e_again + except PolicyMismatchWarning as e: + return self._handle_policy_mismatch(e, wallet_data, qr_format, persisted) except Exception as e: wallet_load_exception = e + + return wallet, wallet_load_exception + + def _load_wallet(self): + """Load a wallet output descriptor from the camera or SD card""" + # Load wallet data + wallet_data, qr_format, persisted = self._load_wallet_data() + if wallet_data is None: + return MENU_CONTINUE + + # Attempt to load the wallet with exception handling + wallet, wallet_load_exception = self._attempt_wallet_load( + wallet_data, qr_format, persisted + ) + + # Handle loading errors if wallet_load_exception: self.ctx.display.clear() self.ctx.display.draw_centered_text( @@ -201,21 +339,27 @@ def _load_wallet(self): theme.error_color, ) self.ctx.input.wait_for_button() + return MENU_CONTINUE - if wallet.is_loaded(): - if not wallet.has_change_addr(): - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Could not determine change address."), theme.error_color - ) - if not self.prompt(t("Proceed anyway?"), BOTTOM_PROMPT_LINE): - return MENU_CONTINUE + # Check if wallet was successfully loaded + if not wallet or not wallet.is_loaded(): + return MENU_CONTINUE + # Warn about missing change address if needed + if not wallet.has_change_addr(): self.ctx.display.clear() - self.display_loading_wallet(wallet) - if self.prompt(t("Load?"), BOTTOM_PROMPT_LINE): - self.ctx.wallet = wallet - self.flash_text(t("Wallet output descriptor loaded!")) + self.ctx.display.draw_centered_text( + t("Could not determine change address."), theme.error_color + ) + if not self.prompt(t("Proceed anyway?"), BOTTOM_PROMPT_LINE): + return MENU_CONTINUE + + # Display wallet and confirm loading + self.ctx.display.clear() + self.display_loading_wallet(wallet) + if self.prompt(t("Load?"), BOTTOM_PROMPT_LINE): + self.ctx.wallet = wallet + self.flash_text(t("Wallet output descriptor loaded!")) return MENU_CONTINUE diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 82e32cfca..1112f3d23 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -50,7 +50,6 @@ from ..krux_settings import t from ..kboard import kboard - DIGITS_HEX = "0123456789ABCDEF" DIGITS_OCT = "01234567" diff --git a/src/krux/pages/mnemonic_editor.py b/src/krux/pages/mnemonic_editor.py index 916ead397..e78c2e021 100644 --- a/src/krux/pages/mnemonic_editor.py +++ b/src/krux/pages/mnemonic_editor.py @@ -210,41 +210,38 @@ def word_color(index): x_padding = MINIMAL_PADDING while word_index < 12: paged_index = word_index + page * 12 + font_color = word_color(paged_index) + bg_color = theme.bg_color if word_index == button_index and self.ctx.input.buttons_active: + # Flip the color values for the selected word + bg_color = font_color + font_color = theme.bg_color + self.ctx.display.draw_string( + x_padding, + y_region, + "{:>2}".format(paged_index + 1) + + ". " + + self.current_mnemonic[paged_index], + color=font_color, + bg_color=bg_color, + ) + if self.mnemonic_length == 24 and not kboard.is_m5stickv: + # Display is wide enough; render the next 12 words on the right side + font_color = word_color(word_index + 12) + bg_color = theme.bg_color + if word_index + 12 == button_index and self.ctx.input.buttons_active: + # Flip the color values for the selected word + bg_color = font_color + font_color = theme.bg_color self.ctx.display.draw_string( - x_padding, - y_region, - str(paged_index + 1) + "." + self.current_mnemonic[paged_index], - theme.bg_color, - word_color(paged_index), - ) - else: - self.ctx.display.draw_string( - x_padding, + MINIMAL_PADDING + self.ctx.display.width() // 2, y_region, - str(paged_index + 1) + "." + self.current_mnemonic[paged_index], - word_color(paged_index), + "{:>2}".format(word_index + 13) + + ". " + + self.current_mnemonic[word_index + 12], + color=font_color, + bg_color=bg_color, ) - if self.mnemonic_length == 24 and not kboard.is_m5stickv: - if word_index + 12 == button_index and self.ctx.input.buttons_active: - self.ctx.display.draw_string( - MINIMAL_PADDING + self.ctx.display.width() // 2, - y_region, - str(word_index + 13) - + "." - + self.current_mnemonic[word_index + 12], - theme.bg_color, - word_color(word_index + 12), - ) - else: - self.ctx.display.draw_string( - MINIMAL_PADDING + self.ctx.display.width() // 2, - y_region, - str(word_index + 13) - + "." - + self.current_mnemonic[word_index + 12], - word_color(word_index + 12), - ) word_index += 1 y_region += word_v_padding @@ -326,7 +323,7 @@ def edit(self): if new_word is not None: self.ctx.display.clear() if self.prompt( - str(button_index + 1) + ".\n\n" + new_word + "\n\n", + str(button_index + page * 12 + 1) + ".\n\n" + new_word + "\n\n", self.ctx.display.height() // 2, ): self.current_mnemonic[button_index + page * 12] = new_word diff --git a/src/krux/pages/mnemonic_loader.py b/src/krux/pages/mnemonic_loader.py index a0c09b81f..366b5af9d 100644 --- a/src/krux/pages/mnemonic_loader.py +++ b/src/krux/pages/mnemonic_loader.py @@ -38,7 +38,6 @@ from ..key import Key from ..krux_settings import t - DIGITS_HEX = "0123456789ABCDEF" DIGITS_OCT = "01234567" diff --git a/src/krux/pages/qr_capture.py b/src/krux/pages/qr_capture.py index 1fedf0cb5..647cdf871 100644 --- a/src/krux/pages/qr_capture.py +++ b/src/krux/pages/qr_capture.py @@ -53,8 +53,8 @@ def light_control(self): else: self.ctx.light.turn_off() - def anti_glare_control(self): - """Controls the anti-glare based on the user input""" + def mode_control(self): + """Controls camera mode based on the user input""" self.ctx.display.to_portrait() mode = self.ctx.camera.toggle_camera_mode() if mode == QR_SCAN_MODE: @@ -137,7 +137,7 @@ def qr_capture_loop(self): previous_part = None ur_highlighted = False - # Flush events ocurred while loading camera + # Flush events occurred while loading camera self.ctx.input.reset_ios_state() start_time = time.ticks_ms() self.ctx.display.clear() @@ -148,6 +148,11 @@ def qr_capture_loop(self): t("Press PAGE to toggle mode"), offset_y ) self.ctx.display.to_landscape() + + # Cache end time for message display to avoid repeated addition + message_end_time = start_time + MESSAGE_DISPLAY_PERIOD + first_frame = True + while True: wdt.feed() @@ -159,8 +164,8 @@ def qr_capture_loop(self): # Anti-glare / zoom / normal mode page_prev_event = self.ctx.input.page_prev_event() if self.ctx.input.page_event() or (kboard.is_yahboom and page_prev_event): - if self.ctx.camera.has_antiglare(): - self.anti_glare_control() + if self.ctx.camera.has_mode_control(): + self.mode_control() else: break @@ -184,15 +189,15 @@ def qr_capture_loop(self): ur_highlighted = False img = self.ctx.camera.snapshot() - if time.ticks_ms() < start_time + MESSAGE_DISPLAY_PERIOD: + if time.ticks_ms() < message_end_time: self.ctx.display.render_image(img, title_lines=title_lines) else: self.ctx.display.render_image(img) - res = img.find_qrcodes() + res = img.find_qrcodes(find_inverted=first_frame) if res: - data = res[0].payload() - new_part = parser.parse(data) + first_frame = False + new_part = parser.parse(res[0].payload()) if ( parser.format == FORMAT_UR diff --git a/src/krux/pages/qr_view.py b/src/krux/pages/qr_view.py index 9abe9c15e..879837a06 100644 --- a/src/krux/pages/qr_view.py +++ b/src/krux/pages/qr_view.py @@ -530,7 +530,7 @@ def toggle_brightness(): self.draw_grided_qr(mode) if self.ctx.display.height() > self.ctx.display.width(): - y_offset = self.ctx.display.qr_offset() + FONT_HEIGHT + y_offset = self.ctx.display.qr_offset() + DEFAULT_PADDING self.ctx.display.draw_hcentered_text( label, y_offset, diff --git a/src/krux/pages/tools.py b/src/krux/pages/tools.py index 4e4c65026..635fe07c6 100644 --- a/src/krux/pages/tools.py +++ b/src/krux/pages/tools.py @@ -33,7 +33,6 @@ ) from ..krux_settings import t - # TODO: re-enable "Create a QR Code" (and keypads ^^^) once encryption is possible w/o Datum Tool diff --git a/src/krux/pages/wallet_settings.py b/src/krux/pages/wallet_settings.py index bc3e9670d..459d6f3b3 100644 --- a/src/krux/pages/wallet_settings.py +++ b/src/krux/pages/wallet_settings.py @@ -92,8 +92,19 @@ def load_passphrase_menu(self, mnemonic): continue # Check if passphrase string is within ascii range if any(byte > 126 for byte in passphrase.encode()): - self.flash_error(t("Failed to load")) - continue + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text( + t( + "Non-ASCII characters were detected in your passphrase. " + "Krux cannot guarantee that other wallets will derive the " + "same key." + ) + ) + if not self.prompt( + t("Proceed?"), + BOTTOM_PROMPT_LINE, + ): + continue from ..themes import theme from ..key import Key diff --git a/src/krux/printers/cnc.py b/src/krux/printers/cnc.py index 0a089bf2e..adff693e5 100644 --- a/src/krux/printers/cnc.py +++ b/src/krux/printers/cnc.py @@ -373,12 +373,12 @@ def __init__(self): fm.register( Settings().hardware.printer.cnc.grbl.tx_pin, fm.fpioa.UART2_TX, - force=False, + force=True, ) fm.register( Settings().hardware.printer.cnc.grbl.rx_pin, fm.fpioa.UART2_RX, - force=False, + force=True, ) self.uart_conn = UART(UART.UART2, Settings().hardware.printer.cnc.grbl.baudrate) diff --git a/src/krux/printers/thermal.py b/src/krux/printers/thermal.py index a3c56fd06..71c47e7ff 100644 --- a/src/krux/printers/thermal.py +++ b/src/krux/printers/thermal.py @@ -56,12 +56,12 @@ def __init__(self): fm.register( Settings().hardware.printer.thermal.adafruit.tx_pin, fm.fpioa.UART2_TX, - force=False, + force=True, ) fm.register( Settings().hardware.printer.thermal.adafruit.rx_pin, fm.fpioa.UART2_RX, - force=False, + force=True, ) self.uart_conn = UART( diff --git a/src/krux/psbt.py b/src/krux/psbt.py index 5c28d76b2..df9566cf1 100644 --- a/src/krux/psbt.py +++ b/src/krux/psbt.py @@ -22,8 +22,7 @@ import gc from embit.psbt import PSBT, CompressMode from ur.ur import UR -import urtypes -from urtypes.crypto import CRYPTO_PSBT +from urtypes.crypto.psbt import PSBT as URTYPE_PSBT, CRYPTO_PSBT from .baseconv import base_decode from .krux_settings import t from .settings import THIN_SPACE, ELLIPSIS @@ -93,9 +92,7 @@ def __init__(self, wallet, psbt_data, qr_format, psbt_filename=None): self.base_encoding = 64 # In case it is exported as QR code elif isinstance(psbt_data, UR): try: - self.psbt = PSBT.parse( - urtypes.crypto.PSBT.from_cbor(psbt_data.cbor).data - ) + self.psbt = PSBT.parse(URTYPE_PSBT.from_cbor(psbt_data.cbor).data) self.ur_type = CRYPTO_PSBT # self.base_encoding = 64 except: @@ -538,7 +535,7 @@ def psbt_qr(self): return ( UR( CRYPTO_PSBT.type, - urtypes.crypto.PSBT(psbt_data).to_cbor(), + URTYPE_PSBT(psbt_data).to_cbor(), ), self.qr_format, ) diff --git a/src/krux/qr.py b/src/krux/qr.py index 34845e6fa..4d8e01b6a 100644 --- a/src/krux/qr.py +++ b/src/krux/qr.py @@ -181,6 +181,7 @@ def parse(self, data): from ur.ur_decoder import URDecoder self.decoder = URDecoder() + data = data.decode() if isinstance(data, bytes) else data self.decoder.receive_part(data) elif self.format == FORMAT_BBQR: from .bbqr import parse_bbqr @@ -209,9 +210,7 @@ def is_complete(self): def result(self): """Returns the combined part data""" if self.format == FORMAT_UR: - from ur.ur import UR - - return UR(self.decoder.result.type, bytearray(self.decoder.result.cbor)) + return self.decoder.result if self.format == FORMAT_BBQR: from .bbqr import decode_bbqr @@ -261,7 +260,7 @@ def to_qr_codes(data, max_width, qr_format): encoder = UREncoder(data, part_size, 0) while True: - part = encoder.next_part() + part = encoder.next_part().upper() code = qrcode.encode(part) yield (code, encoder.fountain_encoder.seq_len()) elif qr_format == FORMAT_BBQR: @@ -372,6 +371,7 @@ def find_min_num_parts(data, max_width, qr_format): def parse_pmofn_qr_part(data): """Parses the QR as a P M-of-N part, extracting the part's content, index, and total""" + data = data.decode() if isinstance(data, bytes) else data of_index = data.index("of") space_index = data.index(" ") part_index = int(data[1:of_index]) @@ -383,6 +383,7 @@ def detect_format(data): """Detects the QR format of the given data""" qr_format = FORMAT_NONE try: + data = data.decode() if isinstance(data, bytes) else data if data.startswith("p"): header = data.split(" ")[0] if "of" in header and header[1:].split("of")[0].isdigit(): diff --git a/src/krux/touch.py b/src/krux/touch.py index ee1435a51..3bdece846 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -23,13 +23,14 @@ import time +from .kboard import kboard from .krux_settings import Settings IDLE = 0 PRESSED = 1 RELEASED = 2 -SWIPE_THRESHOLD = 50 +SWIPE_THRESHOLD = 35 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 SWIPE_UP = 3 @@ -54,7 +55,12 @@ def __init__(self, width, height, irq_pin=None, res_pin=None): self.gesture = None self.state = IDLE self.width, self.height = width, height - if res_pin is not None: + if kboard.is_embed_fire: + from .touchscreens.cst816 import touch_control + + self.touch_driver = touch_control + self.touch_driver.activate(irq_pin) + elif res_pin is not None: from .touchscreens.gt911 import touch_control self.touch_driver = touch_control diff --git a/src/krux/touchscreens/cst816.py b/src/krux/touchscreens/cst816.py new file mode 100644 index 000000000..0d45db6cd --- /dev/null +++ b/src/krux/touchscreens/cst816.py @@ -0,0 +1,145 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import time +from Maix import GPIO +from fpioa_manager import fm +from . import Touchscreen +from ..i2c import i2c_bus + +CST816S_TOUCH_DATA = 0x01 +CST816S_VERSION = 0x15 +CST816S_AUTO_SLEEP_MODE = 0xFE +CST816S_ADDR = 0x15 +ACTIVITY_THRESHOLD = 100 +SCREEN_WIDTH = 240 +EVENT_TOUCH_CONTACT = 0x02 + + +def __handler__(pin_num=None): + """GPIO interrupt handler for touch events""" + # pylint: disable=unused-argument + touch_control.trigger_event() + + +class CST816(Touchscreen): + """CST816S capacitive touchscreen controller driver""" + + def __init__(self): + self.irq_pin = None + self.event_flag = False + self.irq_point = None + self.last_touch_time = 0 + + def _init_pins(self, irq_pin): + fm.register(irq_pin, fm.fpioa.GPIOHS1) + self.irq_pin = GPIO(GPIO.GPIOHS1, GPIO.IN, GPIO.PULL_UP) + + def _init_cst816(self): + try: + version = self._read_reg(CST816S_VERSION, 1) + if version is not None: + print("CST816S touchscreen found") + + self._write_reg(CST816S_AUTO_SLEEP_MODE, bytearray([0x00])) + + except Exception as e: + print("CST816S initialization error:", e) + + def activate(self, irq_pin): + """Enable touchscreen with interrupt handling""" + self._init_pins(irq_pin) + self._init_cst816() + self.irq_pin.irq(__handler__, GPIO.IRQ_FALLING) + + def _write_reg(self, reg_addr, buf): + if i2c_bus is not None: + try: + payload = bytearray([reg_addr]) + payload.extend(bytearray(buf)) + i2c_bus.writeto(CST816S_ADDR, payload) + except Exception as e: + print("CST816S write error:", e) + + def _read_reg(self, reg_addr, buf_len): + if i2c_bus is not None: + try: + addr_bytes = bytearray([reg_addr]) + i2c_bus.writeto(CST816S_ADDR, addr_bytes) + return i2c_bus.readfrom(CST816S_ADDR, buf_len) + except Exception as e: + print("CST816S read error:", e) + return None + + def _read_touch_data(self): + try: + touch_data = self._read_reg(CST816S_TOUCH_DATA, 6) + if touch_data is not None and len(touch_data) >= 6: + points = touch_data[1] + event = touch_data[2] >> 6 + x = ((touch_data[2] & 0x0F) << 8) | touch_data[3] + y = ((touch_data[4] & 0x0F) << 8) | touch_data[5] + + if points > 0 and event == EVENT_TOUCH_CONTACT: + x = max(0, SCREEN_WIDTH - x) + return (x, y) + + except Exception as e: + print("CST816S touch data read error:", e) + return None + + def current_point_validate(self): + """Read and validate current touch point coordinates""" + return self._read_touch_data() + + def current_point(self): + """Return touch coordinates if within activity threshold""" + if time.ticks_ms() - self.last_touch_time <= ACTIVITY_THRESHOLD: + return self._read_touch_data() + return None + + def trigger_event(self): + """Process touch interrupt and update event state""" + current_time = time.ticks_ms() + + if current_time - self.last_touch_time <= ACTIVITY_THRESHOLD: + self.last_touch_time = current_time + return + + irq_point = self.current_point_validate() + if irq_point is not None: + self.irq_point = irq_point + self.event_flag = True + self.last_touch_time = current_time + + def event(self): + """Return and clear touch event flag""" + flag = self.event_flag + self.event_flag = False + return flag + + def threshold(self, _): + """Threshold configuration (not supported on CST816S)""" + print("CST816S: Threshold setting not supported") + + +touch_control = CST816() diff --git a/src/krux/touchscreens/ft6x36.py b/src/krux/touchscreens/ft6x36.py index ec0932108..5ce9eaa36 100644 --- a/src/krux/touchscreens/ft6x36.py +++ b/src/krux/touchscreens/ft6x36.py @@ -93,7 +93,7 @@ def current_point(self): y = ((data_buf[2] & 0x0F) << 8) | (data_buf[3]) return (x, y) except Exception as e: - return e # debug + print("FT6X36 read error:", e) return None def trigger_event(self): diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py index e0900ccb1..82562adf9 100644 --- a/src/krux/translations/__init__.py +++ b/src/krux/translations/__init__.py @@ -36,19 +36,18 @@ 232475068, 1185266064, 1503087751, - 2405586747, + 417909373, 2032388104, 3012047951, 1178563089, 1696437881, + 2792960854, 3348584292, 1099786887, 2739590230, 1310058127, 1949634023, - 4287534872, 2995482424, - 2415648848, 1043817877, 3439746594, 4121028614, @@ -73,6 +72,7 @@ 3138204438, 1207696150, 1583186953, + 2701393562, 3768416405, 388908871, 3442025874, @@ -209,12 +209,12 @@ 1584070924, 1443208255, 1237332019, - 4265479636, 2939797024, 73574491, 3634967631, 4063104189, 2325721074, + 2474845199, 4092516657, 3089363813, 1577637745, @@ -373,6 +373,7 @@ 2504354847, 2076481321, 2297028319, + 3409743444, 4232654916, 2587172867, 2499782468, diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py index 8802adf11..61c045592 100644 --- a/src/krux/translations/de.py +++ b/src/krux/translations/de.py @@ -24,19 +24,18 @@ "% des Betrags.", "%d von %d Multisig", "%d bis %d ", - "%s Bits (%s Bits/px)", + "%s Bits", "%s Bytes", "%s Zchn.", "%s wurde entfernt.", "%s: geladen!", + "(%s Bits/px)", "(Experimental)", "(nur zur Betrachtung)", "12 Wörter", "24 Wörter", "Über", - "Annahme akzeptieren?", "Konto", - "Konto #0 würde angenommen", "Kontoindex", "Wallet-Passphrase hinzufügen oder ändern?", "Zusätzliche Entropie von der Kamera erforderlich für %s", @@ -61,6 +60,7 @@ "Aufnahme abgebrochen", "Change Adresse", "Thema ändern und neu starten?", + "Geldbörse ändern?", "Change:", "Änderungen bleiben bis zum Herunterfahren bestehen.", "Prüfe SD-Karte", @@ -197,12 +197,12 @@ "Mnemonisches XOR", "Mnemotechnik und Passphrase werden beibehalten.", "Geändert:", - "Native Segwit - 84 würde angenommen", "Netzwerk", "Neue Mnemonic", "Neue Firmware erkannt.", "Nein", "Keine Passphrase", + "Nicht-ASCII-Zeichen wurden in Ihrer Passphrase erkannt. Krux kann nicht garantieren, dass andere Wallets den gleichen Schlüssel ableiten.", "Nicht genug Würfe!", "Zahlen", "Oktal", @@ -361,6 +361,7 @@ "Warte auf die Erfassung", "Wallet", "Wallet-Deskriptor", + "Geldbörse passt nicht:", "Wallet Ausgabedeskriptor", "Wallet Ausgabedeskriptor geladen!", "Wallet Ausgabedeskriptor nicht gefunden.", diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py index 835afe873..865eb4a2e 100644 --- a/src/krux/translations/es.py +++ b/src/krux/translations/es.py @@ -24,19 +24,18 @@ "% del monto.", "%d de %d multisig", "%d a %d", - "%s bits (%s bits/px)", + "%s bits", "%s bytes", "%s carac.", "%s eliminado.", "%s: ¡cargado!", + "(%s bits/px)", "(Experimental)", "(Solo para observación)", "12 palabras", "24 palabras", "Nosotros", - "¿Aceptar la suposición?", "Cuenta", - "Se supondría que la cuenta #0", "Índice de la cuenta", "¿Añadir o cambiar passphrase de la cartera?", "Se requiere entropía adicional de la cámara para %s", @@ -61,6 +60,7 @@ "Captura cancelada", "Cambio", "¿Cambiar de tema y reiniciar?", + "¿Cambiar cartera?", "Cambio:", "Los cambios durarán hasta que el dispositivo se apague.", "Revisar Tarjeta SD", @@ -197,12 +197,12 @@ "XOR mnemónico", "Mnemónico y passphrase se mantendrán.", "Modificado:", - "Segwit nativo - 84 se supondría", "Red", "Nuevo Mnemónico", "Nuevo firmware detectado.", "No", "Sin Passphrase", + "Se detectaron caracteres no ASCII en su frase de contraseña. Krux no puede garantizar que otros monederos obtengan la misma clave.", "¡No hay suficientes tiradas!", "Números", "Octales", @@ -361,6 +361,7 @@ "Espera la captura", "Cartera", "Descriptor de Cartera", + "Cartera no coincide:", "Descriptor de salida de cartera", "¡Se ha cargado el descriptor de salida de la cartera!", "No se encontró el descriptor de salida de la cartera.", diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py index 5bb187cd4..c5f4b32f9 100644 --- a/src/krux/translations/fr.py +++ b/src/krux/translations/fr.py @@ -24,19 +24,18 @@ "% du montant.", "%d de %d multisignature", "%d à %d", - "%s bits (%s bits/px)", + "%s bits", "%s octets", "%s car.", "%s supprimé.", "%s\u2009: chargé\u2009!", + "(%s bits/px)", "(Expérimental)", "(consultation)", "12 mots", "24 mots", "À propos", - "Accepter supposition\u2009?", "Compte", - "Le compte n °0 serait supposé", "Index du compte", "Ajoutez ou modifiez la phrase secrète\u2009?", "Entropie supplémentaire de la caméra requise pour %s", @@ -61,6 +60,7 @@ "Capture annulée", "Monnaie", "Changer de thème et redémarrer\u2009?", + "Changer portefeuille?", "La monnaie\u2009:", "Les modifications dureront jusqu'à l'arrêt.", "Vérifiez la carte SD", @@ -197,12 +197,12 @@ "Mnémonique XOR", "Mnémonique et phrase secrète seront conservés.", "Modifié\u2009:", - "Native Segwit - 84 serait supposé", "Réseau", "Nouveau Mnémonique", "Nouveau micrologiciel détecté.", "Non", "Pas de phrase secrète", + "Des caractères non ASCII ont été détectés dans votre phrase secrète. Krux ne peut garantir que d'autres portefeuilles obtiendront la même clé.", "Pas assez de jets\u2009!", "Nombres", "Octale", @@ -361,6 +361,7 @@ "Attendez la capture", "Portefeuille", "Descripteur de Portefeuille", + "Portefeuille différent:", "Descripteur de sortie du portefeuille", "Descripteur de sortie du portefeuille chargé\u2009!", "Descripteur de sortie du portefeuille introuvable.", diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py index 85bd471ce..c002e687a 100644 --- a/src/krux/translations/ja.py +++ b/src/krux/translations/ja.py @@ -24,19 +24,18 @@ "% の金額.", "%d の%d マルチシグネチャ", "%d へ %d", - "%sビット(%sビット/px)", + "%sビット", "%s バイト", "%s 文字", "%s 削除されました.", "%s:読み込まれました!", + "(%sビット/px)", "(実験的)", "(閲覧専用)", "12の単語", "24の単語", "概要", - "仮定を受け入れますか?", "アカウント", - "アカウント#0は仮定されます", "アカウントインデックス", "ウォレットのパスフレーズを追加または変更しますか?", "%sにはカメラからの追加エントロピーが必要です", @@ -61,6 +60,7 @@ "キャプチャがキャンセルされました", "お釣り", "テーマの変更と再起動しますか?", + "ウォレット変更?", "お釣り:", "変更はシャットダウンするまで持続します.", "SDカードを確認する", @@ -197,12 +197,12 @@ "ニーモニックXOR", "Mnemonicとパスフレーズは保持されます.", "修正されました:", - "ネイティブSegwit - 84が仮定されます", "ネットワーク", "新しい Mnemonic", "新しいファームウェアが検出されました.", "いいえ", "パスフレーズなし", + "パスフレーズに非ASCII文字が検出されました.Kruxは、他のウォレットが同じキーを取得することを保証することはできません.", "ロールが足りません!", "数字", "Octal", @@ -361,6 +361,7 @@ "キャプチャを待ってください", "ワレット", "ウォレットディスクリプター", + "ウォレット不一致:", "ウォレット出力ディスクリプター", "ウォレット出力ディスクリプターがロードされました!", "ウォレット出力ディスクリプターが見つかりません.", diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py index 3007c6ea2..4bb8daebb 100644 --- a/src/krux/translations/ko.py +++ b/src/krux/translations/ko.py @@ -24,19 +24,18 @@ "수량: %", "%d의 %d 멀티시그", "%d 부터 %d", - "%s 비트 (%s 비트/픽셀)", + "%s 비트", "%s 바이트", "%s 자", "%s 제거되었습니다.", "%s: 로드됨!", + "(%s 비트/픽셀)", "(실험적 기능)", "(잔액조회 전용)", "12단어", "24단어", "기본 정보", - "가정을 수락하시겠습니까?", "계정", - "계정 #0이 가정됩니다", "계정 인덱스", "패스프레이즈를 추가하거나 변경하시겠습니까?", "%s 에 필요한 카메라의 추가 엔트로피", @@ -61,6 +60,7 @@ "캡처 취소됨", "잔돈", "테마를 변경하고 재부팅하시겠습니까?", + "지갑 변경?", "잔돈:", "변경 사항은 종료될때까지 유지됩니다.", "SD카드 확인", @@ -197,12 +197,12 @@ "니모닉 XOR", "니모닉과 암호는 유지됩니다.", "수정되었습니다:", - "네이티브 세그윗 - BIP84를 적용합니다", "네트워크", "새로운 니모닉", "새로운 펌웨어가 감지되었습니다.", "아니요", "패스프레이즈 없음", + "암호에서 ASCII가 아닌 문자가 감지되었습니다. Krux는 다른 지갑이 동일한 키를 파생할 것이라고 보장할 수 없습니다.", "주사위 횟수가 충분하지 않습니다!", "시드문구 순번", "8진수", @@ -361,6 +361,7 @@ "캡처될때까지 기다리십시오", "지갑 설정", "지갑 디스크립터", + "지갑 불일치:", "지갑 출력 디스크립터", "지갑 출력 디스크립터가 로드되었습니다!", "지갑 출력 디스크립터를 찾을 수 없습니다.", diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py index cab09792e..968f406f7 100644 --- a/src/krux/translations/nl.py +++ b/src/krux/translations/nl.py @@ -24,19 +24,18 @@ "% van het bedrag.", "%d van %d multisig", "%d tot %d", - "%s bits (%s bits/px)", + "%s bits", "%s bytes", "%s tek.", "%s verwijderd.", "%s: geladen!", + "(%s bits/px)", "(Experimenteel)", "(alleen inzien)", "12 woorden", "24 woorden", "Over", - "Aanname accepteren?", "Account", - "Account #0 zou worden aangenomen", "Accountindex", "Wachtwoordzin voor portemonnee toevoegen of wijzigen?", "Extra entropie van camera vereist voor %s", @@ -61,6 +60,7 @@ "Opname geannuleerd", "Change", "Thema veranderen en opnieuw opstarten?", + "Portemonnee wijzigen?", "Wisselgeld:", "Wijzigingen blijven van kracht tot afsluiten.", "Controleer SD kaart", @@ -197,12 +197,12 @@ "Mnemonic XOR", "Geheugensteun en wachtwoord worden bewaard.", "Aangepast:", - "Native Segwit - 84 zal worden gebruikt", "Netwerk", "Geheugensteun aanmaken", "Nieuwe firmware gevonden.", "Nee", "Geen wachtwoordzin", + "Er zijn niet-ASCII-tekens gedetecteerd in uw wachtwoordzin. Krux kan niet garanderen dat andere portefeuilles dezelfde sleutel zullen afleiden.", "Niet genoeg gedobbeld!", "Nummers", "Octaal", @@ -361,6 +361,7 @@ "Wacht op opname", "Portemonnee", "Descriptor", + "Portemonnee onjuist:", "Portemonnee descriptor", "Portemonnee descriptor geladen!", "Portemonnee descriptor niet gevonden.", diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py index 04a718f74..d26a53107 100644 --- a/src/krux/translations/pt.py +++ b/src/krux/translations/pt.py @@ -24,19 +24,18 @@ "% do total.", "%d de %d multisig", "%d a %d", - "%s bits (%s bits/px)", + "%s bits", "%s bytes", "%s carac.", "%s excluído.", "%s: carregado!", + "(%s bits/px)", "(Experimental)", "(Somente visualização)", "12 palavras", "24 palavras", "Sobre", - "Aceitar suposições?", "Conta", - "A conta #0 seria assumida", "Índice da Conta", "Adicionar ou alterar a senha da carteira?", "Entropia adicional da câmera é necessária para %s", @@ -61,6 +60,7 @@ "Captura cancelada", "Troco", "Mudar o tema e reiniciar?", + "Mudar carteira?", "Troco:", "As alterações só durarão até o desligamento.", "Explorar o cartão SD", @@ -197,12 +197,12 @@ "XOR Mnemônico", "Mnemônico e senha serão mantidos.", "Alterado:", - "Segwit nativo - 84 seria assumido", "Rede", "Novo Mnemônico", "Novo firmware detectado.", "Não", "Sem Senha", + "Caracteres não ASCII foram detectados em sua senha. A Krux não pode garantir que outras carteiras obtenham a mesma chave.", "Jogadas insuficientes!", "Números", "Octal", @@ -361,6 +361,7 @@ "Aguarde a captura", "Carteira", "Descritor da Carteira", + "Carteira diferente:", "Descritor da carteira", "Descritor da carteira carregado!", "O descritor da carteira não foi encontrado.", diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py index a72889823..1327eb778 100644 --- a/src/krux/translations/ru.py +++ b/src/krux/translations/ru.py @@ -24,19 +24,18 @@ "% от суммы.", "%d из %d мультиподпись", "%d к %d", - "%s бит (%s бит/px)", + "%s бит", "%s байт", "%s симв.", "%s удален.", "%s: загружено!", + "(%s бит/пиксель)", "(Эксперементальный)", "(Только часы)", "12 слов", "24 слова", "О Программе", - "Принять предположение?", "Учетная запись", - "Будет принят счет №0", "Индекс счета", "Добавить или изменить пароль кошелька?", "Требуется дополнительная энтропия от камеры для %s", @@ -61,6 +60,7 @@ "Захват отменен", "Сдача", "Сменить тему и перезагрузить?", + "Сменить кошелёк?", "Сдача:", "Изменения будут храниться до выключения.", "Проверить SD Карту", @@ -197,12 +197,12 @@ "Мнемонический XOR", "Мнемоника и парольная фраза будут сохранены.", "Изменено:", - "Native Segwit - 84 будет принято", "Сеть", "Новая Мнемоника", "Обнаружена новая прошивка.", "Нет", "Нет Фраза-пароль", + "В вашей парольной фразе обнаружены символы, отличные от ASCII. Krux не может гарантировать, что другие кошельки получат тот же ключ.", "Недостаточно бросков!", "Числа", "Восьмеричный", @@ -361,6 +361,7 @@ "Дождитесь Захвата", "Кошелек", "Дескриптор Кошелька", + "Кошелёк не совпадает:", "Выходной дескриптор кошелька", "Выходной дескриптор кошелька загружен!", "Выходной дескриптор кошелька не найден.", diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py index 0efac3498..fd1cc298d 100644 --- a/src/krux/translations/tr.py +++ b/src/krux/translations/tr.py @@ -24,19 +24,18 @@ "tutarın %'si.", "%d / %d çoklu imza", "%d to %d", - "%s bit (%s bit/piksel)", + "%s bit", "%s bayt", "%s kar.", "%s kaldırıldı.", "%s: yüklendi!", + "(%s bit/piksel)", "(Deneysel)", "(sadece izle)", "12 kelime", "24 kelime", "Hakkında", - "Varsayım kabul edilsin mi?", "Hesap", - "#0 numaralı hesap varsayılacaktır", "Hesap Endeksi", "Cüzdan parolası eklensin mi veya değiştirilsin mi?", "%s için kameradan gelen ek entropi gerekli", @@ -61,6 +60,7 @@ "Yakalama iptal edildi", "Para Üstü", "Temayı değiştir ve yeniden başlat?", + "Cüzdan değiştir?", "Para Üstü:", "Değişiklikler kapanışa kadar devam edecek.", "SD Kartı Kontrol Et", @@ -197,12 +197,12 @@ "Anımsatıcı XOR", "Mnemonik ve parola tutulacaktır.", "Değiştirildi:", - "Yerel Segwit - 84 varsayılacaktır", "Ağ", "Yeni Mnemonic", "Yeni donanım yazılımı tespit edildi.", "Hayır", "Parola Yok", + "Parolanızda ASCII olmayan karakterler algılandı. Krux, diğer cüzdanların aynı anahtarı türeteceğini garanti edemez.", "Yeterli zar atışı yok!", "Numaralar", "Sekizlik", @@ -361,6 +361,7 @@ "Yakalamanın tamamlanmasını bekleyin", "Cüzdan", "Cüzdan Tanımlayıcısı", + "Cüzdan uyuşmazlığı:", "Cüzdan çıktı tanımlayıcısı", "Cüzdan çıktı tanımlayıcısı yüklendi!", "Cüzdan çıktı tanımlayıcısı bulunamadı.", diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py index 6423f5264..28e26bfa8 100644 --- a/src/krux/translations/vi.py +++ b/src/krux/translations/vi.py @@ -24,19 +24,18 @@ "% của số tiền.", "%d của %d đa chữ kí", "%d đến %d", - "%s bit (%s bit/px)", + "%s bit", "%s byte", "%s ký tự", "%s Đã bỏ.", "%s: đã tải!", + "(%s bit/px)", "(Thực nghiệm)", "(chỉ xem)", "12 từ", "24 từ", "Về chúng tôi", - "Chấp nhận giả định?", "Tài khoản", - "Tài khoản #0 sẽ được giả định", "Chỉ mục tài khoản", "Thêm hoặc thay đổi cụm mật khẩu ví?", "Entropy bổ sung từ máy ảnh cần thiết cho %s", @@ -61,6 +60,7 @@ "Hủy chụp hình", "Thay đổi", "Thay đổi giao diện và khởi động lại?", + "Đổi ví?", "Thay đổi:", "Thay đổi sẽ kéo dài cho đến khi tắt máy.", "Kiểm tra thẻ SD", @@ -197,12 +197,12 @@ "Mnemonic XOR", "Từ gợi nhớ và cụm mật khẩu sẽ được lưu giữ.", "Đã sửa đổi:", - "Native Segwit - 84 sẽ được giả định", "Mạng lưới", "Mnemonic mới", "Phát hiện phần sụn mới.", "Không", "Không có cụm mật khẩu", + "Các ký tự không phải ASCII đã được phát hiện trong cụm mật khẩu của bạn. Krux không thể đảm bảo rằng các ví khác sẽ lấy được cùng một khóa.", "Không đủ số lần quay!", "Số", "Bát phân", @@ -361,6 +361,7 @@ "Chờ bắt", "Ví", "Trình mô tả ví", + "Ví không khớp:", "Ví đầu ra mô tả", "Đã tải bộ mô tả đầu ra của ví!", "Không tìm thấy bộ mô tả đầu ra ví.", diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py index 44ba6c228..24f26a3a0 100644 --- a/src/krux/translations/zh.py +++ b/src/krux/translations/zh.py @@ -24,19 +24,18 @@ "% 的金额.", "%d/%d 多签", "%d到%d", - "%s位 (%s位/像素)", + "%s位", "%s 字节", "%s 个字符", "已移除%s", "%s :已加载!", + "(%s位/像素)", "(实验性)", "(仅观察)", "12 单词", "24 单词", "关于", - "接受假设?", "账户", - "将假定为账户 #0", "账户索引", "添加或更改钱包密码?", "%s需要摄像头的额外熵", @@ -61,6 +60,7 @@ "截取已取消", "找零", "更改主题并重新启动?", + "更换钱包?", "找零:", "更改将在关机前保持.", "检查 SD 卡", @@ -197,12 +197,12 @@ "助记符异或", "助记词和密码将被保留.", "修改时间:", - "假定为原生 Segwit - 84", "网络", "新助记词", "检测到新固件.", "否", "无 Passphrase ", + "在您的密码短语中检测到非ASCII字符.Krux无法保证其他钱包将获得相同的密钥.", "投掷次数不足!", "数字", "八进制", @@ -361,6 +361,7 @@ "等待截取", "钱包", "钱包描述", + "钱包不匹配:", "钱包输出描述符", "钱包输出描述符加载重复!", "未找到钱包输出描述符.", diff --git a/src/krux/wallet.py b/src/krux/wallet.py index 11edfc9b4..df516e2ec 100644 --- a/src/krux/wallet.py +++ b/src/krux/wallet.py @@ -22,7 +22,6 @@ from embit.descriptor.descriptor import Descriptor from embit.descriptor.arguments import Key from embit.networks import NETWORKS -from embit.bip32 import HARDENED_INDEX from .krux_settings import t from .qr import FORMAT_BBQR, FORMAT_NONE from .key import ( @@ -33,16 +32,14 @@ P2WPKH, P2WSH, P2TR, - SINGLESIG_SCRIPT_PURPOSE, - MULTISIG_SCRIPT_PURPOSE, TYPE_SINGLESIG, TYPE_MULTISIG, TYPE_MINISCRIPT, ) -class AssumptionWarning(Exception): - """An exception for assumptions that require user acceptance""" +class PolicyMismatchWarning(Exception): + """An exception for wallet policy type mismatches that require user acceptance""" class Wallet: @@ -133,8 +130,47 @@ def is_loaded(self): """Returns a boolean indicating whether or not this wallet has been loaded""" return self.wallet_data is not None - def _validate_descriptor(self, descriptor, descriptor_xpubs): - """Validates the descriptor against the current key and policy type""" + def _determine_descriptor_policy(self, descriptor): + """Returns required policy type and script type from descriptor""" + descriptor_is_multisig = descriptor.is_basic_multisig + descriptor_is_miniscript = not descriptor_is_multisig and ( + descriptor.miniscript is not None or descriptor.taptree + ) + descriptor_is_singlesig = ( + not descriptor_is_multisig + and not descriptor_is_miniscript + and descriptor.key is not None + ) + + if descriptor_is_multisig: + return TYPE_MULTISIG, None + if descriptor_is_miniscript: + script_type = P2TR if descriptor.taptree else P2WSH + return TYPE_MINISCRIPT, script_type + if descriptor_is_singlesig or ( + not descriptor.key and len(descriptor.keys) == 1 + ): + script_type = descriptor.scriptpubkey_type() + # Check for nested segwit (sh(wpkh(...))) + if script_type == "p2sh" and hasattr(descriptor, "witness_script"): + if descriptor.witness_script: + # It's sh(wpkh(...)) = P2SH-P2WPKH + script_type = P2SH_P2WPKH + return TYPE_SINGLESIG, script_type + + raise ValueError("Unable to determine descriptor policy type") + + def _determine_descriptor_network(self, descriptor): + """Returns the network from descriptor's xpub version""" + version = descriptor.keys[0].key.version + for em_network in ("main", "test"): + for em_vertype in ("xpub", "ypub", "zpub", "Ypub", "Zpub"): + if version == NETWORKS[em_network][em_vertype]: + return NETWORKS[em_network] + return None + + def _validate_xpub_match(self, descriptor, descriptor_xpubs): + """Validates that key's xpub matches the descriptor""" if self.is_multisig(): if not descriptor.is_basic_multisig: raise ValueError("not multisig") @@ -150,15 +186,44 @@ def _validate_descriptor(self, descriptor, descriptor_xpubs): if self.key.xpub() not in descriptor_xpubs: raise ValueError("xpub not a miniscript cosigner") else: - if not descriptor.key: - if len(descriptor.keys) > 1: - raise ValueError("not single-sig") + if not descriptor.key and len(descriptor.keys) > 1: + raise ValueError("not single-sig") if self.key.xpub() != descriptor_xpubs[0]: raise ValueError("xpub does not match") - def load(self, wallet_data, qr_format, allow_assumption=None): + def _validate_descriptor(self, descriptor, descriptor_xpubs): + """Validates the descriptor against the current key and policy type""" + # Determine required policy and script type from descriptor + required_policy_type, required_script_type = self._determine_descriptor_policy( + descriptor + ) + + # Determine required network from descriptor + required_network = self._determine_descriptor_network(descriptor) + + # Check for policy, script type, or network mismatch + policy_mismatch = self.key.policy_type != required_policy_type + script_type_mismatch = ( + required_script_type is not None + and self.key.script_type != required_script_type + ) + network_mismatch = required_network and self.key.network != required_network + + if policy_mismatch or script_type_mismatch or network_mismatch: + raise PolicyMismatchWarning( + required_policy_type, + required_script_type, + self.key.policy_type, + required_network, + self.key.network, + ) + + # Validate that xpub matches descriptor requirements + self._validate_xpub_match(descriptor, descriptor_xpubs) + + def load(self, wallet_data, qr_format): """Loads the wallet from the given data""" - descriptor, label = parse_wallet(wallet_data, allow_assumption) + descriptor, label = parse_wallet(wallet_data) # convert descriptor keys to 'xpub' on same network -- for comparison only descriptor_xpubs = [] @@ -200,32 +265,13 @@ def load(self, wallet_data, qr_format, allow_assumption=None): elif self.descriptor.miniscript is not None or self.descriptor.taptree: if self.descriptor.taptree: if not descriptor.keys[0].origin: - import hashlib from embit.ec import NUMS_PUBKEY - from embit.bip32 import HDKey - - # In case internal key is disabled, check if NUMS is known - - # Hash all pubkeys, except internal, to compute deterministic chain code - hasher = hashlib.sha256() - for key in descriptor.keys[1:]: - hasher.update(key.sec()) - det_chain_code = hasher.digest() - - # Create provably unspendable deterministic key - version = self.descriptor.keys[0].key.version - provably_unspendable = HDKey( - NUMS_PUBKEY, det_chain_code, version=version - ) + # Check if BIP-0341 NUMS was used # Compare expected provably unspendable key with first descriptor key - if ( - descriptor.keys[0].key.to_base58() - != provably_unspendable.to_base58() - ): + if descriptor.keys[0].key.get_public_key() != NUMS_PUBKEY: self.wallet_data = None raise ValueError("Internal key not provably unspendable") - taproot_txt = "TR " miniscript_type = P2TR else: @@ -358,7 +404,7 @@ def parse_key_value_file(wallet_data): return None, None -def parse_wallet(wallet_data, allow_assumption=None): +def parse_wallet(wallet_data): """Exhaustively tries to parse the wallet data from a known format, returning a descriptor and label if possible. @@ -368,26 +414,28 @@ def parse_wallet(wallet_data, allow_assumption=None): # Check if wallet_data is a UR object without loading the UR module if wallet_data.__class__.__name__ == "UR": - import urtypes - # Try to parse as a Crypto-Output type try: - output = urtypes.crypto.Output.from_cbor(wallet_data.cbor) + from urtypes.crypto.output import Output + + output = Output.from_cbor(wallet_data.cbor) return Descriptor.from_string(output.descriptor()), None except: pass # Try to parse as a Crypto-Account type try: - account = urtypes.crypto.Account.from_cbor( - wallet_data.cbor - ).output_descriptors[0] + from urtypes.crypto.account import Account + + account = Account.from_cbor(wallet_data.cbor).output_descriptors[0] return Descriptor.from_string(account.descriptor()), None except: pass # Treat the UR as a generic UR bytes object and extract the data for further processing - wallet_data = urtypes.Bytes.from_cbor(wallet_data.cbor).data + from urtypes.bytes import Bytes + + wallet_data = Bytes.from_cbor(wallet_data.cbor).data # Process as a string wallet_data = ( @@ -422,31 +470,7 @@ def parse_wallet(wallet_data, allow_assumption=None): descriptor = Descriptor.from_string(wallet_data.strip()) return descriptor, None except: - # If that fails, try to parse as an xpub as a last resort - pubkey = Key.from_string(wallet_data.strip()) - if pubkey.is_extended: - network, versiontype = version_to_network_versiontype(pubkey.key.version) - - xpub = pubkey.key.to_base58() - - fmt = None - if pubkey.origin is None: - # assume derivation if possible - derivation = xpub_data_to_derivation( - versiontype, - network, - pubkey.key.child_number, - pubkey.key.depth, - allow_assumption=allow_assumption, - ) - if derivation: - fmt = derivation_to_script_wrapper(derivation) - else: - fmt = derivation_to_script_wrapper(pubkey.origin.derivation) - fmt = fmt.format("[" + str(pubkey.origin) + "]{}") - - descriptor = Descriptor.from_string(fmt.format(xpub)) - return descriptor, None + pass raise ValueError("invalid wallet format") @@ -497,96 +521,6 @@ def version_to_network_versiontype(hdkey_version): return network, versiontype -def xpub_data_to_derivation(versiontype, network, child, depth, allow_assumption=None): - """returns assumed derivation list for supported slip32 bips - based on embit.networks.NETWORKS keys for versiontype, network, - child_number (used as account for single-sig) and depth. Depth - is used as weak verification, it must match the expected depth. - Where unsafe assumptions could be made, AssumptionWarning is raised - (with warning text and assumed derivation as first two params) - unless called with allow_assumption=assumed_derivation. - """ - - derivation, network_node, assumption_text = None, None, None - - if network == "main": - network_node = 0 + HARDENED_INDEX - elif network in ("test", "regtest", "signet"): - network_node = 1 + HARDENED_INDEX - - if network_node and child >= HARDENED_INDEX: - if versiontype == "xpub" and depth == 3: - derivation = [ - SINGLESIG_SCRIPT_PURPOSE[P2WPKH] + HARDENED_INDEX, - network_node, - child, - ] - if allow_assumption != derivation: - assumption_text = t("Native Segwit - 84 would be assumed") - elif versiontype == "ypub" and depth == 3: - derivation = [ - SINGLESIG_SCRIPT_PURPOSE[P2SH_P2WPKH] + HARDENED_INDEX, - network_node, - child, - ] - elif versiontype == "zpub" and depth == 3: - derivation = [ - SINGLESIG_SCRIPT_PURPOSE[P2WPKH] + HARDENED_INDEX, - network_node, - child, - ] - elif versiontype == "Ypub" and depth == 4 and child == 1 + HARDENED_INDEX: - derivation = [ - MULTISIG_SCRIPT_PURPOSE[P2SH_P2WSH] + HARDENED_INDEX, - network_node, - 0 + HARDENED_INDEX, - child, - ] - if allow_assumption != derivation: - assumption_text = t("Account #0 would be assumed") - elif versiontype == "Zpub" and depth == 4 and child == 2 + HARDENED_INDEX: - derivation = [ - MULTISIG_SCRIPT_PURPOSE[P2WSH] + HARDENED_INDEX, - network_node, - 0 + HARDENED_INDEX, - child, - ] - if allow_assumption != derivation: - assumption_text = t("Account #0 would be assumed") - - if assumption_text: - raise AssumptionWarning(assumption_text, derivation) - - return derivation - - -def derivation_to_script_wrapper(derivation): - """returns format_str for wrapping xpub into wallet descriptor, - supporting single-sig only for now, based on - embit.descriptor.arguments.KeyOrigin.derivation list""" - format_str = None - - if len(derivation) == 3: - purpose = derivation[0] - network = derivation[1] - account = derivation[2] - - if ( - network in (0 + HARDENED_INDEX, 1 + HARDENED_INDEX) - and account >= 0 + HARDENED_INDEX - ): - if purpose == SINGLESIG_SCRIPT_PURPOSE[P2PKH] + HARDENED_INDEX: - format_str = "pkh({})" - elif purpose == SINGLESIG_SCRIPT_PURPOSE[P2SH_P2WPKH] + HARDENED_INDEX: - format_str = "sh(wpkh({}))" - elif purpose == SINGLESIG_SCRIPT_PURPOSE[P2WPKH] + HARDENED_INDEX: - format_str = "wpkh({})" - elif purpose == SINGLESIG_SCRIPT_PURPOSE[P2TR] + HARDENED_INDEX: - format_str = "tr({})" - - return format_str - - def is_double_mnemonic(mnemonic: str): """Check if the mnemonic is a double mnemonic (12+12+24)""" diff --git a/tests/conftest.py b/tests/conftest.py index 1d6d456e9..0a1426dd4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ board_yahboom, board_bit, board_wonder_k, + board_embed_fire, encode_to_string, encode, statvfs, @@ -175,6 +176,15 @@ def wonder_k(monkeypatch, mp_modules): reset_krux_modules() +@pytest.fixture +def embed_fire(monkeypatch, mp_modules): + import sys + + monkeypatch.setitem(sys.modules, "board", board_embed_fire()) + monkeypatch.setitem(sys.modules, "pmu", None) + reset_krux_modules() + + @pytest.fixture( params=[ "amigo", diff --git a/tests/pages/home_pages/test_wallet_descriptor.py b/tests/pages/home_pages/test_wallet_descriptor.py index cda451271..b65446ff7 100644 --- a/tests/pages/home_pages/test_wallet_descriptor.py +++ b/tests/pages/home_pages/test_wallet_descriptor.py @@ -78,55 +78,7 @@ def test_wallet(mocker, m5stickv, tdata): None, [BUTTON_ENTER, BUTTON_ENTER], ), - # 9 vague BlueWallet-ish p2pkh, requires allow_assumption - ( - False, - tdata.LEGACY1_KEY, - tdata.VAGUE_LEGACY1_XPUB, - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # 10 vague BlueWallet-ish p2pkh w/o key loaded, requires allow_assumption - ( - False, - None, - tdata.VAGUE_LEGACY1_XPUB, - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # 11 vague BlueWallet-ish p2sh-p2wpkh ypub - ( - False, - tdata.NESTEDSW1_KEY, - tdata.VAGUE_NESTEDSW1_YPUB, - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # 12 vague BlueWallet-ish p2sh-p2wpkh ypub w/o key loaded - ( - False, - None, - tdata.VAGUE_NESTEDSW1_YPUB, - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # 13 vague BlueWallet-ish p2wpkh zpub - ( - False, - tdata.NATIVESW1_KEY, - tdata.VAGUE_NATIVESW1_ZPUB, - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # 14 vague BlueWallet-ish p2wpkh zpub w/o key loaded - ( - False, - None, - tdata.VAGUE_NATIVESW1_ZPUB, - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # 15 Load, from SD card, good data, accept + # 9 Load, from SD card, good data, accept ( False, tdata.SINGLESIG_12_WORD_KEY, @@ -447,3 +399,524 @@ def test_loading_miniscript_descriptors(mocker, amigo, wallet_tdata): wallet_descriptor.display_loading_wallet.assert_called_once() assert ctx.wallet.has_change_addr() assert ctx.input.wait_for_button.call_count == len(case[2]) + + +def test_policy_mismatch_singlesig_to_multisig_accept( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: Singlesig key loading multisig descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_MULTISIG + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept policy change (singlesig -> multisig) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (wallet_tdata.UNAMBIGUOUS_MULTISIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with multisig policy + assert ctx.wallet.key.policy_type == TYPE_MULTISIG + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_policy_mismatch_singlesig_to_multisig_decline( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: Singlesig key loading multisig descriptor - user declines change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER, BUTTON_PAGE + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_SINGLESIG + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_PAGE, # Decline policy change + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (wallet_tdata.UNAMBIGUOUS_MULTISIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was NOT changed (still singlesig) + assert ctx.wallet.key.policy_type == TYPE_SINGLESIG + # Verify wallet was NOT loaded + assert not ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_policy_mismatch_singlesig_to_miniscript_accept( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: Singlesig key loading miniscript descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_MINISCRIPT, P2WSH + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept policy change (singlesig -> miniscript) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (wallet_tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with miniscript policy and P2WSH script type + assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT + assert ctx.wallet.key.script_type == P2WSH + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_policy_mismatch_singlesig_to_taproot_miniscript_accept( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: Singlesig key loading taproot miniscript descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_MINISCRIPT, P2TR + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept policy change (singlesig -> miniscript) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: ( + wallet_tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, + FORMAT_PMOFN, + ), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with miniscript policy and P2TR script type + assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT + assert ctx.wallet.key.script_type == P2TR + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_policy_mismatch_multisig_to_singlesig_accept( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: Multisig key loading singlesig descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_SINGLESIG + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept policy change (multisig -> singlesig) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.MULTISIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (wallet_tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with singlesig policy + assert ctx.wallet.key.policy_type == TYPE_SINGLESIG + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_policy_mismatch_multisig_to_miniscript_accept( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: Multisig key loading miniscript descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_MINISCRIPT, P2WSH + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept policy change (multisig -> miniscript) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.MULTISIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (wallet_tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with miniscript policy + assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT + assert ctx.wallet.key.script_type == P2WSH + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_policy_mismatch_miniscript_to_singlesig_accept( + mocker, amigo, tdata, wallet_tdata +): + """Test: Miniscript key loading singlesig descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_SINGLESIG + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept policy change (miniscript -> singlesig) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(wallet_tdata.MINISCRIPT_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + # Use UNAMBIGUOUS_SINGLESIG_DESCRIPTOR which has /<0;1>/* (change branch) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (wallet_tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with singlesig policy + assert ctx.wallet.key.policy_type == TYPE_SINGLESIG + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_network_mismatch_mainnet_to_testnet_accept(mocker, m5stickv, tdata): + """Test: Mainnet key loading testnet descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from embit.networks import NETWORKS + + # Testnet singlesig descriptor matching TEST_12_WORD_MNEMONIC + # Derived from [55f8fc5d/84'/1'/0'] for testnet + TESTNET_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/1h/0h]tpubDCDuqu5HtBX2aD7wxvnHcj1DgFN1UVgzLkA1Ms4Va4P7TpJ3jDknkPLwWT2SqrKXNNAtJBCPcbJ8Tcpm6nLxgFapCZyhKgqwcEGv1BVpD7s/<0;1>/*)" + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept network change (mainnet -> testnet) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (TESTNET_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with testnet network + assert ctx.wallet.key.network == NETWORKS["test"] + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_network_mismatch_mainnet_to_testnet_decline(mocker, m5stickv, tdata): + """Test: Mainnet key loading testnet descriptor - user declines change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER, BUTTON_PAGE + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from embit.networks import NETWORKS + + # Testnet singlesig descriptor matching TEST_12_WORD_MNEMONIC + TESTNET_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/1h/0h]tpubDCDuqu5HtBX2aD7wxvnHcj1DgFN1UVgzLkA1Ms4Va4P7TpJ3jDknkPLwWT2SqrKXNNAtJBCPcbJ8Tcpm6nLxgFapCZyhKgqwcEGv1BVpD7s/<0;1>/*)" + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_PAGE, # Decline network change + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (TESTNET_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was NOT changed (still mainnet) + assert ctx.wallet.key.network == NETWORKS["main"] + # Verify wallet was NOT loaded + assert not ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_script_type_mismatch_p2wpkh_to_p2tr_accept( + mocker, m5stickv, tdata, wallet_tdata +): + """Test: P2WPKH singlesig key loading P2TR descriptor - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import P2TR, TYPE_SINGLESIG + + # Taproot singlesig descriptor for same mnemonic (TEST_12_WORD_MNEMONIC) + TAPROOT_SINGLESIG_DESCRIPTOR = "tr([55f8fc5d/86h/0h/0h]xpub6CNGwJbVG9sQq4vfRxk6zpE4iiES3XsdVjn3SRkbHB8zDe66vhtPJUH7dCuVMmTcQ8fuozNYDYxcQJeJZE28eSD2sZYDHCYgC3Dq48kjbKy/<0;1>/*)" + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept script type change (P2WPKH -> P2TR) + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (TAPROOT_SINGLESIG_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with P2TR script type + assert ctx.wallet.key.policy_type == TYPE_SINGLESIG + assert ctx.wallet.key.script_type == P2TR + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) + + +def test_combined_policy_and_network_mismatch_accept(mocker, m5stickv, tdata): + """Test: Combined policy and network mismatch - user accepts change""" + from krux.pages.home_pages.wallet_descriptor import WalletDescriptor + from krux.wallet import Wallet + from krux.input import BUTTON_ENTER + from krux.qr import FORMAT_PMOFN + from krux.pages.qr_capture import QRCodeCapture + from krux.key import TYPE_MINISCRIPT, P2WSH + from embit.networks import NETWORKS + + # Testnet miniscript descriptor (different policy + different network) + # Key matches TEST_12_WORD_MNEMONIC at path 48'/1'/0'/2' on testnet + # Using simple wsh(pk(...)) which is a valid miniscript construct + TESTNET_MINISCRIPT_DESCRIPTOR = "wsh(pk([55f8fc5d/48h/1h/0h/2h]tpubDDyrxYEe6bifecFTgj8vzsoUhoJmtVWeARR5xRun6haVnVrC2oTAYhj7Ja2KTkcnkW1mZPPuWGDxEsHMtRf8aAf4WfrqhLDN7xi9zAZMphv/<0;1>/*))" + + btn_seq = [ + BUTTON_ENTER, # Load wallet descriptor? Yes + BUTTON_ENTER, # Load from camera + BUTTON_ENTER, # Accept combined policy + network change + BUTTON_ENTER, # Accept wallet load + ] + + wallet = Wallet(tdata.SINGLESIG_12_WORD_KEY) + ctx = create_ctx(mocker, btn_seq, wallet) + + wallet_descriptor = WalletDescriptor(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (TESTNET_MINISCRIPT_DESCRIPTOR, FORMAT_PMOFN), + ) + mocker.patch.object( + wallet_descriptor, + "display_qr_codes", + new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), + ) + # Mock display_loading_wallet to avoid pagination button presses + mocker.patch.object(wallet_descriptor, "display_loading_wallet", return_value=None) + mocker.spy(wallet_descriptor, "_handle_policy_mismatch") + + wallet_descriptor.wallet() + + # Verify mismatch was handled + wallet_descriptor._handle_policy_mismatch.assert_called_once() + # Verify key was re-derived with miniscript policy AND testnet network + assert ctx.wallet.key.policy_type == TYPE_MINISCRIPT + assert ctx.wallet.key.script_type == P2WSH + assert ctx.wallet.key.network == NETWORKS["test"] + # Verify wallet was loaded + assert ctx.wallet.is_loaded() + assert ctx.input.wait_for_button.call_count == len(btn_seq) diff --git a/tests/pages/test_capture_entropy.py b/tests/pages/test_capture_entropy.py index 11503ff02..bbf4286d8 100644 --- a/tests/pages/test_capture_entropy.py +++ b/tests/pages/test_capture_entropy.py @@ -2,11 +2,19 @@ import hashlib import random -ENTROPY_MESSAGE_STR = ( - f"Shannon's entropy:\n%s bits (%s bits/px)\n\nPixels deviation index: %s" -) -ENTROPY_INSUFFICIENT_MESSAGE_STR = "Insufficient entropy!\n\n" + ENTROPY_MESSAGE_STR +def _called_with_insufficient_entropy_message(ctx): + """Helper bool to determine if ctx.display.draw_centered_text was called with a message including "Insufficient" """ + return ( + len( + [ + call + for call in ctx.display.draw_centered_text.call_args_list + if "Insufficient" in call.args[0] + ] + ) + > 0 + ) def test_cancel_capture(amigo, mocker): @@ -51,8 +59,6 @@ def test_insufficient_variance(amigo, mocker): INSUFFICIENT_VARIANCE_TH, INSUFFICIENT_SHANNONS_ENTROPY_TH, ) - from krux.themes import RED - from krux.format import generate_thousands_separator # Mock snapshot to return a successful snapshot mocker.patch( @@ -77,7 +83,6 @@ def test_insufficient_variance(amigo, mocker): # Mock shannon.entropy_img16b to return a value above the threshold shannon_value = INSUFFICIENT_SHANNONS_ENTROPY_TH + 1 - total_shannon = shannon_value * 320 * 240 mocker.patch("shannon.entropy_img16b", return_value=shannon_value) # Call the capture method @@ -87,13 +92,7 @@ def test_insufficient_variance(amigo, mocker): assert result is None # Assert ctx.display.draw_centered_text was called with "Insufficient entropy!" - call_message = mocker.call( - ENTROPY_INSUFFICIENT_MESSAGE_STR - % (generate_thousands_separator(total_shannon), shannon_value, variance), - RED, - ) - - ctx.display.draw_centered_text.assert_has_calls([call_message]) + assert _called_with_insufficient_entropy_message(ctx) def test_insufficient_shannons_entropy(amigo, mocker): @@ -103,8 +102,6 @@ def test_insufficient_shannons_entropy(amigo, mocker): INSUFFICIENT_VARIANCE_TH, INSUFFICIENT_SHANNONS_ENTROPY_TH, ) - from krux.themes import RED - from krux.format import generate_thousands_separator # Mock snapshot to return a successful snapshot mocker.patch( @@ -123,16 +120,12 @@ def test_insufficient_shannons_entropy(amigo, mocker): # Create an instance of CameraEntropy camera_entropy = CameraEntropy(ctx) - # Create an instance of CameraEntropy - camera_entropy = CameraEntropy(ctx) - # Mock rms value to return a value above the threshold variance = INSUFFICIENT_VARIANCE_TH + 1 camera_entropy.rms_value = mocker.MagicMock(return_value=variance) # Mock shannon.entropy_img16b to return a value below the threshold shannon_value = INSUFFICIENT_SHANNONS_ENTROPY_TH - 1 - total_shannon = shannon_value * 320 * 240 mocker.patch("shannon.entropy_img16b", return_value=shannon_value) # Call the capture method @@ -142,13 +135,7 @@ def test_insufficient_shannons_entropy(amigo, mocker): assert result is None # Assert ctx.display.draw_centered_text was called with "Insufficient entropy!" - call_message = mocker.call( - ENTROPY_INSUFFICIENT_MESSAGE_STR - % (generate_thousands_separator(total_shannon), shannon_value, variance), - RED, - ) - - ctx.display.draw_centered_text.assert_has_calls([call_message]) + assert _called_with_insufficient_entropy_message(ctx) def test_poor_variance(amigo, mocker): @@ -162,8 +149,6 @@ def test_poor_variance(amigo, mocker): INSUFFICIENT_SHANNONS_ENTROPY_TH, ) - from krux.format import generate_thousands_separator - # Mock snapshot to return a successful snapshot mocker.patch( "krux.camera.sensor.snapshot", new=snapshot_generator(outcome=SNAP_SUCCESS) @@ -181,16 +166,12 @@ def test_poor_variance(amigo, mocker): # Create an instance of CameraEntropy camera_entropy = CameraEntropy(ctx) - # Create an instance of CameraEntropy - camera_entropy = CameraEntropy(ctx) - # Mock rms value to return a value below the poor threshold, but above insufficient variance = POOR_VARIANCE_TH - 1 camera_entropy.rms_value = mocker.MagicMock(return_value=variance) # Mock shannon.entropy_img16b to return a value above the threshold shannon_value = INSUFFICIENT_SHANNONS_ENTROPY_TH + 1 - total_shannon = shannon_value * 320 * 240 mocker.patch("shannon.entropy_img16b", return_value=shannon_value) # Call the capture method @@ -202,14 +183,8 @@ def test_poor_variance(amigo, mocker): assert result == hasher.digest() - # Assert ctx.display.draw_centered_text was called with "Insufficient entropy!" - call_message = mocker.call( - ENTROPY_MESSAGE_STR - % (generate_thousands_separator(total_shannon), shannon_value, variance), - highlight_prefix=":", - ) - - ctx.display.draw_centered_text.assert_has_calls([call_message]) + # Assert ctx.display.draw_centered_text was NOT called with "Insufficient entropy!" + assert not _called_with_insufficient_entropy_message(ctx) def test_good_variance_good_shannons_entropy(amigo, mocker): @@ -222,7 +197,6 @@ def test_good_variance_good_shannons_entropy(amigo, mocker): POOR_VARIANCE_TH, INSUFFICIENT_SHANNONS_ENTROPY_TH, ) - from krux.format import generate_thousands_separator # Mock snapshot to return a successful snapshot mocker.patch( @@ -247,7 +221,6 @@ def test_good_variance_good_shannons_entropy(amigo, mocker): # Mock shannon.entropy_img16b to return a value above the threshold shannon_value = INSUFFICIENT_SHANNONS_ENTROPY_TH + 1 - total_shannon = shannon_value * 320 * 240 mocker.patch("shannon.entropy_img16b", return_value=shannon_value) # Call the capture method @@ -259,14 +232,8 @@ def test_good_variance_good_shannons_entropy(amigo, mocker): assert result == hasher.digest() - # Assert ctx.display.draw_centered_text was called with "Insufficient entropy!" - call_message = mocker.call( - ENTROPY_MESSAGE_STR - % (generate_thousands_separator(total_shannon), shannon_value, variance), - highlight_prefix=":", - ) - - ctx.display.draw_centered_text.assert_has_calls([call_message]) + # Assert ctx.display.draw_centered_text was NOT called with "Insufficient entropy!" + assert not _called_with_insufficient_entropy_message(ctx) def test_entropy_measurement_update_state_2(amigo, mocker): diff --git a/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py index 0f9912c33..6b24c2c2e 100644 --- a/tests/pages/test_datum_tool.py +++ b/tests/pages/test_datum_tool.py @@ -36,10 +36,7 @@ def test_urobj_to_data(m5stickv, mocker): MULTISIG_DESCR = "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))#t2zpj2eu" cases = [ - { - "control": UR("crypto-bip39", UR_BIP39_WORDS_BYTES), - "expected": MNEMONIC.split(" "), - }, + {"control": UR("crypto-bip39", UR_BIP39_WORDS_BYTES), "expected": MNEMONIC}, { "control": UR("crypto-output", UR_OUTPUT_MULTISIG_DESCR_BYTES), "expected": MULTISIG_DESCR, @@ -320,7 +317,7 @@ def test_datumtoolmenu_scan_qr_abort(m5stickv, mocker): assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) from ur.ur import UR - from urtypes import Bytes + from urtypes.bytes import Bytes # scan UR-QR (for coverage), then back out of datum tool MULTISIG_DESCR = "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))#t2zpj2eu" @@ -1187,3 +1184,86 @@ def test_datumtool_show_contents_button_turbo(mocker, m5stickv): datum._show_contents() time.sleep_ms.assert_called_with(KEY_REPEAT_DELAY_MS) + + +def test_datumtoolmenu_scan_qr_binary_decodable(m5stickv, mocker): + """Test scan_qr when QR scanner returns binary data that can be decoded to text""" + from krux.pages.datum_tool import DatumToolMenu + from krux.pages.qr_capture import QRCodeCapture + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + # Binary data that can be decoded to UTF-8 text + binary_data = b"Hello World" + + # Mock QR scanner to return binary data + mocker.patch.object( + QRCodeCapture, "qr_capture_loop", new=lambda self: (binary_data, 0) + ) + + # Button sequence to exit from DatumTool view + BTN_SEQUENCE = ( + BUTTON_ENTER, # go Scan QR + BUTTON_PAGE_PREV, # to Back + BUTTON_ENTER, # go Back + BUTTON_PAGE, # to Text Entry + BUTTON_PAGE, # to Read File + BUTTON_PAGE, # to Back + BUTTON_ENTER, # go Back + ) + + ctx = create_ctx(mocker, BTN_SEQUENCE) + page = DatumToolMenu(ctx).run() + + # Verify that the button sequence was executed + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + + # Verify that the decoded text "Hello World" was displayed in the info box + # The display should show the decoded string, not the hex representation + display_calls = [ + str(call) for call in ctx.display.draw_hcentered_text.call_args_list + ] + assert any( + '"Hello World"' in call for call in display_calls + ), f"Expected to find decoded text 'Hello World' in display calls, but got: {display_calls}" + + +def test_datumtoolmenu_scan_qr_binary_non_decodable(m5stickv, mocker): + """Test scan_qr when QR scanner returns binary data that cannot be decoded to text""" + from krux.pages.datum_tool import DatumToolMenu + from krux.pages.qr_capture import QRCodeCapture + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + # Binary data that cannot be decoded to UTF-8 (invalid UTF-8 sequence) + # 0xFF and 0xFE are not valid UTF-8 byte sequences + binary_data = b"\xff\xfe\xfd\xfc" + + # Mock QR scanner to return binary data + mocker.patch.object( + QRCodeCapture, "qr_capture_loop", new=lambda self: (binary_data, 0) + ) + + # Button sequence to exit from DatumTool view after scanning binary data + BTN_SEQUENCE = ( + BUTTON_ENTER, # go Scan QR + BUTTON_PAGE_PREV, # to Back + BUTTON_ENTER, # go Back + BUTTON_PAGE, # to Text Entry + BUTTON_PAGE, # to Read File + BUTTON_PAGE, # to Back + BUTTON_ENTER, # go Back + ) + + ctx = create_ctx(mocker, BTN_SEQUENCE) + page = DatumToolMenu(ctx).run() + + # Verify that the button sequence was executed + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + + # Verify that the hex representation was displayed in the info box + # Since the data cannot be decoded to text, it should be shown as hex + display_calls = [ + str(call) for call in ctx.display.draw_hcentered_text.call_args_list + ] + assert any( + "0xfffefdfc" in call for call in display_calls + ), f"Expected to find hex representation '0xfffefdfc' in display calls, but got: {display_calls}" diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index 92f83abff..bb4edb8c0 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -2,7 +2,6 @@ from unittest.mock import patch from . import create_ctx - TEST_KEY = "test key" CBC_WORDS = "dog guitar hotel random owner gadget salute riot patrol work advice panic erode leader pass cross section laundry elder asset soul scale immune scatter" ECB_WORDS = "brass creek fuel snack era success impulse dirt caution purity lottery lizard boil festival neither case swift smooth range mail gravity sample never ivory" @@ -126,6 +125,38 @@ def test_load_key_from_qr_code(m5stickv, mocker): assert key == "qr key" print("case 2: load_key_from_qr_code") + BTN_SEQUENCE = ( + [BUTTON_PAGE] # move to QR code key + + [BUTTON_ENTER] # choose QR code key + + [BUTTON_ENTER] # Confirm + ) + ctx = create_ctx(mocker, BTN_SEQUENCE) + key_generator = EncryptionKey(ctx) + mocker.patch.object( + QRCodeCapture, + "qr_capture_loop", + new=lambda self: (b"decodable bytes qr key", None), + ) + key = key_generator.encryption_key() + assert key == "decodable bytes qr key" + + print("case 3: load_key_from_qr_code") + BTN_SEQUENCE = ( + [BUTTON_PAGE] # move to QR code key + + [BUTTON_ENTER] # choose QR code key + + [BUTTON_ENTER] # Confirm + ) + ctx = create_ctx(mocker, BTN_SEQUENCE) + key_generator = EncryptionKey(ctx) + mocker.patch.object( + QRCodeCapture, "qr_capture_loop", new=lambda self: (b"\xde\xad\xbe\xef", None) + ) + key = key_generator.encryption_key() + assert key == b"\xde\xad\xbe\xef" + call_message = mocker.call("Key (4): 0xdeadbeef", 10, highlight_prefix=":") + ctx.display.draw_hcentered_text.assert_has_calls([call_message]) + + print("case 4: load_key_from_qr_code") # Repeat with too much characters >ENCRYPTION_KEY_MAX_LEN BTN_SEQUENCE = [ BUTTON_PAGE, # move to QR code key diff --git a/tests/pages/test_tools.py b/tests/pages/test_tools.py index c7d7edf44..9a62c01d4 100644 --- a/tests/pages/test_tools.py +++ b/tests/pages/test_tools.py @@ -2,7 +2,6 @@ from unittest.mock import patch from . import create_ctx - SEEDS_JSON = """{ "ecbID": { "version": 0, diff --git a/tests/pages/test_wallet_settings.py b/tests/pages/test_wallet_settings.py index 659eacbba..5e8c31977 100644 --- a/tests/pages/test_wallet_settings.py +++ b/tests/pages/test_wallet_settings.py @@ -199,10 +199,11 @@ def test_qr_passphrase_fails_on_encrypted_non_ascii_bytes(mocker, m5stickv, tdat def test_passphrase_non_ascii_validation(m5stickv, mocker, tdata): from krux.pages.wallet_settings import PassphraseEditor from krux.pages import Menu, MENU_EXIT + from krux.input import BUTTON_PAGE_PREV # Test non-ASCII passphrase rejection - NON_ASCII_PASSPHRASE = "Test™" # Contains non-ASCII character - ctx = create_ctx(mocker, None, tdata.SINGLESIG_12_WORD_KEY) + NON_ASCII_PASSPHRASE = "Testá" # Contains non-ASCII character + ctx = create_ctx(mocker, [BUTTON_PAGE_PREV], tdata.SINGLESIG_12_WORD_KEY) passphrase_editor = PassphraseEditor(ctx) # Mock the Menu's run_loop to return non-ASCII passphrase first, then exit @@ -216,18 +217,18 @@ def test_passphrase_non_ascii_validation(m5stickv, mocker, tdata): side_effect=menu_returns, ) - # Mock flash_error to track if it was called - flash_error_spy = mocker.spy(passphrase_editor, "flash_error") + # Mock prompt to track if it was called + prompt_spy = mocker.spy(passphrase_editor, "prompt") result = passphrase_editor.load_passphrase_menu( tdata.SINGLESIG_12_WORD_KEY.mnemonic ) - # Verify that flash_error was called with the ASCII error message - flash_error_spy.assert_called() + # Verify that it was called + prompt_spy.assert_called() # Get the actual call arguments - call_args = flash_error_spy.call_args[0][0] - assert call_args == "Failed to load" + call_args = prompt_spy.call_args[0][0] + assert call_args == "Proceed?" # Verify that the method returned None (exited without accepting passphrase) assert result is None diff --git a/tests/shared_mocks.py b/tests/shared_mocks.py index cadd0194a..74dd3a635 100644 --- a/tests/shared_mocks.py +++ b/tests/shared_mocks.py @@ -727,6 +727,54 @@ def board_wonder_k(): ) +def board_embed_fire(): + return mock.MagicMock( + config={ + "type": "embed_fire", + "lcd": { + "dcx": 27, + "ss": 28, + "rst": 29, + "clk": 26, + "height": 240, + "width": 320, + "invert": 0, + "dir": 96, + "lcd_type": 0, + }, + "sensor": { + "pin_sda": 24, + "cmos_href": 20, + "cmos_pclk": 23, + "cmos_xclk": 22, + "cmos_pwdn": 21, + "cmos_vsync": 18, + "reg_width": 8, + "i2c_num": 2, + "pin_clk": 25, + "cmos_rst": 19, + }, + "sdcard": {"sclk": 8, "mosi": 7, "miso": 9, "cs": 6}, + "board_info": { + "BOOT_KEY": 16, + "LED_G": 17, + "CONNEXT_A": 1, + "CONNEXT_B": 0, + "I2C_SDA": 12, + "I2C_SCL": 13, + "SPI_SCLK": 8, + "SPI_MOSI": 7, + "SPI_MISO": 9, + "SPI_CS": 6, + }, + "krux": { + "pins": {"BUTTON_B": 16, "TOUCH_IRQ": 15, "I2C_SDA": 12, "I2C_SCL": 13}, + "display": {"touch": True, "font": [8, 16], "font_wide": [16, 16]}, + }, + } + ) + + def mock_context(mocker): import board diff --git a/tests/test_bip39.py b/tests/test_bip39.py index 38acd246b..f3c721770 100644 --- a/tests/test_bip39.py +++ b/tests/test_bip39.py @@ -10,7 +10,7 @@ def test_one_word_mnemonics(): for numwords in (12, 15, 18, 21, 24): for word in WORDLIST: - mnemonic = (word + " ") * numwords + mnemonic = (word + " ") * (numwords - 1) + word assert kruxbip39.k_mnemonic_is_valid(mnemonic) == bip39.mnemonic_is_valid( mnemonic ) @@ -101,3 +101,43 @@ def test_invalid_mnemonic_length(): for case in cases: with pytest.raises(ValueError, match="Invalid recovery phrase"): kruxbip39.k_mnemonic_bytes(case) + + +def test_mnemonic_with_formatting_issues(): + # Use a valid 12-word mnemonic for testing + valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + # Verify the base mnemonic is valid + assert kruxbip39.k_mnemonic_is_valid(valid_mnemonic) == True + + # Test cases with formatting issues that should be invalid + invalid_cases = [ + # Leading spaces + " " + valid_mnemonic, + " " + valid_mnemonic, + # Trailing spaces + valid_mnemonic + " ", + valid_mnemonic + " ", + # Double spaces between words + valid_mnemonic.replace(" ", " ", 1), # One double space + valid_mnemonic.replace(" ", " "), # All double spaces + # Newlines instead of spaces + valid_mnemonic.replace(" ", "\n", 1), # One newline + valid_mnemonic.replace(" ", "\n"), # All newlines + # Commas instead of spaces + valid_mnemonic.replace(" ", ",", 1), # One comma + valid_mnemonic.replace(" ", ","), # All commas + # Semicolons instead of spaces + valid_mnemonic.replace(" ", ";", 1), # One semicolon + valid_mnemonic.replace(" ", ";"), # All semicolons + # Mixed formatting issues + " " + valid_mnemonic.replace(" ", " ", 3) + " ", + ] + + for case in invalid_cases: + assert ( + kruxbip39.k_mnemonic_is_valid(case) == False + ), f"Krux: Expected invalid for: {repr(case)}" + assert ( + bip39.mnemonic_is_valid(case) == False + ), f"Embit: Expected invalid for: {repr(case)}" diff --git a/tests/test_camera.py b/tests/test_camera.py index a35e171c8..78854ee72 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -134,7 +134,7 @@ def test_initialize_run_with_anti_glair_enabled(mocker, m5stickv): assert c.cam_id is not None -def test_toggle_antiglare(mocker, m5stickv): +def test_toggle_mode(mocker, m5stickv): import krux from krux.camera import ( Camera, @@ -153,9 +153,9 @@ def test_toggle_antiglare(mocker, m5stickv): for sensor_id in SENSORS_LIST: mocker.patch("krux.camera.sensor.get_id", lambda: sensor_id) c = Camera() - mocker.spy(c, "has_antiglare") + mocker.spy(c, "has_mode_control") c.initialize_sensor() - if c.has_antiglare(): + if c.has_mode_control(): assert c.mode == QR_SCAN_MODE c.toggle_camera_mode() assert c.mode == ANTI_GLARE_MODE diff --git a/tests/test_input.py b/tests/test_input.py index 38e67cbb6..bd64a3310 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -457,6 +457,7 @@ def test_debounce_presses_with_greater_interval(mocker, m5stickv): def test_debounce_presses_with_smaller_interval(mocker, m5stickv): from krux.input import Input, RELEASED, PRESSED + from krux.krux_settings import Settings input = Input() interval = 10 # ms @@ -473,9 +474,11 @@ def test_debounce_presses_with_smaller_interval(mocker, m5stickv): btn = input.wait_for_button() assert btn == 0 assert input.entropy > 0 - # Assert that the flush_events was called 10 times + # Assert that the flush_events was called debounce / interval times # meaning that the debounce time was respected - assert input.flush_events.call_count == 10 + assert ( + input.flush_events.call_count == Settings().hardware.buttons.debounce / interval + ) def test_wait_for_button_blocks_until_enter_released(mocker, m5stickv): diff --git a/tests/test_kef.py b/tests/test_kef.py index f45277fd9..a44bbf68f 100644 --- a/tests/test_kef.py +++ b/tests/test_kef.py @@ -1,7 +1,6 @@ import pytest from unittest.mock import patch - TEST_WORDS = ( "crush inherit small egg include title slogan mom remain blouse boost bonus" ) diff --git a/tests/test_qr.py b/tests/test_qr.py index dcad8d4b1..d69fb0327 100644 --- a/tests/test_qr.py +++ b/tests/test_qr.py @@ -115,7 +115,7 @@ def test_parser(mocker, m5stickv, tdata): if num == 4: # Multi-part UR - assert parser.total_count() == len(parts) * 2 + assert parser.processed_parts_count() == i + 1 else: assert parser.total_count() == len(parts) if parser.format == FORMAT_UR: @@ -132,7 +132,7 @@ def test_parser(mocker, m5stickv, tdata): parser.parse(parts[0]) if num == 4: - assert parser.total_count() == len(parts) * 2 + assert parser.processed_parts_count() == i + 1 else: assert parser.total_count() == len(parts) diff --git a/tests/test_touch.py b/tests/test_touch.py index f35aa2d69..3f03ff734 100644 --- a/tests/test_touch.py +++ b/tests/test_touch.py @@ -1,8 +1,514 @@ +import pytest import time -from .test_input import mock_timer_ticks -def test_touch_event(mocker, amigo): +@pytest.fixture +def mock_settings(mocker): + """Mock Settings to avoid dependency on hardware config""" + mock_settings_obj = mocker.MagicMock() + mock_settings_obj.hardware.touch.threshold = 40 + # Ensure hardware doesn't have display attribute to avoid coordinate flipping + del mock_settings_obj.hardware.display + mock_settings_class = mocker.patch("krux.touch.Settings") + mock_settings_class.return_value = mock_settings_obj + return mock_settings_obj + + +@pytest.fixture +def mock_touch_driver(mocker): + """Mock a generic touch driver""" + driver = mocker.MagicMock() + driver.current_point.return_value = None + driver.event.return_value = False + driver.irq_point = None + return driver + + +def test_touch_init_ft6x36(mocker, amigo, mock_settings): + """Test Touch initialization with FT6X36 driver (default case)""" + from krux.touch import Touch + + mock_control = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_control) + + # No res_pin = FT6X36 + touch = Touch(width=240, height=135, irq_pin=20) + + assert touch.width == 240 + assert touch.height == 135 + assert touch.state == 0 # IDLE + mock_control.activate_irq.assert_called_once_with(20) + mock_control.threshold.assert_called_once_with(40) + + +def test_touch_init_cst816(mocker, embed_fire, mock_settings): + """Test Touch initialization with CST816 driver (embed_fire board)""" + from krux.touch import Touch + + mock_control = mocker.MagicMock() + mocker.patch("krux.touchscreens.cst816.touch_control", mock_control) + + # embed_fire = CST816 + touch = Touch(width=240, height=135, irq_pin=21) + + assert touch.touch_driver == mock_control + mock_control.activate.assert_called_once_with(21) + mock_control.threshold.assert_called_once_with(40) + + +def test_touch_init_gt911(mocker, wonder_k, mock_settings): + """Test Touch initialization with GT911 driver (res_pin provided)""" + from krux.touch import Touch + + mock_control = mocker.MagicMock() + mocker.patch("krux.touchscreens.gt911.touch_control", mock_control) + + # res_pin != None = GT911 + touch = Touch(width=240, height=135, irq_pin=21, res_pin=22) + + assert touch.touch_driver == mock_control + mock_control.activate.assert_called_once_with(21, 22) + mock_control.threshold.assert_called_once_with(40) + + +def test_clear_regions(mocker, amigo, mock_settings): + """Test clearing touch regions""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.y_regions = [50, 100, 150] + touch.x_regions = [60, 120] + + touch.clear_regions() + + assert touch.y_regions == [] + assert touch.x_regions == [] + + +def test_add_y_delimiter(mocker, amigo, mock_settings): + """Test adding Y delimiters""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.add_y_delimiter(50) + touch.add_y_delimiter(100) + + assert touch.y_regions == [50, 100] + + +def test_add_y_delimiter_out_of_bounds(mocker, amigo, mock_settings): + """Test adding Y delimiter outside display area raises error""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + + with pytest.raises(ValueError, match="outside display area"): + touch.add_y_delimiter(250) # width = 240 + + +def test_add_x_delimiter(mocker, amigo, mock_settings): + """Test adding X delimiters""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.add_x_delimiter(45) + touch.add_x_delimiter(90) + + assert touch.x_regions == [45, 90] + + +def test_add_x_delimiter_out_of_bounds(mocker, amigo, mock_settings): + """Test adding X delimiter outside display area raises error""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + + with pytest.raises(ValueError, match="outside display area"): + touch.add_x_delimiter(140) # height = 135 + + +def test_set_regions(mocker, amigo, mock_settings): + """Test setting regions with lists""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.set_regions(x_list=[30, 60, 90], y_list=[60, 120, 180]) + + assert touch.x_regions == [30, 60, 90] + assert touch.y_regions == [60, 120, 180] + + +def test_set_regions_invalid_type(mocker, amigo, mock_settings): + """Test setting regions with non-list raises error""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + + with pytest.raises(ValueError, match="must be a list"): + touch.set_regions(x_list="not a list") + + with pytest.raises(ValueError, match="must be a list"): + touch.set_regions(y_list=123) + + +@pytest.mark.parametrize( + "data,x_regions,y_regions,expected_valid", + [ + ((100, 100), [], [], True), # No regions = always valid + ((100, 100), [], [50, 150], True), # Within y bounds + ((100, 100), [40, 120], [], True), # Within x bounds + ((100, 100), [40, 120], [50, 150], True), # Within both bounds + ((30, 100), [40, 120], [], False), # Below x lower bound + ((130, 100), [40, 120], [], False), # Above x upper bound + ((100, 30), [], [50, 150], False), # Below y lower bound + ((100, 160), [], [50, 150], False), # Above y upper bound + ], +) +def test_valid_position( + mocker, amigo, mock_settings, data, x_regions, y_regions, expected_valid +): + """Test position validation against regions""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.x_regions = x_regions + touch.y_regions = y_regions + + assert touch.valid_position(data) == expected_valid + + +@pytest.mark.parametrize( + "y_regions,x_regions,touch_point,expected_index", + [ + ([60, 120], [], (100, 50), 0), # y=50 < 60: index 0 + ([60, 120], [], (100, 80), 0), # y=80 between 60 and 120: index 0 + ([60, 120], [], (100, 130), 1), # y=130 > 120: index 1 + ([40, 80, 120], [], (50, 30), 0), # y=30 < 40: index 0 + ([40, 80, 120], [], (50, 50), 0), # y=50 between 40-80: index 0 + ([40, 80, 120], [], (50, 90), 1), # y=90 between 80-120: index 1 + ([40, 80, 120], [], (50, 130), 2), # y=130 > 120: index 2 + ], +) +def test_extract_index( + mocker, amigo, mock_settings, y_regions, x_regions, touch_point, expected_index +): + """Test index extraction from touch coordinates""" + from krux.touch import Touch + + mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock()) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.y_regions = y_regions + touch.x_regions = x_regions + + index = touch._extract_index(touch_point) + assert index == expected_index + + +def test_current_state_idle_to_pressed(mocker, amigo, mock_settings): + """Test state transition from IDLE to PRESSED""" + from krux.touch import Touch, IDLE, PRESSED + + mock_driver = mocker.MagicMock() + mock_driver.current_point.return_value = (100, 100) + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.y_regions = [60, 120] + + assert touch.state == IDLE + + state = touch.current_state() + + assert state == PRESSED + assert len(touch.press_point) == 1 + assert touch.press_point[0] == (100, 100) + + +def test_current_state_pressed_to_released(mocker, amigo, mock_settings): + """Test state transition from PRESSED to RELEASED""" + from krux.touch import Touch, PRESSED, RELEASED + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.state = PRESSED + touch.press_point = [(100, 100)] + touch.release_point = (105, 105) + + # Return None to simulate touch release + mock_driver.current_point.return_value = None + + state = touch.current_state() + + assert state == RELEASED + + +def test_current_state_released_to_idle(mocker, amigo, mock_settings): + """Test state transition from RELEASED to IDLE""" + from krux.touch import Touch, RELEASED, IDLE + + mock_driver = mocker.MagicMock() + mock_driver.current_point.return_value = None + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.state = RELEASED + + state = touch.current_state() + + assert state == IDLE + + +@pytest.mark.parametrize( + "press_point,release_point,expected_gesture", + [ + ((100, 100), (160, 100), 1), # SWIPE_RIGHT (lateral = 60) + ((160, 100), (100, 100), 2), # SWIPE_LEFT (lateral = -60) + ((100, 100), (100, 160), 4), # SWIPE_DOWN (vertical = 60 > lateral) + ((100, 160), (100, 100), 3), # SWIPE_UP (vertical = -60 > lateral) + ((100, 100), (105, 105), None), # No gesture (< threshold) + ], +) +def test_gesture_detection( + mocker, amigo, mock_settings, press_point, release_point, expected_gesture +): + """Test swipe gesture detection""" + from krux.touch import Touch, PRESSED + + mock_driver = mocker.MagicMock() + mock_driver.current_point.return_value = None + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.state = PRESSED + touch.press_point = [press_point] + touch.release_point = release_point + + touch.current_state() + + assert touch.gesture == expected_gesture + + +def test_event_with_validation(mocker, amigo, mock_settings): + """Test event detection with position validation""" + from krux.touch import Touch + + mock_driver = mocker.MagicMock() + mock_driver.event.return_value = True + mock_driver.irq_point = (100, 100) + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.y_regions = [50, 150] + touch.sample_time = 900 # 100ms ago + + result = touch.event(validate_position=True) + + assert result is True + + +def test_event_invalid_position(mocker, amigo, mock_settings): + """Test event detection with invalid position""" + from krux.touch import Touch + + mock_driver = mocker.MagicMock() + mock_driver.event.return_value = True + mock_driver.irq_point = (100, 30) # Below y_regions[0] + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.y_regions = [50, 150] + touch.sample_time = 900 + + result = touch.event(validate_position=True) + + assert result is False + + +def test_event_without_validation(mocker, amigo, mock_settings): + """Test event detection without position validation""" + from krux.touch import Touch + + mock_driver = mocker.MagicMock() + mock_driver.event.return_value = True + mock_driver.irq_point = (100, 30) # Would be invalid if validated + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.y_regions = [50, 150] + touch.sample_time = 900 + + result = touch.event(validate_position=False) + + assert result is True + + +def test_event_sample_period_not_elapsed(mocker, amigo, mock_settings): + """Test event not triggered if sample period hasn't elapsed""" + from krux.touch import Touch + + mock_driver = mocker.MagicMock() + mock_driver.event.return_value = True + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1010) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.sample_time = 1000 # 10ms ago (< TOUCH_S_PERIOD=20) + + result = touch.event() + + assert result is False + + +def test_value(mocker, amigo, mock_settings): + """Test value() method returns button-like behavior""" + from krux.touch import Touch, IDLE, PRESSED + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + + # IDLE state returns 1 + mock_driver.current_point.return_value = None + touch.state = IDLE + assert touch.value() == 1 + + # Non-IDLE state returns 0 + # When touch is pressed, driver returns coordinates + mock_driver.current_point.return_value = (100, 100) + touch.state = IDLE # Start from IDLE + result = touch.value() + # State becomes PRESSED, so value returns 0 + assert result == 0 + + +def test_swipe_right_value(mocker, amigo, mock_settings): + """Test swipe right detection and reset""" + from krux.touch import Touch, SWIPE_RIGHT + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.gesture = SWIPE_RIGHT + assert touch.swipe_right_value() == 0 + assert touch.gesture is None # Gesture cleared + + assert touch.swipe_right_value() == 1 # No gesture + + +def test_swipe_left_value(mocker, amigo, mock_settings): + """Test swipe left detection and reset""" + from krux.touch import Touch, SWIPE_LEFT + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.gesture = SWIPE_LEFT + assert touch.swipe_left_value() == 0 + assert touch.gesture is None + + +def test_swipe_up_value(mocker, amigo, mock_settings): + """Test swipe up detection and reset""" + from krux.touch import Touch, SWIPE_UP + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.gesture = SWIPE_UP + assert touch.swipe_up_value() == 0 + assert touch.gesture is None + + +def test_swipe_down_value(mocker, amigo, mock_settings): + """Test swipe down detection and reset""" + from krux.touch import Touch, SWIPE_DOWN + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + + touch = Touch(width=240, height=135, irq_pin=20) + + touch.gesture = SWIPE_DOWN + assert touch.swipe_down_value() == 0 + assert touch.gesture is None + + +def test_current_index(mocker, amigo, mock_settings): + """Test getting current touch index""" + from krux.touch import Touch + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.index = 5 + + assert touch.current_index() == 5 + + +def test_press_point_averaging(mocker, amigo, mock_settings): + """Test that multiple touch samples are averaged""" + from krux.touch import Touch, PRESSED + + mock_driver = mocker.MagicMock() + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) + mocker.patch("time.ticks_ms", return_value=1000) + + touch = Touch(width=240, height=135, irq_pin=20) + touch.state = PRESSED + touch.press_point = [(100, 100), (102, 102), (98, 98)] + + # Add another point + mock_driver.current_point.return_value = (100, 100) + touch.current_state() + + # Should have 4 points now + assert len(touch.press_point) == 4 + + # Index should be calculated from average + # Average = (100+102+98+100)/4, (100+102+98+100)/4 = (100, 100) + # (Note: actual index depends on regions set) + + +def test_touch_event_integration(mocker, amigo, mock_settings): + """Integration test: full event sequence with touch point averaging and index calculation""" from krux.touch import Touch import board @@ -15,47 +521,31 @@ def test_touch_event(mocker, amigo): EVENTS = [False, True, False, False] POINTS = [TOUCH_POINT_1, TOUCH_POINT_2, None, None] - mocker.patch("krux.touchscreens.ft6x36.FT6X36.event", side_effect=EVENTS) - mocker.patch("krux.touchscreens.ft6x36.FT6X36.current_point", side_effect=POINTS) - - mocker.patch.object(time, "ticks_ms", side_effect=mock_timer_ticks()) - - touch = Touch( - board.config["lcd"]["width"], - board.config["lcd"]["height"], - board.config["krux"]["pins"]["TOUCH_IRQ"], - ) - touch.y_regions = Y_REGIONS - touch.touch_driver.irq_point = TOUCH_POINT_1 # Simulate touch event + mock_driver = mocker.MagicMock() + mock_driver.event.side_effect = EVENTS + mock_driver.current_point.side_effect = POINTS + mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver) - for _ in range(len(EVENTS)): - event = touch.event(validate_position=True) - assert event == False - assert touch.current_index() == 3 + # Mock time advancing + tick_time = [10000] + def mock_time_advance(): + tick_time[0] += 100 + return tick_time[0] - 100 -def test_set_regions(mocker, amigo): - from krux.touch import Touch - import board - import pytest + mocker.patch.object(time, "ticks_ms", side_effect=mock_time_advance) touch = Touch( board.config["lcd"]["width"], board.config["lcd"]["height"], board.config["krux"]["pins"]["TOUCH_IRQ"], ) + touch.y_regions = Y_REGIONS + mock_driver.irq_point = TOUCH_POINT_1 # Simulate touch event - touch.set_regions() - - assert touch.x_regions == touch.y_regions == [] - - with pytest.raises(ValueError, match="x_list must be a list"): - touch.set_regions(1) - - with pytest.raises(ValueError, match="y_list must be a list"): - touch.set_regions(None, 1) - - touch.set_regions([1, 2, 3], [4, 5, 6]) + event_result = None + for _ in range(len(EVENTS)): + event_result = touch.event(validate_position=True) - assert touch.x_regions == [1, 2, 3] - assert touch.y_regions == [4, 5, 6] + assert event_result is False + assert touch.current_index() == 3 diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 8f6ef51d6..f113d0c39 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1155,8 +1155,11 @@ def test_load_singlesig(mocker, m5stickv, tdata): assert wallet.has_change_addr() -def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet +def test_load_singlesig_warns_mismatch_with_multisig_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_MULTISIG, TYPE_SINGLESIG from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR wallet = Wallet(tdata.SINGLESIG_KEY) @@ -1168,24 +1171,72 @@ def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata): (tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR), ] for case in cases: - with pytest.raises(ValueError): + # Should raise PolicyMismatchWarning due to policy type mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(case[0], case[1]) + # Verify exception contains correct policy types + assert exc_info.value.args[0] == TYPE_MULTISIG # required_policy_type + assert exc_info.value.args[2] == TYPE_SINGLESIG # current_policy_type -def test_load_singlesig_fails_with_miniscript_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + # Note: These descriptors don't contain this key's xpub as a cosigner, + # so even after fixing the policy type, loading would fail with + # "xpub not a multisig cosigner" error + + +def test_load_singlesig_warns_mismatch_with_miniscript_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_MINISCRIPT, TYPE_SINGLESIG, P2WSH, P2TR from krux.qr import FORMAT_NONE wallet = Wallet(tdata.SINGLESIG_KEY) - with pytest.raises(ValueError): + # Test P2WSH miniscript + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) - with pytest.raises(ValueError): + assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type + assert exc_info.value.args[1] == P2WSH # required_script_type + assert exc_info.value.args[2] == TYPE_SINGLESIG # current_policy_type + + # Verify wallet can load with correct policy + miniscript_key = Key( + tdata.SINGLESIG_KEY.mnemonic, + TYPE_MINISCRIPT, + tdata.SINGLESIG_KEY.network, + tdata.SINGLESIG_KEY.passphrase, + script_type=P2WSH, + ) + wallet_wsh = Wallet(miniscript_key) + wallet_wsh.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet_wsh.is_loaded() + + # Test P2TR miniscript + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type + assert exc_info.value.args[1] == P2TR # required_script_type + assert exc_info.value.args[2] == TYPE_SINGLESIG # current_policy_type + # Verify wallet can load with correct policy + tap_miniscript_key = Key( + tdata.SINGLESIG_KEY.mnemonic, + TYPE_MINISCRIPT, + tdata.SINGLESIG_KEY.network, + tdata.SINGLESIG_KEY.passphrase, + script_type=P2TR, + ) + wallet_tr = Wallet(tap_miniscript_key) + wallet_tr.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet_tr.is_loaded() -def test_load_multisig_fails_with_singlesig_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + +def test_load_multisig_warns_mismatch_with_singlesig_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_SINGLESIG, TYPE_MULTISIG from krux.qr import FORMAT_NONE, FORMAT_PMOFN wallet = Wallet(tdata.MULTISIG_NATIVE_SW_1) @@ -1195,48 +1246,126 @@ def test_load_multisig_fails_with_singlesig_descriptor(mocker, m5stickv, tdata): (tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE), ] for case in cases: - with pytest.raises(ValueError): + # Should raise PolicyMismatchWarning due to policy type mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(case[0], case[1]) + # Verify exception contains correct policy types + assert exc_info.value.args[0] == TYPE_SINGLESIG # required_policy_type + assert exc_info.value.args[2] == TYPE_MULTISIG # current_policy_type -def test_load_multisig_fails_with_miniscript_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + # Verify wallet can load after re-creating with correct policy + singlesig_key = Key( + tdata.MULTISIG_NATIVE_SW_1.mnemonic, + TYPE_SINGLESIG, + tdata.MULTISIG_NATIVE_SW_1.network, + tdata.MULTISIG_NATIVE_SW_1.passphrase, + ) + wallet_with_correct_policy = Wallet(singlesig_key) + wallet_with_correct_policy.load(case[0], case[1]) + assert wallet_with_correct_policy.is_loaded() + + +def test_load_multisig_warns_mismatch_with_miniscript_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_MINISCRIPT, TYPE_MULTISIG, P2WSH, P2TR from krux.qr import FORMAT_NONE wallet = Wallet(tdata.MULTISIG_NATIVE_SW_1) - with pytest.raises(ValueError): + # Test P2WSH miniscript + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) - with pytest.raises(ValueError): + assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type + assert exc_info.value.args[1] == P2WSH # required_script_type + assert exc_info.value.args[2] == TYPE_MULTISIG # current_policy_type + + # Verify wallet can load with correct policy + miniscript_key = Key( + tdata.MULTISIG_NATIVE_SW_1.mnemonic, + TYPE_MINISCRIPT, + tdata.MULTISIG_NATIVE_SW_1.network, + tdata.MULTISIG_NATIVE_SW_1.passphrase, + script_type=P2WSH, + ) + wallet_wsh = Wallet(miniscript_key) + wallet_wsh.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet_wsh.is_loaded() + + # Test P2TR miniscript + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert exc_info.value.args[0] == TYPE_MINISCRIPT # required_policy_type + assert exc_info.value.args[1] == P2TR # required_script_type + assert exc_info.value.args[2] == TYPE_MULTISIG # current_policy_type + # Verify wallet can load with correct policy + tap_miniscript_key = Key( + tdata.MULTISIG_NATIVE_SW_1.mnemonic, + TYPE_MINISCRIPT, + tdata.MULTISIG_NATIVE_SW_1.network, + tdata.MULTISIG_NATIVE_SW_1.passphrase, + script_type=P2TR, + ) + wallet_tr = Wallet(tap_miniscript_key) + wallet_tr.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet_tr.is_loaded() -def test_load_miniscript_fails_with_singlesig_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + +def test_load_miniscript_warns_mismatch_with_singlesig_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_SINGLESIG, TYPE_MINISCRIPT, P2WPKH from krux.qr import FORMAT_NONE, FORMAT_PMOFN - wallets = [ - Wallet(tdata.MINISCRIPT_KEY), - Wallet(tdata.TAP_MINISCRIPT_KEY), + miniscript_keys = [ + tdata.MINISCRIPT_KEY, + tdata.TAP_MINISCRIPT_KEY, ] cases = [ (tdata.SPECTER_SINGLESIG_WALLET_DATA, FORMAT_PMOFN), (tdata.UNAMBIGUOUS_SINGLESIG_DESCRIPTOR, FORMAT_NONE), ] - for wallet in wallets: + + for miniscript_key in miniscript_keys: + wallet = Wallet(miniscript_key) for case in cases: - with pytest.raises(ValueError): + # Should raise PolicyMismatchWarning due to policy type mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(case[0], case[1]) + # Verify exception contains correct policy types + assert exc_info.value.args[0] == TYPE_SINGLESIG # required_policy_type + assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type + + # Verify wallet can load after re-creating with correct policy + singlesig_key = Key( + miniscript_key.mnemonic, + TYPE_SINGLESIG, + network=miniscript_key.network, + passphrase=miniscript_key.passphrase, + account_index=miniscript_key.account_index, + script_type=P2WPKH, + ) + wallet_with_correct_policy = Wallet(singlesig_key) + wallet_with_correct_policy.load(case[0], case[1]) + assert wallet_with_correct_policy.is_loaded() -def test_load_miniscript_fails_with_multisig_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + +def test_load_miniscript_warns_mismatch_with_multisig_descriptor( + mocker, m5stickv, tdata +): + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_MULTISIG, TYPE_MINISCRIPT, P2WSH from krux.qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR - wallets = [ - Wallet(tdata.MINISCRIPT_KEY), - Wallet(tdata.TAP_MINISCRIPT_KEY), + miniscript_keys = [ + tdata.MINISCRIPT_KEY, + tdata.TAP_MINISCRIPT_KEY, ] cases = [ @@ -1246,35 +1375,100 @@ def test_load_miniscript_fails_with_multisig_descriptor(mocker, m5stickv, tdata) (tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR), ] - for wallet in wallets: + for miniscript_key in miniscript_keys: + wallet = Wallet(miniscript_key) for case in cases: - with pytest.raises(ValueError): + # Should raise PolicyMismatchWarning due to policy type mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(case[0], case[1]) + # Verify exception contains correct policy types + required_policy_type = exc_info.value.args[0] + assert required_policy_type == TYPE_MULTISIG + assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type -def test_load_wsh_miniscript_fails_with_tr_miniscript_descriptor( + # Note: These descriptors don't contain this key's xpub as a cosigner, + # so even after fixing the policy type, loading would fail with + # "xpub not a multisig cosigner" error + + +def test_load_wsh_miniscript_warns_mismatch_with_tr_miniscript_descriptor( mocker, m5stickv, tdata ): - from krux.wallet import Wallet + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_MINISCRIPT, P2WSH, P2TR from krux.qr import FORMAT_NONE wallet = Wallet(tdata.MINISCRIPT_KEY) - with pytest.raises(ValueError): + # Should raise PolicyMismatchWarning due to script type mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + # Verify exception contains correct policy and script types + required_policy_type = exc_info.value.args[0] + required_script_type = exc_info.value.args[1] + assert required_policy_type == TYPE_MINISCRIPT + assert required_script_type == P2TR + assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type + + # Extract required network if present in exception + required_network = exc_info.value.args[3] if len(exc_info.value.args) > 3 else None + + # Verify wallet can load after re-creating with correct script type and network + corrected_key = Key( + tdata.MINISCRIPT_KEY.mnemonic, + required_policy_type, + network=required_network if required_network else tdata.MINISCRIPT_KEY.network, + passphrase=tdata.MINISCRIPT_KEY.passphrase, + account_index=tdata.MINISCRIPT_KEY.account_index, + script_type=required_script_type, + ) + wallet_with_correct_script = Wallet(corrected_key) + wallet_with_correct_script.load( + tdata.LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR, FORMAT_NONE + ) + assert wallet_with_correct_script.is_loaded() + -def test_load_tr_miniscript_fails_with_wsh_miniscript_descriptor( +def test_load_tr_miniscript_warns_mismatch_with_wsh_miniscript_descriptor( mocker, m5stickv, tdata ): - from krux.wallet import Wallet + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key, TYPE_MINISCRIPT, P2WSH, P2TR from krux.qr import FORMAT_NONE wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) - with pytest.raises(ValueError): + # Should raise PolicyMismatchWarning due to script type mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + # Verify exception contains correct policy and script types + required_policy_type = exc_info.value.args[0] + required_script_type = exc_info.value.args[1] + assert required_policy_type == TYPE_MINISCRIPT + assert required_script_type == P2WSH + assert exc_info.value.args[2] == TYPE_MINISCRIPT # current_policy_type + + # Extract required network if present in exception + required_network = exc_info.value.args[3] if len(exc_info.value.args) > 3 else None + + # Verify wallet can load after re-creating with correct script type and network + corrected_key = Key( + tdata.TAP_MINISCRIPT_KEY.mnemonic, + required_policy_type, + network=( + required_network if required_network else tdata.TAP_MINISCRIPT_KEY.network + ), + passphrase=tdata.TAP_MINISCRIPT_KEY.passphrase, + account_index=tdata.TAP_MINISCRIPT_KEY.account_index, + script_type=required_script_type, + ) + wallet_with_correct_script = Wallet(corrected_key) + wallet_with_correct_script.load(tdata.LIANA_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + assert wallet_with_correct_script.is_loaded() + def test_load_singlesig_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata): from krux.wallet import Wallet @@ -1297,59 +1491,83 @@ def test_load_multisig_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata) def test_load_miniscript_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key from krux.qr import FORMAT_NONE wallet = Wallet(tdata.MINISCRIPT_KEY) - with pytest.raises(ValueError): + # First, should raise PolicyMismatchWarning due to network mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.UNRELATED_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + # Extract required policy, script type, and network from exception + required_policy_type = exc_info.value.args[0] + required_script_type = exc_info.value.args[1] + required_network = ( + exc_info.value.args[3] + if len(exc_info.value.args) > 3 + else tdata.MINISCRIPT_KEY.network + ) + + # Create key with correct network but still unrelated to descriptor + corrected_key = Key( + tdata.MINISCRIPT_KEY.mnemonic, + required_policy_type, + network=required_network if required_network else tdata.MINISCRIPT_KEY.network, + passphrase=tdata.MINISCRIPT_KEY.passphrase, + account_index=tdata.MINISCRIPT_KEY.account_index, + script_type=required_script_type, + ) + wallet_corrected = Wallet(corrected_key) + + # Now should raise ValueError because key is not in descriptor + with pytest.raises(ValueError): + wallet_corrected.load(tdata.UNRELATED_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + def test_load_tr_miniscript_fails_when_key_not_in_descriptor(mocker, m5stickv, tdata): - from krux.wallet import Wallet + from krux.wallet import Wallet, PolicyMismatchWarning + from krux.key import Key from krux.qr import FORMAT_NONE wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) - with pytest.raises(ValueError): + # First, should raise PolicyMismatchWarning due to network mismatch + with pytest.raises(PolicyMismatchWarning) as exc_info: wallet.load(tdata.UNRELATED_TAP_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + # Extract required policy, script type, and network from exception + required_policy_type = exc_info.value.args[0] + required_script_type = exc_info.value.args[1] + required_network = ( + exc_info.value.args[3] + if len(exc_info.value.args) > 3 + else tdata.TAP_MINISCRIPT_KEY.network + ) + + # Create key with correct network but still unrelated to descriptor + corrected_key = Key( + tdata.TAP_MINISCRIPT_KEY.mnemonic, + required_policy_type, + network=( + required_network if required_network else tdata.TAP_MINISCRIPT_KEY.network + ), + passphrase=tdata.TAP_MINISCRIPT_KEY.passphrase, + account_index=tdata.TAP_MINISCRIPT_KEY.account_index, + script_type=required_script_type, + ) + wallet_corrected = Wallet(corrected_key) + + # Now should raise ValueError because key is not in descriptor + with pytest.raises(ValueError): + wallet_corrected.load(tdata.UNRELATED_TAP_MINISCRIPT_DESCRIPTOR, FORMAT_NONE) + def test_parse_wallet(mocker, m5stickv, tdata): - from krux.wallet import parse_wallet, AssumptionWarning + from krux.wallet import parse_wallet cases = [ - ( - tdata.KRUX_LEGACY1_XPUB, - tdata.KRUX_LEGACY1_DESCRIPTOR, - None, - ), - ( - tdata.KRUX_NESTEDSW1_XPUB, - tdata.KRUX_NESTEDSW1_DESCRIPTOR, - None, - ), - ( - tdata.KRUX_NESTEDSW1_YPUB, - tdata.KRUX_NESTEDSW1_YPUB_DESCRIPTOR, - None, - ), - ( - tdata.KRUX_NATIVESW1_XPUB, - tdata.KRUX_NATIVESW1_DESCRIPTOR, - None, - ), - ( - tdata.KRUX_NATIVESW1_ZPUB, - tdata.KRUX_NATIVESW1_ZPUB_DESCRIPTOR, - None, - ), - ( - tdata.KRUX_TAPROOT1_XPUB, - tdata.KRUX_TAPROOT1_DESCRIPTOR, - None, - ), ( tdata.SPECTER_SINGLESIG_WALLET_DATA, tdata.SPECTER_SINGLESIG_DESCRIPTOR, @@ -1360,21 +1578,6 @@ def test_parse_wallet(mocker, m5stickv, tdata): tdata.SPECTER_MULTISIG_DESCRIPTOR, "Specter Multisig Wallet", ), - ( - tdata.BLUEWALLET_SINGLESIG_WALLET_DATA, - tdata.BLUEWALLET_SINGLESIG_DESCRIPTOR, - None, - ), - # ( BlueWallet legacy bip44 xpub w/o key-origin is not supported, will default to bip84 - # tdata.BLUEWALLET_LEGACY_WALLET_DATA, - # tdata.BLUEWALLET_LEGACY_DESCRIPTOR, - # None, - # ), - ( - tdata.BLUEWALLET_NESTEDSW_WALLET_DATA, - tdata.BLUEWALLET_NESTEDSW_DESCRIPTOR, - None, - ), ( tdata.BLUEWALLET_MULTISIG_WALLET_DATA, tdata.BLUEWALLET_MULTISIG_DESCRIPTOR, @@ -1426,10 +1629,7 @@ def test_parse_wallet(mocker, m5stickv, tdata): for case in cases: print(case_n) case_n += 1 - try: - descriptor, label = parse_wallet(case[0]) - except AssumptionWarning as e: - descriptor, label = parse_wallet(case[0], allow_assumption=e.args[1]) + descriptor, label = parse_wallet(case[0]) assert descriptor.to_string() == case[1].split("#")[0].replace("'", "h") assert label == case[2] @@ -1463,9 +1663,8 @@ def test_provably_unspendable_non_deterministic_chain_code(mocker, m5stickv, tda NON_DETERMINISTIC_CHAIN_CODE = "tr(xpub661MyMwAqRbcFhaVQsdthkuGZQS3e9MENERDseTmnmhX2dFdgitmaFGLSPeXtcRzQ8jQaG3XCYPUknq7jX86V1qU6p981ripVVbvYnE5XpV/<0;1>/*,{and_v(v:multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<2;3>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<2;3>/*,[d3a80c8b/48'/0'/0'/2']xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*),older(65535)),multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*)})" wallet = Wallet(tdata.TAP_MINISCRIPT_KEY) - with pytest.raises(ValueError, match="Internal key not provably unspendable"): - wallet.load(NON_DETERMINISTIC_CHAIN_CODE, FORMAT_NONE) - assert not wallet.is_loaded() + wallet.load(NON_DETERMINISTIC_CHAIN_CODE, FORMAT_NONE) + assert wallet.is_loaded() # ignored the chaincode def test_parse_wallet_raises_errors(mocker, m5stickv, tdata): @@ -1622,139 +1821,6 @@ def test_version_to_network_versiontype(): ) -def test_xpub_data_to_derivation(): - from krux.wallet import xpub_data_to_derivation, AssumptionWarning - - # purpose - LEGACY = 44 + 2**31 - NATSW = 84 + 2**31 - NESSW = 49 + 2**31 - TAPROOT = 86 + 2**31 - MULTISIG = 48 + 2**31 - - # network - MAIN = 0 + 2**31 - TEST = 1 + 2**31 - - # account - ACCT0 = 0 + 2**31 - ACCT1 = 1 + 2**31 - - # multisig script type - MULTINESSW = 1 + 2**31 - MULTINATSW = 2 + 2**31 - - cases = [ - # versiontype, network, child, depth, allow_assumption, expected_return - ("xpub", "main", ACCT0, 3, None, AssumptionWarning), # don't assume - ("xpub", "main", ACCT0, 3, [NATSW, MAIN, ACCT0], [NATSW, MAIN, ACCT0]), - ("xpub", "test", ACCT0, 3, [NATSW, TEST, ACCT0], [NATSW, TEST, ACCT0]), - ("xpub", "main", ACCT1, 3, [NATSW, MAIN, ACCT1], [NATSW, MAIN, ACCT1]), - ("xpub", "main", ACCT0, 4, [NATSW, MAIN, ACCT0], None), # wrong depth - ("ypub", "main", ACCT0, 3, None, [NESSW, MAIN, ACCT0]), - ("ypub", "test", ACCT0, 3, None, [NESSW, TEST, ACCT0]), - ("ypub", "main", ACCT1, 3, None, [NESSW, MAIN, ACCT1]), - ("ypub", "test", ACCT1, 3, None, [NESSW, TEST, ACCT1]), - ("ypub", "main", ACCT0, 4, None, None), # wrong depth - ("zpub", "main", ACCT0, 3, None, [NATSW, MAIN, ACCT0]), - ("zpub", "test", ACCT0, 3, None, [NATSW, TEST, ACCT0]), - ("zpub", "main", ACCT1, 3, None, [NATSW, MAIN, ACCT1]), - ("zpub", "test", ACCT1, 3, None, [NATSW, TEST, ACCT1]), - ("zpub", "main", ACCT0, 4, None, None), # wrong depth - ("Ypub", "main", MULTINESSW, 4, None, AssumptionWarning), # don't assume - ("Ypub", "test", MULTINESSW, 4, None, AssumptionWarning), - ( - "Ypub", - "main", - MULTINESSW, - 4, - [MULTISIG, MAIN, ACCT0, MULTINESSW], - [MULTISIG, MAIN, ACCT0, MULTINESSW], - ), - ( - "Ypub", - "test", - MULTINESSW, - 4, - [MULTISIG, TEST, ACCT0, MULTINESSW], - [MULTISIG, TEST, ACCT0, MULTINESSW], - ), - ("Ypub", "main", MULTINESSW, 3, None, None), # wrong depth - ("Zpub", "main", MULTINATSW, 4, None, AssumptionWarning), # don't assume - ("Zpub", "test", MULTINATSW, 4, None, AssumptionWarning), - ( - "Zpub", - "main", - MULTINATSW, - 4, - [MULTISIG, MAIN, ACCT0, MULTINATSW], - [MULTISIG, MAIN, ACCT0, MULTINATSW], - ), - ( - "Zpub", - "test", - MULTINATSW, - 4, - [MULTISIG, TEST, ACCT0, MULTINATSW], - [MULTISIG, TEST, ACCT0, MULTINATSW], - ), - ("Zpub", "main", MULTINATSW, 3, None, None), # wrong depth - ] - - for _case in cases: - if type(_case[5]) == type and issubclass(_case[5], Exception): - with pytest.raises(_case[5]): - xpub_data_to_derivation(*_case[:5]) - else: - assert xpub_data_to_derivation(*_case[:5]) == _case[5] - - -def test_derivation_to_script_wrapper(): - from krux.wallet import derivation_to_script_wrapper - - # purpose - LEGACY = 44 + 2**31 - NATSW = 84 + 2**31 - NESSW = 49 + 2**31 - TAPROOT = 86 + 2**31 - - # network - MAIN = 0 + 2**31 - TEST = 1 + 2**31 - - # account - HARD = 0 + 2**31 - - NOTHARD = 2**31 - 1 - - cases = [ - # derivation list as the only function param and expected return - ([HARD, MAIN], None), - ([HARD, TEST], None), - ([HARD, MAIN, HARD], None), - ([NOTHARD, MAIN, HARD], None), - ([LEGACY, MAIN, HARD], "pkh({})"), - ([LEGACY, TEST, HARD], "pkh({})"), - ([LEGACY, MAIN, NOTHARD], None), - ([LEGACY, NOTHARD, HARD], None), - ([NESSW, MAIN, HARD], "sh(wpkh({}))"), - ([NESSW, TEST, HARD], "sh(wpkh({}))"), - ([NESSW, MAIN, NOTHARD], None), - ([NESSW, NOTHARD, HARD], None), - ([NATSW, MAIN, HARD], "wpkh({})"), - ([NATSW, TEST, HARD], "wpkh({})"), - ([NATSW, MAIN, NOTHARD], None), - ([NATSW, NOTHARD, HARD], None), - ([TAPROOT, MAIN, HARD], "tr({})"), - ([TAPROOT, TEST, HARD], "tr({})"), - ([TAPROOT, MAIN, NOTHARD], None), - ([TAPROOT, NOTHARD, HARD], None), - ] - - for _case in cases: - assert derivation_to_script_wrapper(_case[0]) == _case[1] - - def test_parse_wallet_via_ur_output(mocker, m5stickv): from krux.wallet import parse_wallet diff --git a/tests/touchscreens/helpers.py b/tests/touchscreens/helpers.py new file mode 100644 index 000000000..7eff6be29 --- /dev/null +++ b/tests/touchscreens/helpers.py @@ -0,0 +1,46 @@ +# Test helpers for touchscreen tests + + +def create_cst816_touch_data(points, event, x_coord, y_coord): + """Helper to construct CST816 touch data bytes. + + Args: + points: Number of touch points (0x00 or 0x01) + event: Event type (e.g., EVENT_TOUCH_CONTACT) + x_coord: X coordinate (0-4095) + y_coord: Y coordinate (0-4095) + + Returns: + list: 6-byte touch data array + """ + x_high = (x_coord >> 8) & 0x0F + x_low = x_coord & 0xFF + y_high = (y_coord >> 8) & 0x0F + y_low = y_coord & 0xFF + + return [ + 0x00, # gesture + points, + (event << 6) | x_high, + x_low, + y_high, + y_low, + ] + + +def create_ft6x36_touch_data(x_coord, y_coord): + """Helper to construct FT6X36 touch data bytes. + + Args: + x_coord: X coordinate (0-4095) + y_coord: Y coordinate (0-4095) + + Returns: + list: 4-byte touch data array [x_high, x_low, y_high, y_low] + """ + x_high = (x_coord >> 8) & 0x0F + x_low = x_coord & 0xFF + y_high = (y_coord >> 8) & 0x0F + y_low = y_coord & 0xFF + + return [x_high, x_low, y_high, y_low] diff --git a/tests/touchscreens/test_cst816.py b/tests/touchscreens/test_cst816.py new file mode 100644 index 000000000..1e33c511e --- /dev/null +++ b/tests/touchscreens/test_cst816.py @@ -0,0 +1,326 @@ +import pytest + + +@pytest.fixture +def mock_cst816(mocker): + from krux.touchscreens.cst816 import ( + fm, + touch_control, + CST816S_TOUCH_DATA, + CST816S_VERSION, + EVENT_TOUCH_CONTACT, + SCREEN_WIDTH, + ) + + mocker.patch.object(fm, "register") + mock_gpio = mocker.MagicMock() + mocker.patch("Maix.GPIO", return_value=mock_gpio) + + def fake_activate(irq_pin): + touch_control.irq_pin = mock_gpio + + def fake_write_reg(reg_addr, buf): + pass + + def fake_read_reg(reg_addr, buf_len): + if reg_addr == CST816S_VERSION: + return [0x01] + elif reg_addr == CST816S_TOUCH_DATA: + from .helpers import create_cst816_touch_data + + return create_cst816_touch_data( + points=0x01, event=EVENT_TOUCH_CONTACT, x_coord=0x0050, y_coord=0x00A0 + ) + return [0x00] + + mocker.patch.object(touch_control, "_write_reg", fake_write_reg) + mocker.patch.object(touch_control, "_read_reg", fake_read_reg) + mocker.patch.object(touch_control, "activate", fake_activate) + + touch_control.event_flag = False + touch_control.irq_point = None + touch_control.last_touch_time = 0 + mocker.spy(touch_control, "trigger_event") + + return touch_control + + +def test_handler(mocker, embed_fire, mock_cst816): + from krux.touchscreens.cst816 import __handler__, ACTIVITY_THRESHOLD + + mocker.patch("time.ticks_ms", return_value=ACTIVITY_THRESHOLD + 100) + __handler__(10) + + mock_cst816.trigger_event.assert_called_once() + assert mock_cst816.event_flag + assert mock_cst816.irq_point is not None + assert mock_cst816.event() + assert not mock_cst816.event_flag + + +def test_debounce_trigger_event_too_soon(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816, ACTIVITY_THRESHOLD + + touch = CST816() + touch.last_touch_time = 1000 + + mocker.patch("time.ticks_ms", return_value=1000 + ACTIVITY_THRESHOLD - 10) + mock_validate = mocker.patch.object(touch, "current_point_validate") + + touch.trigger_event() + + assert not touch.event_flag + assert touch.irq_point is None + mock_validate.assert_not_called() + assert touch.last_touch_time == 1000 + ACTIVITY_THRESHOLD - 10 + + +def test_init(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816 + + t = CST816() + + assert t.irq_pin is None + assert t.event_flag is False + assert t.irq_point is None + assert t.last_touch_time == 0 + + +def test_point_not_read(mocker, dock): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + data = touch.current_point() + + assert not data + + +def test_point_read(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT, SCREEN_WIDTH + from .helpers import create_cst816_touch_data + + touch = CST816() + mocker.patch("time.ticks_ms", return_value=1000) + touch.last_touch_time = 950 + + x_coord = 0x0050 + y_coord = 0x00A0 + touch_data = create_cst816_touch_data(0x01, EVENT_TOUCH_CONTACT, x_coord, y_coord) + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data) + + data = touch.current_point() + expected = (SCREEN_WIDTH - x_coord, y_coord) + assert data == expected + + +def test_point_validate_read(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT, SCREEN_WIDTH + from .helpers import create_cst816_touch_data + + touch = CST816() + x_coord = 0x0028 + y_coord = 0x0078 + touch_data = create_cst816_touch_data(0x01, EVENT_TOUCH_CONTACT, x_coord, y_coord) + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data) + + data = touch.current_point_validate() + expected = (SCREEN_WIDTH - x_coord, y_coord) + assert data == expected + + +def test_no_touch_points(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT + from .helpers import create_cst816_touch_data + + touch = CST816() + touch_data = create_cst816_touch_data(0x00, EVENT_TOUCH_CONTACT, 0x00, 0x00) + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data) + + data = touch.current_point_validate() + assert data is None + + +def test_wrong_event_type(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + points = 0x01 + event = 0x01 << 6 + touch_data = [0x00, points, event, 0x50, 0x00, 0xA0] + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data) + + data = touch.current_point_validate() + assert data is None + + +def test_point_read_error(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + mocker.patch("time.ticks_ms", return_value=1000) + touch.last_touch_time = 950 + + e = Exception("mocked error") + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", side_effect=e) + + data = touch.current_point() + assert data is None + + +def test_read_touch_data_parsing_error(mocker, embed_fire, capsys): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + mocker.patch.object( + touch, "_read_reg", side_effect=Exception("Touch data parsing error") + ) + + data = touch._read_touch_data() + + assert data is None + captured = capsys.readouterr() + assert "CST816S touch data read error:" in captured.out + assert "Touch data parsing error" in captured.out + + +def test_point_outside_threshold(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816, ACTIVITY_THRESHOLD + + touch = CST816() + mocker.patch("time.ticks_ms", return_value=1000) + touch.last_touch_time = 1000 - ACTIVITY_THRESHOLD - 10 + + data = touch.current_point() + assert data is None + + +def test_activate(mocker, embed_fire): + from krux.touchscreens.cst816 import CST816, fm + + mock_fm_register = mocker.patch.object(fm, "register") + mock_gpio = mocker.MagicMock() + mocker.patch("Maix.GPIO", return_value=mock_gpio) + + touch = CST816() + test_pin = 21 + + assert touch.irq_pin is None + + touch.activate(test_pin) + + mock_fm_register.assert_called_with(test_pin, fm.fpioa.GPIOHS1) + assert touch.irq_pin is not None + + +def test_init_cst816_read_error(mocker, embed_fire, capsys): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + mocker.patch.object(touch, "_read_reg", side_effect=Exception("I2C read error")) + mock_write = mocker.patch.object(touch, "_write_reg") + + touch._init_cst816() + + captured = capsys.readouterr() + assert "CST816S initialization error:" in captured.out + assert "I2C read error" in captured.out + mock_write.assert_not_called() + + +def test_init_cst816_write_error(mocker, embed_fire, capsys): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + mocker.patch.object(touch, "_read_reg", return_value=[0x01]) + mocker.patch.object(touch, "_write_reg", side_effect=Exception("I2C write error")) + + touch._init_cst816() + + captured = capsys.readouterr() + assert "CST816S initialization error:" in captured.out + assert "I2C write error" in captured.out + + +def test_write_reg_exception(mocker, embed_fire, capsys): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + mocker.patch("krux.i2c.i2c_bus.writeto", side_effect=Exception("I2C write failed")) + + touch._write_reg(0x00, bytearray([0x01])) + + captured = capsys.readouterr() + assert "CST816S write error:" in captured.out + assert "I2C write failed" in captured.out + + +def test_write_reg_no_i2c_bus(dock): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + touch._write_reg(0x00, bytearray([0x01])) + + +def test_read_reg_exception(mocker, embed_fire, capsys): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + mocker.patch("krux.i2c.i2c_bus.writeto", side_effect=Exception("I2C read failed")) + + result = touch._read_reg(0x00, 1) + + captured = capsys.readouterr() + assert "CST816S read error:" in captured.out + assert "I2C read failed" in captured.out + assert result is None + + +def test_read_reg_no_i2c_bus(dock): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + result = touch._read_reg(0x00, 1) + + assert result is None + + +def test_threshold(mocker, embed_fire, capsys): + from krux.touchscreens.cst816 import CST816 + + touch = CST816() + touch.threshold(30) + + captured = capsys.readouterr() + assert "Threshold setting not supported" in captured.out + + +@pytest.mark.parametrize( + "x_coord,y_coord,expected_x", + [ + (0x0000, 0x0000, 240), + (0x0050, 0x00A0, 160), + (0x00F0, 0x0140, 0), + (0x0078, 0x0140, 120), + ], +) +def test_coordinate_mirroring(mocker, embed_fire, x_coord, y_coord, expected_x): + from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT + from .helpers import create_cst816_touch_data + + touch = CST816() + touch_data = create_cst816_touch_data(0x01, EVENT_TOUCH_CONTACT, x_coord, y_coord) + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data) + + data = touch.current_point_validate() + assert data == (expected_x, y_coord) diff --git a/tests/touchscreens/test_ft6x36.py b/tests/touchscreens/test_ft6x36.py index 8929ab2b7..757f11295 100644 --- a/tests/touchscreens/test_ft6x36.py +++ b/tests/touchscreens/test_ft6x36.py @@ -40,28 +40,31 @@ def fake_read_reg(reg_addr, buf_len): def test_handler(mocker, amigo, mock_ft6x36): from krux.touchscreens.ft6x36 import __handler__ - # The pin numbers are arbitrary, just for testing - # and they will be ignored in the handler - calls = [] - for pin in [10, 11, 12]: - __handler__(pin) - calls.append(mocker.call()) - - mock_ft6x36.trigger_event.assert_has_calls(calls) + # Trigger the handler (pin number is arbitrary and ignored) + __handler__(10) + + # Verify trigger_event was called + mock_ft6x36.trigger_event.assert_called_once() + + # Verify event flag was set and point was captured assert mock_ft6x36.event_flag assert mock_ft6x36.irq_point is not None - # get the event event flag and cleans it + # Verify event() returns flag and clears it assert mock_ft6x36.event() assert not mock_ft6x36.event_flag def test_init(mocker, amigo): - from krux.touchscreens.ft6x36 import FT6X36 + from krux.touchscreens.ft6x36 import FT6X36, FT6X36_ADDR t = FT6X36() - assert isinstance(t, FT6X36) + # Verify initial state + assert t.touch_irq_pin is None + assert t.event_flag is False + assert t.irq_point is None + assert t.addr == FT6X36_ADDR def test_point_not_read(mocker, dock): @@ -87,23 +90,77 @@ def test_point_read(mocker, amigo): assert data == point -def test_gesture_discarded(mocker, amigo): +@pytest.mark.parametrize( + "touch_points,expected_result", + [ + (0x00, None), # No touch + (0x01, (0x09, 0x09)), # Single touch (valid) + (0x02, None), # Two touches (not supported) + (0x03, None), # Three touches (not supported) + ], +) +def test_touch_point_counts(mocker, amigo, touch_points, expected_result): from krux.touchscreens.ft6x36 import FT6X36 touch = FT6X36() - # four bytes reg_point = [0x00, 0x09, 0x00, 0x09] - mocker.patch("krux.i2c.i2c_bus.readfrom_mem", side_effect=([0x02], reg_point)) + mocker.patch( + "krux.i2c.i2c_bus.readfrom_mem", side_effect=([touch_points], reg_point) + ) data = touch.current_point() - assert data is None + assert data == expected_result def test_point_read_error(mocker, amigo): from krux.touchscreens.ft6x36 import FT6X36 touch = FT6X36() - # four bytes e = Exception("mocked error") mocker.patch("krux.i2c.i2c_bus.readfrom_mem", side_effect=([0x01], e)) data = touch.current_point() - assert data == e + assert data is None + + +def test_activate_irq(mocker, amigo): + from krux.touchscreens.ft6x36 import FT6X36, fm + + mock_fm_register = mocker.patch.object(fm, "register") + mock_gpio = mocker.MagicMock() + mocker.patch("Maix.GPIO", return_value=mock_gpio) + + touch = FT6X36() + test_pin = 20 + + # Before activation, IRQ pin should be None + assert touch.touch_irq_pin is None + + touch.activate_irq(test_pin) + + # After activation, verify: + # 1. Pin was registered with fpioa manager + mock_fm_register.assert_called_with(test_pin, fm.fpioa.GPIOHS1) + + # 2. IRQ pin was configured + assert touch.touch_irq_pin is not None + + +def test_threshold(mocker, amigo): + from krux.touchscreens.ft6x36 import FT6X36, FT_ID_G_THGROUP + + mock_writeto = mocker.patch("krux.i2c.i2c_bus.writeto_mem") + + touch = FT6X36() + test_threshold = 30 + + touch.threshold(test_threshold) + + # Verify write_reg was called with correct threshold value + # write_reg should have been called during __init__ and when setting threshold + calls = mock_writeto.call_args_list + # Find the call that sets our test threshold + threshold_calls = [ + call + for call in calls + if call[0][1] == FT_ID_G_THGROUP and call[0][2] == test_threshold + ] + assert len(threshold_calls) >= 1 diff --git a/tests/touchscreens/test_gt911.py b/tests/touchscreens/test_gt911.py new file mode 100644 index 000000000..2c5c31504 --- /dev/null +++ b/tests/touchscreens/test_gt911.py @@ -0,0 +1,352 @@ +import pytest + + +@pytest.fixture +def mock_gt911(mocker): + from krux.touchscreens.gt911 import ( + fm, + touch_control, + GT911_COORD_ADDR, + GT911_PRODUCT_ID, + ) + + mocker.patch.object(fm, "register") + mock_gpio = mocker.MagicMock() + mocker.patch("Maix.GPIO", return_value=mock_gpio) + mocker.patch("time.sleep_ms") # Mock sleep to avoid delays in tests + + def fake_activate(irq_pin, res_pin): + touch_control.irq_pin = mock_gpio + touch_control.reset_pin = mock_gpio + + def fake_write_reg(reg_addr, buf): + pass + + def fake_read_reg(reg_addr, buf_len): + if reg_addr == GT911_PRODUCT_ID: + # Mock product ID (4 bytes: "911") + return [0x39, 0x31, 0x31, 0x00] # "911" in ASCII + + elif reg_addr == GT911_COORD_ADDR: + # Mock status byte: 0x81 = buffer ready (0x80) + 1 touch point (0x01) + return [0x81] + + elif reg_addr == GT911_COORD_ADDR + 1: + # Mock touch data: 8 bytes + # [track_id, x_low, x_high, y_low, y_high, size_low, size_high, reserved] + x_coord = 0x0064 # X = 100 + y_coord = 0x00C8 # Y = 200 + return [ + 0x00, # track_id + x_coord & 0xFF, # x_low + (x_coord >> 8) & 0xFF, # x_high + y_coord & 0xFF, # y_low + (y_coord >> 8) & 0xFF, # y_high + 0x00, # size_low + 0x00, # size_high + 0x00, # reserved + ] + + return [0x00] + + mocker.patch.object(touch_control, "write_reg", fake_write_reg) + mocker.patch.object(touch_control, "read_reg", fake_read_reg) + mocker.patch.object(touch_control, "activate", fake_activate) + + touch_control.event_flag = False + touch_control.irq_point = None + touch_control.last_touch_time = 0 + mocker.spy(touch_control, "trigger_event") + + return touch_control + + +def test_handler(mocker, wonder_k, mock_gt911): + from krux.touchscreens.gt911 import __handler__, ACTIVITY_THRESHOLD + + # Mock time to ensure debounce criteria is met + mocker.patch("time.ticks_ms", return_value=ACTIVITY_THRESHOLD + 100) + + # Trigger the handler (pin number is arbitrary and ignored) + __handler__(10) + + # Verify trigger_event was called + mock_gt911.trigger_event.assert_called_once() + + # Verify event flag was set and point was captured + assert mock_gt911.event_flag + assert mock_gt911.irq_point is not None + + # Verify event() returns flag and clears it + assert mock_gt911.event() + assert not mock_gt911.event_flag + + +def test_init(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911, GT911_ADDR + + t = GT911() + + # Verify initial state + assert t.irq_pin is None + assert t.reset_pin is None + assert t.event_flag is False + assert t.irq_point is None + assert t.addr == GT911_ADDR + assert t.last_touch_time == 0 + + +def test_point_not_read(mocker, dock): + # Dock does not set i2c_bus because it does not + # use I2C_SCL or I2C_SDA pins + from krux.touchscreens.gt911 import GT911 + + touch = GT911() + data = touch.current_point() + + assert not data + + +def test_point_read(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911 + + touch = GT911() + + # Mock time to ensure we're within activity threshold + mocker.patch("time.ticks_ms", return_value=1000) + touch.last_touch_time = 950 # 50ms ago, within threshold + + # Mock coordinate data: X=100, Y=200 + x_coord = 0x0064 + y_coord = 0x00C8 + coord_data = [ + 0x00, + x_coord & 0xFF, + (x_coord >> 8) & 0xFF, + y_coord & 0xFF, + (y_coord >> 8) & 0xFF, + 0x00, + 0x00, + 0x00, + ] + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=coord_data) + + data = touch.current_point() + assert data == (x_coord, y_coord) + + +def test_point_validate_read(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911 + + touch = GT911() + + # Mock status: 0x81 (buffer ready + 1 touch point) + # Mock coordinates: X=150, Y=250 + x_coord = 0x0096 + y_coord = 0x00FA + coord_data = [ + 0x00, + x_coord & 0xFF, + (x_coord >> 8) & 0xFF, + y_coord & 0xFF, + (y_coord >> 8) & 0xFF, + 0x00, + 0x00, + 0x00, + ] + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch( + "krux.i2c.i2c_bus.readfrom", + side_effect=[[0x81], coord_data], # First call: status, second: coords + ) + + data = touch.current_point_validate() + assert data == (x_coord, y_coord) + + +@pytest.mark.parametrize( + "status,expected", + [ + (0x00, None), # No touch, buffer not ready + (0x80, None), # Buffer ready but no touch points + (0x01, None), # Touch point but buffer not ready + (0x81, (100, 200)), # Valid: buffer ready + touch point + ( + 0x82, + (100, 200), + ), # Valid: buffer ready + 2 touch points (first point returned) + ], +) +def test_status_validation(mocker, wonder_k, status, expected): + """Test different status register values""" + from krux.touchscreens.gt911 import GT911 + + touch = GT911() + + # Mock coordinates: X=100, Y=200 + coord_data = [0x00, 0x64, 0x00, 0xC8, 0x00, 0x00, 0x00, 0x00] + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch( + "krux.i2c.i2c_bus.readfrom", + side_effect=[[status], coord_data], + ) + + data = touch.current_point_validate() + assert data == expected + + +def test_point_read_error(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911 + + touch = GT911() + + # Mock time to ensure we're within activity threshold + mocker.patch("time.ticks_ms", return_value=1000) + touch.last_touch_time = 950 + + # Simulate error + e = Exception("mocked error") + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("krux.i2c.i2c_bus.readfrom", side_effect=e) + + data = touch.current_point() + assert data is None + + +def test_point_outside_threshold(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911, ACTIVITY_THRESHOLD + + touch = GT911() + + # Mock time to ensure we're outside activity threshold + mocker.patch("time.ticks_ms", return_value=1000) + touch.last_touch_time = 1000 - ACTIVITY_THRESHOLD - 10 # Outside threshold + + data = touch.current_point() + assert data is None + + +def test_activate(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911, fm + + mock_fm_register = mocker.patch.object(fm, "register") + mock_gpio = mocker.MagicMock() + mocker.patch("Maix.GPIO", return_value=mock_gpio) + mocker.patch("time.sleep_ms") + + touch = GT911() + test_irq_pin = 21 + test_reset_pin = 22 + + # Before activation + assert touch.irq_pin is None + assert touch.reset_pin is None + + touch.activate(test_irq_pin, test_reset_pin) + + # After activation, verify pins were registered + assert mock_fm_register.call_count == 2 + calls = mock_fm_register.call_args_list + assert calls[0][0] == (test_irq_pin, fm.fpioa.GPIOHS1) + assert calls[1][0] == (test_reset_pin, fm.fpioa.GPIOHS2) + + # Verify pins were configured + assert touch.irq_pin is not None + assert touch.reset_pin is not None + + +def test_threshold(mocker, wonder_k): + from krux.touchscreens.gt911 import GT911, GT911_CONFIG_DATA + + mock_writeto = mocker.patch("krux.i2c.i2c_bus.writeto") + + touch = GT911() + test_threshold = 50 + + touch.threshold(test_threshold) + + # Verify write_reg was called with correct threshold value + calls = mock_writeto.call_args_list + # Find the call that sets our test threshold + # GT911 uses 16-bit addresses, so payload is [addr_high, addr_low, value] + threshold_calls = [ + call + for call in calls + if len(call[0]) >= 2 + and len(call[0][1]) >= 3 + and call[0][1][0] == (GT911_CONFIG_DATA >> 8) + and call[0][1][1] == (GT911_CONFIG_DATA & 0xFF) + and call[0][1][2] == test_threshold + ] + assert len(threshold_calls) >= 1 + + +@pytest.mark.parametrize( + "x_coord,y_coord", + [ + (0x0000, 0x0000), # Min coordinates + (0x0064, 0x00C8), # Mid coordinates: (100, 200) + (0x01E0, 0x0320), # Large coordinates: (480, 800) + (0x00FF, 0x00FF), # Equal X,Y: (255, 255) + ], +) +def test_coordinate_reading(mocker, wonder_k, x_coord, y_coord): + """Test coordinate extraction from touch data""" + from krux.touchscreens.gt911 import GT911 + + touch = GT911() + + coord_data = [ + 0x00, + x_coord & 0xFF, + (x_coord >> 8) & 0xFF, + y_coord & 0xFF, + (y_coord >> 8) & 0xFF, + 0x00, + 0x00, + 0x00, + ] + + mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch( + "krux.i2c.i2c_bus.readfrom", + side_effect=[[0x81], coord_data], + ) + + data = touch.current_point_validate() + assert data == (x_coord, y_coord) + + +def test_trigger_event_clears_irq_flag(mocker, wonder_k): + """Verify that trigger_event clears the interrupt flag""" + from krux.touchscreens.gt911 import GT911, ACTIVITY_THRESHOLD, GT911_COORD_ADDR + + mock_writeto = mocker.patch("krux.i2c.i2c_bus.writeto") + mocker.patch("time.ticks_ms", return_value=ACTIVITY_THRESHOLD + 100) + + touch = GT911() + + # Mock valid touch data + mocker.patch( + "krux.i2c.i2c_bus.readfrom", + side_effect=[[0x81], [0x00, 0x64, 0x00, 0xC8, 0x00, 0x00, 0x00, 0x00]], + ) + + touch.trigger_event() + + # Verify interrupt flag was cleared + # Should write 0x00 to GT911_COORD_ADDR + clear_calls = [ + call + for call in mock_writeto.call_args_list + if len(call[0]) >= 2 + and len(call[0][1]) >= 3 + and call[0][1][0] == (GT911_COORD_ADDR >> 8) + and call[0][1][1] == (GT911_COORD_ADDR & 0xFF) + and call[0][1][2] == 0x00 + ] + assert len(clear_calls) >= 1 diff --git a/vendor/embit b/vendor/embit index ed2aa4e1f..8d7591229 160000 --- a/vendor/embit +++ b/vendor/embit @@ -1 +1 @@ -Subproject commit ed2aa4e1faab90b781a2af8453f3aa671f5fcfd6 +Subproject commit 8d7591229bd34de09aff3986b476bc99fe2b45aa