diff --git a/CHANGELOG.md b/CHANGELOG.md index 9365a790b..4c6548b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +# Changelog 24.04.beta8 - November 20, 2023 + +## Changes + +### IRQ Interfaces +Button and touch presses are now detected by the application through IO interrupts. Meaning inputs events will be registered and handled even if they happened when other tasks were being executed by the processor, resulting in a better UX. + +### Save and Load Wallet Output Descriptor from SD card +Create or load from a wallet output descriptor file on a SD card. The backup file format is compatible with most coordinators. + +### Restore Default Settings +Option to restore the device's settings to its factory state. + +### Wipe Device +Option on settings to wipe the device, permanently removing settings and stored encrypted mnemonics by erasing every single bit of user's flash space. + +### Screensaver +Optional screensaver to reduce pixels' burn-in and grab attention of the user when the device is left powered on. + +### Maix Dock Simulator +Now Krux PC simulator can also run in Maix Dock mode, mimetizing appearance and characteristics of the most DIY Krux device + +### Update Embit to version 0.7 +Use latest Embit release + +### Optimized QR codes +QR codes rendering is faster and uses less RAM + +### Mnemonic Numbers +To match the input options, export mnemonics as decimal, hexadecimal or octal numbers +When loading from numbers, a new numbers confirmation screen was added. + +### Addresses Exploring +More receive and change addresses per page are shown on bigger screens + +### Export QR Codes as Images to SD Card +Some QR codes can be exported as images to SD card + +### Optimized Settings Storage +Device's storage is now used more efficiently, data is stored less frequently, only in case a setting is changed from defaults. + +### Other Small Fixes and Code Optimizations +Many other small fixes and optimizations under the hood + + # Version 23.09.1 - November 18, 2023 This release contain bugfixes: diff --git a/README.md b/README.md index 5d543fa10..480795748 100644 --- a/README.md +++ b/README.md @@ -66,25 +66,25 @@ Note that you can run `poetry install` after making a change to the krux code if ## Format code ```bash -poetry run black ./src -poetry run black ./tests +poetry run poe format ``` ## Run pylint ```bash -poetry run pylint ./src +poetry run poe lint ``` ## Run tests ```bash -poetry run pytest --cache-clear --cov src/krux --cov-report html ./tests +poetry run poe test ``` + This will run all tests and generate a coverage report you can browse to locally in your browser at `file:///path/to/krux/htmlcov/index.html`. For more verbose test output (e.g., to see the output of print statements), run: ```bash -poetry run pytest --cache-clear --cov src/krux --cov-report html --show-capture all --capture tee-sys -r A ./tests +poetry run poe test-verbose ``` To run just a specific test from a specific file, run: diff --git a/docs/faq.en.md b/docs/faq.en.md index 7a1af5e9a..20ae002b7 100644 --- a/docs/faq.en.md +++ b/docs/faq.en.md @@ -72,4 +72,7 @@ If after flashing `maixpy_amigo_tft` to your device you notice that the buttons Starting from version 23.09.0, Krux supports SD card hot plugging. If you are using older versions, it may only detect the SD card at boot, so make sure Krux is turned off when inserting the microSD into it. To test the card compatibility use Krux [Tools>Check SD Card](getting-started/features/tools.md/#check-sd-card). Make sure the SD card is using MBR/DOS partition table and FAT32 format. -Here is some [supported microSD cards](https://github.com/m5stack/m5-docs/blob/master/docs/en/core/m5stickv.md#tf-cardmicrosd-test), and here is the MaixPy FAQ explaining [Why my micro SD card cannot be read](https://wiki.sipeed.com/soft/maixpy/en/others/maixpy_faq.html#Micro-SD-card-cannot-be-read). \ No newline at end of file +Here is some [supported microSD cards](https://github.com/m5stack/m5-docs/blob/master/docs/en/core/m5stickv.md#tf-cardmicrosd-test), and here is the MaixPy FAQ explaining [Why my micro SD card cannot be read](https://wiki.sipeed.com/soft/maixpy/en/others/maixpy_faq.html#Micro-SD-card-cannot-be-read). + +## Why insert an SD card into my device? What is it for? Does it save something? +SD card use is optional, most people use Krux only with QR codes. But you can use SD card to to upgrade the firmware, save settings, cnc/file, QR codes, XPUBs, encrypted mnemonics, and to save and load PSBTs, messages and wallet output descriptors. \ No newline at end of file diff --git a/docs/getting-started/usage/generating-a-mnemonic.en.md b/docs/getting-started/usage/generating-a-mnemonic.en.md index 533338ed1..1d5cdbd16 100644 --- a/docs/getting-started/usage/generating-a-mnemonic.en.md +++ b/docs/getting-started/usage/generating-a-mnemonic.en.md @@ -53,8 +53,13 @@ Note: For 12-word mnemonics, only the first half of the SHA256 hash is used (128
+### How to verify + +Don't trust, verify. We encourage you not to trust any claim you cannot verify yourself. Therefore, there are wallets that use compatible algorithms to calculate the entropy derived from dice rolls. You can use the [SeedSigner](https://seedsigner.com/) or Coldcard hardware wallets, or even the [Bitcoiner Guide website](https://bitcoiner.guide/seed/), they share the same logic that Krux uses and will give the same mnemonic. + ## Alternatives -See [here](https://vault12.com/securemycrypto/cryptocurrency-security-how-to/seed-phrase-creation/) for good methods to generate a mnemonic manually, or visit [Ian Coleman's BIP-39 Tool](https://iancoleman.io/bip39/) offline or on an airgapped device to generate one automatically. -It's worth noting that Ian's tool is able to take a mnemonic and generate a QR code that Krux can read in via the QR input method mentioned on the next page. +You can use any other offline airgapped devices to generate your mnemonic. If you want to use a regular PC, a common strategy is to boot the PC using [Tails](https://tails.boum.org/) from a USB stick, without connecting the device to the internet, and then use a copy of the the [Bitcoiner Guide website](https://bitcoiner.guide/seed/) or even [Ian Coleman's BIP-39 Tool](https://iancoleman.io/bip39/). It's worth noting that both generate a QR code that Krux can read via the QR input method mentioned on the next page (Loading a Mnemonic). + +See [here](https://vault12.com/securemycrypto/cryptocurrency-security-how-to/seed-phrase-creation/) for other good methods to generate a mnemonic manually. 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 8139f326c..ae66608d2 100644 --- a/docs/getting-started/usage/navigating-the-main-menu.en.md +++ b/docs/getting-started/usage/navigating-the-main-menu.en.md @@ -62,7 +62,7 @@ The encrypted mnemonic will be converted to a QR code. When you scan this QR cod -This option displays your master extended public key (xpub) as text as well as a QR code. +This option displays your master extended public key (xpub) as text as well as a QR code. The extended public key (xpub) can also be stored on a SD card if available. After the xpub, a zpub or Zpub is shown depending on if a single-sig or multisig wallet was chosen. This z/Zpub is usually not necessary unless you are using a wallet coordinator that either cannot parse or ignores [key origin information in key expressions](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#Key_Expressions). diff --git a/firmware/MaixPy b/firmware/MaixPy index 198dac960..4cdf15f1a 160000 --- a/firmware/MaixPy +++ b/firmware/MaixPy @@ -1 +1 @@ -Subproject commit 198dac9608bad51affdde0d3bf48e94db678d8d7 +Subproject commit 4cdf15f1a4d7cb496adce5da7314623f552a75a1 diff --git a/firmware/font/README.md b/firmware/font/README.md index 45a7c44d0..a7b575d95 100644 --- a/firmware/font/README.md +++ b/firmware/font/README.md @@ -1,14 +1,14 @@ # Updating the built-in font -Krux uses a [custom fork](https://github.com/bachan/terminus-font-vietnamese) of the [Terminus](http://terminus-font.sourceforge.net/) font for its glyphs that includes Vietnamese characters, the Bitcoin currency symbol (β‚Ώ) and the PokΓ©mon Dollar symbol (β‚½ with 2 horizontal lines). +Krux uses a [custom fork](https://github.com/bachan/terminus-font-vietnamese) of the [Terminus](http://terminus-font.sourceforge.net/) font for its glyphs that includes Vietnamese characters, the Bitcoin currency symbol (β‚Ώ) and custom icons. To rebuild the font for all devices, run: ``` ./bdftokff.sh ter-u14n 8 14 > m5stickv.kff -./bdftokff.sh ter-u16n 8 16 > bit_dock.kff +./bdftokff.sh ter-u16n 8 16 > bit_dock_yahboom.kff ./bdftokff.sh ter-u24b 12 24 > amigo.kff ``` -Once you have a `.kff` file, locate the project that you want to use the updated font under `firmware/MaixPy/projects/` (`maixpy_amigo_tft/` for ex.), open its `compile/overrides/components/micropython/port/src/omv/img/font.c` file and replace the array contents in the `unicode` variable with the byte array found within the `.kff` file, then rebuild the firmware. +Once you have `.kff` files, for each project that you want to use the updated fonts, edit `../MaixPy/projects/*/compile/overrides/components/micropython/port/src/omv/img/font.c` (substituting `maixpy_amigo_tft` for `*` if only for an amigo) and replace the array contents in the `unicode` variable with the byte array found within the appropriate `.kff` file, then rebuild the firmware. # How it works Krux uses bitmap fonts that are custom-built for each device it runs on. The format that the firmware expects fonts to be in is a custom format referred to as "krux font format," or `.kff`. diff --git a/firmware/font/font_icons/Derivation_amigo.pbm b/firmware/font/font_icons/Derivation_amigo.pbm new file mode 100644 index 000000000..0f4a1c22e Binary files /dev/null and b/firmware/font/font_icons/Derivation_amigo.pbm differ diff --git a/firmware/font/font_icons/Derivation_bit_dock_yahboom.pbm b/firmware/font/font_icons/Derivation_bit_dock_yahboom.pbm new file mode 100644 index 000000000..7d5d3e932 --- /dev/null +++ b/firmware/font/font_icons/Derivation_bit_dock_yahboom.pbm @@ -0,0 +1,4 @@ +P4 +# Created by GIMP version 2.10.34 PNM plug-in +8 16 +ππππ`dffd`df \ No newline at end of file diff --git a/firmware/font/font_icons/Derivation_m5stickv.pbm b/firmware/font/font_icons/Derivation_m5stickv.pbm new file mode 100644 index 000000000..1fffa8a52 --- /dev/null +++ b/firmware/font/font_icons/Derivation_m5stickv.pbm @@ -0,0 +1,4 @@ +P4 +# Created by GIMP version 2.10.34 PNM plug-in +8 14 +πππτffd`df \ No newline at end of file diff --git a/firmware/font/font_icons/Fingerprint_amigo.pbm b/firmware/font/font_icons/Fingerprint_amigo.pbm new file mode 100644 index 000000000..b8c4a6716 Binary files /dev/null and b/firmware/font/font_icons/Fingerprint_amigo.pbm differ diff --git a/firmware/font/font_icons/Fingerprint_bit_dock_yahboom.pbm b/firmware/font/font_icons/Fingerprint_bit_dock_yahboom.pbm new file mode 100644 index 000000000..4b4c01a7f Binary files /dev/null and b/firmware/font/font_icons/Fingerprint_bit_dock_yahboom.pbm differ diff --git a/firmware/font/font_icons/Fingerprint_m5stickv.pbm b/firmware/font/font_icons/Fingerprint_m5stickv.pbm new file mode 100644 index 000000000..681d08ffb --- /dev/null +++ b/firmware/font/font_icons/Fingerprint_m5stickv.pbm @@ -0,0 +1,4 @@ +P4 +# Created by GIMP version 2.10.34 PNM plug-in +8 14 +> 3) << 11 + + # 6 significant bits of green shifted to middle + green = (rgb[1] >> 2) << 5 + + # 5 significant bits of blue on the right + blue = rgb[2] >> 3 + + return int(red + green + blue).to_bytes(2, "big" if big else "little") + + +def rgb16_to_rgb24(rgb, big=False): + """ + convert 2 bytes of rgb565 into 3 bytes of rgb888 + default from little-endian, so rgb565 becomes gbrg3553 + """ + def maxv(number_of_bits): + return (2 ** number_of_bits) -1 + + assert type(rgb) == bytes and len(rgb) == 2 + + rgb_int = int.from_bytes(rgb, "big" if big else "little") + + # left 5 bits of red multiplied to fill 8 bit space + red = round((rgb_int >> 11) * maxv(8) / maxv(5)) + + # middle 6 bits of green multiplied to fill 8 bit space + green = round(((rgb_int & maxv(11)) >> 5) * maxv(8) / maxv(6)) + + # right 5 bits of blue multiplied to fill 8 bit space + blue = round((rgb_int & maxv(5)) * maxv(8) / maxv(5)) + + return b''.join([x.to_bytes(1, "big") for x in [red, green, blue]]) + + +def main(*args): + rgb = None + + if len(args) == 1: + arg = args[0] + if arg[:2] == "0x": + rgb = unhexlify(arg[2:]) + else: + rgb = (abs(int(args[0])) % 65536).to_bytes(2, 'big') + else: + rgb = b''.join([(abs(int(x)) % 256).to_bytes(1, 'big') for x in args]) + + if rgb: + if len(rgb) == 2: + answer = rgb16_to_rgb24(rgb) + elif len(rgb) == 3: + answer = rgb24_to_rgb16(rgb) + else: + answer = None + + return(answer) + + +if __name__ == '__main__': + import sys + + def help(): + print("syntax: {} integer-bytes-or-0xhex-color".format(sys.argv[0])) + + if len(sys.argv) > 1: + try: + print('0x' + main(*sys.argv[1:]).hex()) + except: + help() + exit(1) + else: + help() + exit(1) diff --git a/i18n/i18n.py b/i18n/i18n.py index 7e24941ec..c01c1bc5f 100644 --- a/i18n/i18n.py +++ b/i18n/i18n.py @@ -87,14 +87,13 @@ def validate_translation_files(): def fill_missing(): - """Uses googletrans==4.0.0rc1 to automaticalyy fill missing translations""" + """Uses translate 3.6.1 to automaticalyy fill missing translations""" if len(sys.argv) > 2: force_target = sys.argv[2] else: force_target = None - from googletrans import Translator + from translate import Translator - translator = Translator() slugs = find_translation_slugs() translation_filenames = [ f @@ -102,12 +101,11 @@ def fill_missing(): if isfile(join(TRANSLATION_FILES_DIR, f)) ] for translation_filename in translation_filenames: - target = translation_filename[:5].replace("-", "_") + target = translation_filename[:5] if force_target: if force_target != translation_filename: continue - if translation_filename.startswith("poke"): - continue + translator = Translator(to_lang=target) print("Translating %s...\n" % translation_filename) complete = True with open( @@ -117,21 +115,17 @@ def fill_missing(): for slug in slugs: if slug not in translations or translations[slug] == "": try: - translated = '"%s",' % translator.translate( - slug, src="en", dest=target - ).text.replace(" \ n", "\\n") - except: - # some languages fail to translate with a space at end of string - translated = ( - '"%s",' - % translator.translate( - slug + "]", src="en", dest=target - ).text.replace(" \ n", "\\n")[:-1] + translated = '"%s",' % translator.translate(slug).replace( + " \ n", "\\n" ) - print('"%s":' % slug, translated) + print('"%s":' % slug, translated) + except Exception as e: + print("Error:", e) + print("Failed to translate:", slug) + break complete = False if complete: - print("Notthing to add") + print("Nothing to add") else: print("Please review and copy items above") print("\n\n") diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index a43f322a8..1305e33ed 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -19,7 +19,6 @@ "Anti-glare disabled": "Blendschutz deaktiviert", "Anti-glare enabled": "Blendschutz aktiviert", "Are you sure?": "Bist Du sicher?", - "BIP39 Mnemonic": "BIP39 Mnemonic", "Back": "ZurΓΌck", "Backing up bootloader..\n\n%d%%": "Bootloader wird gesichert..\n\n%d%%", "Bad signature": "UngΓΌltige Signatur", @@ -49,6 +48,7 @@ "Cut Method": "Cut-Methode", "Decimal": "Dezimal", "Decrypt?": "EntschlΓΌsseln?", + "Delete %s?": "LΓΆschen %s?", "Delete File?": "Datei lΓΆschen?", "Delete Mnemonic": "Mnemonic lΓΆschen", "Depth Per Pass": "Tiefe pro Durchgang", @@ -94,11 +94,10 @@ "GRBL": "GRBL", "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Dieser Mnemonic eine benutzerdefinierte ID zuteilen? Andernfalls wird der aktuelle Fingerabdruck verwendet", "Go": "Go", - "Heat Interval": "WΓ€rmeintervall", - "Heat Time": "Hitzezeit", "Hex Public Key": "Hex ΓΆffentlicher SchlΓΌssel", "Hexadecimal": "Hexadezimal", "ID already exists\n": "ID existiert bereits\n", + "Incomplete output descriptor": "UnvollstΓ€ndiger Ausgabedeskriptor", "Inputs (%d): ": "Input (%d): ", "Invalid address": "UngΓΌltige Adresse", "Invalid bootloader": "UngΓΌltiger Bootloader", @@ -199,6 +198,7 @@ "Signed PSBT": "Signierte PSBT", "Single-sig": "Single-Sig", "Size: ": "Grâße: ", + "Some checks cannot be performed.": "Einige Schecks kΓΆnnen nicht durchgefΓΌhrt werden.", "Spend (%d): ": "Ausgabe (%d): ", "Stackbit 1248": "Stackbit 1248", "Store on Flash": "Auf Flash speichern", @@ -235,7 +235,7 @@ "Wallet output descriptor": "Wallet Ausgabedeskriptor", "Wallet output descriptor loaded!": "Wallet Ausgabedeskriptor geladen!", "Wallet output descriptor not found.": "Wallet Ausgabedeskriptor nicht gefunden.", - "Warning:\nIncomplete output descriptor": "Warnung:\nUnvollstΓ€ndiger Ausgabedeskriptor", + "Warning:": "Warnung:", "Word %d": "Wort %d", "Word Numbers": "Wortnummern", "Words": "WΓΆrter", diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index 7e9ac20eb..dbff6bd65 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -19,7 +19,6 @@ "Anti-glare disabled": "Antideslumbrante desactivado", "Anti-glare enabled": "Antideslumbrante habilitado", "Are you sure?": "ΒΏEstas seguro?", - "BIP39 Mnemonic": "BIP39 MnemΓ³nico", "Back": "AtrΓ‘s", "Backing up bootloader..\n\n%d%%": "Copia de seguridad del cargador de arranque..\n\n%d%%", "Bad signature": "Mala asignatura", @@ -49,6 +48,7 @@ "Cut Method": "MΓ©todo de corte", "Decimal": "Decimal", "Decrypt?": "Descifrar?", + "Delete %s?": "Eliminar %s?", "Delete File?": "ΒΏBorrar archivo?", "Delete Mnemonic": "Eliminar mnemΓ³nico", "Depth Per Pass": "Profundidad por pasada", @@ -94,11 +94,10 @@ "GRBL": "GRBL", "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "ΒΏDarle a este mnemΓ³nico una identificaciΓ³n personalizada?De lo contrario se utilizarΓ‘ la huella digital actual", "Go": "Ir", - "Heat Interval": "Intervalo de calor", - "Heat Time": "Tiempo de calor", "Hex Public Key": "Clave pΓΊblica hexadecimal", "Hexadecimal": "Hexadecimal", "ID already exists\n": "ID ya existe\n", + "Incomplete output descriptor": "Descriptor incompleto", "Inputs (%d): ": "Entradas (%d): ", "Invalid address": "DirecciΓ³n invΓ‘lida", "Invalid bootloader": "Cargador de arranque invΓ‘lido", @@ -199,6 +198,7 @@ "Signed PSBT": "PSBT firmado", "Single-sig": "Single-sig", "Size: ": "Espacio: ", + "Some checks cannot be performed.": "No se pueden realizar algunos cheques.", "Spend (%d): ": "Gastos (%d): ", "Stackbit 1248": "Stackbit 1248", "Store on Flash": "Almacenar en flash", @@ -235,7 +235,7 @@ "Wallet output descriptor": "Descriptor de salida de billetera", "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.", - "Warning:\nIncomplete output descriptor": "Advertencia:\nDescriptor de salida incompleto", + "Warning:": "Advertencia:", "Word %d": "Palabra %d", "Word Numbers": "NΓΊmeros de palabra", "Words": "Palabras", diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index 95da68cb8..3f2ef4339 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -19,7 +19,6 @@ "Anti-glare disabled": "Anti-Γ©blouissement dΓ©sactivΓ©", "Anti-glare enabled": "Anti-Γ©blouissement activΓ©", "Are you sure?": "Es-tu sΓ»r?", - "BIP39 Mnemonic": "BIP39 MnΓ©monique", "Back": "Retour", "Backing up bootloader..\n\n%d%%": "Sauvegarde du chargeur de dΓ©marrage..\n\n%d%%", "Bad signature": "Mauvaise signature", @@ -49,6 +48,7 @@ "Cut Method": "MΓ©thode de coupe", "Decimal": "DΓ©cimal", "Decrypt?": "DΓ©crypter?", + "Delete %s?": "Supprimer %s?", "Delete File?": "Supprimer le fichier?", "Delete Mnemonic": "Supprimer mnΓ©monique", "Depth Per Pass": "Profondeur par passage", @@ -94,11 +94,10 @@ "GRBL": "GRBL", "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Donnez Γ  ce mnΓ©monique un identifiant personnalisΓ©?Sinon l'empreinte actuelle sera utilisΓ©e", "Go": "Go", - "Heat Interval": "Intervalle de chauffe", - "Heat Time": "Temps de chauffe", "Hex Public Key": "ClΓ© public hexadΓ©cimal", "Hexadecimal": "HexadΓ©cimal", "ID already exists\n": "Id existe dΓ©jΓ \n", + "Incomplete output descriptor": "Descripteur de sortie incomplet", "Inputs (%d): ": "EntrΓ©es (%d) : ", "Invalid address": "Adresse invalide", "Invalid bootloader": "Chargeur de dΓ©marrage invalide", @@ -199,6 +198,7 @@ "Signed PSBT": "PSBT signΓ©", "Single-sig": "ClΓ© unique", "Size: ": "CapacitΓ©: ", + "Some checks cannot be performed.": "Certains chΓ¨ques ne peuvent pas Γͺtre effectuΓ©s.", "Spend (%d): ": "DΓ©pense (%d) : ", "Stackbit 1248": "Stackbit 1248", "Store on Flash": "Stocker sur flash", @@ -225,7 +225,7 @@ "Use camera's entropy to create a new mnemonic": "Utilisez l'entropie de la camΓ©ra pour crΓ©er un nouveau mnΓ©monique", "Used: ": "UtilisΓ©: ", "Value %s out of range: [%s, %s]": "Valeur %s hors de portΓ©e: [%s, %s]", - "Value must be multiple of %s": "La valeur doit Γͺtre multiple de %s", + "Value must be multiple of %s": "La valeur doit Γͺtre un multiple de %s", "Via Camera": "Par camΓ©ra", "Via D20": "Via D20", "Via D6": "Via D6", @@ -235,7 +235,7 @@ "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.", - "Warning:\nIncomplete output descriptor": "Attention:\nDescripteur de sortie incomplet", + "Warning:": "Avertissement:", "Word %d": "Mot %d", "Word Numbers": "NumΓ©ros de mots", "Words": "Mots", diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json index 9ec906282..112100180 100644 --- a/i18n/translations/nl-NL.json +++ b/i18n/translations/nl-NL.json @@ -19,7 +19,6 @@ "Anti-glare disabled": "Anti reflecterend uitgeschakeld", "Anti-glare enabled": "Anti reflecterend ingeschakeld", "Are you sure?": "Weet je het zeker?", - "BIP39 Mnemonic": "BIP-39 geheugensteun", "Back": "Terug", "Backing up bootloader..\n\n%d%%": "Backup van de bootloader...\n\n%d%%", "Bad signature": "Ongeldige handtekening", @@ -49,6 +48,7 @@ "Cut Method": "Snijmethode", "Decimal": "Decimaal", "Decrypt?": "Ontsleutelen?", + "Delete %s?": "Verwijderen %s?", "Delete File?": "Bestand verwijderen?", "Delete Mnemonic": "Geheugensteun verwijderen", "Depth Per Pass": "Diepte per pas", @@ -94,11 +94,10 @@ "GRBL": "GRBL", "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Eigen ID gebruiken voor geheugensteun? Anders vingerafdruk gebruiken", "Go": "Ga", - "Heat Interval": "Warmte interval", - "Heat Time": "Warmte tijd", "Hex Public Key": "Hex publieke sleutel", "Hexadecimal": "Hexadecimaal", "ID already exists\n": "ID bestaat al\n", + "Incomplete output descriptor": "Incomplete portemonnee descriptor", "Inputs (%d): ": "Invoer (%d): ", "Invalid address": "Ongeldig adres", "Invalid bootloader": "Ongeldige bootloader", @@ -199,6 +198,7 @@ "Signed PSBT": "PSBT ondertekend", "Single-sig": "Enkele sleutel", "Size: ": "Grootte: ", + "Some checks cannot be performed.": "Sommige controles kunnen niet worden uitgevoerd.", "Spend (%d): ": "Uitgaven (%d): ", "Stackbit 1248": "Stackbit 1248", "Store on Flash": "Opslaan op apparaat", @@ -235,7 +235,7 @@ "Wallet output descriptor": "Portemonnee descriptor", "Wallet output descriptor loaded!": "Portemonnee descriptor geladen!", "Wallet output descriptor not found.": "Portemonnee descriptor niet gevonden.", - "Warning:\nIncomplete output descriptor": "Waarschuwing:\nIncomplete portemonnee descriptor", + "Warning:": "Waarschuwing:", "Word %d": "Woord %d", "Word Numbers": "Woord nummers", "Words": "Woorden", diff --git a/i18n/translations/pl-PL.json b/i18n/translations/pl-PL.json new file mode 100644 index 000000000..45f270a75 --- /dev/null +++ b/i18n/translations/pl-PL.json @@ -0,0 +1,244 @@ +{ + "%d of %d multisig": "%d z %d multisig", + "%d. Change: \n\n%s\n\n": "%D.Zmiana:\n\n%s\n\n", + "%d. Self-transfer: \n\n%s\n\n": "%D.Samo-transfer:\n\n%s\n\n", + "%d. Spend: \n\n%s\n\n": "%D.WydaΔ‡:\n\n%s\n\n", + "%s\n\nis a valid change address!": "%s\n\nis prawidΕ‚owy adres zmiany!", + "%s\n\nis a valid receive address!": "%s\n\nis waΕΌny adres odbierania!", + "%s\n\nwas NOT FOUND in the first %d change addresses": "%s\n\nwas nie znaleziono w pierwszych adresach zmiany", + "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nwas nie znaleziono w pierwszych %D Otrzymuj adresy", + "(Experimental)": "(Eksperymentalny)", + "12 words": "12 sΕ‚Γ³w", + "24 words": "24 sΕ‚owa", + "ABC": "ABC", + "About": "O", + "Adafruit": "Adafruit", + "Address": "Adres", + "Aditional entropy from camera required for AES-CBC mode": "Dodatkowa entropia z kamery wymaganej dla trybu AES-CBC", + "Align camera and Tiny Seed properly.": "WΕ‚aΕ›ciwie wyrΓ³wnaj kamerΔ™ i maleΕ„kie nasiona.", + "Anti-glare disabled": "NiepeΕ‚nosprawne przeciwblokowane", + "Anti-glare enabled": "WΕ‚Δ…czona anty-zabawa", + "Are you sure?": "JesteΕ› pewny?", + "Back": "Z powrotem", + "Backing up bootloader..\n\n%d%%": "Kopie zapasowe bootloader ..\n\n%d %%", + "Bad signature": "zΕ‚y podpis", + "Baudrate": "Baudrate", + "Bitcoin": "Bitcoin", + "Border Padding": "WyΕ›ciΓ³Ε‚ka graniczna", + "CNC": "CNC", + "Change": "Zmiana", + "Change Addresses": "ZmieΕ„ adresy", + "Change theme and reboot?": "ZmieniΔ‡ motyw i ponownie uruchomiΔ‡?", + "Changes persisted to SD card!": "Zmiany utrzymywaΕ‚y siΔ™ na karcie SD!", + "Changes will last until shutdown.": "Zmiany bΔ™dΔ… trwaΔ‡ do zamkniΔ™cia.", + "Check SD Card": "SprawdΕΊ kartΔ™ SD", + "Check that address belongs to this wallet?": "SprawdΕΊ, czy adres naleΕΌy do tego portfela?", + "Checked %d change addresses with no matches.": "Sprawdzone %d adresy zmiany bez dopasowaΕ„.", + "Checked %d receive addresses with no matches.": "Sprawdzone %D Otrzymuj adresy bez zapaΕ‚ek.", + "Checking change address %d for match..": "Sprawdzanie Zmieniania Adres %D dla dopasowania.", + "Checking for SD card..": "Sprawdzanie karty SD ..", + "Checking receive address %d for match..": "Sprawdzanie adresu Otrzymaj %D dla meczu.", + "Compact SeedQR": "Kompaktowy seedqr", + "Continue?": "KontynuowaΔ‡?", + "Create QR Code": "UtwΓ³rz kod QR", + "Create QR code from text?": "UtwΓ³rz kod QR z tekstu?", + "Created: ": "Utworzony:", + "Custom QR Code": "Niestandardowy kod QR", + "Cut Depth": "Wytnij gΕ‚Δ™bokoΕ›Δ‡", + "Cut Method": "Metoda ciΔ™cia", + "Decimal": "DziesiΔ™tny", + "Decrypt?": "OdszyfrowaΔ‡?", + "Delete %s?": "UsuΕ„ %s?", + "Delete File?": "UsunΔ…Δ‡ plik?", + "Delete Mnemonic": "UsuΕ„ mnemonic", + "Depth Per Pass": "GΕ‚Δ™bokoΕ›Δ‡ na przepustkΔ™", + "Derivation: %s": "Pochodzenie: %s", + "Device flash storage not detected.": "Nie wykryto pamiΔ™ci flash urzΔ…dzenia.", + "Done?": "Zrobione?", + "Driver": "Kierowca", + "Encoder": "Enkoder", + "Encoder Debounce": "Encoder Debunet", + "Encrypt Mnemonic": "Szyfrowanie mnemoniki", + "Encrypted QR Code": "Zaszyfrowany kod QR", + "Encrypted mnemonic was not stored": "Szyfrowana mnemonika nie byΕ‚a przechowywana", + "Encrypted mnemonic was stored with ID: ": "Szyfrowana mnemonika byΕ‚a przechowywana z ID:", + "Encryption": "Szyfrowanie", + "Encryption Mode": "Tryb szyfrowania", + "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39 jako liczbΔ™ od 1 do 2048 r.", + "Enter each word of your BIP-39 mnemonic as a number in hexadecimal from 1 to 800.": "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39 jako liczbΔ™ w heksadecimal od 1 do 800.", + "Enter each word of your BIP-39 mnemonic as a number in octal from 1 to 4000.": "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39 jako liczbΔ™ w wysokoΕ›ci od 1 do 4000.", + "Enter each word of your BIP-39 mnemonic.": "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39.", + "Error:\n%s": "BΕ‚Δ…d:\n%s", + "Esc": "wyjΕ›cie", + "Explore files?": "EksplorowaΔ‡ pliki?", + "Exporting to SD card..": "Eksportowanie do karty SD ..", + "Extended Public Key": "Rozszerzony klucz publiczny", + "Failed to decrypt": "Nie udaΕ‚o siΔ™ odszyfrowaΔ‡", + "Failed to load PSBT": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ PSBT", + "Failed to load address": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ adresu", + "Failed to load key": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ klucza", + "Failed to load message": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ wiadomoΕ›ci", + "Failed to load mnemonic": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ mnemonika", + "Failed to load output descriptor": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ deskryptora wyjΕ›ciowego", + "Failed to load passphrase": "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ pseudonim", + "Failed to store mnemonic": "Nie udaΕ‚o siΔ™ przechowywaΔ‡ mnemonika", + "Fee: ": "OpΕ‚ata:", + "Feed Rate": "SzybkoΕ›Δ‡ pasz", + "Filename": "Nazwa pliku", + "Filename %s exists on SD card, overwrite?": "Nazwa pliku %s istnieje na karcie SD, zastΔ…p?", + "Fingerprint: %s": "Odcisk palca: %s", + "Firmware exceeds max size: %d": "Oprogramowanie ukΕ‚adowe przekracza maksymalny rozmiar: %D", + "Flute Diameter": "Średnica fletu", + "Free: ": "BezpΕ‚atny:", + "From Storage": "Z przechowywania", + "GRBL": "Grbl", + "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Podaj temu mnemonicznemu identyfikatorowi?W przeciwnym razie zostanie uΕΌyty obecny odcisk palca", + "Go": "IΕ›Δ‡", + "Hex Public Key": "Hex Key Public", + "Hexadecimal": "Szesnastkowy", + "ID already exists\n": "ID juΕΌ istnieje\n", + "Incomplete output descriptor": "inComplete Descriptor", + "Inputs (%d): ": "WejΕ›cia (%d):", + "Invalid address": "BΕ‚Δ™dny adres", + "Invalid bootloader": "NieprawidΕ‚owy bootloader", + "Invalid mnemonic length": "NieprawidΕ‚owa dΕ‚ugoΕ›Δ‡ mnemoniczna", + "Invalid public key": "NieprawidΕ‚owy klucz publiczny", + "Invalid wallet:\n%s": "NieprawidΕ‚owy portfel:\n%s", + "Invert": "OdwracaΔ‡", + "Key": "Klucz", + "Key: ": "Klucz:", + "Krux\n\n\nVersion\n%s": "Krux\n\n\neversion\n%s", + "Krux Printer Test QR": "Test drukarki Krux QR", + "Language": "JΔ™zyk", + "Line Delay": "OpΓ³ΕΊnienie linii", + "Line: ": "Linia:", + "Load Mnemonic": "Ładuj mnemoniczne", + "Load from SD card?": "ZaΕ‚aduj z karty SD?", + "Load one?": "ZaΕ‚aduj jeden?", + "Load?": "ObciΔ…ΕΌenie?", + "Loading Camera..": "Ładowanie aparatu ..", + "Loading change address %d..": "Ładowanie ZMIANY Adres %D ..", + "Loading printer..": "Ładowanie drukarki ..", + "Loading receive address %d..": "Ładowanie adresu odbierania %d ..", + "Loading..": "Ładowanie..", + "Locale": "Widownia", + "Location": "Lokalizacja", + "Log Level": "Poziom dziennika", + "Logging": "Logowanie", + "Maximum length exceeded (%s)": "Maksymalna dΕ‚ugoΕ›Δ‡ przekroczona (%s)", + "Message": "WiadomoΕ›Δ‡", + "Missing signature file": "BrakujΔ…cy plik podpisu", + "Mnemonic": "Mnemoniczny", + "Mnemonic ID": "Mnemoniczne id", + "Mnemonic Storage ID": "Mnemoniczny identyfikator przechowywania", + "Mnemonic was not decrypted": "Mnemonik nie zostaΕ‚ odszyfrowany", + "Mnemonic was not encrypted": "Mnemoniczny nie byΕ‚ szyfrowany", + "Modified: ": "Zmodyfikowany:", + "Multisig": "Multisig", + "Network": "SieΔ‡", + "New Mnemonic": "Nowy Mnemonic", + "New firmware detected.\n\nSHA256:\n%s\n\n\n\nInstall?": "Wykryto nowe oprogramowanie.\n\nsha256:\n%s\n\n\n\ninstall?", + "No": "NIE", + "No BIP39 passphrase": "Brak frazy BIP39", + "Not enough rolls!": "Za maΕ‚o rolki!", + "Octal": "Octal", + "PBKDF2 Iter.": "PBKDF2 ITER.", + "PSBT": "PSBT", + "Paint punched dots black so they can be detected.": "Parzyj kropki czarne, aby moΕΌna je byΕ‚o wykryΔ‡.", + "Paper Width": "SzerokoΕ›Δ‡ papieru", + "Part\n%d / %d": "CzΔ™Ε›Δ‡\n %d / %d", + "Part Size": "Rozmiar czΔ™Ε›ci", + "Passphrase": "Fraza", + "Passphrase: ": "FRASSE:", + "Persist": "TrwaΔ‡", + "Plaintext QR": "PlainText QR", + "Please load a wallet output descriptor": "ZaΕ‚aduj deskryptor wyjΕ›ciowy portfela", + "Plunge Rate": "SzybkoΕ›Δ‡ spadku", + "Print Test QR": "Test wydrukuj QR", + "Print to QR?\n\n%s\n\n": "Drukuj do qr?\n\n%s\n\n", + "Print?\n\n%s\n\n": "WydrukowaΔ‡?\n\n%s\n\n", + "Printer": "Drukarka", + "Printer Driver not set!": "Sterownik drukarki nie jest ustawiony!", + "Printing\n%d / %d": "Drukowanie\n %d / %d", + "Printing ...": "Drukowanie ...", + "Proceed?": "PrzystΔ™powaΔ‡?", + "Processing ...": "Przetwarzanie ...", + "QR Code": "Kod QR", + "RX Pin": "Pin Rx", + "Receive": "OdbieraΔ‡", + "Receive Addresses": "OdbieraΔ‡ adresy", + "Region: ": "Region:", + "Review scanned data, edit if necessary": "Przejrzyj zeskanowane dane, w razie potrzeby edytuj", + "Roll dice at least %d times to generate a mnemonic.": "RzuΔ‡ kostkΔ™ co najmniej %d, aby wygenerowaΔ‡ mnemoniczny.", + "Rolls:\n\n%s": "Rolls:\n\n%s", + "Rolls: %d\n": "Rolls: %d\n", + "SD card": "karta SD", + "SD card not detected": "Karta SD nie zostaΕ‚a wykryta", + "SD card not detected.": "Karta SD nie zostaΕ‚a wykryta.", + "SHA256 of rolls:\n\n%s": "SHA256 Rolls:\n\n%s", + "SHA256 of snapshot:\n\n%s": "SHA256 migawki:\n\n%s", + "SHA256:\n%s": "SHA256:\n%s", + "Save to SD card?": "Zapisz na karcie SD?", + "Saved to SD card:\n%s": "Zapisano na karcie SD:\n%s", + "Scale": "Skala", + "Scan Address": "Adres skanowania", + "Scan BIP39 passphrase": "Scan BIP39 Passhraz", + "Scan Key QR code": "Skanuj kod QR", + "Scanning words 1-12 again": "Znowu skanowanie sΕ‚Γ³w 1-12", + "Scanning words 13-24": "Skanowanie sΕ‚Γ³w 13-24", + "SeedQR": "Seedqr", + "Self-transfer or Change (%d): ": "Samo-transfer lub zmiana (%d):", + "Settings": "Ustawienia", + "Shutdown": "ZamkniΔ™cie", + "Shutting down..": "WyΕ‚Δ…czanie..", + "Sign": "PodpisaΔ‡", + "Sign?": "PodpisaΔ‡?", + "Signature": "Podpis", + "Signed Message": "Podpisana wiadomoΕ›Δ‡", + "Signed PSBT": "Podpisano PSBT", + "Single-sig": "Pojedyncze Sig", + "Size: ": "Rozmiar:", + "Some checks cannot be performed.": "Nie moΕΌna wykonaΔ‡ niektΓ³rych kontroli.", + "Spend (%d): ": "WydaΔ‡ (%d):", + "Stackbit 1248": "Stackbit 1248", + "Store on Flash": "Przechowuj na Flash", + "Store on SD Card": "Przechowuj na karcie SD", + "Swipe to change mode": "PrzesuΕ„ tryb zmiany", + "TOUCH or ENTER to capture": "Dotknij lub wejdΕΊ do przechwytywania", + "TX Pin": "Pin TX", + "Text": "Tekst", + "Theme": "Temat", + "Thermal": "Termiczny", + "Tiny Seed": "MaΕ‚e ziarno", + "Tiny Seed (Bits)": "MaΕ‚e nasiona (bity)", + "Tools": "NarzΔ™dzia", + "Touch Threshold": "PrΓ³g dotykowy", + "Touchscreen": "Ekran dotykowy", + "Try more?": "PrΓ³buj bardziej?", + "Type BIP39 passphrase": "PassΓ³wka typu BIP39", + "Type Key": "Klucz typu", + "Unit": "Jednostka", + "Updating bootloader..\n\n%d%%": "Aktualizacja bootloader ..\n\n%d %%", + "Upgrade complete.\n\nShutting down..": "Uaktualnienie zakoΕ„czone.\n\nshutting w dΓ³Ε‚ ..", + "Upgrading firmware..\n\n%d%%": "Aktualizacja oprogramowania ukΕ‚adowego ..\n\n%d %%", + "Use a black background surface.": "UΕΌyj czarnej powierzchni tΕ‚a.", + "Use camera's entropy to create a new mnemonic": "UΕΌyj entropii aparatu, aby stworzyΔ‡ nowy mnemonik", + "Used: ": "UΕΌywany:", + "Value %s out of range: [%s, %s]": "WartoΕ›Δ‡ %s z zakresu: [ %s, %s]", + "Via Camera": "Za poΕ›rednictwem aparatu", + "Via D20": "Via D20", + "Via D6": "Przez D6", + "Via Manual Input": "Poprzez rΔ™czne wejΕ›cie", + "Wait for the capture": "Poczekaj na schwytanie", + "Wallet Descriptor": "Deskryptor portfela", + "Wallet output descriptor": "Deskryptor wyjΕ›ciowy portfela", + "Wallet output descriptor loaded!": "ZaΕ‚adowany deskryptor wyjΕ›ciowy portfela!", + "Wallet output descriptor not found.": "Nie znaleziono deskryptora wyjΕ›ciowego portfela.", + "Warning:": "OstrzeΕΌenie:", + "Word %d": "SΕ‚owo %d", + "Word Numbers": "Numery sΕ‚Γ³w", + "Words": "SΕ‚owa", + "Yes": "Tak", + "Your changes will be kept on device flash storage.": "Twoje zmiany bΔ™dΔ… przechowywane w pamiΔ™ci flash urzΔ…dzenia.", + "Your changes will be kept on the SD card.": "Twoje zmiany bΔ™dΔ… przechowywane na karcie SD." +} \ No newline at end of file diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json index 1597a86a4..a9f049226 100644 --- a/i18n/translations/pt-BR.json +++ b/i18n/translations/pt-BR.json @@ -19,7 +19,6 @@ "Anti-glare disabled": "Antirreflexo desativado", "Anti-glare enabled": "Antirreflexo ativado", "Are you sure?": "Tem certeza?", - "BIP39 Mnemonic": "MnemΓ΄nico BIP39", "Back": "Voltar", "Backing up bootloader..\n\n%d%%": "Backing up bootloader..\n\n%d%%", "Bad signature": "Assinatura InvΓ‘lida", @@ -49,6 +48,7 @@ "Cut Method": "MΓ©todo de Corte", "Decimal": "Decimal", "Decrypt?": "Descriptografar?", + "Delete %s?": "Excluir %s?", "Delete File?": "Excluir Arquivo?", "Delete Mnemonic": "Excluir MnemΓ΄nico", "Depth Per Pass": "Profundidade da Passagem", @@ -94,11 +94,10 @@ "GRBL": "GRBL", "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "DΓͺ a este mnemΓ΄nico um ID personalizado? Caso contrΓ‘rio, a impressΓ£o digital atual serΓ‘ usada", "Go": "Ir", - "Heat Interval": "Intervalo de Aquecimento", - "Heat Time": "Tempo de Aquecimento", "Hex Public Key": "Chave pΓΊblica hexadecimal", "Hexadecimal": "Hexadecimal", "ID already exists\n": "Id jΓ‘ existe\n", + "Incomplete output descriptor": "Descritor incompleto", "Inputs (%d): ": "Entradas (%d): ", "Invalid address": "EndereΓ§o invΓ‘lido", "Invalid bootloader": "Bootloader invΓ‘lido", @@ -199,6 +198,7 @@ "Signed PSBT": "PSBT Assinada", "Single-sig": "Single-sig", "Size: ": "Total: ", + "Some checks cannot be performed.": "Algumas verificaçáes nΓ£o podem ser realizadas.", "Spend (%d): ": "Gastos (%d): ", "Stackbit 1248": "Stackbit 1248", "Store on Flash": "Armazene na Flash", @@ -235,7 +235,7 @@ "Wallet output descriptor": "Descritor da carteira", "Wallet output descriptor loaded!": "Descritor de saΓ­da da carteira carregado!", "Wallet output descriptor not found.": "O descritor de saΓ­da da carteira nΓ£o foi encontrado.", - "Warning:\nIncomplete output descriptor": "Atenção:\nDescritor de saΓ­da incompleto", + "Warning:": "Aviso:", "Word %d": "Palavra %d", "Word Numbers": "NΓΊmeros das Palavras", "Words": "Palavras", diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json new file mode 100644 index 000000000..2a4e4cb60 --- /dev/null +++ b/i18n/translations/ru-RU.json @@ -0,0 +1,244 @@ +{ + "%d of %d multisig": "%d ΠΈΠ· %d ΠΌΡƒΠ»ΡŒΡ‚ΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡŒ", + "%d. Change: \n\n%s\n\n": "%d. Π‘Π΄Π°Ρ‡Π°: \n\n%s\n\n", + "%d. Self-transfer: \n\n%s\n\n": "%d. ΠŸΠ΅Ρ€Π΅Π²ΠΎΠ΄ самому сСбС: \n\n%s\n\n", + "%d. Spend: \n\n%s\n\n": "%d. Расход: \n\n%s\n\n", + "%s\n\nis a valid change address!": "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½Ρ‹ΠΉ адрСс сдачи!", + "%s\n\nis a valid receive address!": "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½Ρ‹ΠΉ адрСс получСния!", + "%s\n\nwas NOT FOUND in the first %d change addresses": "%s\n\nНЕ НАЙДЕН Π² ΠΏΠ΅Ρ€Π²Ρ‹Ρ… %d адрСсах сдачи", + "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nНЕ НАЙДЕН Π² ΠΏΠ΅Ρ€Π²Ρ‹Ρ… %d адрСсах получСния", + "(Experimental)": "(Π­ΠΊΡΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΉ)", + "12 words": "12 слов", + "24 words": "24 слова", + "ABC": "ABC", + "About": "О ΠŸΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ΅", + "Adafruit": "Adafruit", + "Address": "АдрСс", + "Aditional entropy from camera required for AES-CBC mode": "Для Ρ€Π΅ΠΆΠΈΠΌΠ° AES-CBC трСбуСтся Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ энтропия ΠΎΡ‚ ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹", + "Align camera and Tiny Seed properly.": "ΠŸΡ€Π°Π²ΠΈΠ»ΡŒΠ½ΠΎ совмСститС ΠΊΠ°ΠΌΠ΅Ρ€Ρƒ ΠΈ Мини Π‘ΠΈΠ΄-Ρ„Ρ€Π°Π·Ρƒ.", + "Anti-glare disabled": "Антиблик ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½", + "Anti-glare enabled": "Антиблик Π²ΠΊΠ»ΡŽΡ‡Π΅Π½", + "Are you sure?": "Π’Ρ‹ ΡƒΠ²Π΅Ρ€Π΅Π½Ρ‹?", + "Back": "Назад", + "Backing up bootloader..\n\n%d%%": "Π Π΅Π·Π΅Ρ€Π²Π½ΠΎΠ΅ ΠΊΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊΠ°..\n\n%d%%", + "Bad signature": "ΠŸΠ»ΠΎΡ…Π°Ρ подпись", + "Baudrate": "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠŸΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ Π”Π°Π½Π½Ρ‹Ρ…", + "Bitcoin": "Π‘ΠΈΡ‚ΠΊΠΎΠΈΠ½", + "Border Padding": "Π—Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π³Ρ€Π°Π½ΠΈΡ†", + "CNC": "CNC", + "Change": "Π‘Π΄Π°Ρ‡Π°", + "Change Addresses": "АдрСс Π‘Π΄Π°Ρ‡ΠΈ", + "Change theme and reboot?": "Π‘ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ ΠΈ ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ?", + "Changes persisted to SD card!": "ИзмСнСния сохранСны Π½Π° SD ΠΊΠ°Ρ€Ρ‚Π΅!", + "Changes will last until shutdown.": "ИзмСнСния Π±ΡƒΠ΄ΡƒΡ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒΡΡ Π΄ΠΎ Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ.", + "Check SD Card": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ SD ΠšΠ°Ρ€Ρ‚Ρƒ", + "Check that address belongs to this wallet?": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ адрСс ΠΏΡ€ΠΈΠ½Π°Π΄Π»Π΅ΠΆΠΈΡ‚ этому ΠΊΠΎΡˆΠ΅Π»ΡŒΠΊΡƒ?", + "Checked %d change addresses with no matches.": "ΠŸΡ€ΠΎΠ²Π΅Ρ€Π΅Π½ΠΎ %d адрСсов сдачи Π±Π΅Π· совпадСний.", + "Checked %d receive addresses with no matches.": "ΠŸΡ€ΠΎΠ²Π΅Ρ€Π΅Π½ΠΎ %d адрСсов получСния Π±Π΅Π· совпадСний.", + "Checking change address %d for match..": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ адрСс сдачи %d Π½Π° совпадСниС..", + "Checking for SD card..": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° SD ΠΊΠ°Ρ€Ρ‚Ρ‹..", + "Checking receive address %d for match..": "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ адрСс получСния %d Π½Π° совпадСниС..", + "Compact SeedQR": "ΠšΠΎΠΌΠΏΠ°ΠΊΡ‚Π½Ρ‹ΠΉ SeedQR", + "Continue?": "ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ?", + "Create QR Code": "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ QR Код", + "Create QR code from text?": "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ QR ΠΊΠΎΠ΄ ΠΈΠ· тСкста?", + "Created: ": "Π‘ΠΎΠ·Π΄Π°Π½ΠΎ", + "Custom QR Code": "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ QR Код", + "Cut Depth": "Π“Π»ΡƒΠ±ΠΈΠ½Π° Π Π΅Π·ΠΊΠΈ", + "Cut Method": "ΠœΠ΅Ρ‚ΠΎΠ΄ Π Π΅Π·ΠΊΠΈ", + "Decimal": "ДСсятичный", + "Decrypt?": "Π Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ?", + "Delete %s?": "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ %s?", + "Delete File?": "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ Π€Π°ΠΉΠ»?", + "Delete Mnemonic": "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ ΠœΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + "Depth Per Pass": "Π“Π»ΡƒΠ±ΠΈΠ½Π° Π—Π° ΠŸΡ€ΠΎΡ…ΠΎΠ΄", + "Derivation: %s": "ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄Π½Ρ‹ΠΉ ΠΏΡƒΡ‚ΡŒ: %s", + "Device flash storage not detected.": "Ѐлэш ΠΏΠ°ΠΌΡΡ‚ΡŒ устройства Π½Π΅ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π°.", + "Done?": "Π“ΠΎΡ‚ΠΎΠ²ΠΎ?", + "Driver": "Π”Ρ€Π°ΠΉΠ²Π΅Ρ€", + "Encoder": "ΠšΠΎΠ΄Π΅Ρ€", + "Encoder Debounce": "ДСбаунс ΠšΠΎΠ΄Π΅Ρ€Π°", + "Encrypt Mnemonic": "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ ΠœΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + "Encrypted QR Code": "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ QR Код", + "Encrypted mnemonic was not stored": "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±Ρ‹Π»Π° сохранСна", + "Encrypted mnemonic was stored with ID: ": "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π±Ρ‹Π»Π° сохранСна с ID: ", + "Encryption": "Π¨ΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΠ΅", + "Encryption Mode": "ΠœΠ΅Ρ‚ΠΎΠ΄ ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΡ", + "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ числа ΠΎΡ‚ 1 Π΄ΠΎ 2048.", + "Enter each word of your BIP-39 mnemonic as a number in hexadecimal from 1 to 800.": "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ ΡˆΠ΅ΡΡ‚Π½Π°Π΄Ρ†Π°Ρ‚Π΅Ρ€ΠΈΡ‡Π½ΠΎΠ³ΠΎ числа ΠΎΡ‚ 1 Π΄ΠΎ 800.", + "Enter each word of your BIP-39 mnemonic as a number in octal from 1 to 4000.": "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ Π²ΠΎΡΡŒΠΌΠ΅Ρ€ΠΈΡ‡Π½ΠΎΠ³ΠΎ числа ΠΎΡ‚ 1 Π΄ΠΎ 4000.", + "Enter each word of your BIP-39 mnemonic.": "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй BIP-39 ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ.", + "Error:\n%s": "Ошибка:\n%s", + "Esc": "Π’Ρ‹ΠΉΡ‚ΠΈ", + "Explore files?": "Π˜ΡΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚ΡŒ Ρ„Π°ΠΉΠ»Ρ‹?", + "Exporting to SD card..": "ЭкспортированиС Π½Π° SD ΠΊΠ°Ρ€Ρ‚Ρƒ..", + "Extended Public Key": "Π Π°ΡΡˆΠΈΡ€Π΅Π½Π½Ρ‹ΠΉ ΠŸΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠšΠ»ΡŽΡ‡", + "Failed to decrypt": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Ρ€Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ", + "Failed to load PSBT": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ PSBT", + "Failed to load address": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ адрСс", + "Failed to load key": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΊΠ»ΡŽΡ‡", + "Failed to load message": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ сообщСниС", + "Failed to load mnemonic": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + "Failed to load output descriptor": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор", + "Failed to load passphrase": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ Ρ„Ρ€Π°Π·Ρƒ-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + "Failed to store mnemonic": "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + "Fee: ": "Комиссия: ", + "Feed Rate": "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠΏΠΎΠ΄Π°Ρ‡ΠΈ", + "Filename": "Имя Ρ„Π°ΠΉΠ»Π°", + "Filename %s exists on SD card, overwrite?": "Π€Π°ΠΉΠ» %s сущСствуСт Π½Π° SD ΠΊΠ°Ρ€Ρ‚Π΅, ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ?", + "Fingerprint: %s": "Π€ΠΈΠ½Π³Π΅Ρ€ΠΏΡ€ΠΈΠ½Ρ‚: %s", + "Firmware exceeds max size: %d": "ΠŸΡ€ΠΎΡˆΠΈΠ²ΠΊΠ° ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Π΅Ρ‚ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€: %d", + "Flute Diameter": "Flute Π”ΠΈΠ°ΠΌΠ΅Ρ‚Ρ€", + "Free: ": "БСсплатно: ", + "From Storage": "Из ΠŸΠ°ΠΌΡΡ‚ΠΈ", + "GRBL": "GRBL", + "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "ΠΠ°Π·Π½Π°Ρ‡ΠΈΡ‚ΡŒ этой ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ кастомный ID? Π’ ΠΈΠ½ΠΎΠΌ случаС Π±ΡƒΠ΄Π΅Ρ‚ использован Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ Ρ„ΠΈΠ½Π³Π΅Ρ€ΠΏΡ€ΠΈΠ½Ρ‚", + "Go": "OK", + "Hex Public Key": "ШСстнадцатСричный ΠŸΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠšΠ»ΡŽΡ‡", + "Hexadecimal": "ШСстнадцатСричный", + "ID already exists\n": "ID ΡƒΠΆΠ΅ сущСствуСт\n", + "Incomplete output descriptor": "НСполный Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор", + "Inputs (%d): ": "Π’Ρ…ΠΎΠ΄Ρ‹ (%d): ", + "Invalid address": "НСвСрный адрСс", + "Invalid bootloader": "НСвСрный Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊ", + "Invalid mnemonic length": "НСвСрная Π΄Π»ΠΈΠ½Π° ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ", + "Invalid public key": "НСвСрный ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡", + "Invalid wallet:\n%s": "НСвСрный кошСлСк:\n%s", + "Invert": "Π˜Π½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ", + "Key": "ΠšΠ»ΡŽΡ‡", + "Key: ": "ΠšΠ»ΡŽΡ‡: ", + "Krux\n\n\nVersion\n%s": "Krux\n\n\nВСрсия\n%s", + "Krux Printer Test QR": "ВСстовый QR ΠŸΡ€ΠΈΠ½Ρ‚Π΅Ρ€Π° Krux", + "Language": "Π―Π·Ρ‹ΠΊ", + "Line Delay": "Π—Π°Π΄Π΅Ρ€ΠΆΠΊΠ° Π›ΠΈΠ½ΠΈΠΈ", + "Line: ": "Линия: ", + "Load Mnemonic": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠœΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + "Load from SD card?": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ с SD ΠΊΠ°Ρ€Ρ‚Ρ‹?", + "Load one?": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΎΠ΄Π½Ρƒ?", + "Load?": "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ?", + "Loading Camera..": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠšΠ°ΠΌΠ΅Ρ€Ρ‹..", + "Loading change address %d..": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° адрСса сдачи %d..", + "Loading printer..": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΏΡ€ΠΈΠ½Ρ‚Π΅Ρ€Π°..", + "Loading receive address %d..": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° адрСса получСния %d..", + "Loading..": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..", + "Locale": "Π›ΠΎΠΊΠ°Π»ΡŒ", + "Location": "РасполоТСниС", + "Log Level": "Π£Ρ€ΠΎΠ²Π΅Π½ΡŒ логирования", + "Logging": "Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅", + "Maximum length exceeded (%s)": "Максимальная Π΄Π»ΠΈΠ½Π° ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½Π° (%s)", + "Message": "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅", + "Missing signature file": "ΠžΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΠ΅Ρ‚ Ρ„Π°ΠΉΠ» подписи", + "Mnemonic": "МнСмоника", + "Mnemonic ID": "ID ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ", + "Mnemonic Storage ID": "ID памяти ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ", + "Mnemonic was not decrypted": "МнСмоника Π½Π΅ Π±Ρ‹Π»Π° Ρ€Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π°", + "Mnemonic was not encrypted": "МнСмоника Π½Π΅ Π±Ρ‹Π»Π° Π·Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π°", + "Modified: ": "ИзмСнСно: ", + "Multisig": "ΠœΡƒΠ»ΡŒΡ‚ΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡŒ", + "Network": "Π‘Π΅Ρ‚ΡŒ", + "New Mnemonic": "Новая МнСмоника", + "New firmware detected.\n\nSHA256:\n%s\n\n\n\nInstall?": "ΠžΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π° новая ΠΏΡ€ΠΎΡˆΠΈΠ²ΠΊΠ°.\n\nSHA256:\n%s\n\n\n\nΠ£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ?", + "No": "НСт", + "No BIP39 passphrase": "Π‘Π΅Π· BIP39 Ρ„Ρ€Π°Π·Ρ‹-пароля", + "Not enough rolls!": "НСдостаточно бросков!", + "Octal": "Π’ΠΎΡΡŒΠΌΠ΅Ρ€ΠΈΡ‡Π½Ρ‹ΠΉ", + "PBKDF2 Iter.": "PBKDF2 Π˜Ρ‚Π΅Ρ€Π°Ρ†ΠΈΠΈ", + "PSBT": "PSBT", + "Paint punched dots black so they can be detected.": "Π—Π°ΠΊΡ€Π°ΡΡŒΡ‚Π΅ ΠΏΠ΅Ρ€Ρ„ΠΎΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ Ρ‡Π΅Ρ€Π½Ρ‹ΠΌ Ρ†Π²Π΅Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΡ… ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠΈΡ‚ΡŒ.", + "Paper Width": "Π¨ΠΈΡ€ΠΈΠ½Π° Π‘ΡƒΠΌΠ°Π³ΠΈ", + "Part\n%d / %d": "Π§Π°ΡΡ‚ΡŒ\n%d / %d", + "Part Size": "Π Π°Π·ΠΌΠ΅Ρ€ Части", + "Passphrase": "Π€Ρ€Π°Π·Π°-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + "Passphrase: ": "Π€Ρ€Π°Π·Π°-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ: ", + "Persist": "ΠŸΠΎΡΡ‚ΠΎΡΠ½Π½Π°Ρ ΠŸΠ°ΠΌΡΡ‚ΡŒ", + "Plaintext QR": "QR ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Ρ‹ΠΌ тСкстом", + "Please load a wallet output descriptor": "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚Π΅ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька", + "Plunge Rate": "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠŸΠΎΠ³Ρ€ΡƒΠΆΠ΅Π½ΠΈΡ", + "Print Test QR": "ΠΠ°ΠΏΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ ВСстовый QR", + "Print to QR?\n\n%s\n\n": "ΠΠ°ΠΏΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ Π² Π²ΠΈΠ΄Π΅ QR?\n\n%s\n\n", + "Print?\n\n%s\n\n": "ΠŸΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ?\n\n%s\n\n", + "Printer": "ΠŸΡ€ΠΈΠ½Ρ‚Π΅Ρ€", + "Printer Driver not set!": "Π”Ρ€Π°ΠΉΠ²Π΅Ρ€ ΠŸΡ€ΠΈΠ½Ρ‚Π΅Ρ€Π° Π½Π΅ установлСн!", + "Printing\n%d / %d": "Π˜Π΄Π΅Ρ‚ ΠΏΠ΅Ρ‡Π°Ρ‚ΡŒ\n%d / %d", + "Printing ...": "Π˜Π΄Π΅Ρ‚ ΠΏΠ΅Ρ‡Π°Ρ‚ΡŒ ...", + "Proceed?": "ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ?", + "Processing ...": "ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ...", + "QR Code": "QR Код", + "RX Pin": "RX Пин", + "Receive": "ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ", + "Receive Addresses": "АдрСс ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΡ", + "Region: ": "Π Π΅Π³ΠΈΠΎΠ½: ", + "Review scanned data, edit if necessary": "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€ΠΈΡ‚Π΅ отсканированныС Π΄Π°Π½Π½Ρ‹Π΅, ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΏΡ€ΠΈ нСобходимости", + "Roll dice at least %d times to generate a mnemonic.": "Π‘Ρ€ΠΎΡΡŒΡ‚Π΅ ΠΊΡƒΠ±ΠΈΠΊ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ %d Ρ€Π°Π·, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠ³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ.", + "Rolls:\n\n%s": "Броски:\n\n%s", + "Rolls: %d\n": "Броски: %d\n", + "SD card": "SD ΠΊΠ°Ρ€Ρ‚Π°", + "SD card not detected": "SD ΠΊΠ°Ρ€Ρ‚Π° Π½Π΅ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π°", + "SD card not detected.": "SD ΠΊΠ°Ρ€Ρ‚Π° Π½Π΅ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π°.", + "SHA256 of rolls:\n\n%s": "SHA256 бросков:\n\n%s", + "SHA256 of snapshot:\n\n%s": "SHA256 ΡΠ½ΡΠΏΡˆΠΎΡ‚Π°:\n\n%s", + "SHA256:\n%s": "SHA256:\n%s", + "Save to SD card?": "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π° SD ΠΊΠ°Ρ€Ρ‚Ρƒ?", + "Saved to SD card:\n%s": "Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΎ Π½Π° SD ΠΊΠ°Ρ€Ρ‚Ρƒ:\n%s", + "Scale": "Π¨ΠΊΠ°Π»Π°", + "Scan Address": "ΠžΡ‚ΡΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ АдрСс", + "Scan BIP39 passphrase": "ΠžΡ‚ΡΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ BIP39 Ρ„Ρ€Π°Π·Ρƒ-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + "Scan Key QR code": "ΠžΡ‚ΡΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠšΠ»ΡŽΡ‡ QR ΠΊΠΎΠ΄", + "Scanning words 1-12 again": "Π‘ΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ слов 1-12 снова", + "Scanning words 13-24": "Π‘ΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ слов 13-24", + "SeedQR": "SeedQR", + "Self-transfer or Change (%d): ": "ВрансфСр самому сСбС ΠΈΠ»ΠΈ Π‘Π΄Π°Ρ‡Π° (%d): ", + "Settings": "Настройки", + "Shutdown": "Π’Ρ‹ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ", + "Shutting down..": "Π’Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅..", + "Sign": "ΠŸΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒ", + "Sign?": "ΠŸΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒ?", + "Signature": "Подпись", + "Signed Message": "ПодписанноС Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅", + "Signed PSBT": "ПодписанноС PSBT", + "Single-sig": "Одна подпись", + "Size: ": "Π Π°Π·ΠΌΠ΅Ρ€: ", + "Some checks cannot be performed.": "НСкоторыС ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π½Π΅ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½Ρ‹.", + "Spend (%d): ": "Расход (%d): ", + "Stackbit 1248": "Бтэкбит 1248", + "Store on Flash": "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π° Ѐлэш ΠŸΠ°ΠΌΡΡ‚ΡŒ", + "Store on SD Card": "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π° SD ΠšΠ°Ρ€Ρ‚Ρƒ", + "Swipe to change mode": "Π‘Π²Π°ΠΉΠΏΠ½ΠΈΡ‚Π΅, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ€Π΅ΠΆΠΈΠΌ", + "TOUCH or ENTER to capture": "ΠŸΠ Π˜ΠšΠžΠ‘ΠΠ˜Π’Π•Π‘Π¬ ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡ‚Π΅ Π’Π’ΠžΠ”, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π·Π°Ρ…Π²Π°Ρ‚ΠΈΡ‚ΡŒ", + "TX Pin": "TX Пин", + "Text": "ВСкст", + "Theme": "Π’Π΅ΠΌΠ°", + "Thermal": "Π’Π΅Ρ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ", + "Tiny Seed": "Мини Π‘ΠΈΠ΄-Ρ„Ρ€Π°Π·Π°", + "Tiny Seed (Bits)": "Мини Π‘ΠΈΠ΄-Ρ„Ρ€Π°Π·Π° (Π‘ΠΈΡ‚Ρ‹)", + "Tools": "Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹", + "Touch Threshold": "ΠŸΡ€ΠΈΠΊΠΎΡΠ½ΠΈΡ‚Π΅ΡΡŒ Π“Ρ€Π°Π½ΠΈΡ†Ρ‹", + "Touchscreen": "Вачскрин", + "Try more?": "ΠŸΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π΅Ρ‰Ρ‘?", + "Type BIP39 passphrase": "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ BIP39 Ρ„Ρ€Π°Π·Ρƒ-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + "Type Key": "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠšΠ»ΡŽΡ‡", + "Unit": "ΠΠ½ΠΈΡ‚", + "Updating bootloader..\n\n%d%%": "ОбновлСниС Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊΠ°..\n\n%d%%", + "Upgrade complete.\n\nShutting down..": "ОбновлСниС Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΎ.\n\nΠ’Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅..", + "Upgrading firmware..\n\n%d%%": "ОбновлСниС ΠΏΡ€ΠΎΡˆΠΈΠ²ΠΊΠΈ..\n\n%d%%", + "Use a black background surface.": "Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π½ΡƒΡŽ Ρ„ΠΎΠ½ΠΎΠ²ΡƒΡŽ ΠΏΠΎΠ²Π΅Ρ€Ρ…Π½ΠΎΡΡ‚ΡŒ.", + "Use camera's entropy to create a new mnemonic": "Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΡΠ½Ρ‚Ρ€ΠΎΠΏΠΈΡŽ ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + "Used: ": "Использовано: ", + "Value %s out of range: [%s, %s]": "Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ %s Π²Π½Π΅ Π΄ΠΈΠ°ΠΏΠΎΠ·ΠΎΠ½Π°: [%s, %s]", + "Via Camera": "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠšΠ°ΠΌΠ΅Ρ€Ρ‹", + "Via D20": "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ D20", + "Via D6": "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ D6", + "Via Manual Input": "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ Π ΡƒΡ‡Π½ΠΎΠ³ΠΎ Π’Π²ΠΎΠ΄Π°", + "Wait for the capture": "Π”ΠΎΠΆΠ΄ΠΈΡ‚Π΅ΡΡŒ Π—Π°Ρ…Π²Π°Ρ‚Π°", + "Wallet Descriptor": "ДСскриптор КошСлька", + "Wallet output descriptor": "Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька", + "Wallet output descriptor loaded!": "Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½!", + "Wallet output descriptor not found.": "Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½.", + "Warning:": "ΠŸΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΠ΅:", + "Word %d": "Π‘Π»ΠΎΠ²ΠΎ %d", + "Word Numbers": "Числа Π‘Π»ΠΎΠ²", + "Words": "Π‘Π»ΠΎΠ²Π°", + "Yes": "Π”Π°", + "Your changes will be kept on device flash storage.": "Π’Π°ΡˆΠΈ измСнСния Π±ΡƒΠ΄ΡƒΡ‚ сохранСны Π½Π° Ρ„Π»ΡΡˆ памяти устройства.", + "Your changes will be kept on the SD card.": "Π’Π°ΡˆΠΈ измСнСния Π±ΡƒΠ΄ΡƒΡ‚ сохранСны Π½Π° SD ΠΊΠ°Ρ€Ρ‚Π΅." +} \ No newline at end of file diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 8e5de7b8b..ce8294faf 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -19,7 +19,6 @@ "Anti-glare disabled": "Chα»‘ng lΓ³a bα»‹ vΓ΄ hiệu hΓ³a", "Anti-glare enabled": "Đã bαΊ­t chα»‘ng lΓ³a", "Are you sure?": "BαΊ‘n cΓ³ chαΊ―c khΓ΄ng?", - "BIP39 Mnemonic": "MΓ£ mnemonic dαΊ‘ng chuαΊ©n BIP39", "Back": "Trở lαΊ‘i", "Backing up bootloader..\n\n%d%%": "Sao lΖ°u bα»™ tαΊ£i khởi Δ‘α»™ng..\n\n%d%%", "Bad signature": "Chα»― kΓ½ xαΊ₯u", @@ -49,6 +48,7 @@ "Cut Method": "PhΖ°Ζ‘ng phΓ‘p cαΊ―t", "Decimal": "Sα»‘ thαΊ­p phΓ’n", "Decrypt?": "PhαΊ£n Δ‘α»‘i?", + "Delete %s?": "XΓ³a %s?", "Delete File?": "XΓ³a tΓ i liệu?", "Delete Mnemonic": "XΓ³a ghi nhα»›", "Depth Per Pass": "Độ sΓ’u mα»—i lαΊ§n vượt qua", @@ -94,11 +94,10 @@ "GRBL": "GRBL", "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Cung cαΊ₯p cho Mnemonic nΓ y mα»™t ID tΓΉy chỉnh?NαΊΏu khΓ΄ng thΓ¬ dαΊ₯u vΓ’n tay hiện tαΊ‘i sαΊ½ được sα»­ dα»₯ng", "Go": "Đến", - "Heat Interval": "KhoαΊ£ng thời gian nhiệt", - "Heat Time": "Thời gian nhiệt", "Hex Public Key": "KhΓ³a cΓ΄ng khai Hex", "Hexadecimal": "ThαΊ­p lα»₯c phΓ’n", "ID already exists\n": "Id Δ‘Γ£ tα»“n tαΊ‘i\n", + "Incomplete output descriptor": "Bα»™ mΓ΄ tαΊ£ Δ‘αΊ§u ra chΖ°a hoΓ n chỉnh", "Inputs (%d): ": "Đầu vΓ o (%d): ", "Invalid address": "Địa chỉ khΓ΄ng hợp lệ", "Invalid bootloader": "Bα»™ tαΊ£i khởi Δ‘α»™ng khΓ΄ng hợp lệ", @@ -199,6 +198,7 @@ "Signed PSBT": "Đã kΓ½ PSBT", "Single-sig": "KhΓ³a Δ‘Ζ‘n", "Size: ": "Dung lượng: ", + "Some checks cannot be performed.": "Mα»™t sα»‘ kiểm tra khΓ΄ng thể được thα»±c hiện.", "Spend (%d): ": "Chi tiΓͺu (%d): ", "Stackbit 1248": "Stackbit 1248", "Store on Flash": "LΖ°u trα»― trΓͺn flash", @@ -235,7 +235,7 @@ "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Γ­.", - "Warning:\nIncomplete output descriptor": "CαΊ£nh bΓ‘o:\nBα»™ mΓ΄ tαΊ£ Δ‘αΊ§u ra chΖ°a hoΓ n chỉnh", + "Warning:": "CαΊ£nh bΓ‘o:", "Word %d": "KΓ­ tα»± %d", "Word Numbers": "Tα»« sα»‘", "Words": "Tα»« ngα»―", diff --git a/krux b/krux index 950aee6ba..8a562122a 100755 --- a/krux +++ b/krux @@ -74,7 +74,7 @@ elif [ "$1" == "flash" ]; then elif [ "$device" == "maixpy_amigo_ips" ]||[ "$device" == "maixpy_amigo_tft" ]||[ "$device" == "maixpy_bit" ]; then # https://devicehunt.com/view/type/usb/vendor/0403/device/6010 device_vendor_product_id="0403:6010" - elif [ "$device" == "maixpy_dock" ]; then + elif [ "$device" == "maixpy_dock" ]||[ "$device" == "maixpy_yahboom" ]; then # https://devicehunt.com/view/type/usb/vendor/1a86/device/7523 device_vendor_product_id="1a86:7523" fi @@ -191,7 +191,7 @@ elif [ "$1" == "build-release" ]; then cp -f ktool-win.exe ../$release_dir/ktool-win.exe cd .. - devices=("maixpy_m5stickv" "maixpy_amigo_ips" "maixpy_amigo_tft" "maixpy_bit" "maixpy_dock") + devices=("maixpy_m5stickv" "maixpy_amigo_ips" "maixpy_amigo_tft" "maixpy_bit" "maixpy_dock" "maixpy_yahboom") for device in ${devices[@]}; do ./krux build $device mkdir -p $release_dir/$device diff --git a/odudex.PEM b/odudex.PEM new file mode 100644 index 000000000..2ce42000d --- /dev/null +++ b/odudex.PEM @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MDYwEAYHKoZIzj0CAQYFK4EEAAoDIgACgw+uOhd7ie6I08S4q3vIZtYBP8TmBuQJkVDbAP4sty0= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index ea20943c4..b6aca2e77 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,14 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "astroid" -version = "2.15.6" +version = "2.15.8" description = "An abstract syntax tree for Python with inference support." -category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, - {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, + {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, + {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, ] [package.dependencies] @@ -21,65 +20,47 @@ wrapt = [ ] [[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] - -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false +name = "babel" +version = "2.13.1" +description = "Internationalization utilities" +optional = true python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, + {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, ] +[package.dependencies] +setuptools = {version = "*", markers = "python_version >= \"3.12\""} + [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "black" -version = "23.7.0" +version = "23.11.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, - {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, - {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, - {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, - {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, - {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, - {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, - {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, - {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, - {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, - {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, ] [package.dependencies] @@ -89,7 +70,7 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -99,123 +80,123 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - -[[package]] -name = "chardet" -version = "3.0.4" -description = "Universal encoding detector for Python 2 and 3" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" -optional = true +optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -225,7 +206,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -235,72 +215,63 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.2" description = "Code coverage measurement for Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] [package.dependencies] @@ -313,7 +284,6 @@ toml = ["tomli"] name = "dill" version = "0.3.7" description = "serialize all of Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -326,23 +296,38 @@ graph = ["objgraph (>=1.7.2)"] [[package]] name = "embit" -version = "0.5.0" -description = "yet another bitcoin library" -category = "main" +version = "0.7.0" +description = "A minimal bitcoin library for MicroPython and Python3 with a focus on embedded systems." optional = false python-versions = "*" files = [] develop = true +[package.extras] +dev = ["black", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mypy", "pre-commit", "pytest", "pytest-cov", "requests"] + [package.source] type = "directory" url = "vendor/embit" +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." -category = "main" optional = true python-versions = "*" files = [ @@ -356,139 +341,21 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] -[[package]] -name = "googletrans" -version = "4.0.0rc1" -description = "Free Google Translate API for Python. Translates totally free of charge." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "googletrans-4.0.0rc1.tar.gz", hash = "sha256:74df47b092e2d566522019d149e3f1d75732570ad76eaf8e14aebeffc126c372"}, -] - -[package.dependencies] -httpx = "0.13.3" - -[[package]] -name = "h11" -version = "0.9.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, - {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, -] - -[[package]] -name = "h2" -version = "3.2.0" -description = "HTTP/2 State-Machine based protocol implementation" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, - {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, -] - -[package.dependencies] -hpack = ">=3.0,<4" -hyperframe = ">=5.2.0,<6" - -[[package]] -name = "hpack" -version = "3.0.0" -description = "Pure-Python HPACK header compression" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, - {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, -] - -[[package]] -name = "hstspreload" -version = "2023.1.1" -description = "Chromium HSTS Preload list as a Python package" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "hstspreload-2023.1.1-py3-none-any.whl", hash = "sha256:ac8a56dd603b4bf55292fc7a157e0deea18ee5e2e5c114d131da8949cc7a54bb"}, - {file = "hstspreload-2023.1.1.tar.gz", hash = "sha256:b2330a88b3fe3344c9eb431257e1ff3ae06c3bc2ff87ca686a5f253e2881a6c1"}, -] - -[[package]] -name = "httpcore" -version = "0.9.1" -description = "A minimal low-level HTTP client." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, - {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"}, -] - -[package.dependencies] -h11 = ">=0.8,<0.10" -h2 = ">=3.0.0,<4.0.0" -sniffio = ">=1.0.0,<2.0.0" - -[[package]] -name = "httpx" -version = "0.13.3" -description = "The next generation HTTP client." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"}, - {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"}, -] - -[package.dependencies] -certifi = "*" -chardet = ">=3.0.0,<4.0.0" -hstspreload = "*" -httpcore = ">=0.9.0,<0.10.0" -idna = ">=2.0.0,<3.0.0" -rfc3986 = ">=1.3,<2" -sniffio = "*" - -[[package]] -name = "hyperframe" -version = "5.2.0" -description = "HTTP/2 framing layer for Python" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, - {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, -] - [[package]] name = "idna" -version = "2.10" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" files = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" -category = "main" optional = true python-versions = ">=3.8" files = [ @@ -508,7 +375,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -520,7 +386,6 @@ files = [ name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -538,7 +403,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -556,7 +420,6 @@ i18n = ["Babel (>=2.7)"] name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -598,30 +461,146 @@ files = [ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, ] +[[package]] +name = "libretranslatepy" +version = "2.1.1" +description = "Python bindings for LibreTranslate API" +optional = false +python-versions = "*" +files = [ + {file = "libretranslatepy-2.1.1-py3-none-any.whl", hash = "sha256:bf9f8b0003c94f34e141553b7ec0a02876297c06955f5efea49afbc9f85303a9"}, + {file = "libretranslatepy-2.1.1.tar.gz", hash = "sha256:3f28e1b990ba5f514ae215c08ace0c4e2327eeccaa356983aefbca3a25ecc568"}, +] + +[[package]] +name = "lxml" +version = "4.9.3" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +files = [ + {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, + {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, + {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, + {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, + {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, + {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, + {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.35)"] + [[package]] name = "markdown" -version = "3.4.4" +version = "3.5.1" description = "Python implementation of John Gruber's Markdown." -category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, + {file = "Markdown-3.5.1-py3-none-any.whl", hash = "sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc"}, + {file = "Markdown-3.5.1.tar.gz", hash = "sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd"}, ] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -681,7 +660,6 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -693,7 +671,6 @@ files = [ name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -703,14 +680,13 @@ files = [ [[package]] name = "mkdocs" -version = "1.5.2" +version = "1.5.3" description = "Project documentation with Markdown." -category = "main" optional = true python-versions = ">=3.7" files = [ - {file = "mkdocs-1.5.2-py3-none-any.whl", hash = "sha256:60a62538519c2e96fe8426654a67ee177350451616118a41596ae7c876bb7eac"}, - {file = "mkdocs-1.5.2.tar.gz", hash = "sha256:70d0da09c26cff288852471be03c23f0f521fc15cf16ac89c7a3bfb9ae8d24f9"}, + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, ] [package.dependencies] @@ -735,42 +711,48 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp [[package]] name = "mkdocs-material" -version = "8.5.11" +version = "9.4.14" description = "Documentation that simply works" -category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"}, - {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"}, + {file = "mkdocs_material-9.4.14-py3-none-any.whl", hash = "sha256:dbc78a4fea97b74319a6aa9a2f0be575a6028be6958f813ba367188f7b8428f6"}, + {file = "mkdocs_material-9.4.14.tar.gz", hash = "sha256:a511d3ff48fa8718b033e7e37d17abd9cc1de0fdf0244a625ca2ae2387e2416d"}, ] [package.dependencies] -jinja2 = ">=3.0.2" -markdown = ">=3.2" -mkdocs = ">=1.4.0" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.12" -pymdown-extensions = ">=9.4" -requests = ">=2.26" +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5.3,<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" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] [[package]] name = "mkdocs-material-extensions" -version = "1.1.1" +version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." -category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, - {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] [[package]] name = "mkdocs-static-i18n" version = "0.46" description = "MkDocs i18n plugin using static translation markdown files" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -784,7 +766,6 @@ mkdocs = ">=1.2.3" name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -794,54 +775,63 @@ files = [ [[package]] name = "numpy" -version = "1.25.2" +version = "1.26.2" description = "Fundamental package for array computing in Python" -category = "main" optional = true python-versions = ">=3.9" files = [ - {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, - {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, - {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, - {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, - {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, - {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, - {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, - {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, - {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, - {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, - {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, - {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, + {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, + {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, + {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, + {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, + {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, + {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, + {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, + {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, + {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, ] [[package]] name = "opencv-python" -version = "4.8.0.74" +version = "4.8.1.78" description = "Wrapper package for OpenCV python bindings." -category = "main" optional = true python-versions = ">=3.6" files = [ - {file = "opencv-python-4.8.0.74.tar.gz", hash = "sha256:009e3ce356a0cd2d7423723e00a32fd3d3cc5bb5970ed27a9a1f8a8f221d1db5"}, - {file = "opencv_python-4.8.0.74-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:31d0d59fc8fdf703de4cec46c79b9f8d026fdde9d23d6e2e6a66809feeebbda9"}, - {file = "opencv_python-4.8.0.74-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:66eadb5882ee56848b67f9fb57aadcaca2f4c9d9d00a0ef11043041925b51291"}, - {file = "opencv_python-4.8.0.74-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:038ba7075e55cb8e2846663ae970f0fb776a45b48ee69a887bf4ee15e2570083"}, - {file = "opencv_python-4.8.0.74-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43dd0dfe331fb95767af581bf3b2781d7a72cf6560ddf2f55949fe547f3e5c9f"}, - {file = "opencv_python-4.8.0.74-cp37-abi3-win32.whl", hash = "sha256:458e5dc377f15fcf769d80314f3d885bd95457b1a2891bee67df2eb24a1d3a52"}, - {file = "opencv_python-4.8.0.74-cp37-abi3-win_amd64.whl", hash = "sha256:8fe0018d0056a5187c57120b6b3f6c3e706c13b45c48e54e86d245a9a16fac84"}, + {file = "opencv-python-4.8.1.78.tar.gz", hash = "sha256:cc7adbbcd1112877a39274106cb2752e04984bc01a031162952e97450d6117f6"}, + {file = "opencv_python-4.8.1.78-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:91d5f6f5209dc2635d496f6b8ca6573ecdad051a09e6b5de4c399b8e673c60da"}, + {file = "opencv_python-4.8.1.78-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31f47e05447da8b3089faa0a07ffe80e114c91ce0b171e6424f9badbd1c5cd"}, + {file = "opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9814beca408d3a0eca1bae7e3e5be68b07c17ecceb392b94170881216e09b319"}, + {file = "opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c406bdb41eb21ea51b4e90dfbc989c002786c3f601c236a99c59a54670a394"}, + {file = "opencv_python-4.8.1.78-cp37-abi3-win32.whl", hash = "sha256:a7aac3900fbacf55b551e7b53626c3dad4c71ce85643645c43e91fcb19045e47"}, + {file = "opencv_python-4.8.1.78-cp37-abi3-win_amd64.whl", hash = "sha256:b983197f97cfa6fcb74e1da1802c7497a6f94ed561aba6980f1f33123f904956"}, ] [package.dependencies] @@ -849,29 +839,37 @@ numpy = [ {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, ] [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = true +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, ] [[package]] name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -881,78 +879,65 @@ files = [ [[package]] name = "pillow" -version = "9.5.0" +version = "10.1.0" description = "Python Imaging Library (Fork)" -category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, + {file = "Pillow-10.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106"}, + {file = "Pillow-10.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db"}, + {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f"}, + {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818"}, + {file = "Pillow-10.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57"}, + {file = "Pillow-10.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7"}, + {file = "Pillow-10.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061"}, + {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262"}, + {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992"}, + {file = "Pillow-10.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a"}, + {file = "Pillow-10.1.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b"}, + {file = "Pillow-10.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651"}, + {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b"}, + {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f"}, + {file = "Pillow-10.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996"}, + {file = "Pillow-10.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"}, + {file = "Pillow-10.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d"}, + {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80"}, + {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212"}, + {file = "Pillow-10.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14"}, + {file = "Pillow-10.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099"}, + {file = "Pillow-10.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd"}, + {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28"}, + {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2"}, + {file = "Pillow-10.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f"}, + {file = "Pillow-10.1.0.tar.gz", hash = "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38"}, ] [package.extras] @@ -961,14 +946,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "platformdirs" -version = "3.10.0" +version = "4.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, ] [package.extras] @@ -977,168 +961,154 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "pluggy" -version = "1.2.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - [[package]] name = "pycryptodome" -version = "3.18.0" +version = "3.19.0" description = "Cryptographic library for Python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "pycryptodome-3.18.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d1497a8cd4728db0e0da3c304856cb37c0c4e3d0b36fcbabcc1600f18504fc54"}, - {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:928078c530da78ff08e10eb6cada6e0dff386bf3d9fa9871b4bbc9fbc1efe024"}, - {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:157c9b5ba5e21b375f052ca78152dd309a09ed04703fd3721dce3ff8ecced148"}, - {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:d20082bdac9218649f6abe0b885927be25a917e29ae0502eaf2b53f1233ce0c2"}, - {file = "pycryptodome-3.18.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e8ad74044e5f5d2456c11ed4cfd3e34b8d4898c0cb201c4038fe41458a82ea27"}, - {file = "pycryptodome-3.18.0-cp27-cp27m-win32.whl", hash = "sha256:62a1e8847fabb5213ccde38915563140a5b338f0d0a0d363f996b51e4a6165cf"}, - {file = "pycryptodome-3.18.0-cp27-cp27m-win_amd64.whl", hash = "sha256:16bfd98dbe472c263ed2821284118d899c76968db1a6665ade0c46805e6b29a4"}, - {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7a3d22c8ee63de22336679e021c7f2386f7fc465477d59675caa0e5706387944"}, - {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78d863476e6bad2a592645072cc489bb90320972115d8995bcfbee2f8b209918"}, - {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b6a610f8bfe67eab980d6236fdc73bfcdae23c9ed5548192bb2d530e8a92780e"}, - {file = "pycryptodome-3.18.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:422c89fd8df8a3bee09fb8d52aaa1e996120eafa565437392b781abec2a56e14"}, - {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:9ad6f09f670c466aac94a40798e0e8d1ef2aa04589c29faa5b9b97566611d1d1"}, - {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:53aee6be8b9b6da25ccd9028caf17dcdce3604f2c7862f5167777b707fbfb6cb"}, - {file = "pycryptodome-3.18.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:10da29526a2a927c7d64b8f34592f461d92ae55fc97981aab5bbcde8cb465bb6"}, - {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f21efb8438971aa16924790e1c3dba3a33164eb4000106a55baaed522c261acf"}, - {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4944defabe2ace4803f99543445c27dd1edbe86d7d4edb87b256476a91e9ffa4"}, - {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:51eae079ddb9c5f10376b4131be9589a6554f6fd84f7f655180937f611cd99a2"}, - {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:83c75952dcf4a4cebaa850fa257d7a860644c70a7cd54262c237c9f2be26f76e"}, - {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:957b221d062d5752716923d14e0926f47670e95fead9d240fa4d4862214b9b2f"}, - {file = "pycryptodome-3.18.0-cp35-abi3-win32.whl", hash = "sha256:795bd1e4258a2c689c0b1f13ce9684fa0dd4c0e08680dcf597cf9516ed6bc0f3"}, - {file = "pycryptodome-3.18.0-cp35-abi3-win_amd64.whl", hash = "sha256:b1d9701d10303eec8d0bd33fa54d44e67b8be74ab449052a8372f12a66f93fb9"}, - {file = "pycryptodome-3.18.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:cb1be4d5af7f355e7d41d36d8eec156ef1382a88638e8032215c215b82a4b8ec"}, - {file = "pycryptodome-3.18.0-pp27-pypy_73-win32.whl", hash = "sha256:fc0a73f4db1e31d4a6d71b672a48f3af458f548059aa05e83022d5f61aac9c08"}, - {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f022a4fd2a5263a5c483a2bb165f9cb27f2be06f2f477113783efe3fe2ad887b"}, - {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363dd6f21f848301c2dcdeb3c8ae5f0dee2286a5e952a0f04954b82076f23825"}, - {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12600268763e6fec3cefe4c2dcdf79bde08d0b6dc1813887e789e495cb9f3403"}, - {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4604816adebd4faf8810782f137f8426bf45fee97d8427fa8e1e49ea78a52e2c"}, - {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:01489bbdf709d993f3058e2996f8f40fee3f0ea4d995002e5968965fa2fe89fb"}, - {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3811e31e1ac3069988f7a1c9ee7331b942e605dfc0f27330a9ea5997e965efb2"}, - {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4b967bb11baea9128ec88c3d02f55a3e338361f5e4934f5240afcb667fdaec"}, - {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9c8eda4f260072f7dbe42f473906c659dcbadd5ae6159dfb49af4da1293ae380"}, - {file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3006c44c4946583b6de24fe0632091c2653d6256b99a02a3db71ca06472ea1e4"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7c760c8a0479a4042111a8dd2f067d3ae4573da286c53f13cf6f5c53a5c1f631"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:08ce3558af5106c632baf6d331d261f02367a6bc3733086ae43c0f988fe042db"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45430dfaf1f421cf462c0dd824984378bef32b22669f2635cb809357dbaab405"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:a9bcd5f3794879e91970f2bbd7d899780541d3ff439d8f2112441769c9f2ccea"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-win32.whl", hash = "sha256:190c53f51e988dceb60472baddce3f289fa52b0ec38fbe5fd20dd1d0f795c551"}, + {file = "pycryptodome-3.19.0-cp27-cp27m-win_amd64.whl", hash = "sha256:22e0ae7c3a7f87dcdcf302db06ab76f20e83f09a6993c160b248d58274473bfa"}, + {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7822f36d683f9ad7bc2145b2c2045014afdbbd1d9922a6d4ce1cbd6add79a01e"}, + {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:05e33267394aad6db6595c0ce9d427fe21552f5425e116a925455e099fdf759a"}, + {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:829b813b8ee00d9c8aba417621b94bc0b5efd18c928923802ad5ba4cf1ec709c"}, + {file = "pycryptodome-3.19.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:fc7a79590e2b5d08530175823a242de6790abc73638cc6dc9d2684e7be2f5e49"}, + {file = "pycryptodome-3.19.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:542f99d5026ac5f0ef391ba0602f3d11beef8e65aae135fa5b762f5ebd9d3bfb"}, + {file = "pycryptodome-3.19.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:61bb3ccbf4bf32ad9af32da8badc24e888ae5231c617947e0f5401077f8b091f"}, + {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d49a6c715d8cceffedabb6adb7e0cbf41ae1a2ff4adaeec9432074a80627dea1"}, + {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e249a784cc98a29c77cea9df54284a44b40cafbfae57636dd2f8775b48af2434"}, + {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d033947e7fd3e2ba9a031cb2d267251620964705a013c5a461fa5233cc025270"}, + {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:84c3e4fffad0c4988aef0d5591be3cad4e10aa7db264c65fadbc633318d20bde"}, + {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:139ae2c6161b9dd5d829c9645d781509a810ef50ea8b657e2257c25ca20efe33"}, + {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5b1986c761258a5b4332a7f94a83f631c1ffca8747d75ab8395bf2e1b93283d9"}, + {file = "pycryptodome-3.19.0-cp35-abi3-win32.whl", hash = "sha256:536f676963662603f1f2e6ab01080c54d8cd20f34ec333dcb195306fa7826997"}, + {file = "pycryptodome-3.19.0-cp35-abi3-win_amd64.whl", hash = "sha256:04dd31d3b33a6b22ac4d432b3274588917dcf850cc0c51c84eca1d8ed6933810"}, + {file = "pycryptodome-3.19.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:8999316e57abcbd8085c91bc0ef75292c8618f41ca6d2b6132250a863a77d1e7"}, + {file = "pycryptodome-3.19.0-pp27-pypy_73-win32.whl", hash = "sha256:a0ab84755f4539db086db9ba9e9f3868d2e3610a3948cbd2a55e332ad83b01b0"}, + {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0101f647d11a1aae5a8ce4f5fad6644ae1b22bb65d05accc7d322943c69a74a6"}, + {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1601e04d32087591d78e0b81e1e520e57a92796089864b20e5f18c9564b3fa"}, + {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:506c686a1eee6c00df70010be3b8e9e78f406af4f21b23162bbb6e9bdf5427bc"}, + {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7919ccd096584b911f2a303c593280869ce1af9bf5d36214511f5e5a1bed8c34"}, + {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:560591c0777f74a5da86718f70dfc8d781734cf559773b64072bbdda44b3fc3e"}, + {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cc2f2ae451a676def1a73c1ae9120cd31af25db3f381893d45f75e77be2400"}, + {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17940dcf274fcae4a54ec6117a9ecfe52907ed5e2e438fe712fe7ca502672ed5"}, + {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d04f5f623a280fbd0ab1c1d8ecbd753193ab7154f09b6161b0f857a1a676c15f"}, + {file = "pycryptodome-3.19.0.tar.gz", hash = "sha256:bc35d463222cdb4dbebd35e0784155c81e161b9284e567e7e933d722e533331e"}, ] [[package]] name = "pygame" -version = "2.5.0" +version = "2.5.2" description = "Python Game Development" -category = "main" optional = true python-versions = ">=3.6" files = [ - {file = "pygame-2.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e34a2b5660acc298d0a66ce16f13a7ca1c56c2a685e40afef3a0cf6eaf3f44b3"}, - {file = "pygame-2.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:875dbde88b899fb7f48d6f0e87f70c3dcc8ee87a947c3df817d949a9741dbcf5"}, - {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:854dc9106210d1a3a83914af53fc234c0bed65a39f5e6098a8eb489da354ad0c"}, - {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e1898db0fd7b868a31c29204813f447c59390350fd806904d80bebde094f3f8"}, - {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d5eac9b9936c7dc2813a750bc8efd53234ad1afc32eb99d6f64bb403c2b9aa"}, - {file = "pygame-2.5.0-cp310-cp310-win32.whl", hash = "sha256:e9eed550b8921080a3c7524202822fc2cf7226e0ffd3c4e4d16510ba44b24e6f"}, - {file = "pygame-2.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:93128beb1154c443f05a66bfbf3f1d4eb8659157ab3b45e4a0454e5905440431"}, - {file = "pygame-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c71d5b3ec232113cbd2e23a19eb01eef1818db41892d0d5efbac3901f940da66"}, - {file = "pygame-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b763062b1996de26a28600e7a8f138d5b36ba0ddd63c65ccbd06124cd95bab70"}, - {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b6a42109f922352c524565fceb22bf8f8b6e4b00d38306e6f5b4c673048f4a"}, - {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bcb19c8ee3fc794ab3a7cb5b5fb1ad38da6866dfbba4af3699a84a828c8a4b9"}, - {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c66b7abc38511c7ca08c5bb58a3bfc14fa51b4e5f85a1786777afc9e584a14dd"}, - {file = "pygame-2.5.0-cp311-cp311-win32.whl", hash = "sha256:46cf1c9b20fb11c7d836c02dd5fc2ca843b699c0e2bc4130cf4ad2f855db5f7f"}, - {file = "pygame-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:f7b77b5019a9a6342535f53c75cef912b218cd24e98505828418f135aacc0a1b"}, - {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ddec0c823fd0869fe4a75ba906dcb7889db0e0c289ce8c03d4ce0a67351ab66"}, - {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bae93ce29b8337a5e02507603094c51740c9f496272ef070e2624e9647776568"}, - {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c80a1ad38d11102b4dfa0519aa2a26fea534503b259872609acc9adb1860884e"}, - {file = "pygame-2.5.0-cp312-cp312-win32.whl", hash = "sha256:8ffebcafda0add8072f82999498113be37494694fa36e02155cfaf1a0ba56fe2"}, - {file = "pygame-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:574e310ba708da0c34b71c4158aa7cdca3cf3e16c4100dcd1d3c931a9c6705b4"}, - {file = "pygame-2.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:275e4fab379620c3b262cd58c457eea80001e91bc2e04d306ddb0ba548c969bf"}, - {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c116a96a2784bd1724476dbf9c48bfea466ee493a736bdfa04ecbc3f193de0bc"}, - {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4a0787ade8723323a3ba874bb725010bb08990a77327fc85f42474f3a840447"}, - {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb39b660da1b56a1704ec4aa72bac538030786e23607fb25b8a66f357ffe3a"}, - {file = "pygame-2.5.0-cp36-cp36m-win32.whl", hash = "sha256:d051420667dd9fc8103b3cf994c03e46ee90b1c4a72c174737b8c14729ddf68e"}, - {file = "pygame-2.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6b58356510b7c38836eb81cf08983b58f280da99580d4f17e89ed0ddb707c29c"}, - {file = "pygame-2.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:80f167d8fcec7cd3107f829784ad721b1b7532c19fdf42b3aabbb51f7347850f"}, - {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef96e9a2d8fd9526b89657d192c42dd7c551bfa381fa98ec52d45443e9713818"}, - {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e21d53279fb504b267ae06b565b72d9f95ecbf1f2dd8c705329b287f38295d98"}, - {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147cc0256a5df1316590f351febf6205ef2907564fb0d902935834b91e183486"}, - {file = "pygame-2.5.0-cp37-cp37m-win32.whl", hash = "sha256:e47696154d689180e4eea8c1d6f2bac923986119219db6ad0d2e60ab1f525e8c"}, - {file = "pygame-2.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4c87fa8fa1f3ea94069119accd6d4b5fbf869c2b2954a19b45162dfb3b7c885e"}, - {file = "pygame-2.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:143550078ab10f290cd7c8715a46853e0dc598fd6cdd1561ecb4d6e3116a6b26"}, - {file = "pygame-2.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:969de806bed49b28972862acba652f05ece9420bbdf5f925c970c6a18a9fd1f9"}, - {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d3b0da31ea341b86ef96d6b13c0ddcb25f5320186b7215bc870f08119d2f80"}, - {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23effd50121468f1dc41022230485bff515154191a9d343224850aa3ed3b7f0"}, - {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce4116db6924b544ff8ff03f7ef681c8baf9c6e039a1ec21e26b4ebdaa0e994"}, - {file = "pygame-2.5.0-cp38-cp38-win32.whl", hash = "sha256:50a89c15412506d95e98792435f49a73101788db30ad9c562f611c7aa7b437fa"}, - {file = "pygame-2.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:df1d8affdbe9f417cc7141581e3d08e4b09f708060d3127d2016fd591b2e4f68"}, - {file = "pygame-2.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:99965da24d0bf138d9ac6b7494b9a12781c1510cf936616d1d0c46a736777f6a"}, - {file = "pygame-2.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:daa81057c1beb71a9fb96253457197ad03ee988ba546a166f253bd92a98a9a11"}, - {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ca85da605f6621c99c05f272a5dcf85bf0badcdca45f16ff2bee9a9d41ae042"}, - {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:603d403997d46b07022097861c8b0ff76c6192f8a2f5f89f1a6a978d4411b715"}, - {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7babaeac11544f3e4d7a15756a27f943dc5fff276481fdc9d90415a903ad31a9"}, - {file = "pygame-2.5.0-cp39-cp39-win32.whl", hash = "sha256:9d2126f91699223f0c36845d1c7b2cdfe2f1753ef85b8410ea613e8bd212ca33"}, - {file = "pygame-2.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:8062adc409f0b2742d7996b9b470494025c5e3b73d0d03e3798708dcf5d195cd"}, - {file = "pygame-2.5.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:1bd14adf6151b6ac2f617a8fd71621f1c125209c41a359d3c1cf4bf3904dba5f"}, - {file = "pygame-2.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11708f1c7b1671db15246275adcb18cf384f5f7e73532e26999968876c5099"}, - {file = "pygame-2.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6830e431575697f7a11f7731798445242e37eb07ae9007f7be33083f700e9b1e"}, - {file = "pygame-2.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7dd80addfdf7dc1f0e04f81c98acb96580726783172256f2ebc955a967e84124"}, - {file = "pygame-2.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c2cbd9d1a0a3969d6e1c6b0741279c843b4a36ef3804d324874d0a2f0e49816"}, - {file = "pygame-2.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef373b9865c740f18236f2324e17e7f2111e27c6a4a5b67c490c72a8a8b8de77"}, - {file = "pygame-2.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3959038a3e2034cee3f15471786a3bac35baeaa1f7503dc7402bb49d25b5ddbc"}, - {file = "pygame-2.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583d9c8ad033ad51da8485427139d047afb649f49e42d4fa88477f73734ad4b0"}, - {file = "pygame-2.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b650e925d2e8c82c16bdeae6e7fc5d6ca4ac659a1412da4ecd923ef9d688cb"}, - {file = "pygame-2.5.0.tar.gz", hash = "sha256:edd5745b79435976d92c0a7318aedcafcb7ac4567125ac6ba88aa473559ef9ab"}, + {file = "pygame-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da"}, + {file = "pygame-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718"}, + {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1"}, + {file = "pygame-2.5.2-cp310-cp310-win32.whl", hash = "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584"}, + {file = "pygame-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d"}, + {file = "pygame-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75"}, + {file = "pygame-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f"}, + {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f"}, + {file = "pygame-2.5.2-cp311-cp311-win32.whl", hash = "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50"}, + {file = "pygame-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42"}, + {file = "pygame-2.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8"}, + {file = "pygame-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab"}, + {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4"}, + {file = "pygame-2.5.2-cp312-cp312-win32.whl", hash = "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592"}, + {file = "pygame-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4"}, + {file = "pygame-2.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6"}, + {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945"}, + {file = "pygame-2.5.2-cp36-cp36m-win32.whl", hash = "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9"}, + {file = "pygame-2.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92"}, + {file = "pygame-2.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097"}, + {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937"}, + {file = "pygame-2.5.2-cp37-cp37m-win32.whl", hash = "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7"}, + {file = "pygame-2.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef"}, + {file = "pygame-2.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b"}, + {file = "pygame-2.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd"}, + {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda"}, + {file = "pygame-2.5.2-cp38-cp38-win32.whl", hash = "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff"}, + {file = "pygame-2.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554"}, + {file = "pygame-2.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5"}, + {file = "pygame-2.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299"}, + {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae"}, + {file = "pygame-2.5.2-cp39-cp39-win32.whl", hash = "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d"}, + {file = "pygame-2.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6"}, + {file = "pygame-2.5.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677"}, + {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4"}, + {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90"}, + {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63"}, + {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6"}, + {file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"}, ] [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = true python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "2.17.5" +version = "2.17.7" description = "python code static checker" -category = "dev" optional = false python-versions = ">=3.7.2" files = [ - {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"}, - {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"}, + {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, + {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, ] [package.dependencies] -astroid = ">=2.15.6,<=2.17.0-dev0" +astroid = ">=2.15.8,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -1157,25 +1127,26 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "9.11" +version = "10.5" description = "Extension pack for Python Markdown." -category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-9.11-py3-none-any.whl", hash = "sha256:a499191d8d869f30339de86fcf072a787e86c42b6f16f280f5c2cf174182b7f3"}, - {file = "pymdown_extensions-9.11.tar.gz", hash = "sha256:f7e86c1d3981f23d9dc43294488ecb54abadd05b0be4bf8f0e15efc90f7853ff"}, + {file = "pymdown_extensions-10.5-py3-none-any.whl", hash = "sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879"}, + {file = "pymdown_extensions-10.5.tar.gz", hash = "sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b"}, ] [package.dependencies] -markdown = ">=3.2" +markdown = ">=3.5" pyyaml = "*" +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pyqrcode" version = "1.2.1" description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output." -category = "dev" optional = false python-versions = "*" files = [ @@ -1188,34 +1159,30 @@ png = ["pypng (>=0.0.13)"] [[package]] name = "pytest" -version = "6.2.5" +version = "7.4.3" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1232,14 +1199,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.11.1" +version = "3.12.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, - {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, ] [package.dependencies] @@ -1252,7 +1218,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1267,7 +1232,6 @@ six = ">=1.5" name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1276,6 +1240,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1283,8 +1248,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1301,6 +1273,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1308,6 +1281,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1317,7 +1291,6 @@ files = [ name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1332,7 +1305,6 @@ pyyaml = "*" name = "pyzbar" version = "0.1.9" description = "Read one-dimensional barcodes and QR codes from Python 2 and 3." -category = "main" optional = true python-versions = "*" files = [ @@ -1344,12 +1316,108 @@ files = [ [package.extras] scripts = ["Pillow (>=3.2.0)"] +[[package]] +name = "regex" +version = "2023.10.3" +description = "Alternative regular expression module, to replace re." +optional = true +python-versions = ">=3.7" +files = [ + {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, + {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, + {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, + {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, + {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, + {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, + {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, + {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, + {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, + {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, + {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, + {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, + {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, + {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, + {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, + {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, + {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, + {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, + {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, + {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, + {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, + {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, + {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, + {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, + {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, + {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, + {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, + {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, + {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, + {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, + {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, + {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, +] + [[package]] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, @@ -1367,25 +1435,25 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" +name = "setuptools" +version = "69.0.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = true +python-versions = ">=3.8" files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, ] [package.extras] -idna2008 = ["idna"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1393,35 +1461,10 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1431,33 +1474,47 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.1" +version = "0.12.3" description = "Style preserving TOML library" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, - {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + +[[package]] +name = "translate" +version = "3.6.1" +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 = "*" +files = [ + {file = "translate-3.6.1-py2.py3-none-any.whl", hash = "sha256:cebfb004989d9a2ab0d24c0c5805783c7f4e07243ea4ed2a8f1809d072bf712b"}, + {file = "translate-3.6.1.tar.gz", hash = "sha256:7e70ffa46f193cc744be7c88b8e1323f10f6b2bb90d24bb5d29fdf1e56618783"}, ] +[package.dependencies] +click = "*" +libretranslatepy = "2.1.1" +lxml = "*" +requests = "*" + [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] name = "ur" version = "0.1.0" description = "UR Implementation in Python -- ported from the C++ Reference Implementation by Blockchain Commons" -category = "main" optional = false python-versions = "*" files = [] @@ -1469,19 +1526,17 @@ url = "vendor/foundation-ur-py" [[package]] name = "urllib3" -version = "2.0.4" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = true -python-versions = ">=3.7" +optional = false +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1489,7 +1544,6 @@ zstd = ["zstandard (>=0.18.0)"] name = "urtypes" version = "0.1.0" description = "Python implementation of the Blockchain Commons UR Types specification" -category = "main" optional = false python-versions = "^3.9.1" files = [] @@ -1503,7 +1557,6 @@ url = "vendor/urtypes" name = "watchdog" version = "3.0.0" description = "Filesystem events monitoring" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1541,103 +1594,96 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wrapt" -version = "1.15.0" +version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." -category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = true python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [extras] @@ -1647,4 +1693,4 @@ simulator = ["Pillow", "numpy", "opencv-python", "pygame", "pyzbar"] [metadata] lock-version = "2.0" python-versions = "^3.9.1" -content-hash = "61aec9aaffffc7193a66fbf8d873f82bd9c062accf5a1099e02fea82a84c1d39" +content-hash = "74804059f1ad9da0ddd388e482b1f65a474558e44d4c07734724896cd4108351" diff --git a/pyproject.toml b/pyproject.toml index e0cf73730..c704a07a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,39 +22,59 @@ [tool.poetry] name = "krux" -version = "23.09.1" +version = "24.04.beta13" description = "Open-source signing device firmware for Bitcoin" authors = ["Jeff S "] [tool.poetry.dependencies] python = "^3.9.1" -googletrans = "^4.0.0rc1" +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.5.2", optional = true } -mkdocs-material = { version = "^8.5.11", optional = true } +mkdocs-material = { version = "^9.4.14", optional = true } +urllib3 = "^2.1.0" # Used by mkdocs-material, forces updated secure version mkdocs-static-i18n = { version = "^0.46", optional = true } -pymdown-extensions = { version = "^9.11", optional = true } +pymdown-extensions = { version = "^10.0", optional = true } # Simulator dependencies. Optional extras numpy = { version = "^1.25.2", optional = true } opencv-python = { version = "^4.8.0.74", optional = true } -Pillow = { version = "^9.0.1", optional = true } +Pillow = { version = "^10.0.1", optional = true } pygame = { version = "^2.5.0", optional = true } pyzbar = { version = "^0.1.9", optional = true } [tool.poetry.group.dev.dependencies] black = "^23.3.0" pylint = "^2.12.2" -pytest = "^6.2.5" +pytest = "^7.4.3" pytest-cov = "^3.0.0" pytest-mock = "^3.6.1" PyQRCode = "^1.2.1" pycryptodome = "^3.17.0" +poethepoet = "^0.24.4" [tool.poetry.extras] docs = ["mkdocs", "mkdocs-material", "mkdocs-static-i18n", "pymdown-extensions"] simulator = ["numpy", "opencv-python", "Pillow", "pygame", "pyzbar"] + +[tool.poe.tasks] +# tasks for format code with black +# all tasks that start with `_` are'nt callable +format-src = "black ./src" +format-tests = "black ./tests" +format = ["format-src", "format-tests"] + +# tasks for lint code with pylint +# all tasks that start with `_` are'nt callable +lint = "pylint ./src" + +# tasks for test code +test = "poetry run pytest --cache-clear --cov src/krux --cov-report html ./tests" +test-verbose = "poetry run pytest --cache-clear --cov src/krux --cov-report html --show-capture all --capture tee-sys -r A ./tests" + +# pre commit (do formatting, linting and tests) +pre-commit = ["format", "lint", "test"] diff --git a/sign_release.py b/sign_release.py new file mode 100644 index 000000000..db40b3124 --- /dev/null +++ b/sign_release.py @@ -0,0 +1,142 @@ +# 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. + +# This script batch signs firmware releases through air-gapped interaction with +# a Krux signing device. +# Load a 24 words mnemonic in testnet to sign the firmware hashes as messages. + +import os +import cv2 +import hashlib +from io import StringIO +import base64 +from qrcode import QRCode +import subprocess + +DEVICES = [ + "maixpy_m5stickv", + "maixpy_amigo_ips", + "maixpy_amigo_tft", + "maixpy_bit", + "maixpy_dock", + "maixpy_yahboom", +] + + +def release_folder(): + """Returns release folder specified on pyproject.toml""" + with open("pyproject.toml", "r") as project_file: + lines = project_file.readlines() + for line in lines: + if line.startswith("version"): + version = line.split('"')[-2] + release_folder_name = "krux-v" + version + print("Signing firmwares in:", release_folder_name) + return release_folder_name + + +def print_qr_code(data): + """Prints ascii QR code""" + qr_code = QRCode() + qr_code.add_data(data) + qr_string = StringIO() + qr_code.print_ascii(out=qr_string, invert=True) + print(qr_string.getvalue()) + + +def scan(): + """Opens a scan window and uses cv2 to detect and decode a QR code, returning its data""" + vid = cv2.VideoCapture(0) + detector = cv2.QRCodeDetector() + qr_data = None + while True: + # Capture the video frame by frame + _, frame = vid.read() + qr_data, _, _ = detector.detectAndDecode(frame) + if len(qr_data) > 0: + break + # Display the resulting frame + cv2.imshow("frame", frame) + # the 'q' button is set as the + # quitting button you may use any + # desired button of your choice + if cv2.waitKey(1) & 0xFF == ord("q"): + break + # After the loop release the cap object + vid.release() + # Destroy all the windows + cv2.destroyAllWindows() + return qr_data + + +def verify(file_to_verify, key_to_verify, signature_to_verify): + """Uses openssl to verify the signature and public key""" + print("Verifying signature of",file_to_verify.split("/")[1] ) + verification = subprocess.run( + "openssl sha256 <%s -binary | openssl pkeyutl -verify -pubin -inkey %s -sigfile %s" + % (file_to_verify, key_to_verify, signature_to_verify), + shell=True, + capture_output=True, + text=True, + check=True + ) + print(verification.stdout) + + +# Main routine +folder = release_folder() +for device in DEVICES: + print("Signing", device) + file_name = os.path.join(folder, device, "firmware.bin") + hash_string = "" + try: + with open(file_name, "rb") as f: + sig_bytes = f.read() # read file as bytes + hash_string = hashlib.sha256(sig_bytes).hexdigest() + except: + print("Unable to read target file") + # Prints the hash of the file + print("Hash of", file_name, ":") + print(hash_string + "\n") + + # Prints the QR code + print_qr_code(hash_string) + + # Scans the signature QR code + _ = input("Press enter to scan signature") + signature = scan() + binary_signature = base64.b64decode(signature.encode()) + # Prints signature + print("Signature:", signature) + # Saves a signature file + signature_file = os.path.join(folder, device, "firmware.bin.sig") + + print("Saving a signature file:", signature_file, "\n\n") + with open(signature_file, "wb") as f: + f.write(binary_signature) + +# Verify signatures +PUBLIC_KEY_FILE = "odudex.PEM" # "selfcustody.PEM" +for device in DEVICES: + file_name = os.path.join(folder, device, "firmware.bin") + signature_file = os.path.join(folder, device, "firmware.bin.sig") + verify(file_name, PUBLIC_KEY_FILE, signature_file) diff --git a/simulator/assets/maixpy_dock.png b/simulator/assets/maixpy_dock.png new file mode 100644 index 000000000..985f47c43 Binary files /dev/null and b/simulator/assets/maixpy_dock.png differ diff --git a/simulator/kruxsim/devices.py b/simulator/kruxsim/devices.py index 6df384c20..cbbbdeac8 100644 --- a/simulator/kruxsim/devices.py +++ b/simulator/kruxsim/devices.py @@ -33,7 +33,7 @@ AMIGO_IPS: (480, 768), AMIGO_TFT: (480, 768), PC: (480, 640), - DOCK: (440, 640), + DOCK: (440, 820), } @@ -48,8 +48,6 @@ def load_image(device): device = with_prefix(device) if device == PC: return None - if device == DOCK: - return None if device not in images: images[device] = pg.image.load( os.path.join("assets", "%s.png" % device) diff --git a/simulator/kruxsim/mocks/Maix.py b/simulator/kruxsim/mocks/Maix.py index 1ba92e2ea..6b446b111 100644 --- a/simulator/kruxsim/mocks/Maix.py +++ b/simulator/kruxsim/mocks/Maix.py @@ -29,13 +29,6 @@ PRESSED = 0 RELEASED = 1 -sequence_executor = None - - -def register_sequence_executor(s): - global sequence_executor - sequence_executor = s - class GPIO: IN = 0 @@ -83,6 +76,7 @@ class GPIO: GPIO5 = 37 GPIO6 = 38 GPIO7 = 39 + IRQ_FALLING = 0 def __init__(self, gpio_num, dir=None, val=None): self.key = None @@ -96,27 +90,12 @@ def __init__(self, gpio_num, dir=None, val=None): def value(self, val=1): if not self.key: - return 1 - if ( - sequence_executor - and sequence_executor.key is not None - and sequence_executor.key == self.key - ): - sequence_executor.key_checks += 1 - # wait for release - if sequence_executor.key_checks == 1: - return RELEASED - # wait for press - # if pressed - elif sequence_executor.key_checks == 2 or sequence_executor.key_checks == 3: - return PRESSED - # released - elif sequence_executor.key_checks == 4: - sequence_executor.key = None - sequence_executor.key_checks = 0 - return RELEASED + return RELEASED return PRESSED if pg.key.get_pressed()[self.key] else RELEASED + def irq(self, pin, mode): + pass + if "Maix" not in sys.modules: sys.modules["Maix"] = mock.MagicMock( diff --git a/simulator/kruxsim/mocks/board.py b/simulator/kruxsim/mocks/board.py index 0666ca3e2..613d10203 100644 --- a/simulator/kruxsim/mocks/board.py +++ b/simulator/kruxsim/mocks/board.py @@ -58,8 +58,10 @@ def register_device(device): BUTTON_A = BOARD_CONFIG["krux"]["pins"]["BUTTON_A"] if "ENCODER" in BOARD_CONFIG["krux"]["pins"]: + # replace encoder with regular buttons in simulator del BOARD_CONFIG["krux"]["pins"]["ENCODER"] BOARD_CONFIG["krux"]["pins"]["BUTTON_B"] = 37 + BOARD_CONFIG["krux"]["pins"]["BUTTON_C"] = 38 BUTTON_B = BOARD_CONFIG["krux"]["pins"]["BUTTON_B"] if "BUTTON_C" in BOARD_CONFIG["krux"]["pins"]: BUTTON_C = BOARD_CONFIG["krux"]["pins"]["BUTTON_C"] diff --git a/simulator/kruxsim/mocks/buttons.py b/simulator/kruxsim/mocks/buttons.py new file mode 100644 index 000000000..dc6d54a45 --- /dev/null +++ b/simulator/kruxsim/mocks/buttons.py @@ -0,0 +1,175 @@ +# 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 Maix import GPIO +from fpioa_manager import fm +import pygame as pg + +PRESSED = 0 +RELEASED = 1 + + +class TactileButtons: + """Interface with Buttons""" + + def __init__(self): + self.enter = None + self.page = None + self.page_prev = None + self.enter_event_flag = False + self.page_event_flag = False + self.page_prev_event_flag = False + + def init_enter(self, pin): + """Register ENTER button IO""" + fm.register(pin, fm.fpioa.GPIOHS21) + self.enter = GPIO(GPIO.GPIOHS21, GPIO.IN, GPIO.PULL_UP) + + def init_page(self, pin): + """Register PAGE button IO""" + fm.register(pin, fm.fpioa.GPIOHS22) + self.page = GPIO(GPIO.GPIOHS22, GPIO.IN, GPIO.PULL_UP) + + def init_page_prev(self, pin): + """Register PAGE_PREV button IO""" + fm.register(pin, fm.fpioa.GPIOHS0) + self.page_prev = GPIO(GPIO.GPIOHS0, GPIO.IN, GPIO.PULL_UP) + + +buttons_control = TactileButtons() # Singleton + +sequence_executor = None + +def register_sequence_executor(s): + global sequence_executor + sequence_executor = s + +class Button: + """Generic button handler format""" + + def __init__(self) -> None: + pass + + def value(self): + """Returns IO state""" + return RELEASED + + def event(self): + """Returns event state""" + return False + + +class ButtonEnter: + """Class to manage button ENTER state and events""" + + def __init__(self, pin) -> None: + buttons_control.init_enter(pin) + + def value(self): + """Returns the ENTER IO state""" + if buttons_control.enter is not None: + return buttons_control.enter.value() + return RELEASED + + def event(self): + """Returns the ENTER event state""" + if buttons_control.enter is not None: + + if ( + sequence_executor + and sequence_executor.key is not None + and sequence_executor.key == pg.K_RETURN + ): + sequence_executor.key = None + return True + if buttons_control.enter_event_flag: + buttons_control.enter_event_flag = False + return True + return False + + +class ButtonPage: + """Class to manage button PAGE state and events""" + + def __init__(self, pin) -> None: + buttons_control.init_page(pin) + + def value(self): + """Returns the PAGE IO state""" + if buttons_control.page is not None: + return buttons_control.page.value() + return RELEASED + + def event(self): + """Returns the PAGE event state""" + if buttons_control.page is not None: + if ( + sequence_executor + and sequence_executor.key is not None + and sequence_executor.key == pg.K_DOWN + ): + sequence_executor.key = None + return True + if buttons_control.page_event_flag: + buttons_control.page_event_flag = False + return True + return False + + +class ButtonPagePrev: + """Class to manage button PAGE_PREV state and events""" + + def __init__(self, pin) -> None: + buttons_control.init_page_prev(pin) + + def value(self): + """Returns the PAGE_PREV IO state""" + if buttons_control.page_prev is not None: + return buttons_control.page_prev.value() + return RELEASED + + def event(self): + """Returns the PAGE_PREV event state""" + if buttons_control.page_prev is not None: + if ( + sequence_executor + and sequence_executor.key is not None + and sequence_executor.key == pg.K_UP + ): + sequence_executor.key = None + return True + if buttons_control.page_prev_event_flag: + buttons_control.page_prev_event_flag = False + return True + return False + + +if "krux.buttons" not in sys.modules: + sys.modules["krux.buttons"] = mock.MagicMock( + TactileButtons=TactileButtons, + ButtonEnter=ButtonEnter, + ButtonPage=ButtonPage, + ButtonPagePrev=ButtonPagePrev, + PRESSED=PRESSED, + RELEASED=RELEASED, + ) diff --git a/simulator/kruxsim/mocks/ft6x36.py b/simulator/kruxsim/mocks/ft6x36.py index 21dc08d9f..3fe6b198b 100644 --- a/simulator/kruxsim/mocks/ft6x36.py +++ b/simulator/kruxsim/mocks/ft6x36.py @@ -19,9 +19,7 @@ # 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 imp import sys -import time from unittest import mock import pygame as pg from . import lcd @@ -34,45 +32,49 @@ def register_sequence_executor(s): sequence_executor = s -def to_screen_pos(pos): - if lcd.screen: - rect = lcd.screen.get_rect() - rect.center = pg.display.get_surface().get_rect().center - if rect.collidepoint(pos): - out = pos[0] - rect.left, pos[1] - rect.top - return out - return None - - class FT6X36: def __init__(self): + self.event_flag = False + + def to_screen_pos(self, pos): + if lcd.screen: + rect = lcd.screen.get_rect() + rect.center = pg.display.get_surface().get_rect().center + if rect.collidepoint(pos): + out = pos[0] - rect.left, pos[1] - rect.top + return out + return None + + def activate_irq(self, irq_pin): pass def current_point(self): + return ( + self.to_screen_pos(pg.mouse.get_pos()) + if pg.mouse.get_pressed()[0] + else None + ) + + def trigger_event(self): + self.event_flag = True + self.irq_point = self.current_point() + + def event(self): if sequence_executor and sequence_executor.touch_pos is not None: - sequence_executor.touch_checks += 1 - # wait for release - if sequence_executor.touch_checks == 1: - return None - # wait for press - # if pressed - elif ( - sequence_executor.touch_checks == 2 - or sequence_executor.touch_checks == 3 - ): - return sequence_executor.touch_pos - # released - elif sequence_executor.touch_checks == 4: - sequence_executor.touch_pos = None - sequence_executor.touch_checks = 0 - return None - return to_screen_pos(pg.mouse.get_pos()) if pg.mouse.get_pressed()[0] else None + sequence_executor.touch_pos = None + return True + flag = self.event_flag + self.event_flag = False # Always clean event flag + return flag def threshold(self, value): pass +touch_control = FT6X36() + + if "krux.touchscreens.ft6x36" not in sys.modules: sys.modules["krux.touchscreens.ft6x36"] = mock.MagicMock( - FT6X36=FT6X36, + touch_control=touch_control, ) diff --git a/simulator/kruxsim/mocks/lcd.py b/simulator/kruxsim/mocks/lcd.py index 555ff5436..5e0772f27 100644 --- a/simulator/kruxsim/mocks/lcd.py +++ b/simulator/kruxsim/mocks/lcd.py @@ -21,6 +21,7 @@ # THE SOFTWARE. import sys import os +import math from unittest import mock import pygame as pg import cv2 @@ -174,6 +175,50 @@ def run(): light_color = rgb565torgb888(light_color) pg.event.post(pg.event.Event(events.LCD_DRAW_QR_CODE_EVENT, {"f": run})) +def draw_qr_code_binary(offset_y, code_bin, max_width, dark_color, light_color, background): + def run(): + starting_size = int(math.sqrt(len(code_bin) * 8)) + block_size_divisor = starting_size + 2; # adds 2 to create room for a 1 block border + scale = max_width // block_size_divisor + width = starting_size * scale + border_size = (max_width - width) // 2 + opposite_border_offset = border_size + width - 1 + # Top border + for rx in range(max_width): + for ry in range(border_size): + screen.set_at((rx, ry),light_color) + + # Bottom border + for rx in range(max_width): + for ry in range(opposite_border_offset, max_width): + screen.set_at((rx, ry),light_color) + + # Left border + for rx in range(border_size): + for ry in range(border_size, opposite_border_offset): + screen.set_at((rx, ry),light_color) + + # Right border + for rx in range(opposite_border_offset, max_width): + for ry in range(border_size, opposite_border_offset): + screen.set_at((rx, ry),light_color) + # QR code rendering + for og_y in range(starting_size): + for og_x in range(starting_size): + og_yx_index = og_y * starting_size + og_x + color_byte = code_bin[og_yx_index >> 3] + color_byte &= (1 << (og_yx_index % 8)) + color = dark_color if color_byte else light_color + for i in range(scale): + y = border_size + og_y * scale + i + for j in range(scale): + x = border_size + og_x * scale + j + screen.set_at((x, y),color) + + dark_color = rgb565torgb888(dark_color) + light_color = rgb565torgb888(light_color) + pg.event.post(pg.event.Event(events.LCD_DRAW_QR_CODE_EVENT, {"f": run})) + def fill_rectangle(x, y, w, h, color): def run(): @@ -205,6 +250,7 @@ def run(): height=height, draw_string=draw_string, draw_qr_code=draw_qr_code, + draw_qr_code_binary=draw_qr_code_binary, fill_rectangle=fill_rectangle, BLACK=COLOR_BLACK, WHITE=COLOR_WHITE, diff --git a/simulator/kruxsim/mocks/machine.py b/simulator/kruxsim/mocks/machine.py index f5197128f..21f1a2683 100644 --- a/simulator/kruxsim/mocks/machine.py +++ b/simulator/kruxsim/mocks/machine.py @@ -30,7 +30,7 @@ def simulate_printer(): global simulating_printer simulating_printer = True - Settings().printer.driver = "thermal/adafruit" + Settings().hardware.printer.driver = "thermal/adafruit" def reset(): @@ -49,14 +49,14 @@ def __init__(self, pin, baudrate): def read(self, num_bytes): if simulating_printer: - module, cls = PrinterSettings.PRINTERS[Settings().printer.driver] + module, cls = PrinterSettings.PRINTERS[Settings().hardware.printer.driver] if module == "thermal" and cls == "AdafruitPrinter": return chr(0b00000000) return None def readline(self): if simulating_printer: - module, cls = PrinterSettings.PRINTERS[Settings().printer.driver] + module, cls = PrinterSettings.PRINTERS[Settings().hardware.printer.driver] if module == "cnc" and cls == "FilePrinter": return "ok\n".encode() return None diff --git a/simulator/kruxsim/mocks/pmu.py b/simulator/kruxsim/mocks/pmu.py index 183245ebc..9493b465b 100644 --- a/simulator/kruxsim/mocks/pmu.py +++ b/simulator/kruxsim/mocks/pmu.py @@ -38,46 +38,51 @@ def register_sequence_executor(s): class PMU_Button: def __init__(self): self.key = pg.K_UP + self.state = RELEASED def value(self): - if ( - sequence_executor - and sequence_executor.key is not None - and sequence_executor.key == self.key - ): - sequence_executor.key_checks += 1 - # wait for release - if sequence_executor.key_checks == 1: - return RELEASED - # wait for press - # if pressed - elif sequence_executor.key_checks == 2 or sequence_executor.key_checks == 3: - return PRESSED - # released - elif sequence_executor.key_checks == 4: + return PRESSED if pg.key.get_pressed()[self.key] else RELEASED + + def event(self): + if self.state == RELEASED: + if ( + sequence_executor + and sequence_executor.key is not None + and sequence_executor.key == pg.K_UP + ): sequence_executor.key = None - sequence_executor.key_checks = 0 - return RELEASED - return 0 if pg.key.get_pressed()[self.key] else 1 + return True + if self.value() == PRESSED: + self.state = PRESSED + return True + self.state = self.value() + return False -class Battery: - def getVbatVoltage(self): +class PMUController: + def __init__(self, i2c_bus): + pass + + def get_battery_voltage(self): return 3400 - def getUSBVoltage(self): + def get_usb_voltage(self): return 0 - def enablePMICSleepMode(self, val): + def enable_pek_button_monitor(self, val): pass - def setEnterSleepMode(self): + def enter_sleep_mode(self): pass + def enable_adcs(self, on_off): + pass + + def set_screen_brightness(self, level): + pass if "pmu" not in sys.modules: sys.modules["pmu"] = mock.MagicMock( PMU_Button=PMU_Button, - axp192=Battery, - axp173=Battery, + PMUController=PMUController, ) diff --git a/simulator/kruxsim/mocks/qrcode.py b/simulator/kruxsim/mocks/qrcode.py index b727de39e..61adb39e9 100644 --- a/simulator/kruxsim/mocks/qrcode.py +++ b/simulator/kruxsim/mocks/qrcode.py @@ -23,38 +23,33 @@ from unittest import mock import pyqrcode +def encode(data): + # Uses string encoded qr as it already cleaned up the frames + # PyQRcode also doesn't offer any binary output -def encode_to_string(data): try: - code_str = pyqrcode.create(data, error="L", mode="binary").text() + code_str = pyqrcode.create(data, error="L", mode="binary").text(quiet_zone=0) except: # pre-decode if binary (SeedQR) data = data.decode("latin-1") - code_str = pyqrcode.create(data, error="L", mode="binary").text() + code_str = pyqrcode.create(data, error="L", mode="binary").text(quiet_zone=0) size = 0 while code_str[size] != "\n": size += 1 - i = 0 - padding = 0 - while code_str[i] != "1": - if code_str[i] == "\n": - padding += 1 - i += 1 - code_str = code_str[(padding) * (size + 1) : -(padding) * (size + 1)] - size -= 2 * padding - - new_code_str = "" - for i in range(size): - for j in range(size + 2 * padding + 1): - if padding <= j < size + padding: - index = i * (size + 2 * padding + 1) + j - new_code_str += code_str[index] - new_code_str += "\n" - - return new_code_str + binary_qr = bytearray(b"\x00" * ((size * size + 7) // 8)) + for y in range(size): + for x in range(size): + bit_index = y * size + x + bit_string_index = y * (size + 1) + x + if code_str[bit_string_index] == "1": + binary_qr[bit_index>>3] |= 1 << (bit_index % 8) + return binary_qr + + + if "qrcode" not in sys.modules: sys.modules["qrcode"] = mock.MagicMock( - encode_to_string=encode_to_string, + encode = encode, ) diff --git a/simulator/kruxsim/mocks/rotary.py b/simulator/kruxsim/mocks/rotary.py index 00ff8e383..c04127bb8 100644 --- a/simulator/kruxsim/mocks/rotary.py +++ b/simulator/kruxsim/mocks/rotary.py @@ -22,6 +22,7 @@ import sys from unittest import mock + class RotaryEncoder: def __init__(self): self.debounce = 0 diff --git a/simulator/kruxsim/sequence.py b/simulator/kruxsim/sequence.py index f6187bb81..f4f541350 100644 --- a/simulator/kruxsim/sequence.py +++ b/simulator/kruxsim/sequence.py @@ -28,6 +28,7 @@ from kruxsim.mocks.board import BOARD_CONFIG COMMANDS = ["press", "touch", "qrcode", "screenshot", "wait", "include", "x"] +THREAD_PERIOD = 0.1 class SequenceExecutor: @@ -50,9 +51,9 @@ def __init__(self, sequence_filepath): def execute(self): if self.command_fn: - if time.time() - self.command_timer > 0.1: + if time.time() > self.command_timer + THREAD_PERIOD: print("Executing (%s, %r)" % (self.command, self.command_params)) - self.command_timer = 0 + self.command_timer = time.time() self.command_fn() self.command_fn = None self.command_params = [] diff --git a/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt b/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt index 2123b108f..67fe6743d 100644 --- a/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt +++ b/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt @@ -3,9 +3,6 @@ include _wait-for-logo.txt # Navigate to via QR x3 press BUTTON_A -# Prompt -press BUTTON_A - qrcode 12-word-mnemonic.png wait 0.5 diff --git a/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt b/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt index a1311d4ac..dc5fbcdfc 100644 --- a/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt +++ b/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt @@ -3,9 +3,6 @@ include _wait-for-logo.txt # Navigate to via QR x3 press BUTTON_A -# Prompt -press BUTTON_A - qrcode 24-word-mnemonic.png wait 0.5 diff --git a/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt b/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt index 2123b108f..67fe6743d 100644 --- a/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt +++ b/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt @@ -3,9 +3,6 @@ include _wait-for-logo.txt # Navigate to via QR x3 press BUTTON_A -# Prompt -press BUTTON_A - qrcode 12-word-mnemonic.png wait 0.5 diff --git a/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt b/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt index 832ec34e4..c798eca0a 100644 --- a/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt +++ b/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt @@ -3,9 +3,6 @@ include _wait-for-logo.txt # Navigate to via QR x3 press BUTTON_A -# Prompt -press BUTTON_A - qrcode 24-word-mnemonic.png wait 0.5 diff --git a/simulator/simulator.py b/simulator/simulator.py index 547ea30f3..db4a7ac95 100644 --- a/simulator/simulator.py +++ b/simulator/simulator.py @@ -99,24 +99,23 @@ from kruxsim.mocks import secp256k1 from kruxsim.mocks import qrcode from kruxsim.mocks import sensor - from kruxsim.mocks import ft6x36 -from kruxsim.sequence import SequenceExecutor - +from kruxsim.mocks import buttons from kruxsim.mocks import rotary +from kruxsim.sequence import SequenceExecutor sequence_executor = None if args.sequence: sequence_executor = SequenceExecutor(args.sequence) -Maix.register_sequence_executor(sequence_executor) +buttons.register_sequence_executor(sequence_executor) pmu.register_sequence_executor(sequence_executor) sensor.register_sequence_executor(sequence_executor) ft6x36.register_sequence_executor(sequence_executor) def run_krux(): - with open("../src/boot.py") as boot_file: + with open("../src/boot.py", "r", encoding='utf-8') as boot_file: exec(boot_file.read()) @@ -202,6 +201,16 @@ def shutdown(): ) else: event.dict["f"]() + if event.type == pg.KEYDOWN: + if event.key == pg.K_RETURN: + buttons.buttons_control.enter_event_flag = True + if event.key == pg.K_DOWN: + buttons.buttons_control.page_event_flag = True + if event.key == pg.K_UP: + buttons.buttons_control.page_prev_event_flag = True + if event.type == pg.MOUSEBUTTONDOWN: + ft6x36.touch_control.trigger_event() + if lcd.screen: lcd_rect = lcd.screen.get_rect() diff --git a/src/boot.py b/src/boot.py index 0d8071c4c..48ac45032 100644 --- a/src/boot.py +++ b/src/boot.py @@ -31,38 +31,33 @@ from krux.power import power_manager MIN_SPLASH_WAIT_TIME = 1000 +SPLASH = """ +β–ˆβ–ˆ +β–ˆβ–ˆ +β–ˆβ–ˆ +β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ +β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ +β–ˆβ–ˆ β–ˆβ–ˆ +β–ˆβ–ˆβ–ˆβ–ˆ +β–ˆβ–ˆ β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ +"""[ + 1:-1 +].split( + "\n" +) -def splash(): +def splash(logo): """Display splash while loading modules""" from krux.display import Display - SPLASH = """ - - - - β–ˆβ–ˆ - β–ˆβ–ˆ - β–ˆβ–ˆ - β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ - β–ˆβ–ˆ - β–ˆβ–ˆ β–ˆβ–ˆ - β–ˆβ–ˆ β–ˆβ–ˆ - β–ˆβ–ˆβ–ˆβ–ˆ - β–ˆβ–ˆ β–ˆβ–ˆ - β–ˆβ–ˆ β–ˆβ–ˆ - β–ˆβ–ˆ β–ˆβ–ˆ - - - -"""[ - 1:-1 - ] - disp = Display() disp.initialize_lcd() disp.clear() - disp.draw_centered_text(SPLASH.split("\n")) + disp.draw_centered_text(logo) def check_for_updates(): @@ -112,13 +107,13 @@ def home(ctx_home): preimport_ticks = time.ticks_ms() -splash() +splash(SPLASH) check_for_updates() gc.collect() from krux.context import Context -ctx = Context() +ctx = Context(SPLASH) ctx.power_manager = power_manager postimport_ticks = time.ticks_ms() diff --git a/src/krux/buttons.py b/src/krux/buttons.py new file mode 100644 index 000000000..97484ff2f --- /dev/null +++ b/src/krux/buttons.py @@ -0,0 +1,153 @@ +# 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. + +from Maix import GPIO +from fpioa_manager import fm + +PRESSED = 0 +RELEASED = 1 + + +def __handler__(pin_num=None): + # pylint: disable=unused-argument + """GPIO interrupt handler""" + buttons_control.event_handler(pin_num) + + +class TactileButtons: + """Interface with Buttons""" + + def __init__(self): + self.enter = None + self.page = None + self.page_prev = None + self.enter_event_flag = False + self.page_event_flag = False + self.page_prev_event_flag = False + + def event_handler(self, pin_num): + """Set up a button event flag according to interruption source""" + if pin_num == buttons_control.enter: + self.enter_event_flag = True + elif pin_num == buttons_control.page: + self.page_event_flag = True + elif pin_num == buttons_control.page_prev: + self.page_prev_event_flag = True + + def init_enter(self, pin): + """Register ENTER button IO""" + fm.register(pin, fm.fpioa.GPIOHS21) + self.enter = GPIO(GPIO.GPIOHS21, GPIO.IN, GPIO.PULL_UP) + self.enter.irq(__handler__, GPIO.IRQ_FALLING) + + def init_page(self, pin): + """Register PAGE button IO""" + fm.register(pin, fm.fpioa.GPIOHS22) + self.page = GPIO(GPIO.GPIOHS22, GPIO.IN, GPIO.PULL_UP) + self.page.irq(__handler__, GPIO.IRQ_FALLING) + + def init_page_prev(self, pin): + """Register PAGE_PREV button IO""" + fm.register(pin, fm.fpioa.GPIOHS0) + self.page_prev = GPIO(GPIO.GPIOHS0, GPIO.IN, GPIO.PULL_UP) + self.page_prev.irq(__handler__, GPIO.IRQ_FALLING) + + +buttons_control = TactileButtons() # Singleton + + +class Button: + """Generic button handler format""" + + def __init__(self) -> None: + pass + + def value(self): + """Returns IO state""" + return RELEASED + + def event(self): + """Returns event state""" + return False + + +class ButtonEnter: + """Class to manage button ENTER state and events""" + + def __init__(self, pin) -> None: + buttons_control.init_enter(pin) + + def value(self): + """Returns the ENTER IO state""" + if buttons_control.enter is not None: + return buttons_control.enter.value() + return RELEASED + + def event(self): + """Returns the ENTER event state""" + if buttons_control.enter is not None: + if buttons_control.enter_event_flag: + buttons_control.enter_event_flag = False + return True + return False + + +class ButtonPage: + """Class to manage button PAGE state and events""" + + def __init__(self, pin) -> None: + buttons_control.init_page(pin) + + def value(self): + """Returns the PAGE IO state""" + if buttons_control.page is not None: + return buttons_control.page.value() + return RELEASED + + def event(self): + """Returns the PAGE event state""" + if buttons_control.page is not None: + if buttons_control.page_event_flag: + buttons_control.page_event_flag = False + return True + return False + + +class ButtonPagePrev: + """Class to manage button PAGE_PREV state and events""" + + def __init__(self, pin) -> None: + buttons_control.init_page_prev(pin) + + def value(self): + """Returns the PAGE_PREV IO state""" + if buttons_control.page_prev is not None: + return buttons_control.page_prev.value() + return RELEASED + + def event(self): + """Returns the PAGE_PREV event state""" + if buttons_control.page_prev is not None: + if buttons_control.page_prev_event_flag: + buttons_control.page_prev_event_flag = False + return True + return False diff --git a/src/krux/camera.py b/src/krux/camera.py index 722efdd9e..910f69427 100644 --- a/src/krux/camera.py +++ b/src/krux/camera.py @@ -36,29 +36,37 @@ class Camera: """Camera is a singleton interface for interacting with the device's camera""" def __init__(self): + self.initialized = False self.cam_id = None self.antiglare_enabled = False self.initialize_sensor() def initialize_sensor(self, grayscale=False): """Initializes the camera""" - sensor.reset(freq=16000000) + self.initialized = False + self.antiglare_enabled = False self.cam_id = sensor.get_id() + if self.cam_id == OV7740_ID: + sensor.reset(freq=18200000) + else: + sensor.reset() if grayscale: sensor.set_pixformat(sensor.GRAYSCALE) else: sensor.set_pixformat(sensor.RGB565) if self.cam_id == OV5642_ID: - # CIF mode will use central pixels and discard darker periphery - sensor.set_framesize(sensor.CIF) sensor.set_hmirror(1) if self.cam_id == OV2640_ID: - sensor.set_framesize(sensor.CIF) sensor.set_vflip(1) + if board.config["type"] == "bit": + # CIF mode will use central pixels and discard darker periphery + sensor.set_framesize(sensor.CIF) else: sensor.set_framesize(sensor.QVGA) if self.cam_id == OV7740_ID: self.config_ov_7740() + if self.cam_id == OV2640_ID: + self.config_ov_2640() sensor.skip_frames() def config_ov_7740(self): @@ -80,48 +88,90 @@ def config_ov_7740(self): # Regions 13,14,15,16 sensor.__write_reg(0x59, 0x0) # pylint: disable=W0212 + def config_ov_2640(self): + """Specialized config for OV2640 sensor""" + # Set register bank 0 + sensor.__write_reg(0xFF, 0x00) # pylint: disable=W0212 + # Enable AEC + sensor.__write_reg(0xC2, 0x8C) # pylint: disable=W0212 + # Set register bank 1 + sensor.__write_reg(0xFF, 0x01) # pylint: disable=W0212 + sensor.__write_reg(0x03, 0xCF) # pylint: disable=W0212 + # Allowed luminance thresholds: + # luminance high threshold, default=0x78 + sensor.__write_reg(0x24, 0x70) # pylint: disable=W0212 + # luminance low threshold, default=0x68 + sensor.__write_reg(0x25, 0x60) # pylint: disable=W0212 + + # Average-based sensing window definition + # Ingnore periphery and measure luminance only on central area + # Regions 1,2,3,4 + sensor.__write_reg(0x5D, 0xFF) # pylint: disable=W0212 + # Regions 5,6,7,8 + sensor.__write_reg(0x5E, 0b11000011) # pylint: disable=W0212 + # Regions 9,10,11,12 + sensor.__write_reg(0x5F, 0b11000011) # pylint: disable=W0212 + # Regions 13,14,15,16 + sensor.__write_reg(0x60, 0xFF) # pylint: disable=W0212 + def has_antiglare(self): """Returns whether the camera has anti-glare functionality""" - return self.cam_id == OV7740_ID + return self.cam_id in (OV7740_ID, OV2640_ID) def enable_antiglare(self): """Enables anti-glare mode""" - if self.cam_id == OV7740_ID: + if self.cam_id == OV2640_ID: + # Set register bank 1 + sensor.__write_reg(0xFF, 0x01) # pylint: disable=W0212 + # luminance high level, default=0x78 + sensor.__write_reg(0x24, 0x28) # pylint: disable=W0212 + else: # luminance high level, default=0x78 sensor.__write_reg(0x24, 0x38) # pylint: disable=W0212 - # luminance low level, default=0x68 - sensor.__write_reg(0x25, 0x20) # pylint: disable=W0212 + # luminance low level, default=0x68 + sensor.__write_reg(0x25, 0x20) # pylint: disable=W0212 + if self.cam_id == OV7740_ID: # Disable frame integrtation (night mode) sensor.__write_reg(0x15, 0x00) # pylint: disable=W0212 - sensor.skip_frames() - self.antiglare_enabled = True + sensor.skip_frames() + self.antiglare_enabled = True def disable_antiglare(self): """Disables anti-glare mode""" - if self.cam_id == OV7740_ID: - # luminance high level, default=0x78 - sensor.__write_reg(0x24, 0x70) # pylint: disable=W0212 - # luminance low level, default=0x68 - sensor.__write_reg(0x25, 0x60) # pylint: disable=W0212 - sensor.skip_frames() - self.antiglare_enabled = False + if self.cam_id == OV2640_ID: + # Set register bank 1 + sensor.__write_reg(0xFF, 0x01) # pylint: disable=W0212 + # luminance high level, default=0x78 + sensor.__write_reg(0x24, 0x70) # pylint: disable=W0212 + # luminance low level, default=0x68 + sensor.__write_reg(0x25, 0x60) # pylint: disable=W0212 + sensor.skip_frames() + self.antiglare_enabled = False def snapshot(self): """Helper to take a customized snapshot from sensor""" img = sensor.snapshot() - if self.cam_id in (OV2640_ID, OV5642_ID): - img.lens_corr(strength=1.1, zoom=0.96) - if self.cam_id == OV2640_ID: + if board.config["type"] == "bit": + img.lens_corr(strength=1.1) img.rotation_corr(z_rotation=180) return img + def initialize_run(self): + """Initializes and runs sensor""" + self.initialize_sensor() + sensor.run(1) + + def stop_sensor(self): + """Stops capturing from sensor""" + gc.collect() + sensor.run(0) + def capture_qr_code_loop(self, callback): """Captures either singular or animated QRs and parses their contents until all parts of the message have been captured. The part data are then ordered and assembled into one message and returned. """ - self.initialize_sensor() - sensor.run(1) + self.initialize_run() parser = QRPartParser() @@ -130,6 +180,10 @@ def capture_qr_code_loop(self, callback): while True: wdt.feed() command = callback(parser.total_count(), parser.parsed_count(), new_part) + if not self.initialized: + # Ignores first callback as it may contain unintentional events + self.initialized = True + command = 0 if command == 1: break new_part = False @@ -151,14 +205,13 @@ def capture_qr_code_loop(self, callback): parser.parse(data) - if parser.parsed_count() > prev_parsed_count: - prev_parsed_count = parser.parsed_count() + if parser.processed_parts_count() > prev_parsed_count: + prev_parsed_count = parser.processed_parts_count() new_part = True if parser.is_complete(): break - gc.collect() - sensor.run(0) + self.stop_sensor() if parser.is_complete(): return (parser.result(), parser.format) @@ -168,8 +221,7 @@ def capture_entropy(self, callback): """Captures camera's entropy as the hash of image buffer""" import hashlib - self.initialize_sensor() - sensor.run(1) + self.initialize_run() command = 0 while True: @@ -178,6 +230,10 @@ def capture_entropy(self, callback): img = self.snapshot() command = callback() + if not self.initialized: + # Ignores first callback as it may contain unintentional events + self.initialized = True + command = 0 if command > 0: break @@ -185,8 +241,7 @@ def capture_entropy(self, callback): img.lens_corr(strength=1.0, zoom=0.56) lcd.display(img) - gc.collect() - sensor.run(0) + self.stop_sensor() # User cancelled if command == 2: diff --git a/src/krux/context.py b/src/krux/context.py index ac9c030af..ce6890123 100644 --- a/src/krux/context.py +++ b/src/krux/context.py @@ -20,12 +20,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import gc +import time import board from .logging import logger from .display import Display from .input import Input from .camera import Camera from .light import Light +from .themes import theme + +SCREENSAVER_ANIMATION_TIME = 150 class Context: @@ -33,9 +37,19 @@ class Context: duration of the program, including references to all device interfaces. """ - def __init__(self): + def __init__(self, logo=None): self.display = Display() - self.input = Input() + + if logo is None: + logo = [] + self.logo = logo + for _ in range((self.display.total_lines - len(logo)) // 2): + self.logo.insert(0, "") + self.logo.append("") + self.logo.append("") + self.logo.append("") + + self.input = Input(screensaver_fallback=self.screensaver) self.camera = Camera() self.light = Light() if "LED_W" in board.config["krux"]["pins"] else None self.power_manager = None @@ -53,3 +67,31 @@ def clear(self): if self.printer is not None: self.printer.clear() gc.collect() + + def screensaver(self): + """Displays a screensaver until user input""" + anim_frame = 0 + screensaver_time = 0 + + fg_color = theme.fg_color + bg_color = theme.bg_color + + self.display.clear() + + while True: + if screensaver_time + SCREENSAVER_ANIMATION_TIME < time.ticks_ms(): + screensaver_time = time.ticks_ms() + + # show animation on the screeen + if anim_frame < len(self.logo): + self.display.draw_line_hcentered_with_fullw_bg( + self.logo[anim_frame], anim_frame, fg_color, bg_color + ) + + anim_frame = anim_frame + 1 + if anim_frame > len(self.logo) * 1.4: + anim_frame = 0 + bg_color, fg_color = fg_color, bg_color + + if self.input.wait_for_button(block=False) is not None: + break diff --git a/src/krux/display.py b/src/krux/display.py index 83d81f616..18b4dc23d 100644 --- a/src/krux/display.py +++ b/src/krux/display.py @@ -19,12 +19,9 @@ # 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 import lcd import board -from machine import I2C from .themes import theme -from .qr import add_qr_frame DEFAULT_PADDING = 10 FONT_WIDTH, FONT_HEIGHT = board.config["krux"]["display"]["font"] @@ -32,10 +29,7 @@ QR_DARK_COLOR, QR_LIGHT_COLOR = board.config["krux"]["display"]["qr_colors"] -MAX_BACKLIGHT = 8 -MIN_BACKLIGHT = 1 - -FLASH_MSG_TIME = 2000 +DEFAULT_BACKLIGHT = 1 class Display: @@ -43,13 +37,10 @@ class Display: def __init__(self): self.portrait = True - # self.initialize_lcd() - self.i2c = None self.font_width = FONT_WIDTH self.font_height = FONT_HEIGHT - self.bottom_line = board.config["lcd"]["width"] // FONT_HEIGHT # total lines - self.bottom_line -= 1 - self.bottom_line *= FONT_HEIGHT + self.total_lines = board.config["lcd"]["width"] // FONT_HEIGHT + self.bottom_line = (self.total_lines - 1) * FONT_HEIGHT if board.config["type"] == "m5stickv": self.bottom_prompt_line = self.bottom_line - DEFAULT_PADDING else: @@ -110,7 +101,7 @@ def initialize_lcd(self): 0x2C, ], ) - self.initialize_backlight() + self.set_backlight(DEFAULT_BACKLIGHT) else: invert = ( board.config["type"].startswith("amigo") @@ -122,21 +113,6 @@ def initialize_lcd(self): if board.config["type"].startswith("amigo"): lcd.mirror(True) - def initialize_backlight(self): - """Initializes the backlight""" - if ( - "I2C_SCL" not in board.config["krux"]["pins"] - or "I2C_SDA" not in board.config["krux"]["pins"] - ): - return - self.i2c = I2C( - I2C.I2C0, - freq=400000, - scl=board.config["krux"]["pins"]["I2C_SCL"], - sda=board.config["krux"]["pins"]["I2C_SDA"], - ) - self.set_backlight(MIN_BACKLIGHT) - def qr_offset(self): """Retuns y offset to subtitle QR codes""" return self.width() + DEFAULT_PADDING // 2 @@ -165,6 +141,8 @@ def qr_data_width(self): """ if self.width() > 300: return self.width() // 6 # reduce density even more on larger screens + if self.width() > 200: + return self.width() // 5 return self.width() // 4 def to_landscape(self): @@ -185,26 +163,27 @@ def to_lines(self, text): columns = self.usable_width() // self.font_width else: columns = self.width() // self.font_width - words = [] + + # Processing the words and maintaining newline characters + processed_text = [] for word in text.split(" "): subwords = word.split("\n") for i, subword in enumerate(subwords): if len(subword) > columns: j = 0 while j < len(subword): - words.append(subword[j : j + columns]) + processed_text.append(subword[j : j + columns]) j += columns else: - words.append(subword) + processed_text.append(subword) if len(subwords) > 1 and i < len(subwords) - 1: - # Only add newline to the end of the word if the word - # is less than the amount of columns. If it's exactly equal, - # a newline will be implicit. - if len(words[-1]) < columns: - words[-1] += "\n" + # Ensure proper handling of newline characters at the end of lines + if not processed_text[-1].endswith("\n"): + processed_text.append("\n") - num_words = len(words) + num_words = len(processed_text) + words = processed_text # calculate cost of all pairs of words cost_between = [[0 for _ in range(num_words + 1)] for _ in range(num_words + 1)] @@ -298,16 +277,48 @@ def draw_hcentered_text( offset_y=DEFAULT_PADDING, color=theme.fg_color, bg_color=theme.bg_color, + info_box=False, ): """Draws text horizontally-centered on the display, at the given offset_y""" lines = text if isinstance(text, list) else self.to_lines(text) - for i, line in enumerate(lines): - offset_x = (self.width() - self.font_width * len(line)) // 2 - offset_x = max(0, offset_x) - self.draw_string( - offset_x, offset_y + (i * self.font_height), line, color, bg_color + if info_box: + bg_color = theme.disabled_color + self.fill_rectangle( + DEFAULT_PADDING - 3, + offset_y - 1, + self.usable_width() + 6, + (len(lines)) * self.font_height + 2, + bg_color, ) + for i, line in enumerate(lines): + if len(line) > 0: + offset_x = self._obtain_hcentered_offset(line) + self.draw_string( + offset_x, offset_y + (i * self.font_height), line, color, bg_color + ) + + def _obtain_hcentered_offset(self, line_str): + """Return the offset_x to the horizontally-centered line_str""" + return max(0, (self.width() - self.font_width * len(line_str)) // 2) + + def draw_line_hcentered_with_fullw_bg( + self, + line_str, + qtd_offset_y, + color=theme.fg_color, + bg_color=theme.bg_color, + ): + """Draw a line_str horizontally-centered on the display, at qtd_offset_y times font_height, + useful for screensaver""" + lcd.fill_rectangle( + 0, qtd_offset_y * self.font_height, self.width(), self.font_height, bg_color + ) + offset_x = self._obtain_hcentered_offset(line_str) + self.draw_string( + offset_x, (qtd_offset_y * self.font_height), line_str, color, bg_color + ) + def draw_centered_text(self, text, color=theme.fg_color, bg_color=theme.bg_color): """Draws text horizontally and vertically centered on the display""" lines = text if isinstance(text, list) else self.to_lines(text) @@ -315,33 +326,22 @@ def draw_centered_text(self, text, color=theme.fg_color, bg_color=theme.bg_color offset_y = max(0, (self.height() - lines_height) // 2) self.draw_hcentered_text(text, offset_y, color, bg_color) - def flash_text( - self, - text, - color=theme.fg_color, - bg_color=theme.bg_color, - duration=FLASH_MSG_TIME, - ): - """Flashes text centered on the display for duration ms""" - self.clear() - self.draw_centered_text(text, color, bg_color) - time.sleep_ms(duration) - self.clear() - def draw_qr_code( self, offset_y, qr_code, dark_color=QR_DARK_COLOR, light_color=QR_LIGHT_COLOR ): """Draws a QR code on the screen""" - _, qr_code = add_qr_frame(qr_code) - lcd.draw_qr_code( - offset_y, qr_code, self.width(), dark_color, light_color, theme.bg_color + lcd.draw_qr_code_binary( + offset_y, qr_code, self.width(), dark_color, light_color, light_color ) def set_backlight(self, level): """Sets the backlight of the display to the given power level, from 0 to 8""" - if not self.i2c: - return - # Ranges from 0 to 8 - level = max(0, min(level, 8)) - val = (level + 7) << 4 - self.i2c.writeto_mem(0x34, 0x91, int(val)) + + from .power import power_manager + + power_manager.set_screen_brightness(level) + + def max_lines(self, line_offset=0): + """The max lines of text supported by the display""" + pad = DEFAULT_PADDING if line_offset else 2 * DEFAULT_PADDING + return (self.height() - pad - line_offset) // (2 * self.font_height) diff --git a/src/krux/encryption.py b/src/krux/encryption.py index 90857940a..63af9ed28 100644 --- a/src/krux/encryption.py +++ b/src/krux/encryption.py @@ -150,9 +150,11 @@ def store_encrypted(self, key, mnemonic_id, mnemonic, sd_card=False, i_vector=No # load current MNEMONICS_FILE try: with SDHandler() as sd: - mnemonics = json.loads(sd.read(MNEMONICS_FILE)) + contents = sd.read(MNEMONICS_FILE) + orig_len = len(contents) + mnemonics = json.loads(contents) except: - pass + orig_len = 0 # save the new MNEMONICS_FILE try: @@ -165,7 +167,11 @@ def store_encrypted(self, key, mnemonic_id, mnemonic, sd_card=False, i_vector=No "key_iterations" ] = Settings().encryption.pbkdf2_iterations mnemonics[mnemonic_id]["data"] = encrypted - sd.write(MNEMONICS_FILE, json.dumps(mnemonics)) + contents = json.dumps(mnemonics) + # pad contents to orig_len to avoid abandoned bytes on sdcard + if len(contents) < orig_len: + contents += " " * (orig_len - len(contents)) + sd.write(MNEMONICS_FILE, contents) except: return False else: @@ -196,7 +202,12 @@ def del_mnemonic(self, mnemonic_id, sd_card=False): if sd_card: self.stored_sd.pop(mnemonic_id) with SDHandler() as sd: - sd.write(MNEMONICS_FILE, json.dumps(self.stored_sd)) + orig_len = len(sd.read(MNEMONICS_FILE)) + contents = json.dumps(self.stored_sd) + # pad contents to orig_len to avoid abandoned bytes on sdcard + if len(contents) < orig_len: + contents += " " * (orig_len - len(contents)) + sd.write(MNEMONICS_FILE, contents) else: self.stored.pop(mnemonic_id) with open("/flash/" + MNEMONICS_FILE, "w") as f: diff --git a/src/krux/firmware.py b/src/krux/firmware.py index 383a4e676..83bef0303 100644 --- a/src/krux/firmware.py +++ b/src/krux/firmware.py @@ -32,14 +32,20 @@ from .krux_settings import t from .wdt import wdt +FLASH_MSG_TIME = 2000 + +FLASH_SIZE = 2**24 MAX_FIRMWARE_SIZE = 0x300000 FIRMWARE_SLOT_1 = 0x00080000 -FIRMWARE_SLOT_2 = 0x00280000 +FIRMWARE_SLOT_2 = 0x00280000 # TODO: Move to 0x00390000 - Test all possible cases +SPIFFS_ADDR = 0xD00000 MAIN_BOOT_CONFIG_SECTOR_ADDRESS = 0x00004000 BACKUP_BOOT_CONFIG_SECTOR_ADDRESS = 0x00005000 +ERASE_BLOCK_SIZE = 0x1000 + FLASH_IO_WAIT_TIME = 100 @@ -171,6 +177,13 @@ def sha256(firmware_filename, firmware_size=None): def upgrade(): """Installs new firmware from SD card""" + def flash_text(text): + """Flashes text centered on the display for duration ms""" + display.clear() + display.draw_centered_text(text) + time.sleep_ms(FLASH_MSG_TIME) + display.clear() + firmware_path = "" try: firmware_filenames = list( @@ -204,31 +217,31 @@ def upgrade(): return False if new_size > MAX_FIRMWARE_SIZE: - display.flash_text(t("Firmware exceeds max size: %d") % MAX_FIRMWARE_SIZE) + flash_text(t("Firmware exceeds max size: %d") % MAX_FIRMWARE_SIZE) return False pubkey = None try: pubkey = ec.PublicKey.from_string(SIGNER_PUBKEY) except: - display.flash_text(t("Invalid public key")) + flash_text(t("Invalid public key")) return False sig = None try: sig = open(firmware_path + ".sig", "rb").read() except: - display.flash_text(t("Missing signature file")) + flash_text(t("Missing signature file")) return False try: # Parse, serialize, and reparse to ensure signature is compact prior to verification sig = ec.Signature.parse(ec.Signature.parse(sig).serialize()) if not pubkey.verify(sig, firmware_hash): - display.flash_text(t("Bad signature")) + flash_text(t("Bad signature")) return False except: - display.flash_text(t("Bad signature")) + flash_text(t("Bad signature")) return False boot_config_sector = flash.read(MAIN_BOOT_CONFIG_SECTOR_ADDRESS, 4096) @@ -237,7 +250,7 @@ def upgrade(): boot_config_sector = flash.read(BACKUP_BOOT_CONFIG_SECTOR_ADDRESS, 4096) address, _, entry_index = find_active_firmware(boot_config_sector) if address is None: - display.flash_text(t("Invalid bootloader")) + flash_text(t("Invalid bootloader")) return False # Write new firmware to the opposite slot @@ -276,5 +289,5 @@ def status_text(text): 4096, ) - display.flash_text(t("Upgrade complete.\n\nShutting down..")) + flash_text(t("Upgrade complete.\n\nShutting down..")) return True diff --git a/src/krux/i2c.py b/src/krux/i2c.py new file mode 100644 index 000000000..1fb7e606b --- /dev/null +++ b/src/krux/i2c.py @@ -0,0 +1,45 @@ +# 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 board +from machine import I2C + + +class I2CBus: + """Shared I2C bus singleton""" + + def __init__(self): + self.i2c = None + if ( + "I2C_SCL" not in board.config["krux"]["pins"] + or "I2C_SDA" not in board.config["krux"]["pins"] + ): + return + self.i2c = I2C( + I2C.I2C0, + freq=400000, + scl=board.config["krux"]["pins"]["I2C_SCL"], + sda=board.config["krux"]["pins"]["I2C_SDA"], + ) + + +i2c_bus = I2CBus().i2c diff --git a/src/krux/input.py b/src/krux/input.py index d9846bb6f..0925fdac2 100644 --- a/src/krux/input.py +++ b/src/krux/input.py @@ -21,10 +21,9 @@ # THE SOFTWARE. import time import board -from Maix import GPIO -from fpioa_manager import fm from .wdt import wdt -from .touch import Touch +from .buttons import PRESSED, RELEASED +from .krux_settings import Settings BUTTON_ENTER = 0 BUTTON_PAGE = 1 @@ -38,22 +37,26 @@ QR_ANIM_PERIOD = 300 # milliseconds LONG_PRESS_PERIOD = 1000 # milliseconds -PRESSED = 0 -RELEASED = 1 - BUTTON_WAIT_PRESS_DELAY = 10 +DEBOUNCE = 100 class Input: """Input is a singleton interface for interacting with the device's buttons""" - def __init__(self): + def __init__(self, screensaver_fallback=None): + self.screensaver_fallback = screensaver_fallback + self.screensaver_time = 0 + self.screensaver_active = False self.entropy = 0 + self.debounce_time = 0 + self.flushed_flag = False self.enter = None if "BUTTON_A" in board.config["krux"]["pins"]: - fm.register(board.config["krux"]["pins"]["BUTTON_A"], fm.fpioa.GPIOHS21) - self.enter = GPIO(GPIO.GPIOHS21, GPIO.IN, GPIO.PULL_UP) + from .buttons import ButtonEnter + + self.enter = ButtonEnter(board.config["krux"]["pins"]["BUTTON_A"]) self.page = None self.page_prev = None @@ -64,12 +67,16 @@ def __init__(self): self.page_prev = EncoderPagePrev() else: if "BUTTON_B" in board.config["krux"]["pins"]: - fm.register(board.config["krux"]["pins"]["BUTTON_B"], fm.fpioa.GPIOHS22) - self.page = GPIO(GPIO.GPIOHS22, GPIO.IN, GPIO.PULL_UP) + from .buttons import ButtonPage + + self.page = ButtonPage(board.config["krux"]["pins"]["BUTTON_B"]) if "BUTTON_C" in board.config["krux"]["pins"]: - fm.register(board.config["krux"]["pins"]["BUTTON_C"], fm.fpioa.GPIOHS0) - self.page_prev = GPIO(GPIO.GPIOHS0, GPIO.IN, GPIO.PULL_UP) + from .buttons import ButtonPagePrev + + self.page_prev = ButtonPagePrev( + board.config["krux"]["pins"]["BUTTON_C"] + ) else: try: from pmu import PMU_Button @@ -85,25 +92,29 @@ def __init__(self): "touch" in board.config["krux"]["display"] and board.config["krux"]["display"]["touch"] ): + from .touch import Touch + self.touch = Touch( - board.config["lcd"]["width"], board.config["lcd"]["height"] + board.config["lcd"]["width"], + board.config["lcd"]["height"], + board.config["krux"]["pins"]["TOUCH_IRQ"], ) self.buttons_active = False def enter_value(self): - """Intermediary method to pull button A state, if available""" + """Intermediary method to pull button ENTER state""" if self.enter is not None: return self.enter.value() return RELEASED def page_value(self): - """Intermediary method to pull button B state, if available""" + """Intermediary method to pull button PAGE state""" if self.page is not None: return self.page.value() return RELEASED def page_prev_value(self): - """Intermediary method to pull button C state, if available""" + """Intermediary method to pull button PAGE_PREV state""" if self.page_prev is not None: return self.page_prev.value() return RELEASED @@ -114,6 +125,30 @@ def touch_value(self): return self.touch.value() return RELEASED + def enter_event(self): + """Intermediary method to pull button ENTER event""" + if self.enter is not None: + return self.enter.event() + return False + + def page_event(self): + """Intermediary method to pull button PAGE event""" + if self.page is not None: + return self.page.event() + return False + + def page_prev_event(self): + """Intermediary method to pull button PAGE_PREV event""" + if self.page_prev is not None: + return self.page_prev.event() + return False + + def touch_event(self): + """Intermediary method to pull button TOUCH event""" + if self.touch is not None: + return self.touch.event() + return False + def swipe_right_value(self): """Intermediary method to pull touch gesture, if touch available""" if self.touch is not None: @@ -138,54 +173,74 @@ def swipe_down_value(self): return self.touch.swipe_down_value() return RELEASED - def wait_for_release(self): - """Loop until all buttons are released (if currently pressed)""" - while True: - if self.enter_value() == RELEASED and self.touch_value() == RELEASED: - if "ENCODER" in board.config["krux"]["pins"]: - # Encoder is event based, this check may disable event flag unintentionally - break - # TODO: Change standard buttons to be event(interrupt) based too - # So presses during high processing time(camera and animated QR) won't be lost - if self.page_value() == RELEASED and self.page_prev_value() == RELEASED: - break - self.entropy += 1 - wdt.feed() - - def wait_for_press(self, block=True, wait_duration=QR_ANIM_PERIOD): + def wait_for_press( + self, block=True, wait_duration=QR_ANIM_PERIOD, enable_screensaver=False + ): """Wait for first button press or for wait_duration ms. Use block to wait indefinitely""" start_time = time.ticks_ms() + self.debounce_time = time.ticks_ms() if not self.flushed_flag else 0 + while time.ticks_ms() < self.debounce_time + DEBOUNCE: + self.flush_events() + if not self.flushed_flag or block: + # Makes sure events that happened between pages load are cleared. + # On animated pages, where block=False, intermediary events must be checked, + # so events won't be cleared after flushed_flag is set + self.flush_events() + self.flushed_flag = not block + + self.screensaver_time = start_time while True: - if self.enter_value() == PRESSED: + if self.enter_event(): return BUTTON_ENTER - if self.page_value() == PRESSED: + if self.page_event(): return BUTTON_PAGE - if self.page_prev_value() == PRESSED: + if self.page_prev_event(): return BUTTON_PAGE_PREV - if self.touch_value() == PRESSED: + if self.touch_event(): return BUTTON_TOUCH + self.entropy += 1 wdt.feed() # here is where krux spends most of its time + if not block and time.ticks_ms() > start_time + wait_duration: return None + + # Check for screensaver + if ( + block + and enable_screensaver + and not self.screensaver_active + and self.screensaver_fallback + and self.screensaver_time + + (Settings().appearance.screensaver_time * 60000) + < time.ticks_ms() + ): + self.screensaver_active = True + self.screensaver_fallback() + self.screensaver_active = False + self.screensaver_time = time.ticks_ms() + return None + time.sleep_ms(BUTTON_WAIT_PRESS_DELAY) - def wait_for_button(self, block=True): + def wait_for_button(self, block=True, enable_screensaver=False): """Waits for any button to release, optionally blocking if block=True. - Returns the button that was released, or None if nonblocking. + Returns the button that was released, or None if non blocking. """ - self.wait_for_release() - btn = self.wait_for_press(block) + if Settings().appearance.screensaver_time == 0: + enable_screensaver = False + btn = self.wait_for_press(block, enable_screensaver=enable_screensaver) if btn == BUTTON_ENTER: # Wait for release while self.enter_value() == PRESSED: self.entropy += 1 wdt.feed() - if self.buttons_active: - return BUTTON_ENTER - self.buttons_active = True + if not self.buttons_active: + self.buttons_active = True + btn = None + elif btn == BUTTON_PAGE: start_time = time.ticks_ms() # Wait for release @@ -193,10 +248,11 @@ def wait_for_button(self, block=True): self.entropy += 1 wdt.feed() if time.ticks_ms() > start_time + LONG_PRESS_PERIOD: - return SWIPE_LEFT - if self.buttons_active: - return BUTTON_PAGE - self.buttons_active = True + btn = SWIPE_LEFT + break + if not self.buttons_active: + self.buttons_active = True + btn = None elif btn == BUTTON_PAGE_PREV: start_time = time.ticks_ms() # Wait for release @@ -204,10 +260,11 @@ def wait_for_button(self, block=True): self.entropy += 1 wdt.feed() if time.ticks_ms() > start_time + LONG_PRESS_PERIOD: - return SWIPE_RIGHT - if self.buttons_active: - return BUTTON_PAGE_PREV - self.buttons_active = True + btn = SWIPE_RIGHT + break + if not self.buttons_active: + self.buttons_active = True + btn = None elif btn == BUTTON_TOUCH: # Wait for release while self.touch_value() == PRESSED: @@ -215,13 +272,19 @@ def wait_for_button(self, block=True): wdt.feed() self.buttons_active = False if self.swipe_right_value() == PRESSED: - return SWIPE_RIGHT + btn = SWIPE_RIGHT if self.swipe_left_value() == PRESSED: - return SWIPE_LEFT + btn = SWIPE_LEFT if self.swipe_up_value() == PRESSED: - return SWIPE_UP + btn = SWIPE_UP if self.swipe_down_value() == PRESSED: - return SWIPE_DOWN - return BUTTON_TOUCH + btn = SWIPE_DOWN + + return btn - return None + def flush_events(self): + """Clean eventual event flags unintentionally collected""" + self.enter_event() + self.page_event() + self.page_prev_event() + self.touch_event() diff --git a/src/krux/key.py b/src/krux/key.py index bb986fd02..39499ff8a 100644 --- a/src/krux/key.py +++ b/src/krux/key.py @@ -74,20 +74,37 @@ def account_pubkey_str(self, version=None): def fingerprint_hex_str(self, pretty=False): """Returns the master key fingerprint in hex format""" - formatted_txt = t("Fingerprint: %s") if pretty else "%s" + formatted_txt = t("⊚ %s") if pretty else "%s" return formatted_txt % hexlify(self.fingerprint).decode("utf-8") def derivation_str(self, pretty=False): """Returns the derivation path for the Hierarchical Deterministic Wallet to be displayed as string """ - formatted_txt = t("Derivation: %s") if pretty else "%s" + formatted_txt = t("↳ %s") if pretty else "%s" return (formatted_txt % self.derivation).replace("h", HARDENED_STR_REPLACE) def sign(self, message_hash): """Signs a message with the extended master private key""" return self.root.derive(self.derivation).sign(message_hash) + def sign_at(self, derivation, message_hash): + """Signs a message at an adress derived from master key (code adapted from specterDIY)""" + from embit import ec + from embit.util import secp256k1 + + prv = self.root.derive(derivation).key + sig = secp256k1.ecdsa_sign_recoverable( + message_hash, prv._secret # pylint: disable=W0212 + ) + flag = sig[64] + flag = bytes([27 + flag + 4]) + ec_signature = ec.Signature(sig[:64]) + ser = flag + secp256k1.ecdsa_signature_serialize_compact( + ec_signature._sig # pylint: disable=W0212 + ) + return ser + @staticmethod def pick_final_word(entropy, words): """Returns a random final word with a valid checksum for the given list of @@ -113,6 +130,6 @@ def get_default_derivation_str(multisig, network): """Return the Krux default derivation path for single-sig or multisig to be displayd as string """ - return Key.get_default_derivation(multisig, network).replace( + return "↳ " + Key.get_default_derivation(multisig, network).replace( "h", HARDENED_STR_REPLACE ) diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py index 10ea25fa3..f2046eb98 100644 --- a/src/krux/krux_settings.py +++ b/src/krux/krux_settings.py @@ -155,8 +155,6 @@ class AdafruitPrinterSettings(SettingsNamespace): paper_width = NumberSetting(int, "paper_width", 384, [100, 1000]) tx_pin = NumberSetting(int, "tx_pin", DEFAULT_TX_PIN, [0, 10000]) rx_pin = NumberSetting(int, "rx_pin", DEFAULT_RX_PIN, [0, 10000]) - heat_time = NumberSetting(int, "heat_time", 120, [3, 255]) - heat_interval = NumberSetting(int, "heat_interval", 40, [0, 255]) line_delay = NumberSetting(int, "line_delay", 20, [0, 255]) scale = NumberSetting(int, "scale", 75, [25, 100]) @@ -167,8 +165,6 @@ def label(self, attr): "paper_width": t("Paper Width"), "tx_pin": t("TX Pin"), "rx_pin": t("RX Pin"), - "heat_time": t("Heat Time"), - "heat_interval": t("Heat Interval"), "line_delay": t("Line Delay"), "scale": t("Scale"), }[attr] @@ -254,7 +250,7 @@ class EncoderSettings(SettingsNamespace): """Encoder debounce settings""" namespace = "settings.encoder" - debounce = NumberSetting(int, "debounce", 50, [25, 250]) + debounce = NumberSetting(int, "debounce", 100, [100, 250]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" @@ -276,6 +272,38 @@ def label(self, attr): }[attr] +class HardwareSettings(SettingsNamespace): + """Hardware Related Settings""" + + namespace = "settings.hardware" + + def __init__(self): + self.printer = PrinterSettings() + if ( + board.config["type"].startswith("amigo") + or board.config["type"] == "yahboom" + ): + self.touch = TouchSettings() + if board.config["type"] == "dock": + self.encoder = EncoderSettings() + + def label(self, attr): + """Returns a label for UI when given a setting name or namespace""" + + hardware_menu = { + "printer": t("Printer"), + } + if ( + board.config["type"].startswith("amigo") + or board.config["type"] == "yahboom" + ): + hardware_menu["touchscreen"] = t("Touchscreen") + if board.config["type"] == "dock": + hardware_menu["encoder"] = t("Encoder") + + return hardware_menu[attr] + + class PersistSettings(SettingsNamespace): """Persistent settings""" @@ -316,21 +344,29 @@ class ThemeSettings(SettingsNamespace): DARK_THEME = 0 LIGHT_THEME = 1 ORANGE_THEME = 3 + GREEN_THEME = 4 + PINK_THEME = 5 DARK_THEME_NAME = "Dark" LIGHT_THEME_NAME = "Light" ORANGE_THEME_NAME = "Orange" + GREEN_THEME_NAME = "CypherPunk" + PINK_THEME_NAME = "CypherPink" THEME_NAMES = { DARK_THEME: DARK_THEME_NAME, LIGHT_THEME: LIGHT_THEME_NAME, ORANGE_THEME: ORANGE_THEME_NAME, + GREEN_THEME: GREEN_THEME_NAME, + PINK_THEME: PINK_THEME_NAME, } namespace = "settings.appearance" theme = CategorySetting("theme", DARK_THEME_NAME, list(THEME_NAMES.values())) + screensaver_time = NumberSetting(int, "screensaver_time", 5, [0, 30]) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" return { "theme": t("Theme"), + "screensaver_time": t("Screensaver time"), }[attr] @@ -341,30 +377,23 @@ class Settings(SettingsNamespace): def __init__(self): self.bitcoin = BitcoinSettings() + self.hardware = HardwareSettings() self.i18n = I18nSettings() self.logging = LoggingSettings() self.encryption = EncryptionSettings() - self.printer = PrinterSettings() self.persist = PersistSettings() self.appearance = ThemeSettings() - if board.config["type"].startswith("amigo"): - self.touch = TouchSettings() - if board.config["type"] == "dock": - self.encoder = EncoderSettings() def label(self, attr): """Returns a label for UI when given a setting name or namespace""" main_menu = { "bitcoin": t("Bitcoin"), + "hardware": t("Hardware"), "i18n": t("Language"), "logging": t("Logging"), "encryption": t("Encryption"), "persist": t("Persist"), - "printer": t("Printer"), - "appearance": t("Theme"), + "appearance": t("Appearance"), } - if board.config["type"].startswith("amigo"): - main_menu["touchscreen"] = t("Touchscreen") - if board.config["type"] == "dock": - main_menu["encoder"] = t("Encoder") + return main_menu[attr] diff --git a/src/krux/metadata.py b/src/krux/metadata.py index e2ff900d4..3713e7b38 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 = "23.09.1" +VERSION = "24.04.beta13" SIGNER_PUBKEY = "03339e883157e45891e61ca9df4cd3bb895ef32d475b8e793559ea10a36766689b" diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py index 0726dec27..90d7c7681 100644 --- a/src/krux/pages/__init__.py +++ b/src/krux/pages/__init__.py @@ -38,17 +38,19 @@ from ..display import DEFAULT_PADDING from ..qr import to_qr_codes from ..krux_settings import t, Settings, LoggingSettings, BitcoinSettings +from ..sd_card import SDHandler MENU_CONTINUE = 0 MENU_EXIT = 1 MENU_SHUTDOWN = 2 +FLASH_MSG_TIME = 2000 + ESC_KEY = 1 FIXED_KEYS = 3 # 'More' key only appears when there are multiple keysets ANTI_GLARE_WAIT_TIME = 500 QR_CODE_STEP_TIME = 100 -CAMERA_INIT_TIME = 1000 SHUTDOWN_WAIT_TIME = 300 TOGGLE_BRIGHTNESS = (BUTTON_PAGE, BUTTON_PAGE_PREV) @@ -85,6 +87,20 @@ def esc_prompt(self): return ESC_KEY return None + def flash_text( + self, + text, + color=theme.fg_color, + bg_color=theme.bg_color, + duration=FLASH_MSG_TIME, + ): + """Flashes text centered on the display for duration ms""" + self.ctx.display.clear() + self.ctx.display.draw_centered_text(text, color, bg_color) + time.sleep_ms(duration) + self.ctx.display.clear() + self.ctx.input.flush_events() + def capture_from_keypad( self, title, @@ -92,6 +108,7 @@ def capture_from_keypad( autocomplete_fn=None, possible_keys_fn=None, delete_key_fn=None, + progress_bar_fn=None, go_on_change=False, starting_buffer="", esc_prompt=True, @@ -110,7 +127,10 @@ def capture_from_keypad( self.ctx.display.draw_hcentered_text(title, offset_y) offset_y += self.ctx.display.font_height * 3 // 2 self.ctx.display.draw_hcentered_text(buffer, offset_y) - offset_y = pad.keypad_offset() + + # offset_y = pad.keypad_offset() # Dead code? + if progress_bar_fn: + progress_bar_fn() possible_keys = pad.keys if possible_keys_fn is not None: possible_keys = possible_keys_fn(buffer) @@ -170,40 +190,35 @@ def capture_qr_code(self): def callback(part_total, num_parts_captured, new_part): # Turn on the light as long as the enter button is held down (M5stickV and Amigo) - if self._time_to_check_input(): - if self.ctx.light: - if self.ctx.input.enter_value() == PRESSED: - self.ctx.light.turn_on() + if self.ctx.light: + if self.ctx.input.enter_value() == PRESSED: + self.ctx.light.turn_on() + else: + self.ctx.light.turn_off() + # If board don't have light, ENTER stops the capture + elif self.ctx.input.enter_event(): + return 1 + + # Anti-glare mode (M5stickV and Amigo) + if self.ctx.input.page_event(): + if self.ctx.camera.has_antiglare(): + self._time_frame = time.ticks_ms() + self.ctx.display.to_portrait() + if not self.ctx.camera.antiglare_enabled: + self.ctx.camera.enable_antiglare() + self.ctx.display.draw_centered_text(t("Anti-glare enabled")) else: - self.ctx.light.turn_off() - # If board don't have light, ENTER stops the capture - elif self.ctx.input.enter_value() == PRESSED: - return 1 - - # Anti-glare mode (M5stickV and Amigo) - if self.ctx.input.page_value() == PRESSED: - if self.ctx.camera.has_antiglare(): - self._time_frame = time.ticks_ms() - self.ctx.display.to_portrait() - if not self.ctx.camera.antiglare_enabled: - self.ctx.camera.enable_antiglare() - self.ctx.display.draw_centered_text(t("Anti-glare enabled")) - else: - self.ctx.camera.disable_antiglare() - self.ctx.display.draw_centered_text( - t("Anti-glare disabled") - ) - time.sleep_ms(ANTI_GLARE_WAIT_TIME) - self.ctx.display.to_landscape() - return 0 - return 1 - - # Exit the capture loop with PAGE_PREV or TOUCH - if ( - self.ctx.input.page_prev_value() == PRESSED - or self.ctx.input.touch_value() == PRESSED - ): - return 1 + self.ctx.camera.disable_antiglare() + self.ctx.display.draw_centered_text(t("Anti-glare disabled")) + time.sleep_ms(ANTI_GLARE_WAIT_TIME) + self.ctx.display.to_landscape() + self.ctx.input.flush_events() + return 0 + return 1 + + # Exit the capture loop with PAGE_PREV or TOUCH + if self.ctx.input.page_prev_event() or self.ctx.input.touch_event(): + return 1 # Indicate progress to the user that a new part was captured if new_part: @@ -248,28 +263,18 @@ def callback(part_total, num_parts_captured, new_part): ) return (code, qr_format) - def _time_to_check_input(self): - return time.ticks_ms() > self._time_frame + CAMERA_INIT_TIME - def capture_camera_entropy(self): "Helper to capture camera's entropy as the hash of image buffer" self._time_frame = time.ticks_ms() def callback(): - if self._time_to_check_input(): - # Accepted - if ( - self.ctx.input.enter_value() == PRESSED - or self.ctx.input.touch_value() == PRESSED - ): - return 1 - - # Exited - if ( - self.ctx.input.page_value() == PRESSED - or self.ctx.input.page_prev_value() == PRESSED - ): - return 2 + # Accepted + if self.ctx.input.enter_event() or self.ctx.input.touch_event(): + return 1 + + # Exited + if self.ctx.input.page_event() or self.ctx.input.page_prev_event(): + return 2 return 0 self.ctx.display.clear() @@ -320,26 +325,28 @@ def display_qr_codes(self, data, qr_format, title=""): ) self.ctx.display.draw_hcentered_text(subtitle, offset_y) i = (i + 1) % num_parts - # There are cases we can allow any btn to change the screen + self.ctx.input.buttons_active = True btn = self.ctx.input.wait_for_button(num_parts == 1) if btn in TOGGLE_BRIGHTNESS: bright = not bright elif btn in PROCEED: + if self.ctx.input.touch is not None: + self.ctx.input.buttons_active = False done = True # interval done in input.py using timers - def display_mnemonic(self, mnemonic): + def display_mnemonic(self, mnemonic, suffix=""): """Displays the 12 or 24-word list of words to the user""" words = mnemonic.split(" ") word_list = [ str(i + 1) + "." + (" " if i + 1 < 10 else " ") + word for i, word in enumerate(words) ] + header = t("BIP39") + " " + suffix self.ctx.display.clear() - self.ctx.display.draw_hcentered_text(t("BIP39 Mnemonic")) + self.ctx.display.draw_hcentered_text(header) starting_y_offset = DEFAULT_PADDING // 4 + ( - len(self.ctx.display.to_lines(t("BIP39 Mnemonic"))) - * self.ctx.display.font_height + len(self.ctx.display.to_lines(header)) * self.ctx.display.font_height + self.ctx.display.font_height ) for i, word in enumerate(word_list[:12]): @@ -350,7 +357,7 @@ def display_mnemonic(self, mnemonic): if board.config["type"] == "m5stickv": self.ctx.input.wait_for_button() self.ctx.display.clear() - self.ctx.display.draw_hcentered_text(t("BIP39 Mnemonic")) + self.ctx.display.draw_hcentered_text(header) for i, word in enumerate(word_list[12:]): offset_x = DEFAULT_PADDING offset_y = starting_y_offset + (i * self.ctx.display.font_height) @@ -365,12 +372,12 @@ def print_qr_prompt(self): """Prompts the user to print a QR code in the specified format if a printer is connected """ - if Settings().printer.driver == "none": + if not self.has_printer(): return False self.ctx.display.clear() if self.prompt( - t("Print to QR?\n\n%s\n\n") % Settings().printer.driver, + t("Print to QR?\n\n%s\n\n") % Settings().hardware.printer.driver, self.ctx.display.height() // 2, ): return True @@ -464,6 +471,37 @@ def prompt(self, text, offset_y=0): # BUTTON_ENTER return answer + def fit_to_line(self, text, prefix="", fixed_chars=0): + """Fits text with prefix plus fixed_chars at the beginning into one line, + removing the central content and leaving the ends""" + + add_chars_amount = ( + self.ctx.display.usable_width() // self.ctx.display.font_width + ) + add_chars_amount -= len(prefix) + fixed_chars + 2 + add_chars_amount //= 2 + return ( + prefix + + text[: add_chars_amount + fixed_chars] + + ".." + + text[-add_chars_amount:] + ) + + def has_printer(self): + """Checks if the device has a printer setup""" + return Settings().hardware.printer.driver != "none" + + def has_sd_card(self): + """Checks if the device has a SD card inserted""" + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Checking for SD card..")) + try: + # Check for SD hot-plug + with SDHandler(): + return True + except: + return False + def shutdown(self): """Handler for the 'shutdown' menu item""" if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): @@ -528,12 +566,12 @@ class Menu: and invoke menu item callbacks that return a status """ - def __init__(self, ctx, menu): + def __init__(self, ctx, menu, offset=0): self.ctx = ctx self.menu = menu + self.menu_offset = offset max_viewable = min( - (self.ctx.display.height() - 2 * DEFAULT_PADDING) - // (2 * self.ctx.display.font_height), + self.ctx.display.max_lines(self.menu_offset), len(self.menu), ) self.menu_view = ListView(self.menu, max_viewable) @@ -549,13 +587,23 @@ def run_loop(self, start_from_index=None): selected_item_index = start_from_index while True: gc.collect() - self.ctx.display.clear() + if self.menu_offset: + self.ctx.display.fill_rectangle( + 0, + self.menu_offset, + self.ctx.display.width(), + self.ctx.display.height() - self.menu_offset, + theme.bg_color, + ) + else: + self.ctx.display.clear() if self.ctx.input.touch is not None: self._draw_touch_menu(selected_item_index) else: self._draw_menu(selected_item_index) self.draw_status_bar() + self.ctx.input.flush_events() if start_from_submenu: status = self._clicked_item(selected_item_index) @@ -563,7 +611,7 @@ def run_loop(self, start_from_index=None): return (self.menu_view.index(selected_item_index), status) start_from_submenu = False else: - btn = self.ctx.input.wait_for_button() + btn = self.ctx.input.wait_for_button(enable_screensaver=True) if self.ctx.input.touch is not None: if btn == BUTTON_TOUCH: selected_item_index = self.ctx.input.touch.current_index() @@ -594,6 +642,8 @@ def run_loop(self, start_from_index=None): self.menu_view.move_backward() def _clicked_item(self, selected_item_index): + if self.menu_view[selected_item_index][1] is None: + return MENU_CONTINUE try: self.ctx.display.clear() status = self.menu_view[selected_item_index][1]() @@ -698,10 +748,13 @@ def _draw_touch_menu(self, selected_item_index): for menu_item in self.menu_view: offset_y += len(self.ctx.display.to_lines(menu_item[0])) + 1 Page.y_keypad_map.append(offset_y) - height_multiplier = self.ctx.display.height() - 2 * DEFAULT_PADDING + height_multiplier = ( + self.ctx.display.height() - 2 * DEFAULT_PADDING - self.menu_offset + ) height_multiplier //= offset_y Page.y_keypad_map = [ - n * height_multiplier + DEFAULT_PADDING for n in Page.y_keypad_map + n * height_multiplier + DEFAULT_PADDING + self.menu_offset + for n in Page.y_keypad_map ] self.ctx.input.touch.y_regions = Page.y_keypad_map @@ -711,11 +764,6 @@ def _draw_touch_menu(self, selected_item_index): self.ctx.display.fill_rectangle( 0, y, self.ctx.display.width(), 1, theme.frame_color ) - height = Page.y_keypad_map[i + 1] - y - if selected_item_index == i and self.ctx.input.buttons_active: - self.ctx.display.fill_rectangle( - 0, y + 1, self.ctx.display.width(), height - 2, theme.fg_color - ) # draw centralized strings in regions for i, menu_item in enumerate(self.menu_view): @@ -724,30 +772,46 @@ def _draw_touch_menu(self, selected_item_index): offset_y -= len(menu_item_lines) * self.ctx.display.font_height offset_y //= 2 offset_y += Page.y_keypad_map[i] + fg_color = ( + theme.fg_color if menu_item[1] is not None else theme.disabled_color + ) for j, text in enumerate(menu_item_lines): if selected_item_index == i and self.ctx.input.buttons_active: + self.ctx.display.fill_rectangle( + 0, + offset_y + 1 - self.ctx.display.font_height // 2, + self.ctx.display.width(), + (len(menu_item_lines) + 1) * self.ctx.display.font_height, + fg_color, + ) self.ctx.display.draw_hcentered_text( text, offset_y + self.ctx.display.font_height * j, theme.bg_color, - theme.fg_color, + fg_color, ) else: self.ctx.display.draw_hcentered_text( - text, offset_y + self.ctx.display.font_height * j + text, offset_y + self.ctx.display.font_height * j, fg_color ) def _draw_menu(self, selected_item_index): - offset_y = len(self.menu_view) * 2 - extra_lines = 0 - for menu_item in self.menu_view: - extra_lines += len(self.ctx.display.to_lines(menu_item[0])) - 1 - offset_y += extra_lines - offset_y *= self.ctx.display.font_height - offset_y = self.ctx.display.height() - offset_y - offset_y //= 2 - offset_y += self.ctx.display.font_height // 2 + if self.menu_offset: + offset_y = self.menu_offset + self.ctx.display.font_height // 2 + else: + offset_y = len(self.menu_view) * 2 + extra_lines = 0 + for menu_item in self.menu_view: + extra_lines += len(self.ctx.display.to_lines(menu_item[0])) - 1 + offset_y += extra_lines + offset_y *= self.ctx.display.font_height + offset_y = self.ctx.display.height() - offset_y + offset_y //= 2 + offset_y += self.ctx.display.font_height // 2 for i, menu_item in enumerate(self.menu_view): + fg_color = ( + theme.fg_color if menu_item[1] is not None else theme.disabled_color + ) menu_item_lines = self.ctx.display.to_lines(menu_item[0]) delta_y = (len(menu_item_lines) + 1) * self.ctx.display.font_height if selected_item_index == i: @@ -756,19 +820,18 @@ def _draw_menu(self, selected_item_index): offset_y + 1 - self.ctx.display.font_height // 2, self.ctx.display.width(), delta_y - 2, - theme.fg_color, + fg_color, ) for j, text in enumerate(menu_item_lines): self.ctx.display.draw_hcentered_text( text, offset_y + self.ctx.display.font_height * j, theme.bg_color, - theme.fg_color, + fg_color, ) else: for j, text in enumerate(menu_item_lines): self.ctx.display.draw_hcentered_text( - text, - offset_y + self.ctx.display.font_height * j, + text, offset_y + self.ctx.display.font_height * j, fg_color ) offset_y += delta_y diff --git a/src/krux/pages/addresses.py b/src/krux/pages/addresses.py index 3447d8ed7..f271c0f06 100644 --- a/src/krux/pages/addresses.py +++ b/src/krux/pages/addresses.py @@ -21,10 +21,10 @@ # THE SOFTWARE. import gc -import board from ..krux_settings import t from ..themes import theme from ..qr import FORMAT_NONE +from .utils import Utils from . import ( Page, Menu, @@ -32,10 +32,6 @@ MENU_EXIT, ) -LIST_ADDRESS_QTD = 4 # qtd of address per page -LIST_ADDRESS_DIGITS = 8 # len on large devices per menu item -LIST_ADDRESS_DIGITS_SMALL = 4 # len on small devices per menu item - SCAN_ADDRESS_LIMIT = 20 @@ -45,12 +41,13 @@ class Addresses(Page): def __init__(self, ctx): super().__init__(ctx, None) self.ctx = ctx + self.utils = Utils(self.ctx) def addresses_menu(self): """Handler for the 'address' menu item""" # only show address for single-sig or multisig with wallet output descriptor loaded if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig(): - self.ctx.display.flash_text( + self.flash_text( t("Please load a wallet output descriptor"), theme.error_color ) return MENU_CONTINUE @@ -58,10 +55,10 @@ def addresses_menu(self): submenu = Menu( self.ctx, [ - ((t("Scan Address"), self.pre_scan_address)), + (t("Scan Address"), self.pre_scan_address), (t("Receive Addresses"), self.list_address_type), (t("Change Addresses"), lambda: self.list_address_type(1)), - (t("Back"), lambda: MENU_EXIT), + (t("Back"), lambda: None), ], ) submenu.run_loop() @@ -71,52 +68,34 @@ def list_address_type(self, addr_type=0): """Handler for the 'receive addresses' or 'change addresses' menu item""" # only show address for single-sig or multisig with wallet output descriptor loaded if self.ctx.wallet.is_loaded() or not self.ctx.wallet.is_multisig(): - custom_start_digits = ( - LIST_ADDRESS_DIGITS + 3 - ) # 3 more because of bc1 address - custom_end_digts = LIST_ADDRESS_DIGITS - custom_separator = ". " - if board.config["type"] == "m5stickv": - custom_start_digits = ( - LIST_ADDRESS_DIGITS_SMALL + 3 - ) # 3 more because of bc1 address - custom_end_digts = LIST_ADDRESS_DIGITS_SMALL - custom_separator = " " - start_digits = custom_start_digits - loading_txt = t("Loading receive address %d..") if addr_type == 1: loading_txt = t("Loading change address %d..") + max_addresses = self.ctx.display.max_lines() - 3 + num_checked = 0 while True: items = [] - if num_checked + 1 > LIST_ADDRESS_QTD: + if num_checked + 1 > max_addresses: items.append( ( - "%d..%d" - % (num_checked - LIST_ADDRESS_QTD + 1, num_checked), + "%d..%d" % (num_checked - max_addresses + 1, num_checked), lambda: MENU_EXIT, ) ) for addr in self.ctx.wallet.obtain_addresses( - num_checked, limit=LIST_ADDRESS_QTD, branch_index=addr_type + num_checked, limit=max_addresses, branch_index=addr_type ): self.ctx.display.clear() self.ctx.display.draw_centered_text(loading_txt % (num_checked + 1)) - if num_checked + 1 > 99: - start_digits = custom_start_digits - 1 - pos_str = str(num_checked + 1) - qr_title = pos_str + ". " + addr + pos_str = str(num_checked + 1) + "." + " " # thin space + qr_title = pos_str + addr items.append( ( - pos_str - + custom_separator - + addr[:start_digits] - + ".." - + addr[len(addr) - custom_end_digts :], + self.fit_to_line(addr, pos_str, fixed_chars=3), lambda address=addr, title=qr_title: self.show_address( address, title ), @@ -127,7 +106,7 @@ def list_address_type(self, addr_type=0): items.append( ( - "%d..%d" % (num_checked + 1, num_checked + LIST_ADDRESS_QTD), + "%d..%d" % (num_checked + 1, num_checked + max_addresses), lambda: MENU_EXIT, ) ) @@ -147,27 +126,27 @@ def list_address_type(self, addr_type=0): if index == len(submenu.menu) - 2: stay_on_this_addr_menu = False # Prev - if index == 0 and num_checked > LIST_ADDRESS_QTD: + if index == 0 and num_checked > max_addresses: stay_on_this_addr_menu = False - num_checked -= 2 * LIST_ADDRESS_QTD + num_checked -= 2 * max_addresses return MENU_CONTINUE - def show_address(self, addr, title="", qr_format=FORMAT_NONE): + def show_address(self, addr, title="", quick_exit=False): """Show addr provided as a QRCode""" - self.display_qr_codes(addr, qr_format, title) - if self.print_qr_prompt(): - from .print_page import PrintPage + from .qr_view import SeedQRView - print_page = PrintPage(self.ctx) - print_page.print_qr(addr, qr_format, title) + seed_qr_view = SeedQRView(self.ctx, data=addr, title=title) + seed_qr_view.display_qr( + allow_export=True, transcript_tools=False, quick_exit=quick_exit + ) return MENU_CONTINUE def pre_scan_address(self): """Handler for the 'scan address' menu item""" # only show address for single-sig or multisig with wallet output descriptor loaded if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig(): - self.ctx.display.flash_text( + self.flash_text( t("Please load a wallet output descriptor"), theme.error_color ) return MENU_CONTINUE @@ -187,7 +166,7 @@ def scan_address(self, addr_type=0): """Handler for the 'receive' or 'change' menu item""" data, qr_format = self.capture_qr_code() if data is None or qr_format != FORMAT_NONE: - self.ctx.display.flash_text(t("Failed to load address"), theme.error_color) + self.flash_text(t("Failed to load address"), theme.error_color) return MENU_CONTINUE addr = None @@ -196,10 +175,10 @@ def scan_address(self, addr_type=0): addr = parse_address(data) except: - self.ctx.display.flash_text(t("Invalid address"), theme.error_color) + self.flash_text(t("Invalid address"), theme.error_color) return MENU_CONTINUE - self.show_address(data, title=addr, qr_format=qr_format) + self.show_address(data, title=addr, quick_exit=True) if self.ctx.wallet.is_loaded() or not self.ctx.wallet.is_multisig(): self.ctx.display.clear() diff --git a/src/krux/pages/capture_entropy.py b/src/krux/pages/capture_entropy.py new file mode 100644 index 000000000..1a5d6bbdd --- /dev/null +++ b/src/krux/pages/capture_entropy.py @@ -0,0 +1,164 @@ +# 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 math +from . import Page + +POOR_VARIANCE_TH = 10 +INSUFFICIENT_VARIANCE_TH = 5 # % +INSUFFICIENT_SHANNONS_ENTROPY_TH = 3 # bits per pixel +SHANNONS_BLOCK_SIZE = 0x4000 # 16384B, result in ~10 blocks for a QVGA img + + +class CameraEntropy(Page): + """Class for capturing entropy from a snapshot""" + + def __init__(self, ctx): + super().__init__(ctx, None) + self.ctx = ctx + + def _callback(self): + # Accepted + if self.ctx.input.enter_event() or self.ctx.input.touch_event(): + return 1 + + # Exited + if self.ctx.input.page_event() or self.ctx.input.page_prev_event(): + return 2 + return 0 + + def capture(self): + """Captures camera's entropy as the hash of image buffer""" + import hashlib + import board + import gc + import sensor + import lcd + from ..wdt import wdt + from ..krux_settings import t + from ..themes import theme + import shannon + + def rms_value(data): + if not data: + return 0 + square_sum = sum(x**2 for x in data) + mean_square = square_sum / len(data) + rms = math.sqrt(mean_square) + return int(rms) + + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("TOUCH or ENTER to capture")) + self.ctx.display.to_landscape() + self.ctx.camera.initialize_sensor() + sensor.run(1) + self.ctx.display.clear() + command = 0 + y_label_offset = self.ctx.display.bottom_line + if board.config["type"].startswith("amigo"): + y_label_offset = self.ctx.display.bottom_prompt_line + while True: + wdt.feed() + img = sensor.snapshot() + stdev_index = rms_value( + [ + img.get_statistics().l_stdev(), + img.get_statistics().a_stdev(), + img.get_statistics().b_stdev(), + ] + ) + self.ctx.display.to_portrait() + self.ctx.display.fill_rectangle( + 0, + y_label_offset, + self.ctx.display.width(), + self.ctx.display.font_height, + theme.bg_color, + ) + if stdev_index > POOR_VARIANCE_TH: + self.ctx.display.draw_hcentered_text( + "Good entropy", y_label_offset, theme.go_color + ) + elif stdev_index > INSUFFICIENT_VARIANCE_TH: + self.ctx.display.draw_hcentered_text( + "Poor entropy", y_label_offset, theme.del_color + ) + else: + self.ctx.display.draw_hcentered_text( + "Insufficient entropy", y_label_offset, theme.error_color + ) + self.ctx.display.to_landscape() + command = self._callback() + if command > 0: + break + + if board.config["type"] == "m5stickv": + img.lens_corr(strength=1.0, zoom=0.56) + lcd.display(img, oft=(0, 0), roi=(68, 52, 185, 135)) + elif board.config["type"].startswith("amigo"): + lcd.display(img, oft=(40, 40)) + else: + lcd.display(img, oft=(0, 0), roi=(0, 0, 304, 240)) + + self.ctx.display.to_portrait() + gc.collect() + sensor.run(0) + + # User cancelled + if command == 2: + self.flash_text(t("Capture cancelled")) + return None + + self.ctx.display.draw_centered_text(t("Calculating Shannon's entropy")) + + img_bytes = img.to_bytes() + del img + + # Calculate Shannon's entropy: + shannon_16b = shannon.entropy_img16b(img_bytes) + shannon_16b_total = shannon_16b * 320 * 240 + + entropy_msg = "Shannon's entropy:\n" + entropy_msg += str(round(shannon_16b, 2)) + " bits/px\n" + entropy_msg += "(" + str(int(shannon_16b_total)) + " total)\n\n" + entropy_msg += "Pixels deviation index: " + entropy_msg += str(stdev_index) + self.ctx.display.clear() + if ( + shannon_16b < INSUFFICIENT_SHANNONS_ENTROPY_TH + or stdev_index < INSUFFICIENT_VARIANCE_TH + ): + error_msg = t("Insufficient Entropy!") + error_msg += "\n\n" + error_msg += entropy_msg + self.ctx.display.draw_centered_text(error_msg, theme.error_color) + self.ctx.input.wait_for_button() + return None + self.ctx.display.draw_centered_text(entropy_msg) + self.ctx.input.wait_for_button() + hasher = hashlib.sha256() + image_len = len(img_bytes) + hasher_index = 0 + while hasher_index < image_len: + hasher.update(img_bytes[hasher_index : hasher_index + 128]) + hasher_index += 128 + return hasher.digest() diff --git a/src/krux/pages/encryption_ui.py b/src/krux/pages/encryption_ui.py index eb93ae298..fd2be65cd 100644 --- a/src/krux/pages/encryption_ui.py +++ b/src/krux/pages/encryption_ui.py @@ -55,7 +55,7 @@ def encryption_key(self): ) _, key = submenu.run_loop() if key in (ESC_KEY, MENU_CONTINUE): - return None + return "" if key: self.ctx.display.clear() @@ -78,10 +78,10 @@ def load_qr_encryption_key(self): """Loads and returns a key from a QR code""" data, _ = self.capture_qr_code() if data is None: - self.ctx.display.flash_text(t("Failed to load key"), theme.error_color) + self.flash_text(t("Failed to load key"), theme.error_color) return None if len(data) > ENCRYPTION_KEY_MAX_LEN: - self.ctx.display.flash_text( + self.flash_text( t("Maximum length exceeded (%s)") % ENCRYPTION_KEY_MAX_LEN, theme.error_color, ) @@ -99,23 +99,19 @@ def __init__(self, ctx): def encrypt_menu(self): """Menu with mnemonic encryption output options""" - from ..encryption import MnemonicStorage + def _sd_store_function(): + return self.store_mnemonic_on_memory(sd_card=True) - encrypt_outputs_menu = [] - encrypt_outputs_menu.append( - (t("Store on Flash"), self.store_mnemonic_on_memory) - ) - mnemonic_storage = MnemonicStorage() - if mnemonic_storage.has_sd_card: - encrypt_outputs_menu.append( - ( - t("Store on SD Card"), - lambda: self.store_mnemonic_on_memory(sd_card=True), - ) - ) - del mnemonic_storage - encrypt_outputs_menu.append((t("Encrypted QR Code"), self.encrypted_qr_code)) - encrypt_outputs_menu.append((t("Back"), lambda: MENU_EXIT)) + if self.has_sd_card(): + sd_store_func = _sd_store_function + else: + sd_store_func = None + encrypt_outputs_menu = [ + (t("Store on Flash"), self.store_mnemonic_on_memory), + (t("Store on SD Card"), sd_store_func), + (t("Encrypted QR Code"), self.encrypted_qr_code), + (t("Back"), lambda: MENU_EXIT), + ] submenu = Menu(self.ctx, encrypt_outputs_menu) _, _ = submenu.run_loop() return MENU_CONTINUE @@ -127,7 +123,7 @@ def store_mnemonic_on_memory(self, sd_card=False): key_capture = EncryptionKey(self.ctx) key = key_capture.encryption_key() if key is None: - self.ctx.display.flash_text(t("Mnemonic was not encrypted")) + self.flash_text(t("Mnemonic was not encrypted")) return version = Settings().encryption.version @@ -135,7 +131,7 @@ def store_mnemonic_on_memory(self, sd_card=False): if version == "AES-CBC": self.ctx.display.clear() self.ctx.display.draw_centered_text( - t("Aditional entropy from camera required for AES-CBC mode") + t("Additional entropy from camera required for AES-CBC mode") ) if not self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): return @@ -156,7 +152,7 @@ def store_mnemonic_on_memory(self, sd_card=False): if mnemonic_id in (None, ESC_KEY): mnemonic_id = self.ctx.wallet.key.fingerprint_hex_str() if mnemonic_id in mnemonic_storage.list_mnemonics(sd_card): - self.ctx.display.flash_text( + self.flash_text( t("ID already exists\n") + t("Encrypted mnemonic was not stored") ) del mnemonic_storage @@ -181,7 +177,7 @@ def encrypted_qr_code(self): key_capture = EncryptionKey(self.ctx) key = key_capture.encryption_key() if key is None: - self.ctx.display.flash_text(t("Mnemonic was not encrypted")) + self.flash_text(t("Mnemonic was not encrypted")) return version = Settings().encryption.version i_vector = None @@ -191,7 +187,7 @@ def encrypted_qr_code(self): t("Aditional entropy from camera required for AES-CBC mode") ) if not self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): - self.ctx.display.flash_text(t("Mnemonic was not encrypted")) + self.flash_text(t("Mnemonic was not encrypted")) return i_vector = self.capture_camera_entropy()[:AES_BLOCK_SIZE] mnemonic_id = None @@ -222,7 +218,7 @@ def encrypted_qr_code(self): from .qr_view import SeedQRView seed_qr_view = SeedQRView(self.ctx, data=qr_data, title=mnemonic_id) - seed_qr_view.display_seed_qr() + seed_qr_view.display_qr(allow_export=True) class LoadEncryptedMnemonic(Page): @@ -277,20 +273,21 @@ def _load_encrypted_mnemonic(self, mnemonic_id, sd_card=False): key_capture = EncryptionKey(self.ctx) key = key_capture.encryption_key() - if key is None: - raise ValueError(t("Failed to decrypt")) + if key in (None, "", ESC_KEY): + self.flash_text(t("Key was not provided"), theme.error_color) + return MENU_CONTINUE self.ctx.display.clear() self.ctx.display.draw_centered_text(t("Processing ...")) - if key in ("", ESC_KEY): - raise ValueError(t("Failed to decrypt")) mnemonic_storage = MnemonicStorage() try: words = mnemonic_storage.decrypt(key, mnemonic_id, sd_card).split() except: - raise ValueError(t("Failed to decrypt")) + self.flash_text(t("Failed to decrypt"), theme.error_color) + return MENU_CONTINUE if len(words) not in (12, 24): - raise ValueError(t("Failed to decrypt")) + self.flash_text(t("Failed to decrypt"), theme.error_color) + return MENU_CONTINUE del mnemonic_storage return words @@ -300,6 +297,6 @@ def _delete_encrypted_mnemonic(self, mnemonic_id, sd_card=False): mnemonic_storage = MnemonicStorage() self.ctx.display.clear() - if self.prompt(t("Delete %s?" % mnemonic_id), self.ctx.display.height() // 2): + if self.prompt(t("Delete %s?") % mnemonic_id, self.ctx.display.height() // 2): mnemonic_storage.del_mnemonic(mnemonic_id, sd_card) del mnemonic_storage diff --git a/src/krux/pages/files_manager.py b/src/krux/pages/files_manager.py index dd4d9a0a8..d012a5e80 100644 --- a/src/krux/pages/files_manager.py +++ b/src/krux/pages/files_manager.py @@ -65,12 +65,19 @@ def select_file( dir_files = os.listdir(path) for filename in dir_files: - # only include files that match extension and directories + extension_match = False + if isinstance(file_extension, str): + # No extension filter or matches + extension_match = filename.endswith(file_extension) + else: + # Check for any matches for tuple / list + for ext in file_extension: + if filename.endswith(ext): + extension_match = True + break + if ( - # No extension filter - file_extension == "" - # Matches filter - or filename.endswith(file_extension) + extension_match # Is a directory or SDHandler.dir_exists(path + "/" + filename) ): diff --git a/src/krux/pages/files_operations.py b/src/krux/pages/files_operations.py new file mode 100644 index 000000000..050a2d3aa --- /dev/null +++ b/src/krux/pages/files_operations.py @@ -0,0 +1,138 @@ +# 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. + +from . import ( + Page, + ESC_KEY, + LETTERS, + UPPERCASE_LETTERS, +) +from ..krux_settings import t +from ..sd_card import SDHandler + +FILE_SPECIAL = "0123456789()-.[]_~" + + +class SaveFile(Page): + """File saver user interface""" + + def __init__(self, ctx): + super().__init__(ctx, None) + + def save_file( + self, + data, + empty_name, + filename="", + file_description="", + file_extension="", + file_suffix="", + save_as_binary=True, + ): + """File saver handler page""" + try: + with SDHandler() as sd: + # Wait until user defines a filename or select NO on the prompt + filename_undefined = True + while filename_undefined: + self.ctx.display.clear() + if self.prompt( + file_description + "\n" + t("Save to SD card?") + "\n\n", + self.ctx.display.height() // 2, + ): + filename, filename_undefined = self.set_filename( + filename, + empty_name, + file_suffix, + file_extension, + ) + + # if user defined a filename and it is ok, save! + if not filename_undefined: + if save_as_binary: + sd.write_binary(filename, data) + else: + sd.write(filename, data) + self.flash_text(t("Saved to SD card:\n%s") % filename) + else: + filename_undefined = False + except: + self.flash_text(t("SD card not detected.")) + + def set_filename( + self, curr_filename="", empty_filename="some_file", suffix="", file_extension="" + ): + """Helper to set the filename based on a suggestion and the user input""" + started_filename = curr_filename + filename_undefined = True + + # remove the file_extension if exists + curr_filename = ( + curr_filename[: len(curr_filename) - len(file_extension)] + if curr_filename.endswith(file_extension) + else curr_filename + ) + + # remove the suffix if exists (because we will add it later) + curr_filename = ( + curr_filename[: len(curr_filename) - len(suffix)] + if curr_filename.endswith(suffix) + else curr_filename + ) + + curr_filename = self.capture_from_keypad( + t("Filename"), + [LETTERS, UPPERCASE_LETTERS, FILE_SPECIAL], + starting_buffer=("%s" + suffix) % curr_filename + if curr_filename + else empty_filename + suffix, + ) + + # Verify if user defined a filename and it is not just dots + if ( + curr_filename + and curr_filename != ESC_KEY + and not all(c in "." for c in curr_filename) + ): + # add the extension ".psbt" + curr_filename = ( + curr_filename + if curr_filename.endswith(file_extension) + else curr_filename + file_extension + ) + # check and warn for overwrite filename + # add the "/sd/" prefix + if SDHandler.file_exists("/sd/" + curr_filename): + self.ctx.display.clear() + if self.prompt( + t("Filename %s exists on SD card, overwrite?") % curr_filename + + "\n\n", + self.ctx.display.height() // 2, + ): + filename_undefined = False + else: + filename_undefined = False + + if curr_filename == ESC_KEY: + curr_filename = started_filename + + return (curr_filename, filename_undefined) diff --git a/src/krux/pages/home.py b/src/krux/pages/home.py index 05ed46fb2..78f844041 100644 --- a/src/krux/pages/home.py +++ b/src/krux/pages/home.py @@ -21,34 +21,24 @@ # THE SOFTWARE. import gc +from .utils import Utils from ..themes import theme from ..display import DEFAULT_PADDING from ..psbt import PSBTSigner from ..qr import FORMAT_NONE, FORMAT_PMOFN -from ..krux_settings import t, Settings, THERMAL_ADAFRUIT_TXT +from ..krux_settings import t from . import ( Page, Menu, MENU_CONTINUE, MENU_EXIT, - ESC_KEY, - LETTERS, - UPPERCASE_LETTERS, ) -from ..sd_card import SDHandler - -# to start xpub value without the xpub/zpub/ypub prefix -WALLET_XPUB_START = 4 -# len of the xpub to show -WALLET_XPUB_DIGITS = 4 - -FILE_SPECIAL = "0123456789()-.[]_~" - -PSBT_FILE_SUFFIX = "-signed" -PSBT_FILE_EXTENSION = ".psbt" -PUBKEY_FILE_EXTENSION = ".pub" -SIGNATURE_FILE_EXTENSION = ".sig" -SIGNATURE_FILE_SUFFIX = PSBT_FILE_SUFFIX +from ..sd_card import ( + PSBT_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + DESCRIPTOR_FILE_EXTENSION, + JSON_FILE_EXTENSION, +) class Home(Page): @@ -70,98 +60,14 @@ def __init__(self, ctx): ], ), ) + self.utils = Utils(self.ctx) def mnemonic(self): """Handler for the 'mnemonic' menu item""" - submenu = Menu( - self.ctx, - [ - (t("Words"), self.display_mnemonic_words), - (t("Plaintext QR"), self.display_standard_qr), - (t("Compact SeedQR"), lambda: self.display_seed_qr(True)), - (t("SeedQR"), self.display_seed_qr), - (t("Stackbit 1248"), self.stackbit), - (t("Tiny Seed"), self.tiny_seed), - (t("Back"), lambda: MENU_EXIT), - ], - ) - submenu.run_loop() - return MENU_CONTINUE - - def display_mnemonic_words(self): - """Displays only the mnemonic words""" - self.display_mnemonic(self.ctx.wallet.key.mnemonic) - self.ctx.input.wait_for_button() - - # Avoid printing text on a cnc - if Settings().printer.driver == THERMAL_ADAFRUIT_TXT: - self.ctx.display.clear() - if self.prompt( - t("Print?\n\n%s\n\n") % Settings().printer.driver, - self.ctx.display.height() // 2, - ): - from .print_page import PrintPage - - print_page = PrintPage(self.ctx) - print_page.print_mnemonic_text() - return MENU_CONTINUE - - def display_standard_qr(self): - """Displays regular words QR code""" - title = t("Plaintext QR") - data = self.ctx.wallet.key.mnemonic - self.display_qr_codes(data, FORMAT_NONE, title) - self.print_standard_qr(data, FORMAT_NONE, title) - return MENU_CONTINUE - - def display_seed_qr(self, binary=False): - """Display Seed QR with with different view modes""" - - from .qr_view import SeedQRView - - seed_qr_view = SeedQRView(self.ctx, binary) - return seed_qr_view.display_seed_qr() + from .mnemonic_view import MnemonicsView - def stackbit(self): - """Displays which numbers 1248 user should punch on 1248 steel card""" - from .stack_1248 import Stackbit - - stackbit = Stackbit(self.ctx) - word_index = 1 - words = self.ctx.wallet.key.mnemonic.split(" ") - - while word_index < len(words): - y_offset = 2 * self.ctx.display.font_height - for _ in range(6): - stackbit.export_1248(word_index, y_offset, words[word_index - 1]) - if self.ctx.display.height() > 240: - y_offset += 3 * self.ctx.display.font_height - else: - y_offset += 5 + 2 * self.ctx.display.font_height - word_index += 1 - self.ctx.input.wait_for_button() - - # removed the hability to go back in favor or the Krux UI patter (always move forward) - # if self.ctx.input.wait_for_button() == BUTTON_PAGE_PREV: - # if word_index > 12: - # word_index -= 12 - # else: - # word_index = 1 - self.ctx.display.clear() - return MENU_CONTINUE - - def tiny_seed(self): - """Displays the seed in Tiny Seed format""" - from .tiny_seed import TinySeed - - tiny_seed = TinySeed(self.ctx) - tiny_seed.export() - - # Allow to print on thermal printer only - if Settings().printer.driver == THERMAL_ADAFRUIT_TXT: - if self.print_qr_prompt(): - tiny_seed.print_tiny_seed() - return MENU_CONTINUE + mnemonics_viewer = MnemonicsView(self.ctx) + return mnemonics_viewer.mnemonic() def encrypt_mnemonic(self): """Handler for Mnemonic > Encrypt Mnemonic menu item""" @@ -172,40 +78,10 @@ def encrypt_mnemonic(self): def public_key(self): """Handler for the 'xpub' menu item""" - zpub = "Zpub" if self.ctx.wallet.key.multisig else "zpub" - for version in [None, self.ctx.wallet.key.network[zpub]]: - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - self.ctx.wallet.key.fingerprint_hex_str(True) - + "\n\n" - + self.ctx.wallet.key.derivation_str(True) - + "\n\n" - + self.ctx.wallet.key.account_pubkey_str(version) - ) - self.ctx.input.wait_for_button() + from .pub_key_view import PubkeyView - # title receives first 4 chars (ex: XPUB) - title = self.ctx.wallet.key.account_pubkey_str(version)[ - :WALLET_XPUB_START - ].upper() - xpub = self.ctx.wallet.key.key_expression(version) - self.display_qr_codes(xpub, FORMAT_NONE, title) - self.print_standard_qr(xpub, FORMAT_NONE, title) - - # Try to save the XPUB file on the SD card - try: - self._save_file( - xpub, - title, - title, - title + ":", - PUBKEY_FILE_EXTENSION, - save_as_binary=False, - ) - except OSError: - pass - - return MENU_CONTINUE + pubkey_viewer = PubkeyView(self.ctx) + return pubkey_viewer.public_key() def wallet(self): """Handler for the 'wallet' menu item""" @@ -219,17 +95,39 @@ def wallet(self): else: self.display_wallet(self.ctx.wallet) wallet_data, qr_format = self.ctx.wallet.wallet_qr() - self.print_standard_qr( - wallet_data, qr_format, t("Wallet output descriptor") - ) + title = t("Wallet output descriptor") + self.utils.print_standard_qr(wallet_data, qr_format, title) + + # Try to save the Wallet output descriptor on the SD card + if self.has_sd_card(): + from .files_operations import SaveFile + + save_page = SaveFile(self.ctx) + save_page.save_file( + self.ctx.wallet.descriptor.to_string(), + self.ctx.wallet.label, + self.ctx.wallet.label, + title + ":", + DESCRIPTOR_FILE_EXTENSION, + save_as_binary=False, + ) return MENU_CONTINUE def _load_wallet(self): wallet_data, qr_format = self.capture_qr_code() if wallet_data is None: - self.ctx.display.flash_text( - t("Failed to load output descriptor"), theme.error_color - ) + # Try to read the wallet output descriptor from a file on the SD card + qr_format = FORMAT_NONE + try: + _, wallet_data = self.utils.load_file( + (DESCRIPTOR_FILE_EXTENSION, JSON_FILE_EXTENSION) + ) + except OSError: + pass + + if wallet_data is None: + # Both the camera and the file on SD card failed! + self.flash_text(t("Failed to load output descriptor"), theme.error_color) return MENU_CONTINUE try: @@ -245,7 +143,7 @@ def _load_wallet(self): "Wallet output descriptor: %s" % self.ctx.wallet.descriptor.to_string() ) - self.ctx.display.flash_text(t("Wallet output descriptor loaded!")) + self.flash_text(t("Wallet output descriptor loaded!")) # BlueWallet single sig descriptor without fingerprint if ( @@ -254,7 +152,8 @@ def _load_wallet(self): ): self.ctx.display.clear() self.ctx.display.draw_centered_text( - t("Warning:\nIncomplete output descriptor"), theme.error_color + t("Warning:") + "\n" + t("Incomplete output descriptor"), + theme.error_color, ) self.ctx.input.wait_for_button() @@ -273,7 +172,7 @@ def addresses_menu(self): from .addresses import Addresses adresses = Addresses(self.ctx) - adresses.addresses_menu() + return adresses.addresses_menu() def sign(self): """Handler for the 'sign' menu item""" @@ -296,10 +195,11 @@ def sign_psbt(self): # Warns in case multisig wallet descriptor is not loaded if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig(): self.ctx.display.draw_centered_text( - t( - """Warning:\nWallet output descriptor not found.\n\n - Some checks cannot be performed.""" - ) + t("Warning:") + + "\n" + + t("Wallet output descriptor not found.") + + "\n\n" + + t("Some checks cannot be performed.") ) if not self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): return MENU_CONTINUE @@ -312,13 +212,13 @@ def sign_psbt(self): # Try to read a PSBT from a file on the SD card qr_format = FORMAT_NONE try: - psbt_filename, data = self._load_file(PSBT_FILE_EXTENSION) + psbt_filename, data = self.utils.load_file(PSBT_FILE_EXTENSION) except OSError: pass if data is None: # Both the camera and the file on SD card failed! - self.ctx.display.flash_text(t("Failed to load PSBT"), theme.error_color) + self.flash_text(t("Failed to load PSBT"), theme.error_color) return MENU_CONTINUE # PSBT read OK! Will try to sign @@ -356,7 +256,7 @@ def sign_psbt(self): title = t("Signed PSBT") try: self.display_qr_codes(qr_signed_psbt, qr_format) - self.print_standard_qr(qr_signed_psbt, qr_format, title, width=45) + self.utils.print_standard_qr(qr_signed_psbt, qr_format, title, width=45) except Exception as e: self.ctx.log.exception( "Exception occurred in sign_psbt when trying to show the qr_signed_psbt" @@ -372,249 +272,26 @@ def sign_psbt(self): gc.collect() # Try to save the signed PSBT file on the SD card - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Checking for SD card..")) - try: - self._save_file( + if self.has_sd_card(): + from .files_operations import SaveFile + + save_page = SaveFile(self.ctx) + save_page.save_file( serialized_signed_psbt, "QRCode", psbt_filename, title + ":", PSBT_FILE_EXTENSION, - PSBT_FILE_SUFFIX, + SIGNED_FILE_SUFFIX, ) - except OSError: - pass - return MENU_CONTINUE - def _load_file(self, file_ext=""): - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Checking for SD card..")) - with SDHandler() as sd: - self.ctx.display.clear() - if self.prompt( - t("Load from SD card?") + "\n\n", self.ctx.display.height() // 2 - ): - from .files_manager import FileManager - - file_manager = FileManager(self.ctx) - filename = file_manager.select_file(file_extension=file_ext) - - if filename: - filename = file_manager.display_file(filename) - - if self.prompt(t("Load?"), self.ctx.display.bottom_prompt_line): - return filename, sd.read_binary(filename) - return "", None - def sign_message(self): """Handler for the 'sign message' menu item""" + from .sign_message_ui import SignMessage - import binascii - import hashlib - from ..baseconv import base_encode - - # Try to read a message from camera - message_filename = "" - data, qr_format = self.capture_qr_code() - - if data is None: - # Try to read a message from a file on the SD card - qr_format = FORMAT_NONE - try: - message_filename, data = self._load_file() - except OSError: - pass - - if data is None: - self.ctx.display.flash_text(t("Failed to load message"), theme.error_color) - return MENU_CONTINUE - - # message read OK! - data = data.encode() if isinstance(data, str) else data - - message_hash = None - if len(data) == 32: - # It's a sha256 hash already - message_hash = data - else: - if len(data) == 64: - # It may be a hex-encoded sha256 hash - try: - message_hash = binascii.unhexlify(data) - except: - pass - if message_hash is None: - # It's a message, so compute its sha256 hash - message_hash = hashlib.sha256(data).digest() - - # memory management - del data - gc.collect() - - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("SHA256:\n%s") % binascii.hexlify(message_hash).decode() - ) - if not self.prompt(t("Sign?"), self.ctx.display.bottom_prompt_line): - return MENU_CONTINUE - - # User confirmed to sign! - sig = self.ctx.wallet.key.sign(message_hash).serialize() - - # Encode sig as base64 string - encoded_sig = base_encode(sig, 64).decode() - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Signature") + ":\n\n%s" % encoded_sig) - self.ctx.input.wait_for_button() - - # Show the base64 signed message as a QRCode - title = t("Signed Message") - self.display_qr_codes(encoded_sig, qr_format, title) - self.print_standard_qr(encoded_sig, qr_format, title) - - # memory management - del encoded_sig - gc.collect() - - # Show the public key as a QRCode - pubkey = binascii.hexlify(self.ctx.wallet.key.account.sec()).decode() - self.ctx.display.clear() - - title = t("Hex Public Key") - self.ctx.display.draw_centered_text(title + ":\n\n%s" % pubkey) - self.ctx.input.wait_for_button() - - # Show the public key in hexadecimal format as a QRCode - self.display_qr_codes(pubkey, qr_format, title) - self.print_standard_qr(pubkey, qr_format, title) - - # memory management - gc.collect() - - # Try to save the signature file on the SD card - try: - self._save_file( - sig, - "message", - message_filename, - t("Signature") + ":", - SIGNATURE_FILE_EXTENSION, - SIGNATURE_FILE_SUFFIX, - ) - except OSError: - pass - - # Try to save the public key on the SD card - try: - self._save_file( - pubkey, "pubkey", "", title + ":", PUBKEY_FILE_EXTENSION, "", False - ) - except OSError: - pass - - return MENU_CONTINUE - - def _save_file( - self, - data, - empty_name, - filename="", - file_description="", - file_extension="", - file_suffix="", - save_as_binary=True, - ): - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Checking for SD card..")) - with SDHandler() as sd: - # Wait until user defines a filename or select NO on the prompt - filename_undefined = True - while filename_undefined: - self.ctx.display.clear() - if self.prompt( - file_description + "\n" + t("Save to SD card?") + "\n\n", - self.ctx.display.height() // 2, - ): - filename, filename_undefined = self._set_filename( - filename, - empty_name, - file_suffix, - file_extension, - ) - - # if user defined a filename and it is ok, save! - if not filename_undefined: - if save_as_binary: - sd.write_binary(filename, data) - else: - sd.write(filename, data) - self.ctx.display.clear() - self.ctx.display.flash_text( - t("Saved to SD card:\n%s") % filename - ) - else: - filename_undefined = False - - def _set_filename( - self, curr_filename="", empty_filename="some_file", suffix="", file_extension="" - ): - """Helper to set the filename based on a suggestion and the user input""" - started_filename = curr_filename - filename_undefined = True - - # remove the file_extension if exists - curr_filename = ( - curr_filename[: len(curr_filename) - len(file_extension)] - if curr_filename.endswith(file_extension) - else curr_filename - ) - - # remove the suffix if exists (because we will add it later) - curr_filename = ( - curr_filename[: len(curr_filename) - len(suffix)] - if curr_filename.endswith(suffix) - else curr_filename - ) - - curr_filename = self.capture_from_keypad( - t("Filename"), - [LETTERS, UPPERCASE_LETTERS, FILE_SPECIAL], - starting_buffer=("%s" + suffix) % curr_filename - if curr_filename - else empty_filename + suffix, - ) - - # Verify if user defined a filename and it is not just dots - if ( - curr_filename - and curr_filename != ESC_KEY - and not all(c in "." for c in curr_filename) - ): - # add the extension ".psbt" - curr_filename = ( - curr_filename - if curr_filename.endswith(file_extension) - else curr_filename + file_extension - ) - # check and warn for overwrite filename - # add the "/sd/" prefix - if SDHandler.file_exists("/sd/" + curr_filename): - self.ctx.display.clear() - if self.prompt( - t("Filename %s exists on SD card, overwrite?") % curr_filename - + "\n\n", - self.ctx.display.height() // 2, - ): - filename_undefined = False - else: - filename_undefined = False - - if curr_filename == ESC_KEY: - curr_filename = started_filename - - return (curr_filename, filename_undefined) + message_signer = SignMessage(self.ctx) + return message_signer.sign_message() def display_wallet(self, wallet, include_qr=True): """Displays a wallet, including its label and abbreviated xpubs. @@ -622,35 +299,51 @@ def display_wallet(self, wallet, include_qr=True): which will contain the same data as was originally loaded, in the same QR format """ - about = wallet.label + "\n" + about = [wallet.label] if wallet.is_multisig(): - xpubs = [] - for i, xpub in enumerate(wallet.policy["cosigners"]): - xpubs.append( - str(i + 1) - + ". " - + xpub[WALLET_XPUB_START : WALLET_XPUB_START + WALLET_XPUB_DIGITS] - + ".." - + xpub[len(xpub) - WALLET_XPUB_DIGITS :] + import binascii + + fingerprints = [] + for i, key in enumerate(wallet.descriptor.keys): + fingerprints.append( + str(i + 1) + ". " + binascii.hexlify(key.fingerprint).decode() ) - about += "\n".join(xpubs) + about.extend(fingerprints) else: + about.append(wallet.key.fingerprint_hex_str()) xpub = wallet.key.xpub() - about += ( - xpub[WALLET_XPUB_START : WALLET_XPUB_START + WALLET_XPUB_DIGITS] - + ".." - + xpub[len(xpub) - WALLET_XPUB_DIGITS :] - ) - if include_qr: + about.append(self.fit_to_line(xpub)) + + if not wallet.is_multisig() and include_qr: wallet_data, qr_format = wallet.wallet_qr() self.display_qr_codes(wallet_data, qr_format, title=about) else: self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING) - def print_standard_qr(self, data, qr_format, title="", width=33): - """Loads printer driver and UI""" - if self.print_qr_prompt(): - from .print_page import PrintPage + # If multisig, show loaded wallet again with all XPUB + if wallet.is_multisig(): + about = [wallet.label] + xpubs = [] + for i, xpub in enumerate(wallet.policy["cosigners"]): + xpubs.append(self.fit_to_line(xpub, str(i + 1) + ". ")) + about.extend(xpubs) - print_page = PrintPage(self.ctx) - print_page.print_qr(data, qr_format, title, width) + if include_qr: + self.ctx.input.wait_for_button() + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING) + self.ctx.input.wait_for_button() + + # Try to show the wallet output descriptor as a QRCode + try: + wallet_data, qr_format = wallet.wallet_qr() + self.display_qr_codes(wallet_data, qr_format, title=wallet.label) + except Exception as e: + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("Error:\n%s") % repr(e), theme.error_color + ) + self.ctx.input.wait_for_button() + else: + self.ctx.input.wait_for_button() + self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING) diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py index 0d5a86ff6..5e18e215f 100644 --- a/src/krux/pages/login.py +++ b/src/krux/pages/login.py @@ -24,6 +24,7 @@ from embit.networks import NETWORKS from embit.wordlists.bip39 import WORDLIST from embit import bip39 +from .utils import Utils from ..themes import theme from ..krux_settings import Settings from ..qr import FORMAT_UR @@ -41,17 +42,10 @@ NUM_SPECIAL_2, ) -D6_STATES = [str(i + 1) for i in range(6)] -D20_STATES = [str(i + 1) for i in range(20)] DIGITS = "0123456789" DIGITS_HEX = "0123456789ABCDEF" DIGITS_OCT = "01234567" -D6_12W_MIN_ROLLS = 50 -D6_24W_MIN_ROLLS = 99 -D20_12W_MIN_ROLLS = 30 -D20_24W_MIN_ROLLS = 60 - SD_MSG_TIME = 2500 PASSPHRASE_MAX_LEN = 200 @@ -146,8 +140,8 @@ def new_key(self): self.ctx, [ (t("Via Camera"), self.new_key_from_snapshot), - (t("Via D6"), self.new_key_from_d6), - (t("Via D20"), self.new_key_from_d20), + (t("Via D6"), self.new_key_from_dice), + (t("Via D20"), lambda: self.new_key_from_dice(True)), (t("Back"), lambda: MENU_EXIT), ], ) @@ -156,13 +150,16 @@ def new_key(self): return MENU_CONTINUE return status - def new_key_from_d6(self): - """Handler for the 'via D6' menu item""" - return self._new_key_from_die(D6_STATES, D6_12W_MIN_ROLLS, D6_24W_MIN_ROLLS) + def new_key_from_dice(self, d_20=False): + """Handler for the 'via DX' menu item. Default is D6""" + from .new_mnemonic.dice_rolls import DiceEntropy - def new_key_from_d20(self): - """Handler for the 'via D20' menu item""" - return self._new_key_from_die(D20_STATES, D20_12W_MIN_ROLLS, D20_24W_MIN_ROLLS) + dice_entropy = DiceEntropy(self.ctx, d_20) + captured_entropy = dice_entropy.new_key() + if captured_entropy is not None: + words = bip39.mnemonic_from_bytes(captured_entropy).split() + return self._load_key_from_words(words) + return MENU_CONTINUE def new_key_from_snapshot(self): """Use camera's entropy to create a new mnemonic""" @@ -186,7 +183,10 @@ def new_key_from_snapshot(self): + t("(Experimental)") ) if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): - entropy_bytes = self.capture_camera_entropy() + from .capture_entropy import CameraEntropy + + camera_entropy = CameraEntropy(self.ctx) + entropy_bytes = camera_entropy.capture() if entropy_bytes is not None: import binascii @@ -201,113 +201,40 @@ def new_key_from_snapshot(self): return self._load_key_from_words(words) return MENU_CONTINUE - def _new_key_from_die(self, roll_states, min_rolls_12w, min_rolls_24w): - submenu = Menu( - self.ctx, - [ - (t("12 words"), lambda: MENU_EXIT), - (t("24 words"), lambda: MENU_EXIT), - (t("Back"), lambda: MENU_EXIT), - ], - ) - index, _ = submenu.run_loop() - if index == 2: - return MENU_CONTINUE - - min_rolls = min_rolls_12w if index == 0 else min_rolls_24w - self.ctx.display.clear() - - delete_flag = False - self.ctx.display.draw_hcentered_text( - t("Roll dice at least %d times to generate a mnemonic.") % (min_rolls) - ) - if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): - rolls = [] - - def delete_roll(buffer): - # buffer not used here - nonlocal delete_flag - delete_flag = True - return buffer - - while True: - roll = "" - while True: - dice_title = t("Rolls: %d\n") % len(rolls) - entropy = ( - "".join(rolls) if len(roll_states) < 10 else "-".join(rolls) - ) - if len(entropy) <= 10: - dice_title += entropy - else: - dice_title += "..." + entropy[-10:] - roll = self.capture_from_keypad( - dice_title, - [roll_states], - delete_key_fn=delete_roll, - go_on_change=True, - ) - if roll == ESC_KEY: - return MENU_CONTINUE - break - - if roll != "": - rolls.append(roll) - else: - # If its not a roll it is Del or Go - if delete_flag: # Del - delete_flag = False - if len(rolls) > 0: - rolls.pop() - elif len(rolls) < min_rolls: # Not enough to Go - self.ctx.display.flash_text(t("Not enough rolls!")) - else: # Go - break - - entropy = "".join(rolls) if len(roll_states) < 10 else "-".join(rolls) - - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Rolls:\n\n%s") % entropy) - - import hashlib - import binascii - - self.ctx.input.wait_for_button() - entropy_bytes = entropy.encode() - entropy_hash = binascii.hexlify( - hashlib.sha256(entropy_bytes).digest() - ).decode() - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("SHA256 of rolls:\n\n%s") % entropy_hash - ) - self.ctx.input.wait_for_button() - num_bytes = 16 if min_rolls == min_rolls_12w else 32 - words = bip39.mnemonic_from_bytes( - hashlib.sha256(entropy_bytes).digest()[:num_bytes] - ).split() - return self._load_key_from_words(words) - - return MENU_CONTINUE - def _load_qr_passphrase(self): data, _ = self.capture_qr_code() if data is None: - self.ctx.display.flash_text( - t("Failed to load passphrase"), theme.error_color - ) + self.flash_text(t("Failed to load passphrase"), theme.error_color) return MENU_CONTINUE if len(data) > PASSPHRASE_MAX_LEN: - self.ctx.display.flash_text( + self.flash_text( t("Maximum length exceeded (%s)") % PASSPHRASE_MAX_LEN, theme.error_color, ) return MENU_CONTINUE return data - def _load_key_from_words(self, words): + def _load_key_from_words(self, words, charset=LETTERS): mnemonic = " ".join(words) - self.display_mnemonic(mnemonic) + + if charset != LETTERS: + charset_type = { + DIGITS: Utils.BASE_DEC, + DIGITS_HEX: Utils.BASE_HEX, + DIGITS_OCT: Utils.BASE_OCT, + } + suffix_dict = { + DIGITS: Utils.BASE_DEC_SUFFIX, + DIGITS_HEX: Utils.BASE_HEX_SUFFIX, + DIGITS_OCT: Utils.BASE_OCT_SUFFIX, + } + numbers_str = Utils.get_mnemonic_numbers(mnemonic, charset_type[charset]) + self.display_mnemonic(numbers_str, suffix_dict[charset]) + if not self.prompt(t("Continue?"), self.ctx.display.bottom_prompt_line): + return MENU_CONTINUE + self.ctx.display.clear() + + self.display_mnemonic(mnemonic, t("Mnemonic")) if not self.prompt(t("Continue?"), self.ctx.display.bottom_prompt_line): return MENU_CONTINUE self.ctx.display.clear() @@ -413,24 +340,24 @@ def _encrypted_qr_code(self, data): key_capture = EncryptionKey(self.ctx) key = key_capture.encryption_key() - if key is None: - self.ctx.display.flash_text(t("Mnemonic was not decrypted")) - return None + if key in (None, "", ESC_KEY): + self.flash_text(t("Key was not provided"), theme.error_color) + return MENU_CONTINUE self.ctx.display.clear() self.ctx.display.draw_centered_text(t("Processing ...")) - if key in ("", ESC_KEY): - raise ValueError(t("Failed to decrypt")) word_bytes = encrypted_qr.decrypt(key) if word_bytes is None: - raise ValueError(t("Failed to decrypt")) + self.flash_text(t("Failed to decrypt"), theme.error_color) + return MENU_CONTINUE return bip39.mnemonic_from_bytes(word_bytes).split() + return MENU_CONTINUE # prompt NO return None def load_key_from_qr_code(self): """Handler for the 'via qr code' menu item""" data, qr_format = self.capture_qr_code() if data is None: - self.ctx.display.flash_text(t("Failed to load mnemonic"), theme.error_color) + self.flash_text(t("Failed to load mnemonic"), theme.error_color) return MENU_CONTINUE words = [] @@ -447,25 +374,34 @@ def load_key_from_qr_code(self): pass if not words: + data_bytes = "" try: data_bytes = ( data.encode("latin-1") if isinstance(data, str) else data ) - # CompactSeedQR format - if len(data_bytes) in (16, 32): - words = bip39.mnemonic_from_bytes(data_bytes).split() - # SeedQR format - elif len(data_bytes) in (48, 96): - words = [ - WORDLIST[int(data_bytes[i : i + 4])] - for i in range(0, len(data_bytes), 4) - ] except: - pass + try: + data_bytes = ( + data.encode("shift-jis") if isinstance(data, str) else data + ) + except: + pass + + if len(data_bytes) in (16, 32): + # CompactSeedQR format + words = bip39.mnemonic_from_bytes(data_bytes).split() + # SeedQR format + elif len(data_bytes) in (48, 96): + words = [ + WORDLIST[int(data_bytes[i : i + 4])] + for i in range(0, len(data_bytes), 4) + ] if not words: words = self._encrypted_qr_code(data) + if words == MENU_CONTINUE: + return MENU_CONTINUE if not words or (len(words) != 12 and len(words) != 24): - self.ctx.display.flash_text(t("Invalid mnemonic length"), theme.error_color) + self.flash_text(t("Invalid mnemonic length"), theme.error_color) return MENU_CONTINUE return self._load_key_from_words(words) @@ -481,6 +417,12 @@ def _load_key_from_keypad( self.ctx.display.draw_hcentered_text(title) if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): while len(words) < 24: + if len(words) in (11, 23): + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("Leave blank if you'd like Krux to pick a valid final word") + ) + self.ctx.input.wait_for_button() if len(words) == 12: self.ctx.display.clear() if self.prompt(t("Done?"), self.ctx.display.height() // 2): @@ -524,7 +466,7 @@ def _load_key_from_keypad( ): words.append(word) - return self._load_key_from_words(words) + return self._load_key_from_words(words, charset) return MENU_CONTINUE @@ -773,7 +715,7 @@ def load_key_from_tiny_seed_image(self): words = tiny_scanner.scanner(w24) del tiny_scanner if words is None: - self.ctx.display.flash_text(t("Failed to load mnemonic"), theme.error_color) + self.flash_text(t("Failed to load mnemonic"), theme.error_color) return MENU_CONTINUE return self._load_key_from_words(words) diff --git a/src/krux/pages/mnemonic_view.py b/src/krux/pages/mnemonic_view.py new file mode 100644 index 000000000..6816c31bc --- /dev/null +++ b/src/krux/pages/mnemonic_view.py @@ -0,0 +1,176 @@ +# 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. + +from .utils import Utils +from ..qr import FORMAT_NONE +from ..krux_settings import t, Settings, THERMAL_ADAFRUIT_TXT +from . import ( + Page, + Menu, + MENU_CONTINUE, + MENU_EXIT, +) + + +class MnemonicsView(Page): + """UI to show mnemonic in different formats""" + + def __init__(self, ctx): + super().__init__(ctx, None) + self.ctx = ctx + self.utils = Utils(self.ctx) + + def mnemonic(self): + """Menu with export mnemonic formats""" + submenu = Menu( + self.ctx, + [ + ( + t("Words"), + lambda: self.show_mnemonic( + self.ctx.wallet.key.mnemonic, t("Mnemonic") + ), + ), + (t("Numbers"), self.display_mnemonic_numbers), + (t("Plaintext QR"), self.display_standard_qr), + (t("Compact SeedQR"), lambda: self.display_seed_qr(True)), + (t("SeedQR"), self.display_seed_qr), + (t("Stackbit 1248"), self.stackbit), + (t("Tiny Seed"), self.tiny_seed), + (t("Back"), lambda: MENU_EXIT), + ], + ) + submenu.run_loop() + return MENU_CONTINUE + + def show_mnemonic(self, mnemonic, suffix=""): + """Displays only the mnemonic words or indexes""" + self.display_mnemonic(mnemonic, suffix) + self.ctx.input.wait_for_button() + + # Avoid printing text on a cnc + if Settings().hardware.printer.driver == THERMAL_ADAFRUIT_TXT: + self.ctx.display.clear() + if self.prompt( + t("Print?\n\n%s\n\n") % Settings().hardware.printer.driver, + self.ctx.display.height() // 2, + ): + from .print_page import PrintPage + + print_page = PrintPage(self.ctx) + print_page.print_mnemonic_text(mnemonic, suffix) + return MENU_CONTINUE + + def display_mnemonic_numbers(self): + """Handler for the 'numbers' menu item""" + submenu = Menu( + self.ctx, + [ + ( + t("Decimal"), + lambda: self.show_mnemonic( + Utils.get_mnemonic_numbers( + self.ctx.wallet.key.mnemonic, Utils.BASE_DEC + ), + Utils.BASE_DEC_SUFFIX, + ), + ), + ( + t("Hexadecimal"), + lambda: self.show_mnemonic( + Utils.get_mnemonic_numbers( + self.ctx.wallet.key.mnemonic, Utils.BASE_HEX + ), + Utils.BASE_HEX_SUFFIX, + ), + ), + ( + t("Octal"), + lambda: self.show_mnemonic( + Utils.get_mnemonic_numbers( + self.ctx.wallet.key.mnemonic, Utils.BASE_OCT + ), + Utils.BASE_OCT_SUFFIX, + ), + ), + (t("Back"), lambda: MENU_EXIT), + ], + ) + submenu.run_loop() + return MENU_CONTINUE + + def display_standard_qr(self): + """Displays regular words QR code""" + title = t("Plaintext QR") + data = self.ctx.wallet.key.mnemonic + self.display_qr_codes(data, FORMAT_NONE, title) + self.utils.print_standard_qr(data, FORMAT_NONE, title) + return MENU_CONTINUE + + def display_seed_qr(self, binary=False): + """Display Seed QR with with different view modes""" + + from .qr_view import SeedQRView + + seed_qr_view = SeedQRView(self.ctx, binary) + return seed_qr_view.display_qr() + + def stackbit(self): + """Displays which numbers 1248 user should punch on 1248 steel card""" + from .stack_1248 import Stackbit + + stackbit = Stackbit(self.ctx) + word_index = 1 + words = self.ctx.wallet.key.mnemonic.split(" ") + + while word_index < len(words): + y_offset = 2 * self.ctx.display.font_height + for _ in range(6): + stackbit.export_1248(word_index, y_offset, words[word_index - 1]) + if self.ctx.display.height() > 240: + y_offset += 3 * self.ctx.display.font_height + else: + y_offset += 5 + 2 * self.ctx.display.font_height + word_index += 1 + self.ctx.input.wait_for_button() + + # removed the hability to go back in favor or the Krux UI patter (always move forward) + # if self.ctx.input.wait_for_button() == BUTTON_PAGE_PREV: + # if word_index > 12: + # word_index -= 12 + # else: + # word_index = 1 + self.ctx.display.clear() + return MENU_CONTINUE + + def tiny_seed(self): + """Displays the seed in Tiny Seed format""" + from .tiny_seed import TinySeed + + tiny_seed = TinySeed(self.ctx) + tiny_seed.export() + + # Allow to print on thermal printer only + if Settings().hardware.printer.driver == THERMAL_ADAFRUIT_TXT: + if self.print_qr_prompt(): + tiny_seed.print_tiny_seed() + return MENU_CONTINUE diff --git a/src/krux/pages/new_mnemonic/__init__.py b/src/krux/pages/new_mnemonic/__init__.py new file mode 100644 index 000000000..181abdfd2 --- /dev/null +++ b/src/krux/pages/new_mnemonic/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/src/krux/pages/new_mnemonic/dice_rolls.py b/src/krux/pages/new_mnemonic/dice_rolls.py new file mode 100644 index 000000000..49dc561dd --- /dev/null +++ b/src/krux/pages/new_mnemonic/dice_rolls.py @@ -0,0 +1,292 @@ +# 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. + +from .. import ( + Page, + Menu, + MENU_EXIT, + ESC_KEY, +) +from ...themes import theme +from ...krux_settings import t +from ...display import DEFAULT_PADDING + +D6_STATES = [str(i + 1) for i in range(6)] +D20_STATES = [str(i + 1) for i in range(20)] + +D6_12W_MIN_ROLLS = 50 +D6_24W_MIN_ROLLS = 99 +D20_12W_MIN_ROLLS = 30 +D20_24W_MIN_ROLLS = 60 +MIN_ENTROPY_12W = 128 +MIN_ENTROPY_24W = 256 + +BAR_GRAPH_POSITION = 20 # % of screen (top of the graph) +BAR_GRAPH_SIZE = 60 # % of screen + + +class DiceEntropy(Page): + """Capture and analise entropy from dice rolls""" + + def __init__(self, ctx, is_d20=False): + super().__init__(ctx, None) + self.ctx = ctx + self.is_d20 = is_d20 + self.roll_states = D20_STATES if is_d20 else D6_STATES + self.len_states = len(self.roll_states) + self.min_rolls = 0 + self.min_entropy = 0 + self.rolls = [] + self.roll_counts = [0] * self.len_states + + def _count_rolls(self): + """Recompute rolls count every time entropy is calculated""" + self.roll_counts = [0] * self.len_states + for roll in self.rolls: + self.roll_counts[int(roll) - 1] += 1 + + def calculate_entropy(self): + """Calculates Shannon's entropy of a given list""" + import math + + # Total number of pixels (equal to the number of bytes) + total_rolls = len(self.rolls) + if not total_rolls: + return 0 + self._count_rolls() + + # Calculate entropy + entropy = 0 + for count in self.roll_counts: + probability = count / total_rolls + entropy -= probability * (probability and math.log2(probability)) + + total_entropy = entropy * total_rolls + return int(total_entropy) + + def stats_for_nerds(self): + """ + Displays statistical information and a graphical representation of dice roll outcomes. + This method provides a deeper insight into the entropy collection process by showing: + 1. Distribution of dice rolls as a bar graph. + 2. The calculated Shannon's entropy in bits. + It's intended for users interested in the quality and distribution of their entropy source. + """ + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text( + t("Rolls distribution:"), self.ctx.display.font_height + ) + shannons_entropy = self.calculate_entropy() + max_count = max(self.roll_counts) + + scale_factor = (self.ctx.display.height() * BAR_GRAPH_SIZE) // 100 + scale_factor //= max_count + bar_graph = [] + for count in self.roll_counts: + bar_graph.append(int(count * scale_factor)) + bar_pad = self.ctx.display.width() // (self.len_states + 2) + offset_x = bar_pad + # Bar graph offset (bottom of the graph) + offset_y = BAR_GRAPH_POSITION + BAR_GRAPH_SIZE + offset_y *= self.ctx.display.height() + offset_y //= 100 + + for individual_bar in bar_graph: + bar_offset = offset_y - individual_bar + self.ctx.display.fill_rectangle( + offset_x + 1, + bar_offset, + bar_pad - 2, + individual_bar, + theme.highlight_color, + ) + offset_x += bar_pad + offset_y += self.ctx.display.font_height + self.ctx.display.draw_hcentered_text( + t("Shannon's Entropy: ") + str(shannons_entropy) + "bits", + offset_y, + ) + + self.ctx.input.wait_for_button() + + def draw_progress_bar(self): + """ + Draws a progress bar on the display to show the current progress of dice rolls. + The progress bar consists of two sections: one indicating the number of rolls + made relative to the minimum required, and the other indicating the Shannon's + entropy of the rolls relative to the minimum required entropy. It changes color + to indicate when the minimum criteria have been met. + """ + offset_y = DEFAULT_PADDING + 2 * self.ctx.display.font_height + pb_height = self.ctx.display.font_height - 4 + if len(self.rolls) > 0: # Only draws if rolls > 0 + progress = min(self.min_rolls, len(self.rolls)) + progress *= self.ctx.display.usable_width() - 3 + progress //= self.min_rolls + self.ctx.display.fill_rectangle( + DEFAULT_PADDING + 2, + offset_y + 2, + progress, + (pb_height // 2) - 2, + theme.fg_color, + ) + + shannon_entropy = self.calculate_entropy() + if shannon_entropy: # Only draws if Shannon's > 0 + shannon_progress = min(self.min_entropy, shannon_entropy) + shannon_progress *= self.ctx.display.usable_width() - 3 + shannon_progress //= self.min_entropy + self.ctx.display.fill_rectangle( + DEFAULT_PADDING + 2, + offset_y + (pb_height // 2) + 1, + shannon_progress, + (pb_height // 2) - 2, + theme.highlight_color, + ) + if shannon_entropy >= self.min_entropy and len(self.rolls) >= self.min_rolls: + outline_color = theme.go_color + else: + outline_color = theme.no_esc_color + self.ctx.display.outline( + DEFAULT_PADDING, + offset_y, + self.ctx.display.usable_width(), + pb_height, + outline_color, + ) + + def new_key(self): + """Create a new key from dice rolls""" + submenu = Menu( + self.ctx, + [ + (t("12 words"), lambda: MENU_EXIT), + (t("24 words"), lambda: MENU_EXIT), + (t("Back"), lambda: MENU_EXIT), + ], + ) + index, _ = submenu.run_loop() + is_24_words = index == 1 + if index == 2: + return None + elif index == 1: # 24 words + self.min_entropy = MIN_ENTROPY_24W + self.min_rolls = D20_24W_MIN_ROLLS if self.is_d20 else D6_24W_MIN_ROLLS + else: # 12 words + self.min_entropy = MIN_ENTROPY_12W + self.min_rolls = D20_12W_MIN_ROLLS if self.is_d20 else D6_12W_MIN_ROLLS + self.ctx.display.clear() + + delete_flag = False + self.ctx.display.draw_hcentered_text( + t("Roll dice at least %d times to generate a mnemonic.") % (self.min_rolls) + ) + if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line): + + def delete_roll(buffer): + # buffer not used here + nonlocal delete_flag + delete_flag = True + return buffer + + while True: + roll = "" + while True: + dice_title = t("Rolls: %d\n") % len(self.rolls) + entropy = ( + "".join(self.rolls) + if self.len_states < 10 + else "-".join(self.rolls) + ) + if len(entropy) <= 10: + dice_title += entropy + else: + dice_title += "..." + entropy[-10:] + roll = self.capture_from_keypad( + dice_title, + [self.roll_states], + delete_key_fn=delete_roll, + progress_bar_fn=self.draw_progress_bar, + go_on_change=True, + ) + if roll == ESC_KEY: + return None + break + + if roll != "": + self.rolls.append(roll) + else: + # If its not a roll it is Del or Go + if delete_flag: # Del + delete_flag = False + if len(self.rolls) > 0: + self.rolls.pop() + elif len(self.rolls) < self.min_rolls: # Not enough to Go + self.flash_text(t("Not enough rolls!")) + elif self.calculate_entropy() < self.min_entropy: + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text( + t("Poor entropy detected. More rolls are recommended") + ) + if self.prompt( + t("Proceed anyway?"), self.ctx.display.bottom_prompt_line + ): + break + else: # Go + break + + entropy = ( + "".join(self.rolls) if self.len_states < 10 else "-".join(self.rolls) + ) + self.ctx.display.clear() + rolls_str = t("Rolls:\n\n%s") % entropy + self.ctx.display.draw_hcentered_text(rolls_str, info_box=True) + + submenu = Menu( + self.ctx, + [ + (t("Stats for Nerds"), lambda: MENU_EXIT), + (t("Generate Words"), lambda: MENU_EXIT), + ], + offset=(len(self.ctx.display.to_lines(rolls_str)) + 1) + * self.ctx.display.font_height, + ) + index, _ = submenu.run_loop() + if index == 0: + self.stats_for_nerds() + + import hashlib + import binascii + + entropy_bytes = entropy.encode() + entropy_hash = binascii.hexlify( + hashlib.sha256(entropy_bytes).digest() + ).decode() + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("SHA256 of rolls:\n\n%s") % entropy_hash + ) + self.ctx.input.wait_for_button() + num_bytes = 32 if is_24_words else 16 + return hashlib.sha256(entropy_bytes).digest()[:num_bytes] + + return None diff --git a/src/krux/pages/print_page.py b/src/krux/pages/print_page.py index 7e3bbace2..17f939501 100644 --- a/src/krux/pages/print_page.py +++ b/src/krux/pages/print_page.py @@ -31,7 +31,6 @@ class PrintPage(Page): """Printing user interface""" def __init__(self, ctx): - # Returns True if printer successfully created super().__init__(ctx, None) self.ctx = ctx self.ctx.display.clear() @@ -40,7 +39,7 @@ def __init__(self, ctx): def _send_qr_to_printer(self, qr_code, i=0, count=1): self.ctx.display.clear() - if Settings().printer.driver == "cnc/file": + if Settings().hardware.printer.driver == "cnc/file": self.ctx.display.draw_centered_text(t("Exporting to SD card..")) else: self.ctx.display.draw_centered_text(t("Printing\n%d / %d") % (i + 1, count)) @@ -52,7 +51,7 @@ def print_qr(self, data, qr_format=FORMAT_NONE, title="", width=33, is_qr=False) if a printer is connected """ if self.printer is None: - self.ctx.display.flash_text(t("Printer Driver not set!"), theme.error_color) + self.flash_text(t("Printer Driver not set!"), theme.error_color) return self.ctx.display.clear() if title: @@ -67,14 +66,14 @@ def print_qr(self, data, qr_format=FORMAT_NONE, title="", width=33, is_qr=False) self._send_qr_to_printer(qr_code, i, count) i += 1 - def print_mnemonic_text(self): + def print_mnemonic_text(self, mnemonic, suffix=""): """Prints Mnemonics words as text""" self.ctx.display.clear() self.ctx.display.draw_hcentered_text( t("Printing ..."), self.ctx.display.height() // 2 ) - self.printer.print_string(t("Mnemonic") + "\n\n") - words = self.ctx.wallet.key.mnemonic.split(" ") + self.printer.print_string(t("BIP39") + " " + suffix + "\n\n") + words = mnemonic.split(" ") lines = len(words) // 3 for i in range(lines): index = i + 1 @@ -88,4 +87,4 @@ def print_mnemonic_text(self): index += lines string += str(index) + ":" + words[index - 1] + "\n" self.printer.print_string(string) - self.printer.feed(3) + self.printer.feed(4) diff --git a/src/krux/pages/pub_key_view.py b/src/krux/pages/pub_key_view.py new file mode 100644 index 000000000..9fae2be4a --- /dev/null +++ b/src/krux/pages/pub_key_view.py @@ -0,0 +1,122 @@ +# 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. + +from ..krux_settings import t +from . import ( + Page, + Menu, + MENU_CONTINUE, + MENU_EXIT, +) + +from ..sd_card import PUBKEY_FILE_EXTENSION + +# to start xpub value without the xpub/zpub/ypub prefix +WALLET_XPUB_START = 4 + + +class PubkeyView(Page): + """UI to show and export extended public key""" + + def __init__(self, ctx): + super().__init__(ctx, None) + self.ctx = ctx + + def public_key(self): + """Handler for the 'xpub' menu item""" + + def _save_xpub_to_sd(version): + from .files_operations import SaveFile + + save_page = SaveFile(self.ctx) + xpub = self.ctx.wallet.key.key_expression(version) + title = self.ctx.wallet.key.account_pubkey_str(version)[ + :WALLET_XPUB_START + ].upper() + save_page.save_file( + xpub, + title, + title, + title + ":", + PUBKEY_FILE_EXTENSION, + save_as_binary=False, + ) + + def _pub_key_text(version): + def _save_sd_pubk_function(): + return _save_xpub_to_sd(version) + + if self.has_sd_card(): + save_sd_pubk_func = _save_sd_pubk_function + else: + save_sd_pubk_func = None + + pub_text_menu_items = [ + (t("Save to SD card"), save_sd_pubk_func), + (t("Back"), lambda: MENU_EXIT), + ] + full_pub_key = self.ctx.wallet.key.account_pubkey_str(version) + menu_offset = 5 + len(self.ctx.display.to_lines(full_pub_key)) + menu_offset *= self.ctx.display.font_height + pub_key_menu = Menu(self.ctx, pub_text_menu_items, offset=menu_offset) + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text( + self.ctx.wallet.key.fingerprint_hex_str(pretty=True) + + "\n\n" + + self.ctx.wallet.key.derivation_str(pretty=True) + + "\n\n" + + full_pub_key, + offset_y=self.ctx.display.font_height, + info_box=True, + ) + pub_key_menu.run_loop() + + def _pub_key_qr(version): + title = self.ctx.wallet.key.account_pubkey_str(version)[ + :WALLET_XPUB_START + ].upper() + xpub = self.ctx.wallet.key.key_expression(version) + from .qr_view import SeedQRView + + seed_qr_view = SeedQRView(self.ctx, data=xpub, title=title) + seed_qr_view.display_qr(allow_export=True, transcript_tools=False) + + zpub = "Zpub" if self.ctx.wallet.key.multisig else "zpub" + pub_key_menu_items = [] + for version in [None, self.ctx.wallet.key.network[zpub]]: + title = self.ctx.wallet.key.account_pubkey_str(version)[ + :WALLET_XPUB_START + ].upper() + pub_key_menu_items.append( + (title + " - " + t("Text"), lambda ver=version: _pub_key_text(ver)) + ) + pub_key_menu_items.append( + (title + " - " + t("QR Code"), lambda ver=version: _pub_key_qr(ver)) + ) + pub_key_menu_items.append((t("Back"), lambda: MENU_EXIT)) + pub_key_menu = Menu(self.ctx, pub_key_menu_items) + while True: + _, status = pub_key_menu.run_loop() + if status == MENU_EXIT: + break + + return MENU_CONTINUE diff --git a/src/krux/pages/qr_view.py b/src/krux/pages/qr_view.py index b8e107d7a..d92992d71 100644 --- a/src/krux/pages/qr_view.py +++ b/src/krux/pages/qr_view.py @@ -22,12 +22,12 @@ import qrcode from embit.wordlists.bip39 import WORDLIST -from . import Page +from . import Page, Menu, MENU_CONTINUE, MENU_EXIT +from ..sd_card import SDHandler from ..themes import theme, WHITE, BLACK from ..krux_settings import t -from ..qr import get_size, add_qr_frame +from ..qr import get_size from ..display import DEFAULT_PADDING -from . import MENU_CONTINUE from ..input import ( BUTTON_ENTER, BUTTON_PAGE, @@ -54,7 +54,7 @@ def __init__(self, ctx, binary=False, data=None, title=None): self.ctx = ctx self.binary = binary if data: - self.code = qrcode.encode_to_string(data) + self.code = qrcode.encode(data) self.title = title else: if self.binary: @@ -67,17 +67,18 @@ def __init__(self, ctx, binary=False, data=None, title=None): self.region_size = 7 if self.qr_size == 21 else 5 self.columns = (self.qr_size + self.region_size - 1) // self.region_size self.lr_index = 0 + self.bright = theme.bg_color == WHITE def _seed_qr(self): words = self.ctx.wallet.key.mnemonic.split(" ") numbers = "" for word in words: numbers += str("%04d" % WORDLIST.index(word)) - return qrcode.encode_to_string(numbers) + return qrcode.encode(numbers) def _binary_seed_qr(self): binary_seed = self._to_compact_seed_qr(self.ctx.wallet.key.mnemonic) - return qrcode.encode_to_string(binary_seed) + return qrcode.encode(binary_seed) def _to_compact_seed_qr(self, mnemonic): mnemonic = mnemonic.split(" ") @@ -91,38 +92,38 @@ def _to_compact_seed_qr(self, mnemonic): def highlight_qr_region(self, code, region=(0, 0, 0, 0), zoom=False): """Draws in white a highlighted region of the QR code""" reg_x, reg_y, reg_width, reg_height = region - size, code = add_qr_frame(code) max_width = self.ctx.display.width() if zoom: max_width -= DEFAULT_PADDING - if size == 23: # 21 + 2(frame) + if self.qr_size == 21: qr_size = 7 else: qr_size = 5 offset_x = 0 offset_y = 0 + scale = max_width // qr_size else: - qr_size = size - offset_x = reg_x + 1 - offset_y = reg_y + 1 - - scale = max_width // qr_size + qr_size = self.qr_size + offset_x = reg_x + offset_y = reg_y + scale = max_width // (qr_size + 2) qr_width = qr_size * scale offset = (self.ctx.display.width() - qr_width) // 2 for y in range(reg_height): # vertical blocks loop for x in range(reg_width): # horizontal blocks loop - y_index = reg_y + y + 1 - x_index = reg_x + x + 1 - xy_index = y_index * (size + 1) + y_index = reg_y + y + x_index = reg_x + x + xy_index = y_index * self.qr_size xy_index += x_index - if y_index < size and x_index < size: - if code[xy_index] == "0": + if y_index < self.qr_size and x_index < self.qr_size: + bit_value = code[xy_index >> 3] & (1 << (xy_index % 8)) + if bit_value: self.ctx.display.fill_rectangle( offset + (offset_x + x) * scale, offset + (offset_y + y) * scale, scale, scale, - WHITE, + BLACK, ) else: self.ctx.display.fill_rectangle( @@ -130,7 +131,7 @@ def highlight_qr_region(self, code, region=(0, 0, 0, 0), zoom=False): offset + (offset_y + y) * scale, scale, scale, - BLACK, + WHITE, ) def _region_legend(self, row, column): @@ -153,7 +154,7 @@ def draw_grided_qr(self, mode): grid_pad = self.ctx.display.width() // (self.qr_size + 2) grid_offset += grid_pad if mode == STANDARD_MODE: - if theme.bg_color == WHITE: + if self.bright: self.ctx.display.draw_qr_code(0, self.code, light_color=WHITE) else: self.ctx.display.draw_qr_code(0, self.code) @@ -291,36 +292,166 @@ def draw_grided_qr(self, mode): theme.highlight_color, ) - def display_seed_qr(self): - """Disables touch and displays compact SeedQR code with grid to help - drawing""" + def add_frame(self, binary_image, size): + """Adds a 1 block frame to QR codes""" + new_size = size + 2 + # Create a new bytearray to store the framed image + framed_image = bytearray(b"\x00" * ((new_size * new_size + 7) >> 3)) + # Copy the original image into the center of the framed image + for y in range(0, size): + for x in range(0, size): + original_index = y * size + x + original_bit = ( + binary_image[original_index >> 3] >> (original_index % 8) + ) & 1 + if original_bit: + framed_index = (y + 1) * new_size + x + 1 + framed_image[framed_index >> 3] |= 1 << (framed_index % 8) + + return framed_image, new_size + + def save_pbm_image(self, file_name): + """Saves QR code image as compact B&W bitmap format file""" + from ..sd_card import PBM_IMAGE_EXTENSION + + code, size = self.add_frame(self.code, self.qr_size) + pbm_data = bytearray() + pbm_data.extend(("P4\n{0} {0}\n".format(size)).encode()) + for row in range(size): + byte = 0 + for col in range(size): + bit_index = row * size + col + if code[bit_index >> 3] & (1 << (bit_index % 8)): + byte |= 1 << (7 - (col % 8)) + + # If we filled a byte or reached the end of the row, append it + if col % 8 == 7 or col == size - 1: + pbm_data.append(byte) + byte = 0 + + file_name += PBM_IMAGE_EXTENSION + with SDHandler() as sd: + sd.write_binary(file_name, pbm_data) + self.flash_text(t("Saved to SD card:\n%s") % file_name) + + def save_bmp_image(self, file_name, resolution): + """Save QR code image as .bmp file""" + from ..sd_card import BMP_IMAGE_EXTENSION + + # TODO: Try Compression? + import image + import lcd + + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Saving ...")) + code, size = self.add_frame(self.code, self.qr_size) + raw_image = image.Image(size=(size, size)) + for y_index in range(0, size): + for x_index in range(0, size): + index = y_index * size + x_index + bit_value = (code[index >> 3] >> (index % 8)) & 1 + if bit_value: + raw_image.set_pixel((x_index, y_index), lcd.BLACK) + else: + raw_image.set_pixel((x_index, y_index), lcd.WHITE) + bmp_img = image.Image(size=(resolution, resolution), copy_to_fb=True) + scale = resolution // size + bmp_img.draw_image( + raw_image, + 0, + 0, + x_scale=scale, + y_scale=scale, + ) + file_name += BMP_IMAGE_EXTENSION + bmp_img.save("/sd/" + file_name) + self.flash_text(t("Saved to SD card:\n%s") % file_name) + + def save_qr_image_menu(self): + """Options to save QR codes as images on SD card""" + from .files_operations import SaveFile + + file_saver = SaveFile(self.ctx) + suggested_file_name = self.title.replace(" ", "_") + suggested_file_name = suggested_file_name.replace( + " ", "" + ) # Replaces thin spaces too + if len(suggested_file_name) > 10: + # Crop file name + suggested_file_name = suggested_file_name[:10] + file_name, filename_undefined = file_saver.set_filename( + suggested_file_name, + ) + if filename_undefined: + return + size = self.qr_size + 2 + bmp_resolutions = [] + resolution = size + for _ in range(4): + resolution *= 2 + if resolution <= 480: + bmp_resolutions.append(resolution) + self.ctx.display.clear() + self.ctx.display.draw_hcentered_text( + t("Res. - Format"), self.ctx.display.font_height, info_box=True + ) + qr_menu = [] + qr_menu.append( + ("%dx%d - PBM" % (size, size), lambda: self.save_pbm_image(file_name)) + ) + for bmp_resolution in bmp_resolutions: + qr_menu.append( + ( + "%dx%d - BMP" % (bmp_resolution, bmp_resolution), + lambda res=bmp_resolution: self.save_bmp_image(file_name, res), + ) + ) + submenu = Menu(self.ctx, qr_menu, offset=2 * self.ctx.display.font_height) + submenu.run_loop() + # return MENU_EXIT # Uncomment to exit QR Viewer after saving + + def print_qr(self): + "Printer handler" + from .utils import Utils + + utils = Utils(self.ctx) + utils.print_standard_qr(self.code, title=self.title, is_qr=True) + # return MENU_EXIT # Uncomment to exit QR Viewer after printing + + def display_qr(self, allow_export=False, transcript_tools=True, quick_exit=False): + """Displays QR codes in multiple modes""" + if self.title: label = self.title else: label = "" - label += "\n" + t("Swipe to change mode") + if transcript_tools: + label += "\n" + t("Swipe to change mode") mode = 0 while True: button = None while button not in (SWIPE_DOWN, SWIPE_UP): + + def toggle_brightness(): + self.bright = not self.bright + self.draw_grided_qr(mode) if self.ctx.input.touch is not None: self.ctx.display.draw_hcentered_text( label, self.ctx.display.qr_offset() + self.ctx.display.font_height, ) - # # Avoid the need of double click - # self.ctx.input.buttons_active = True button = self.ctx.input.wait_for_button() - if button in (BUTTON_PAGE, SWIPE_LEFT): # page, swipe - mode += 1 - mode %= 5 - self.lr_index = 0 - elif button in (BUTTON_PAGE_PREV, SWIPE_RIGHT): # page, swipe - mode -= 1 - mode %= 5 - self.lr_index = 0 - elif button in (BUTTON_ENTER, BUTTON_TOUCH): + if transcript_tools: + if button in (BUTTON_PAGE, SWIPE_LEFT): # page, swipe + mode += 1 + mode %= 5 + self.lr_index = 0 + elif button in (BUTTON_PAGE_PREV, SWIPE_RIGHT): # page, swipe + mode -= 1 + mode %= 5 + self.lr_index = 0 + if button in (BUTTON_ENTER, BUTTON_TOUCH): if mode in (LINE_MODE, REGION_MODE, ZOOMED_R_MODE): self.lr_index += 1 else: @@ -330,15 +461,21 @@ def display_seed_qr(self): self.lr_index %= self.qr_size elif mode in (REGION_MODE, ZOOMED_R_MODE): self.lr_index %= self.columns * self.columns - self.ctx.display.clear() - if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2): - break - if not self.print_qr_prompt(): - return MENU_CONTINUE - - from .print_page import PrintPage - - print_page = PrintPage(self.ctx) - print_page.print_qr(self.code, title=self.title, is_qr=True) - - return MENU_CONTINUE + if quick_exit: + return MENU_CONTINUE + if self.has_sd_card() and allow_export: + sd_func = self.save_qr_image_menu + else: + sd_func = None + printer_func = self.print_qr if self.has_printer() else None + qr_menu = [ + (t("Return to QR Viewer"), lambda: None), + (t("Toggle Brightness"), toggle_brightness), + (t("Save QR Image to SD Card"), sd_func), + (t("Print to QR"), printer_func), + (t("Back to Menu"), lambda: MENU_EXIT), + ] + submenu = Menu(self.ctx, qr_menu) + _, status = submenu.run_loop() + if status == MENU_EXIT: + return MENU_CONTINUE diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index 364660f22..9db6a78be 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -25,9 +25,10 @@ from ..settings import ( CategorySetting, NumberSetting, + store, SD_PATH, FLASH_PATH, - Store, + SETTINGS_FILENAME, ) from ..krux_settings import ( Settings, @@ -40,10 +41,10 @@ from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH from ..sd_card import SDHandler from ..encryption import QR_CODE_ITER_MULTIPLE -from ..display import FLASH_MSG_TIME from . import ( Page, Menu, + FLASH_MSG_TIME, MENU_CONTINUE, MENU_EXIT, ESC_KEY, @@ -77,16 +78,12 @@ def settings(self): """Handler for the settings""" location = Settings().persist.location if location == SD_PATH: - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Checking for SD card..")) - try: - # Check for SD hot-plug - with SDHandler(): - self._display_centered_text( - t("Your changes will be kept on the SD card."), - duration=SD_MSG_TIME, - ) - except OSError: + if self.has_sd_card(): + self._display_centered_text( + t("Your changes will be kept on the SD card."), + duration=SD_MSG_TIME, + ) + else: self._display_centered_text( t("SD card not detected.") + "\n\n" @@ -162,6 +159,66 @@ def _touch_to_physical(self, index): return BUTTON_ENTER return BUTTON_PAGE + def restore_settings(self): + """Restore default settings by deleting the settings files""" + self.ctx.display.clear() + if self.prompt( + t("Restore factory settings and reboot?"), self.ctx.display.height() // 2 + ): + self.ctx.display.clear() + try: + # Delete settings from SD + with SDHandler() as sd: + sd.delete(SETTINGS_FILENAME) + except: + pass + try: + # Delete settings from flash + os.remove("/%s/%s" % (FLASH_PATH, SETTINGS_FILENAME)) + except: + pass + self.ctx.power_manager.reboot() + + def erase_spiffs(self): + """Erase all SPIFFS, removing all saved configs and mnemonics""" + + import flash + from ..firmware import FLASH_SIZE, SPIFFS_ADDR, ERASE_BLOCK_SIZE + + empty_buf = b"\xff" * ERASE_BLOCK_SIZE + for address in range(SPIFFS_ADDR, FLASH_SIZE, ERASE_BLOCK_SIZE): + if flash.read(address, ERASE_BLOCK_SIZE) == empty_buf: + continue + flash.erase(address, ERASE_BLOCK_SIZE) + + def wipe_device(self): + """Fully formats SPIFFS memory""" + self.ctx.display.clear() + if self.prompt( + t( + "Permanently remove all stored encrypted mnemonics and settings from flash?" + ), + self.ctx.display.height() // 2, + ): + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Wiping Device..")) + self.erase_spiffs() + # Reboot so default settings take place and SPIFFS is formatted. + self.ctx.power_manager.reboot() + + def restore_menu(self): + """Option to restore settings and wipe device""" + submenu = Menu( + self.ctx, + [ + (t("Restore Default Settings"), self.restore_settings), + (t("Wipe Device"), self.wipe_device), + (t("Back"), lambda: MENU_EXIT), + ], + ) + submenu.run_loop() + return MENU_CONTINUE + def _settings_exit_check(self): """Handler for the 'Back' on settings screen""" @@ -173,14 +230,29 @@ def _settings_exit_check(self): try: # Check for SD hot-plug with SDHandler(): - Store.save_settings() + if store.save_settings(): + self._display_centered_text( + t("Changes persisted to SD card!"), + duration=SD_MSG_TIME, + ) + except OSError: + self._display_centered_text( + t("SD card not detected.") + + "\n\n" + + t("Changes will last until shutdown."), + duration=SD_MSG_TIME, + ) + else: + self.ctx.display.clear() + try: + if store.save_settings(): self._display_centered_text( - t("Changes persisted to SD card!"), + t("Changes persisted to Flash!"), duration=SD_MSG_TIME, ) - except OSError: + except: self._display_centered_text( - t("SD card not detected.") + t("Unexpected error saving to Flash.") + "\n\n" + t("Changes will last until shutdown."), duration=SD_MSG_TIME, @@ -218,6 +290,7 @@ def handler(): # Case for "Back" on the main Settings if settings_namespace.namespace == Settings.namespace: + items.append((t("Factory Settings"), self.restore_menu)) items.append((t("Back"), self._settings_exit_check)) else: items.append((t("Back"), lambda: MENU_EXIT)) @@ -253,14 +326,16 @@ def _touch_threshold_exit_check(self): # Update touch detection threshold if self.ctx.input.touch is not None: - self.ctx.input.touch.touch_driver.threshold(Settings().touch.threshold) + self.ctx.input.touch.touch_driver.threshold( + Settings().hardware.touch.threshold + ) def _encoder_threshold_exit_check(self): """Handler for the 'Back' on encoder settings screen""" from ..rotary import encoder # Update rotary encoder debounce time - encoder.debounce = Settings().encoder.debounce + encoder.debounce = Settings().hardware.encoder.debounce def category_setting(self, settings_namespace, setting): """Handler for viewing and editing a CategorySetting""" @@ -304,13 +379,13 @@ def category_setting(self, settings_namespace, setting): if self.prompt( t("Change theme and reboot?"), self.ctx.display.height() // 2 ): + self._settings_exit_check() self.ctx.display.clear() self.ctx.power_manager.reboot() else: # Restore previous theme setting.__set__(settings_namespace, starting_category) theme.update() - Store.save_settings() return MENU_CONTINUE @@ -339,14 +414,14 @@ def number_setting(self, settings_namespace, setting): setting.attr == "pbkdf2_iterations" and (new_value % QR_CODE_ITER_MULTIPLE) != 0 ): - self.ctx.display.flash_text( + self.flash_text( t("Value must be multiple of %s") % QR_CODE_ITER_MULTIPLE, theme.error_color, ) else: setting.__set__(settings_namespace, new_value) else: - self.ctx.display.flash_text( + self.flash_text( t("Value %s out of range: [%s, %s]") % (new_value, setting.value_range[0], setting.value_range[1]), theme.error_color, diff --git a/src/krux/pages/sign_message_ui.py b/src/krux/pages/sign_message_ui.py new file mode 100644 index 000000000..249a3cc9f --- /dev/null +++ b/src/krux/pages/sign_message_ui.py @@ -0,0 +1,223 @@ +# 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 board +import gc +from embit import bip32, compact +import hashlib +import binascii +from . import Page, MENU_CONTINUE +from ..themes import theme +from ..baseconv import base_encode +from ..krux_settings import t +from ..qr import FORMAT_NONE +from ..sd_card import ( + SIGNATURE_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + PUBKEY_FILE_EXTENSION, +) +from .utils import Utils + + +class SignMessage(Page): + """Message Signing user interface""" + + def __init__(self, ctx): + super().__init__(ctx, None) + self.utils = Utils(self.ctx) + + def sign_at_address(self, data, qr_format): + """Message signed at a derived Bitcoin address - Sparrow/Specter""" + + if data.startswith(b"signmessage"): + data_blocks = data.split(b" ") + if len(data_blocks) >= 3: + derivation = data_blocks[1].decode() + message = b" ".join(data_blocks[2:]) + message = message.split(b":") + if len(message) >= 2 and message[0] == b"ascii": + message = b" ".join(message[1:]) + derivation = bip32.parse_path(derivation) + self.ctx.display.clear() + address = self.ctx.wallet.descriptor.derive( + derivation[4], branch_index=0 + ).address(network=self.ctx.wallet.key.network) + short_address = self.fit_to_line( + address, str(derivation[4]) + ". ", fixed_chars=3 + ) + # Amount of lines to subtract for free room for message + subtract_lines = 6 if board.config["type"] == "m5stickv" else 10 + + message_to_display = self.ctx.display.to_lines(message.decode()) + if ( + len(message_to_display) + > self.ctx.display.total_lines - subtract_lines + ): + message_cut = ( + self.ctx.display.total_lines - subtract_lines + ) // 2 + message_to_display = ( + message_to_display[:message_cut] + + ["\n...\n"] + + message_to_display[-message_cut:] + ) + message_to_display = "".join(message_to_display) + else: + message_to_display = message.decode() + + self.ctx.display.draw_hcentered_text( + t("Message:") + + "\n" + + message_to_display + + "\n\n" + + "Address:" + + "\n" + + short_address + ) + if not self.prompt(t("Sign?"), self.ctx.display.bottom_prompt_line): + return True + message_hash = hashlib.sha256( + hashlib.sha256( + b"\x18Bitcoin Signed Message:\n" + + compact.to_bytes(len(message)) + + message + ).digest() + ).digest() + sig = self.ctx.wallet.key.sign_at(derivation, message_hash) + + # Encode sig as base64 string + encoded_sig = base_encode(sig, 64).strip().decode() + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("Signature") + ":\n\n%s" % encoded_sig + ) + self.ctx.input.wait_for_button() + title = t("Signed Message") + self.display_qr_codes(encoded_sig, qr_format, title) + self.utils.print_standard_qr(encoded_sig, qr_format, title) + return True + return False + + def sign_message(self): + """Sign message user interface""" + + # Try to read a message from camera + message_filename = "" + data, qr_format = self.capture_qr_code() + + if data is None: + # Try to read a message from a file on the SD card + qr_format = FORMAT_NONE + try: + message_filename, data = self.utils.load_file() + except OSError: + pass + + if data is None: + self.flash_text(t("Failed to load message"), theme.error_color) + return MENU_CONTINUE + + # message read OK! + data = data.encode() if isinstance(data, str) else data + + if self.sign_at_address(data, qr_format): + return MENU_CONTINUE + + message_hash = None + if len(data) == 32: + # It's a sha256 hash already + message_hash = data + else: + if len(data) == 64: + # It may be a hex-encoded sha256 hash + try: + message_hash = binascii.unhexlify(data) + except: + pass + if message_hash is None: + # It's a message, so compute its sha256 hash + message_hash = hashlib.sha256(data).digest() + + # memory management + del data + gc.collect() + + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("SHA256:\n%s") % binascii.hexlify(message_hash).decode() + ) + if not self.prompt(t("Sign?"), self.ctx.display.bottom_prompt_line): + return MENU_CONTINUE + + # User confirmed to sign! + sig = self.ctx.wallet.key.sign(message_hash).serialize() + + # Encode sig as base64 string + encoded_sig = base_encode(sig, 64).decode() + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Signature") + ":\n\n%s" % encoded_sig) + self.ctx.input.wait_for_button() + + # Show the base64 signed message as a QRCode + title = t("Signed Message") + self.display_qr_codes(encoded_sig, qr_format, title) + self.utils.print_standard_qr(encoded_sig, qr_format, title) + + # memory management + del encoded_sig + gc.collect() + + # Show the public key as a QRCode + pubkey = binascii.hexlify(self.ctx.wallet.key.account.sec()).decode() + self.ctx.display.clear() + + title = t("Hex Public Key") + self.ctx.display.draw_centered_text(title + ":\n\n%s" % pubkey) + self.ctx.input.wait_for_button() + + # Show the public key in hexadecimal format as a QRCode + self.display_qr_codes(pubkey, qr_format, title) + self.utils.print_standard_qr(pubkey, qr_format, title) + + # memory management + gc.collect() + + # Try to save the signature file on the SD card + if self.has_sd_card(): + from .files_operations import SaveFile + + save_page = SaveFile(self.ctx) + save_page.save_file( + sig, + "message", + message_filename, + t("Signature") + ":", + SIGNATURE_FILE_EXTENSION, + SIGNED_FILE_SUFFIX, + ) + + # Try to save the public key on the SD card + save_page.save_file( + pubkey, "pubkey", "", title + ":", PUBKEY_FILE_EXTENSION, "", False + ) + + return MENU_CONTINUE diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py index 778dc4adb..03204f654 100644 --- a/src/krux/pages/tiny_seed.py +++ b/src/krux/pages/tiny_seed.py @@ -5,13 +5,13 @@ import sensor import time from embit.wordlists.bip39 import WORDLIST -from . import Page +from . import Page, FLASH_MSG_TIME from ..themes import theme from ..wdt import wdt from ..krux_settings import t -from ..display import DEFAULT_PADDING, FLASH_MSG_TIME +from ..display import DEFAULT_PADDING from ..camera import OV7740_ID, OV2640_ID, OV5642_ID -from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH, PRESSED +from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH # Tiny Seed last bit index positions according to checksums TS_LAST_BIT_NO_CS = 143 @@ -240,7 +240,7 @@ def print_tiny_seed(self): wdt.feed() grid_x_offset = border_x + 16 # 4,1mm*8px grid_y_offset = border_y + 25 # 6,2mm*8px - self.printer.feed(3) + self.printer.feed(4) self.ctx.display.clear() def _draw_index(self, index): @@ -843,7 +843,7 @@ def _detect_and_draw_punches(self, img, gradient_corners): page_seed_numbers[word_index], bit ) index += 1 - print(page_seed_numbers) + # print(page_seed_numbers) return page_seed_numbers def _set_camera_sensitivity(self): @@ -880,21 +880,14 @@ def _exit_camera(self): self.ctx.display.clear() def _check_buttons(self, w24, page): - if time.ticks_ms() > self.time_frame + 1000: - enter_or_touch = ( - self.ctx.input.enter_value() == PRESSED - or self.ctx.input.touch_value() == PRESSED - ) - if w24: - if page == 0 and enter_or_touch: - self.capturing = True - elif enter_or_touch: - return True - if ( - self.ctx.input.page_value() == PRESSED - or self.ctx.input.page_prev_value() == PRESSED - ): - return True + enter_or_touch = self.ctx.input.enter_event() or self.ctx.input.touch_event() + if w24: + if page == 0 and enter_or_touch: + self.capturing = True + elif enter_or_touch: + return True + if self.ctx.input.page_event() or self.ctx.input.page_prev_event(): + return True return False def _process_12w_scan(self, page_seed_numbers): @@ -913,8 +906,7 @@ def _process_12w_scan(self, page_seed_numbers): if words: # If words confirmed return words # Else esc command was given, turn camera on again and reset words - self.ctx.display.clear() - self.ctx.display.flash_text( + self.flash_text( t("Scanning words 1-12 again") + "\n\n" + t("Wait for the capture") ) self._run_camera() @@ -934,15 +926,13 @@ def _process_24w_pg0_scan(self, page_seed_numbers): words = self.tiny_seed.enter_tiny_seed(True, page_seed_numbers, True) self.capturing = False if words is not None: # Fisrt 12 words confirmed, moving to 13-24 - self.ctx.display.clear() - self.ctx.display.flash_text( + self.flash_text( t("Scanning words 13-24") + "\n\n" + t("Wait for the capture") ) self._run_camera() return words # Esc command was given - self.ctx.display.clear() - self.ctx.display.flash_text( + self.flash_text( t("Scanning words 1-12 again") + "\n\n" + t("TOUCH or ENTER to capture") ) self._run_camera() # Run camera and rotate screen after message was given @@ -971,6 +961,7 @@ def scanner(self, w24=False): if precamera_ticks + FLASH_MSG_TIME > postcamera_ticks: time.sleep_ms(precamera_ticks + FLASH_MSG_TIME - postcamera_ticks) del message, precamera_ticks, postcamera_ticks + self.ctx.input.flush_events() # # Debug FPS 1/4 # clock = time.clock() # fps = 0 @@ -983,7 +974,7 @@ def scanner(self, w24=False): rect = self._detect_tiny_seed(img) if rect: gradient_corners = self._gradient_corners(rect, img) - print(gradient_corners) + # print(gradient_corners) # map_regions self._map_punches_region(rect, page) page_seed_numbers = self._detect_and_draw_punches(img, gradient_corners) diff --git a/src/krux/pages/tools.py b/src/krux/pages/tools.py index 3a12cd820..667830f8f 100644 --- a/src/krux/pages/tools.py +++ b/src/krux/pages/tools.py @@ -97,7 +97,7 @@ def sd_check(self): select_file_handler=file_manager.show_file_details ) except OSError: - self.ctx.display.flash_text(t("SD card not detected"), theme.error_color) + self.flash_text(t("SD card not detected"), theme.error_color) return MENU_CONTINUE @@ -138,5 +138,5 @@ def create_qr(self): title = t("Custom QR Code") seed_qr_view = SeedQRView(self.ctx, data=text, title=title) - return seed_qr_view.display_seed_qr() + return seed_qr_view.display_qr(allow_export=True) return MENU_CONTINUE diff --git a/src/krux/pages/utils.py b/src/krux/pages/utils.py new file mode 100644 index 000000000..5a89bd647 --- /dev/null +++ b/src/krux/pages/utils.py @@ -0,0 +1,88 @@ +# 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. + +from . import Page +from ..krux_settings import t +from ..sd_card import SDHandler +from embit.wordlists.bip39 import WORDLIST + + +class Utils(Page): + """Methods as subpages, shared by other pages""" + + BASE_DEC = 10 + BASE_HEX = 16 + BASE_OCT = 8 + + BASE_DEC_SUFFIX = "DEC" + BASE_HEX_SUFFIX = "HEX" + BASE_OCT_SUFFIX = "OCT" + + def __init__(self, ctx): + super().__init__(ctx, None) + + def print_standard_qr(self, data, qr_format=None, title="", width=33, is_qr=False): + """Loads printer driver and UI""" + # Only loads printer related modules if needed + if self.print_qr_prompt(): + from .print_page import PrintPage + + print_page = PrintPage(self.ctx) + print_page.print_qr(data, qr_format, title, width, is_qr) + + def load_file(self, file_ext=""): + """Load a file from SD card""" + if self.has_sd_card(): + with SDHandler() as sd: + self.ctx.display.clear() + if self.prompt( + t("Load from SD card?") + "\n\n", self.ctx.display.height() // 2 + ): + from .files_manager import FileManager + + file_manager = FileManager(self.ctx) + filename = file_manager.select_file(file_extension=file_ext) + + if filename: + filename = file_manager.display_file(filename) + + if self.prompt(t("Load?"), self.ctx.display.bottom_prompt_line): + return filename, sd.read_binary(filename) + return "", None + + @staticmethod + def get_mnemonic_numbers(words, base=BASE_DEC): + """Returns the mnemonic as indexes in decimal, hexadecimal, or octal""" + word_numbers = [] + for word in words.split(" "): + word_numbers.append(WORDLIST.index(word) + 1) + + if base == Utils.BASE_HEX: + for i, number in enumerate(word_numbers): + word_numbers[i] = hex(number)[2:].upper() + + if base == Utils.BASE_OCT: + for i, number in enumerate(word_numbers): + word_numbers[i] = oct(number)[2:] + + numbers_str = [str(value) for value in word_numbers] + return " ".join(numbers_str) diff --git a/src/krux/power.py b/src/krux/power.py index 59cbb93f9..5f4c80523 100644 --- a/src/krux/power.py +++ b/src/krux/power.py @@ -22,6 +22,7 @@ import machine import sys import board +from .i2c import i2c_bus # https://github.com/m5stack/M5StickC/blob/0527606d9e56c956ab17b278c25e3d07d7664f5e/src/AXP192.cpp#L20 MAX_BATTERY_MV = 4200 @@ -34,38 +35,27 @@ class PowerManager: def __init__(self): self.pmu = None - if board.config["type"].startswith("amigo"): - try: - from pmu import axp173 - - self.pmu = axp173() - self.pmu.enableADCs(True) - self.pmu.enablePMICSleepMode(False) - # Amigo already have a dedicated reset button - # Will only enable button checking when in sleep mode - except: - pass - else: - try: - from pmu import axp192 + try: + from pmu import PMUController - self.pmu = axp192() - self.pmu.enableADCs(True) - self.pmu.enablePMICSleepMode(True) - except: - pass + self.pmu = PMUController(i2c_bus) + self.pmu.enable_adcs(True) + if board.config["type"] == "m5stickv": + self.pmu.enable_pek_button_monitor() + except Exception as e: + print(e) def has_battery(self): """Returns if the device has a battery""" try: - assert int(self.pmu.getVbatVoltage()) > 0 + assert int(self.pmu.get_battery_voltage()) > 0 except: return False return True def battery_charge_remaining(self): """Returns the state of charge of the device's battery""" - mv = int(self.pmu.getVbatVoltage()) + mv = int(self.pmu.get_battery_voltage()) if board.config["type"].startswith("amigo"): charge = max(0, (mv - 3394.102415024943) / 416.73204356) elif board.config["type"] == "m5stickv": @@ -76,21 +66,25 @@ def battery_charge_remaining(self): def charging(self): """Returns true if device has power delivered through USB""" - return self.pmu.getUSBVoltage() > 4200 + return self.pmu.get_usb_voltage() > 4200 + + def set_screen_brightness(self, value): + """Sets the screen brightness by modifying the backlight voltage""" + # Accpeted values range from 0 to 8 + # pmu register allow values from 0 to 15, but values below 7 result in no backlight + value += 7 + self.pmu.set_screen_brightness(value) def shutdown(self): """Shuts down the device""" if self.pmu is not None: - # Enable button checking before shutdown - self.pmu.enablePMICSleepMode(True) - self.pmu.setEnterSleepMode() + self.pmu.enable_adcs(False) + self.pmu.enter_sleep_mode() machine.reset() sys.exit() def reboot(self): """Reboots the device""" - if self.pmu is not None: - self.pmu.enablePMICSleepMode(False) machine.reset() sys.exit() diff --git a/src/krux/printers/__init__.py b/src/krux/printers/__init__.py index 4d61c9812..5270fbe09 100644 --- a/src/krux/printers/__init__.py +++ b/src/krux/printers/__init__.py @@ -58,7 +58,7 @@ def print_string(self, text): def create_printer(): """Instantiates a new printer dynamically based on the default in Settings""" - module, cls = PrinterSettings.PRINTERS[Settings().printer.driver] + module, cls = PrinterSettings.PRINTERS[Settings().hardware.printer.driver] if not cls: return None return getattr( diff --git a/src/krux/printers/cnc.py b/src/krux/printers/cnc.py index cb460d31a..a584b7546 100644 --- a/src/krux/printers/cnc.py +++ b/src/krux/printers/cnc.py @@ -38,15 +38,15 @@ class GCodeGenerator(Printer): """ def __init__(self): - self.unit = Settings().printer.cnc.unit - self.flute_diameter = Settings().printer.cnc.flute_diameter - self.plunge_rate = Settings().printer.cnc.plunge_rate - self.feed_rate = Settings().printer.cnc.feed_rate - self.cut_depth = Settings().printer.cnc.cut_depth - self.pass_depth = Settings().printer.cnc.depth_per_pass - self.part_size = Settings().printer.cnc.part_size - self.border_padding = Settings().printer.cnc.border_padding - self.invert = Settings().printer.cnc.invert + self.unit = Settings().hardware.printer.cnc.unit + self.flute_diameter = Settings().hardware.printer.cnc.flute_diameter + self.plunge_rate = Settings().hardware.printer.cnc.plunge_rate + self.feed_rate = Settings().hardware.printer.cnc.feed_rate + self.cut_depth = Settings().hardware.printer.cnc.cut_depth + self.pass_depth = Settings().hardware.printer.cnc.depth_per_pass + self.part_size = Settings().hardware.printer.cnc.part_size + self.border_padding = Settings().hardware.printer.cnc.border_padding + self.invert = Settings().hardware.printer.cnc.invert if self.plunge_rate > self.feed_rate / 2: raise ValueError("plunge rate must be less than half of feed rate") @@ -103,7 +103,7 @@ def print_qr_code(self, qr_code): def cut_cell(self, x, y, cell_size, plunge_depth): """Hollows out the specified cell using a cutting method defined in settings""" - if Settings().printer.cnc.cut_method == "spiral": + if Settings().hardware.printer.cnc.cut_method == "spiral": self.spiral_cut_cell(x, y, cell_size, plunge_depth) else: self.row_cut_cell(x, y, cell_size, plunge_depth) @@ -281,10 +281,10 @@ def clear(self): # def __init__(self): # super().__init__() -# fm.register(Settings().printer.cnc.grbl.tx_pin, fm.fpioa.UART2_TX, force=False) -# fm.register(Settings().printer.cnc.grbl.rx_pin, fm.fpioa.UART2_RX, force=False) -# self.uart_conn = UART(UART.UART2, Settings().printer.cnc.grbl.baudrate) -# self.byte_time = 11.0 / float(Settings().printer.cnc.grbl.baudrate) +# fm.register(Settings().hardware.printer.cnc.grbl.tx_pin, fm.fpioa.UART2_TX, force=False) +# fm.register(Settings().hardware.printer.cnc.grbl.rx_pin, fm.fpioa.UART2_RX, force=False) +# self.uart_conn = UART(UART.UART2, Settings().hardware.printer.cnc.grbl.baudrate) +# self.byte_time = 11.0 / float(Settings().hardware.printer.cnc.grbl.baudrate) # res = self.uart_conn.readline() # if res is None or not res.decode().lower().startswith("grbl"): # raise ValueError("not connected") diff --git a/src/krux/printers/thermal.py b/src/krux/printers/thermal.py index 74a012633..96edd7173 100644 --- a/src/krux/printers/thermal.py +++ b/src/krux/printers/thermal.py @@ -56,80 +56,28 @@ class AdafruitPrinter(Printer): def __init__(self): fm.register( - Settings().printer.thermal.adafruit.tx_pin, fm.fpioa.UART2_TX, force=False + Settings().hardware.printer.thermal.adafruit.tx_pin, + fm.fpioa.UART2_TX, + force=False, ) fm.register( - Settings().printer.thermal.adafruit.rx_pin, fm.fpioa.UART2_RX, force=False + Settings().hardware.printer.thermal.adafruit.rx_pin, + fm.fpioa.UART2_RX, + force=False, ) - self.uart_conn = UART(UART.UART2, Settings().printer.thermal.adafruit.baudrate) + self.uart_conn = UART( + UART.UART2, Settings().hardware.printer.thermal.adafruit.baudrate + ) self.character_height = 24 self.byte_time = 1 # miliseconds - self.dot_print_time = Settings().printer.thermal.adafruit.line_delay + self.dot_print_time = Settings().hardware.printer.thermal.adafruit.line_delay self.dot_feed_time = 2 # miliseconds - self.setup() - if not self.has_paper(): raise ValueError("missing paper") - def setup(self): - """Sets up the connection to the printer and sets default settings""" - # The printer can't start receiving data immediately - # upon power up -- it needs a moment to cold boot - # and initialize. Allow at least 1/2 sec of uptime - # before printer can receive data. - time.sleep_ms(INITIALIZE_WAIT_TIME) - - # Wake up the printer to get ready for printing - self.write_bytes(255) - self.write_bytes(27, 118, 0) # Sleep off (important!) - - # Reset the printer - self.write_bytes(27, 64) # Esc @ = init command - # Configure tab stops on recent printers - self.write_bytes(27, 68) # Set tab stops - self.write_bytes(4, 8, 12, 16) # every 4 columns, - self.write_bytes(20, 24, 28, 0) # 0 is end-of-list. - - # Description of print settings from p. 23 of manual: - # ESC 7 n1 n2 n3 Setting Control Parameter Command - # Decimal: 27 55 n1 n2 n3 - # max heating dots, heating time, heating interval - # n1 = 0-255 Max heat dots, Unit (8dots), Default: 7 (64 dots) - # n2 = 3-255 Heating time, Unit (10us), Default: 80 (800us) - # n3 = 0-255 Heating interval, Unit (10us), Default: 2 (20us) - # The more max heating dots, the more peak current - # will cost when printing, the faster printing speed. - # The max heating dots is 8*(n1+1). The more heating - # time, the more density, but the slower printing - # speed. If heating time is too short, blank page - # may occur. The more heating interval, the more - # clear, but the slower printing speed. - self.write_bytes( - 27, # Esc - 55, # 7 (print settings) - 11, # Heat dots - Settings().printer.thermal.adafruit.heat_time, - Settings().printer.thermal.adafruit.heat_interval, - ) - - # Description of print density from p. 23 of manual: - # DC2 # n Set printing density - # Decimal: 18 35 n - # D4..D0 of n is used to set the printing density. - # Density is 50% + 5% * n(D4-D0) printing density. - # D7..D5 of n is used to set the printing break time. - # Break time is n(D7-D5)*250us. - # (Unsure of default values -- not documented) - print_density = 10 # 100% - print_break_time = 2 # 500 uS - - self.write_bytes( - 18, 35, (print_break_time << 5) | print_density # DC2 # Print density - ) - def write_bytes(self, *args): """Writes bytes to the printer at a stable speed""" for arg in args: @@ -143,9 +91,11 @@ def write_bytes(self, *args): def feed(self, x=1): """Feeds paper through the machine x times""" - self.write_bytes(27, 100, x) - # Wait for the paper to feed - time.sleep_ms(self.dot_feed_time * self.character_height) + while x > 0: + x -= 1 + self.write_bytes(10) + # Wait for the paper to feed + time.sleep_ms(self.dot_feed_time * self.character_height) def has_paper(self): """Returns a boolean indicating if the printer has paper or not""" @@ -181,33 +131,40 @@ def clear(self): def print_qr_code(self, qr_code): """Prints a QR code, scaling it up as large as possible""" - size = 0 - while qr_code[size] != "\n": - size += 1 + from ..qr import get_size - scale = Settings().printer.thermal.adafruit.paper_width // size - scale *= Settings().printer.thermal.adafruit.scale - scale //= 200 # 100*2 because printer will scale 2X later to save data + size = get_size(qr_code) + + scale = Settings().hardware.printer.thermal.adafruit.paper_width // size + scale *= Settings().hardware.printer.thermal.adafruit.scale # Scale in % + scale //= 200 # 100% * 2 because printer will scale 2X later to save data # Being at full size sometimes makes prints more faded (can't apply too much heat?) line_bytes_size = (size * scale + 7) // 8 # amount of bytes per line - self.set_bitmap_mode(line_bytes_size, scale * size, 3) - for y in range(size): - # Scale the line (width) by scaling factor - line = 0 - for char in qr_code[y * (size + 1) : y * (size + 1) + size]: - bit = int(char) - for _ in range(scale): - line <<= 1 - line |= bit - line_bytes = line.to_bytes(line_bytes_size, "big") - # Print height * scale lines out to scale by - + self.set_bitmap_mode(line_bytes_size, size * scale, 3) + for row in range(size): + byte = 0 + line_bytes = bytearray() + for col in range(size): + bit_index = row * size + col + bit = qr_code[bit_index >> 3] & (1 << (bit_index % 8)) + for i in range(scale): + byte <<= 1 + if bit: + byte |= 1 + end_line = col == size - 1 and i == scale - 1 + shift_index = (col * scale + i) % 8 + # If we filled a byte or reached the end of the row, append it + if shift_index == 7 or end_line: + if end_line: + # Shift pending bits if on last byte of row + byte <<= 7 - shift_index + line_bytes.append(byte) + byte = 0 for _ in range(scale): - # command += line_bytes self.uart_conn.write(line_bytes) time.sleep_ms(self.dot_print_time) - self.feed(3) + self.feed(4) def set_bitmap_mode(self, width, height, scale_mode=1): """Set image format to be printed""" diff --git a/src/krux/qr.py b/src/krux/qr.py index 9ad82e515..87c0cd0fb 100644 --- a/src/krux/qr.py +++ b/src/krux/qr.py @@ -23,7 +23,6 @@ import io import math import qrcode -from ur.ur_encoder import UREncoder from ur.ur_decoder import URDecoder from ur.ur import UR @@ -31,6 +30,43 @@ FORMAT_PMOFN = 1 FORMAT_UR = 2 +PMOFN_PREFIX_LENGTH_1D = 6 +PMOFN_PREFIX_LENGTH_2D = 8 +UR_GENERIC_PREFIX_LENGTH = 22 + +# CBOR_PREFIX = 6 bytes for tags, 1 for index, 1 for max_index, 2 for message len, 4 for checksum +# Check UR's fountain_encoder.py file, on Part.cbor() function for more details +UR_CBOR_PREFIX_LEN = 14 +UR_BYTEWORDS_CRC_LEN = 4 # 32 bits CRC used on Bytewords encoding + +UR_MIN_FRAGMENT_LENGTH = 10 + +# List of capacities, based on versions +# Version 1(index 0)=21x21px = 17 bytes, version 2=25x25px = 32 bytes ... +# Limited to version 20 +QR_CAPACITY = [ + 17, + 32, + 53, + 78, + 106, + 134, + 154, + 192, + 230, + 271, + 321, + 367, + 425, + 458, + 520, + 586, + 644, + 718, + 792, + 858, +] + class QRPartParser: """Responsible for parsing either a singular or animated series of QR codes @@ -50,7 +86,15 @@ def parsed_count(self): if self.decoder.fountain_decoder.expected_part_indexes is None: return 1 if self.decoder.result is not None else 0 completion_pct = self.decoder.estimated_percent_complete() - return math.ceil(completion_pct * self.total_count()) + return math.ceil(completion_pct * self.total_count() / 2) + len( + self.decoder.fountain_decoder.received_part_indexes + ) + return len(self.parts) + + def processed_parts_count(self): + """Returns quantity of processed QR code parts""" + if self.format == FORMAT_UR: + return self.decoder.fountain_decoder.processed_parts_count return len(self.parts) def total_count(self): @@ -59,7 +103,7 @@ def total_count(self): # Single-part URs have no expected part indexes if self.decoder.fountain_decoder.expected_part_indexes is None: return 1 - return self.decoder.expected_part_count() + return self.decoder.expected_part_count() * 2 return self.total def parse(self, data): @@ -93,6 +137,9 @@ def result(self): return UR(self.decoder.result.type, bytearray(self.decoder.result.cbor)) code_buffer = io.StringIO("") for _, part in sorted(self.parts.items()): + if isinstance(part, bytes): + # Encoded data won't write on StringIO + return part code_buffer.write(part) code = code_buffer.getvalue() code_buffer.close() @@ -104,83 +151,86 @@ def to_qr_codes(data, max_width, qr_format): the max_width constraint """ if qr_format == FORMAT_NONE: - code = qrcode.encode_to_string(data) + code = qrcode.encode(data) yield (code, 1) else: - num_parts = find_min_num_parts(data, max_width, qr_format) - while math.ceil(data_len(data) / num_parts) > 128: - num_parts += 1 - part_size = math.ceil(data_len(data) / num_parts) - + num_parts, part_size = find_min_num_parts(data, max_width, qr_format) if qr_format == FORMAT_PMOFN: - for i in range(num_parts): - part_number = "p%dof%d " % (i + 1, num_parts) + part_index = 0 + while True: + part_number = "p%dof%d " % (part_index + 1, num_parts) part = None - if i == num_parts - 1: - part = part_number + data[i * part_size :] + if part_index == num_parts - 1: + part = part_number + data[part_index * part_size :] + part_index = 0 else: - part = part_number + data[i * part_size : i * part_size + part_size] - code = qrcode.encode_to_string(part) + part = ( + part_number + + data[part_index * part_size : (part_index + 1) * part_size] + ) + part_index += 1 + code = qrcode.encode(part) yield (code, num_parts) elif qr_format == FORMAT_UR: + from ur.ur_encoder import UREncoder + encoder = UREncoder(data, part_size, 0) while True: part = encoder.next_part() - code = qrcode.encode_to_string(part) + code = qrcode.encode(part) yield (code, encoder.fountain_encoder.seq_len()) -def add_qr_frame(qr_code): - """Add a 1 block white border around the code before displaying""" - qr_code = qr_code.strip() - lines = qr_code.split("\n") - size = len(lines) - size += 2 - new_lines = ["0" * size] - for line in lines: - new_lines.append("0" + line + "0") - new_lines.append("0" * size) - return size, "\n".join(new_lines) - - def get_size(qr_code): """Returns the size of the qr code as the number of chars until the first newline""" - size = 0 - while qr_code[size] != "\n": - size += 1 - return size + size = math.sqrt(len(qr_code) * 8) + return int(size) -def data_len(data): - """Returns the length of the payload data, accounting for the UR type""" - if isinstance(data, UR): - return len(data.cbor) - return len(data) +def max_qr_bytes(max_width): + """Calculates the maximum length, in bytes, a QR code of a given size can store""" + # Given qr_size = 17 + 4 * version + 2 * frame_size + max_width -= 2 # Subtract frame width + qr_version = (max_width - 17) // 4 + try: + return QR_CAPACITY[qr_version - 1] + except: + # Limited to version 20 + return QR_CAPACITY[-1] def find_min_num_parts(data, max_width, qr_format): """Finds the minimum number of QR parts necessary to encode the data in the specified format within the max_width constraint """ - num_parts = 1 - part_size = math.ceil(data_len(data) / num_parts) - while True: - part = "" - if qr_format == FORMAT_PMOFN: - part_number = "p1of%d " % num_parts - part = part_number + data[0:part_size] - elif qr_format == FORMAT_UR: - encoder = UREncoder(data, part_size, 0) - part = encoder.next_part() - # The worst-case number of bytes needed to store one QR Code, up to and including - # version 40. This value equals 3918, which is just under 4 kilobytes. - if len(part) < 3918: - code = qrcode.encode_to_string(part) - if get_size(code) <= max_width: - break - num_parts += 1 - part_size = math.ceil(data_len(data) / num_parts) - return num_parts + qr_capacity = max_qr_bytes(max_width) + if qr_format == FORMAT_PMOFN: + data_length = len(data) + part_size = qr_capacity - PMOFN_PREFIX_LENGTH_1D + # where prefix = "pXofY " where Y < 9 + num_parts = (data_length + part_size - 1) // part_size + if num_parts > 9: # Prefix has 2 digits numbers, so re-calculate + part_size = qr_capacity - PMOFN_PREFIX_LENGTH_2D + # where prefix = "pXXofYY " where max YY = 99 + num_parts = (data_length + part_size - 1) // part_size + part_size = (data_length + num_parts - 1) // num_parts + elif qr_format == FORMAT_UR: + qr_capacity -= ( + # This is an approximation, UR index grows indefinitely + UR_GENERIC_PREFIX_LENGTH # index: ~ "ur:crypto-psbt/xxx-xx/" + ) + # UR will add a bunch of info (some duplicated) on the body of each QR + # Info's lenght is multiplied by 2 in Bytewords.encode step + qr_capacity -= (UR_CBOR_PREFIX_LEN + UR_BYTEWORDS_CRC_LEN) * 2 + data_length = len(data.cbor) + data_length *= 2 # UR will Bytewords.encode, which multiply bytes length by 2 + num_parts = (data_length + qr_capacity - 1) // qr_capacity + # For UR, part size will be the input for "max_fragment_len" + part_size = len(data.cbor) // num_parts + part_size = max(part_size, UR_MIN_FRAGMENT_LENGTH) + else: + raise ValueError("Invalid format type") + return num_parts, part_size def parse_pmofn_qr_part(data): diff --git a/src/krux/rotary.py b/src/krux/rotary.py index e75fb6a01..5e0ca3ed4 100644 --- a/src/krux/rotary.py +++ b/src/krux/rotary.py @@ -25,7 +25,7 @@ from fpioa_manager import fm import time from .krux_settings import Settings -from .logging import logger as log +from .buttons import Button RIGHT = 1 LEFT = 0 @@ -55,9 +55,7 @@ def __init__(self): self.value = 0 self.time_frame = 0 - self.debounce = Settings().encoder.debounce - - log.info("Encoder Initiated Pins: %d and %d" % (pins[0], pins[1])) + self.debounce = Settings().hardware.encoder.debounce def process(self, new_state): """Sets new encoder state after position is changed""" @@ -103,29 +101,23 @@ def _left(): encoder = RotaryEncoder() # Singleton -class EncoderPage: +class EncoderPage(Button): """Encoder class that mimics Krux Page GPIO Button behavior""" - def __init__(self): - pass - - def value(self): - """Returns encoder status while mimics Krux GPIO Buttons behavior""" + def event(self): + """Returns encoder events while mimics Krux GPIO Buttons behavior""" if encoder.value > 0: - encoder.value -= 1 - return 0 - return 1 + encoder.value = 0 + return True + return False -class EncoderPagePrev: +class EncoderPagePrev(Button): """Encoder class that mimics Krux Page_prev GPIO Button behavior""" - def __init__(self): - pass - - def value(self): - """Returns encoder status while mimics Krux GPIO Buttons behavior""" + def event(self): + """Returns encoder events while mimics Krux GPIO Buttons behavior""" if encoder.value < 0: - encoder.value += 1 - return 0 - return 1 + encoder.value = 0 + return True + return False diff --git a/src/krux/sd_card.py b/src/krux/sd_card.py index d877e2617..5cbb33d62 100644 --- a/src/krux/sd_card.py +++ b/src/krux/sd_card.py @@ -23,6 +23,15 @@ from machine import SDCard from .settings import SD_PATH +SIGNED_FILE_SUFFIX = "-signed" +PSBT_FILE_EXTENSION = ".psbt" +DESCRIPTOR_FILE_EXTENSION = ".txt" +JSON_FILE_EXTENSION = ".json" +SIGNATURE_FILE_EXTENSION = ".sig" +PUBKEY_FILE_EXTENSION = ".pub" +BMP_IMAGE_EXTENSION = ".bmp" +PBM_IMAGE_EXTENSION = ".pbm" + class SDHandler: """A simple handler to work with files on SDCard""" diff --git a/src/krux/settings.py b/src/krux/settings.py index fe7f49200..801225b46 100644 --- a/src/krux/settings.py +++ b/src/krux/settings.py @@ -75,7 +75,12 @@ def __get__(self, obj, objtype=None): return store.get(obj.namespace, self.attr, self.default_value) def __set__(self, obj, value): - store.set(obj.namespace, self.attr, value) + if self.attr == "location": + store.update_file_location(value) + if value == self.default_value: + store.delete(obj.namespace, self.attr) # do not store defaults + else: + store.set(obj.namespace, self.attr, value) # do store custom settings class CategorySetting(Setting): @@ -101,6 +106,7 @@ class Store: def __init__(self): self.settings = {} self.file_location = "/" + FLASH_PATH + "/" + self.dirty = False # Check for the correct settings persist location try: @@ -134,49 +140,84 @@ def __init__(self): ) def get(self, namespace, setting_name, default_value): - """Loads a setting under the given namespace, returning the default value if not set""" - s = self.settings + """Returns a setting value under the given namespace, or default value if not set""" + s = json.loads( + json.dumps(self.settings) + ) # deepcopy to avoid building out namespaces for level in namespace.split("."): s[level] = s.get(level, {}) s = s[level] if setting_name not in s: - self.set(namespace, setting_name, default_value) + return default_value return s[setting_name] def set(self, namespace, setting_name, setting_value): - """Stores a setting value under the given namespace. We don't use SDHandler - here because set is called too many times every time the user changes a setting - and SDHandler remount causes a small delay + """Stores a setting value under the given namespace if new/changed. + Does NOT automatically save settings to flash or sd! """ s = self.settings for level in namespace.split("."): s[level] = s.get(level, {}) s = s[level] old_value = s.get(setting_name, None) - s[setting_name] = setting_value + if old_value != setting_value: + s[setting_name] = setting_value + self.dirty = True - # if is a change in settings persist location, delete file from old location, - # and later it will save on the new location - if setting_name == "location" and old_value: - # update the file location - self.file_location = "/" + setting_value + "/" + def delete(self, namespace, setting_name): + """Deletes dict storage in self.settings for namespace.setting_name, + also deletes storage for setting_name's parent namespace nodes, if empty. + """ + s = self.settings + levels = [] + for level in namespace.split("."): + s[level] = s.get(level, {}) + levels.append([s, level]) + s = s[level] + if setting_name in s: + del s[setting_name] + self.dirty = True + for s, level in reversed(levels): + if not s[level]: + del s[level] + self.dirty = True + + def update_file_location(self, location): + """Assumes settings.persist.location will be changed to location: + tries to delete current persistent settings file + then updates file_location attribute + """ + if "/" + location + "/" != self.file_location: try: - # remove old SETTINGS_FILENAME - os.remove("/" + old_value + "/" + SETTINGS_FILENAME) + if os.stat(self.file_location + SETTINGS_FILENAME): + os.remove(self.file_location + SETTINGS_FILENAME) except: pass + self.file_location = "/" + location + "/" - Store.save_settings() - - @staticmethod - def save_settings(): + def save_settings(self): """Helper to persist SETTINGS_FILENAME where user selected""" - try: - # save the new SETTINGS_FILENAME - with open(store.file_location + SETTINGS_FILENAME, "w") as f: - f.write(json.dumps(store.settings)) - except: - pass + persisted = False + + if self.dirty: + settings_filename = self.file_location + SETTINGS_FILENAME + new_contents = json.dumps(self.settings) + try: + with open(settings_filename, "r") as f: + old_contents = f.read() + except: + old_contents = None + + if new_contents != old_contents: + try: + with open(settings_filename, "w") as f: + f.write(new_contents) + persisted = True + except: + pass + self.dirty = False + + return persisted # Initialize singleton diff --git a/src/krux/themes.py b/src/krux/themes.py index b098946d6..c2688de2d 100644 --- a/src/krux/themes.py +++ b/src/krux/themes.py @@ -24,41 +24,35 @@ DEFAULT_THEME = ThemeSettings.DARK_THEME_NAME -# Colors: Ditching firmware colors -# To create new colors from RGB values use firmware/scripts/krux_colors.py script +# To create new colors from RGB values use firmware/scripts/rgbconv.py script BLACK = 0x0000 WHITE = 0xFFFF LIGHTBLACK = 0x0842 DARKGREY = 0xEF7B -LIGHTGREY = 0x18C6 +LIGHTGREY = 0x38C6 +LIGHT_GREEN = 0xEC9F GREEN = 0xE007 DARKGREEN = 0x8005 RED = 0x00F8 +LIGHT_PINK = 0xDFFC +PINK = 0x1FF8 +PURPLE = 0x0F78 ORANGE = 0x20FD -DARKORANGE = 0x40C3 -YELLOW = 0xE0FF +DARKORANGE = 0xA0CA +YELLOW = 0x85F6 BLUE = 0xF800 LIGHTBLUE = 0xBD0E CYAN = 0xFF07 -MAGENTA = 0x1FF8 - -# define NAVY 0x0F00 -# define DARKGREEN 0xE003 -# define DARKCYAN 0xEF03 -# define MAROON 0x0078 -# define PURPLE 0x0F78 -# define OLIVE 0xE07B -# define RED 0x00F8 -# define GREENYELLOW 0xE5AF -# define PINK 0x1FF8 +MAGENTA = 0x1FF8 # Remove this color after logger is removed + THEMES = { ThemeSettings.DARK_THEME_NAME: { "background": BLACK, "foreground": WHITE, "frame": DARKGREY, - "disabled": LIGHTBLACK, + "disabled": DARKGREY, "go": GREEN, "esc_no": RED, "del": YELLOW, @@ -88,7 +82,31 @@ "del": YELLOW, "toggle": CYAN, "error": RED, - "highlight": ORANGE, + "highlight": YELLOW, + }, + ThemeSettings.PINK_THEME_NAME: { + "background": BLACK, + "foreground": LIGHT_PINK, + "frame": PURPLE, + "disabled": DARKGREY, + "go": PINK, + "esc_no": RED, + "del": YELLOW, + "toggle": CYAN, + "error": RED, + "highlight": PINK, + }, + ThemeSettings.GREEN_THEME_NAME: { + "background": BLACK, + "foreground": LIGHT_GREEN, + "frame": DARKGREEN, + "disabled": DARKGREY, + "go": GREEN, + "esc_no": RED, + "del": YELLOW, + "toggle": CYAN, + "error": RED, + "highlight": GREEN, }, } diff --git a/src/krux/touch.py b/src/krux/touch.py index 1ddffe761..a594e2177 100644 --- a/src/krux/touch.py +++ b/src/krux/touch.py @@ -22,10 +22,15 @@ # pylint: disable=R0902 import time -from .touchscreens.ft6x36 import FT6X36 + +from .touchscreens.ft6x36 import touch_control from .logging import logger as log from .krux_settings import Settings +IDLE = 0 +PRESSED = 1 +RELEASED = 2 + SWIPE_THRESHOLD = 50 SWIPE_RIGHT = 1 SWIPE_LEFT = 2 @@ -33,30 +38,27 @@ SWIPE_DOWN = 4 TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10 -TOUCH_DEBOUNCE = 200 # Time to wait before sampling touch again after a release class Touch: """Touch is a singleton API to interact with touchscreen driver""" - idle, press, release = 0, 1, 2 - - def __init__(self, width, height): + def __init__(self, width, height, irq_pin=None): """Touch API init - width and height are in Landscape mode For Krux width = max_y, height = max_x """ self.sample_time = 0 - self.debounce = 0 self.y_regions = [] self.x_regions = [] self.index = 0 self.press_point = [] self.release_point = (0, 0) self.gesture = None - self.state = Touch.idle + self.state = IDLE self.width, self.height = width, height - self.touch_driver = FT6X36() - self.touch_driver.threshold(Settings().touch.threshold) + self.touch_driver = touch_control + self.touch_driver.activate_irq(irq_pin) + self.touch_driver.threshold(Settings().hardware.touch.threshold) def clear_regions(self): """Remove previously stored buttons map""" @@ -75,6 +77,18 @@ def add_x_delimiter(self, region): raise ValueError("Touch region added outside display area") self.x_regions.append(region) + def valid_position(self, data): + """Checks if touch position is within buttons area""" + if self.x_regions and data[0] < self.x_regions[0]: + return False + if self.x_regions and data[0] > self.x_regions[-1]: + return False + if self.y_regions and data[1] < self.y_regions[0]: + return False + if self.y_regions and data[1] > self.y_regions[-1]: + return False + return True + def _extract_index(self, data): """Gets an index from touched points, x and y delimiters""" index = 0 @@ -82,35 +96,26 @@ def _extract_index(self, data): for region in self.y_regions: if data[1] > region: index += 1 - if index == 0 or index >= len(self.y_regions): # outside y areas - self.state = self.release - else: - index -= 1 - if self.x_regions: # if 2D array - index *= len(self.x_regions) - 1 - x_index = 0 - for x_region in self.x_regions: - if data[0] > x_region: - x_index += 1 - if x_index == 0 or x_index >= len( - self.x_regions - ): # outside x areas - self.state = self.release - else: - x_index -= 1 - index += x_index - else: - index = 0 - return index + index -= 1 + if self.x_regions: # if 2D array + index *= len(self.x_regions) - 1 + x_index = 0 + for x_region in self.x_regions: + if data[0] > x_region: + x_index += 1 + x_index -= 1 + index += x_index + return index + return 0 def _store_points(self, data): """Store pressed points and calculare an average pressed point""" - if self.state == self.idle: - self.state = self.press + if self.state == IDLE: + self.state = PRESSED self.press_point = [data] self.index = self._extract_index(self.press_point[0]) # Calculare an average (max. 10 samples) pressed point to increase precision - elif self.state == self.press and len(self.press_point) < 10: + elif self.state == PRESSED and len(self.press_point) < 10: self.press_point.append(data) len_press = len(self.press_point) x = 0 @@ -125,20 +130,15 @@ def _store_points(self, data): def current_state(self): """Returns the touchscreen state""" - current_time = time.ticks_ms() - if ( - current_time > self.sample_time + TOUCH_S_PERIOD - and current_time > self.debounce + TOUCH_DEBOUNCE - ): - self.sample_time = time.ticks_ms() - data = self.touch_driver.current_point() - if isinstance(data, tuple): - self._store_points(data) - elif data is None: # gets release then return to idle. - if self.state == self.release: - self.state = self.idle - self.debounce = time.ticks_ms() - elif self.state == self.press: + self.sample_time = time.ticks_ms() + data = self.touch_driver.current_point() + if isinstance(data, tuple): + self._store_points(data) + elif data is None: # gets release then return to idle. + if self.state == RELEASED: # On touch release + self.state = IDLE + elif self.state == PRESSED: + if self.release_point is not None: lateral_lenght = self.release_point[0] - self.press_point[0][0] if lateral_lenght > SWIPE_THRESHOLD: self.gesture = SWIPE_RIGHT @@ -156,14 +156,27 @@ def current_state(self): and -vertical_lenght > lateral_lenght ): self.gesture = SWIPE_UP - self.state = self.release - else: - log.warn("Touch error: " + str(data)) + self.state = RELEASED + else: + log.warn("Touch error: " + str(data)) return self.state + def event(self): + """Checks if a touch happened and stores the point""" + current_time = time.ticks_ms() + if current_time > self.sample_time + TOUCH_S_PERIOD: + if self.touch_driver.event(): + # Resets touch and gets irq point + self.state = IDLE + if isinstance(self.touch_driver.irq_point, tuple): + if self.valid_position(self.touch_driver.irq_point): + self._store_points(self.touch_driver.irq_point) + return True + return False + def value(self): """Wraps touch states to behave like a regular button""" - return 0 if self.current_state() == self.press else 1 + return 1 if self.current_state() == IDLE else 0 def swipe_right_value(self): """Returns detected gestures and clean respective variable""" diff --git a/src/krux/touchscreens/ft6x36.py b/src/krux/touchscreens/ft6x36.py index 323387403..3b10c715f 100644 --- a/src/krux/touchscreens/ft6x36.py +++ b/src/krux/touchscreens/ft6x36.py @@ -24,9 +24,10 @@ # FT6x36 specs: # Max sample rate: 100 samples per second +from Maix import GPIO +from fpioa_manager import fm from . import Touchscreen -import board -from machine import I2C +from ..i2c import i2c_bus FT_DEVICE_MODE = 0x00 GEST_ID = 0x01 @@ -40,33 +41,45 @@ TOUCH_THRESHOLD = 22 # Default 22 +def __handler__(pin_num=None): + # pylint: disable=unused-argument + """GPIO interrupt handler""" + touch_control.trigger_event() + + class FT6X36(Touchscreen): """FT6X36 is a minimal wrapper around a I2C connection to setup and get data from FT6X36 touchscreen IC, part of Sipeed's Maix Amigo device""" def __init__(self): + self.touch_irq_pin = None + self.event_flag = False + self.irq_point = None self.addr = FT6X36_ADDR - self.i2c = I2C( - I2C.I2C0, - freq=100000, - scl=board.config["krux"]["pins"]["I2C_SCL"], - sda=board.config["krux"]["pins"]["I2C_SDA"], - ) """Setup registers""" # Device mode self.write_reg(FT_DEVICE_MODE, 0) # Threshold for touch detection self.write_reg(FT_ID_G_THGROUP, TOUCH_THRESHOLD) - # Mode = 0 = polling mode + # Mode = 0 = polling mode | Mode = 1 = trigger mode self.write_reg(FT_ID_G_MODE, 0) + def activate_irq(self, irq_pin): + """Register IRQ pin IO and its IRQ handler""" + fm.register(irq_pin, fm.fpioa.GPIOHS1) + self.touch_irq_pin = GPIO(GPIO.GPIOHS1, GPIO.IN, GPIO.PULL_UP) + self.touch_irq_pin.irq(__handler__, GPIO.IRQ_FALLING) + def write_reg(self, reg_addr, buf): """Writes buffer content to a register address""" - self.i2c.writeto_mem(self.addr, reg_addr, buf, mem_size=8) + if i2c_bus is not None: + i2c_bus.writeto_mem(self.addr, reg_addr, buf, mem_size=8) def read_reg(self, reg_addr, buf_len): """Reads from a register address""" - return self.i2c.readfrom_mem(self.addr, reg_addr, buf_len, mem_size=8) + if i2c_bus is not None: + return i2c_bus.readfrom_mem(self.addr, reg_addr, buf_len, mem_size=8) + return None def current_point(self): """If touch is pressed, returns x and y points""" @@ -83,6 +96,20 @@ def current_point(self): return e # debug return None + def trigger_event(self): + """Called by IRQ handler to set event flag and capture touch point""" + self.event_flag = True + self.irq_point = self.current_point() + + def event(self): + """Returns event status and clears its flag""" + flag = self.event_flag + self.event_flag = False # Always clean event flag + return flag + def threshold(self, value): """Sets touch sensitivity threshold""" self.write_reg(FT_ID_G_THGROUP, value) + + +touch_control = FT6X36() diff --git a/src/krux/translations.py b/src/krux/translations.py index 266e1614d..16ef4e2c5 100644 --- a/src/krux/translations.py +++ b/src/krux/translations.py @@ -42,7 +42,6 @@ 88746165: "Anti reflecterend uitgeschakeld", 1521033296: "Anti reflecterend ingeschakeld", 1056821534: "Weet je het zeker?", - 3247612282: "BIP-39 geheugensteun", 3455872521: "Terug", 2541860807: "Backup van de bootloader...\n\n%d%%", 2256777600: "Ongeldige handtekening", @@ -72,6 +71,7 @@ 597912140: "Snijmethode", 2504034831: "Decimaal", 2751113454: "Ontsleutelen?", + 3510912550: "Verwijderen %s?", 1016609898: "Bestand verwijderen?", 1364509700: "Geheugensteun verwijderen", 4102535566: "Diepte per pas", @@ -117,11 +117,10 @@ 4120536442: "GRBL", 299338213: "Eigen ID gebruiken voor geheugensteun? Anders vingerafdruk gebruiken", 602716148: "Ga", - 831562513: "Warmte interval", - 2300171403: "Warmte tijd", 3580020863: "Hex publieke sleutel", 2691246967: "Hexadecimaal", 2736309107: "ID bestaat al\n", + 1706005805: "Incomplete portemonnee descriptor", 631342955: "Invoer (%d): ", 2585599782: "Ongeldig adres", 2874529150: "Ongeldige bootloader", @@ -222,6 +221,7 @@ 3672006076: "PSBT ondertekend", 2281377987: "Enkele sleutel", 4221794628: "Grootte: ", + 2344747135: "Sommige controles kunnen niet worden uitgevoerd.", 2309020186: "Uitgaven (%d): ", 3355862324: "Stackbit 1248", 3303592908: "Opslaan op apparaat", @@ -258,7 +258,7 @@ 4232654916: "Portemonnee descriptor", 2587172867: "Portemonnee descriptor geladen!", 2499782468: "Portemonnee descriptor niet gevonden.", - 1831109430: "Waarschuwing:\nIncomplete portemonnee descriptor", + 2671738224: "Waarschuwing:", 797660533: "Woord %d", 3742424146: "Woord nummers", 2965123464: "Woorden", @@ -287,7 +287,6 @@ 88746165: "Chα»‘ng lΓ³a bα»‹ vΓ΄ hiệu hΓ³a", 1521033296: "Đã bαΊ­t chα»‘ng lΓ³a", 1056821534: "BαΊ‘n cΓ³ chαΊ―c khΓ΄ng?", - 3247612282: "MΓ£ mnemonic dαΊ‘ng chuαΊ©n BIP39", 3455872521: "Trở lαΊ‘i", 2541860807: "Sao lΖ°u bα»™ tαΊ£i khởi Δ‘α»™ng..\n\n%d%%", 2256777600: "Chα»― kΓ½ xαΊ₯u", @@ -317,6 +316,7 @@ 597912140: "PhΖ°Ζ‘ng phΓ‘p cαΊ―t", 2504034831: "Sα»‘ thαΊ­p phΓ’n", 2751113454: "PhαΊ£n Δ‘α»‘i?", + 3510912550: "XΓ³a %s?", 1016609898: "XΓ³a tΓ i liệu?", 1364509700: "XΓ³a ghi nhα»›", 4102535566: "Độ sΓ’u mα»—i lαΊ§n vượt qua", @@ -362,11 +362,10 @@ 4120536442: "GRBL", 299338213: "Cung cαΊ₯p cho Mnemonic nΓ y mα»™t ID tΓΉy chỉnh?NαΊΏu khΓ΄ng thΓ¬ dαΊ₯u vΓ’n tay hiện tαΊ‘i sαΊ½ được sα»­ dα»₯ng", 602716148: "Đến", - 831562513: "KhoαΊ£ng thời gian nhiệt", - 2300171403: "Thời gian nhiệt", 3580020863: "KhΓ³a cΓ΄ng khai Hex", 2691246967: "ThαΊ­p lα»₯c phΓ’n", 2736309107: "Id Δ‘Γ£ tα»“n tαΊ‘i\n", + 1706005805: "Bα»™ mΓ΄ tαΊ£ Δ‘αΊ§u ra chΖ°a hoΓ n chỉnh", 631342955: "Đầu vΓ o (%d): ", 2585599782: "Địa chỉ khΓ΄ng hợp lệ", 2874529150: "Bα»™ tαΊ£i khởi Δ‘α»™ng khΓ΄ng hợp lệ", @@ -467,6 +466,7 @@ 3672006076: "Đã kΓ½ PSBT", 2281377987: "KhΓ³a Δ‘Ζ‘n", 4221794628: "Dung lượng: ", + 2344747135: "Mα»™t sα»‘ kiểm tra khΓ΄ng thể được thα»±c hiện.", 2309020186: "Chi tiΓͺu (%d): ", 3355862324: "Stackbit 1248", 3303592908: "LΖ°u trα»― trΓͺn flash", @@ -503,7 +503,7 @@ 4232654916: "VΓ­ Δ‘αΊ§u ra mΓ΄ tαΊ£", 2587172867: "Đã tαΊ£i bα»™ mΓ΄ tαΊ£ Δ‘αΊ§u ra của vΓ­!", 2499782468: "KhΓ΄ng tΓ¬m thαΊ₯y bα»™ mΓ΄ tαΊ£ Δ‘αΊ§u ra vΓ­.", - 1831109430: "CαΊ£nh bΓ‘o:\nBα»™ mΓ΄ tαΊ£ Δ‘αΊ§u ra chΖ°a hoΓ n chỉnh", + 2671738224: "CαΊ£nh bΓ‘o:", 797660533: "KΓ­ tα»± %d", 3742424146: "Tα»« sα»‘", 2965123464: "Tα»« ngα»―", @@ -532,7 +532,6 @@ 88746165: "Anti-Γ©blouissement dΓ©sactivΓ©", 1521033296: "Anti-Γ©blouissement activΓ©", 1056821534: "Es-tu sΓ»r?", - 3247612282: "BIP39 MnΓ©monique", 3455872521: "Retour", 2541860807: "Sauvegarde du chargeur de dΓ©marrage..\n\n%d%%", 2256777600: "Mauvaise signature", @@ -562,6 +561,7 @@ 597912140: "MΓ©thode de coupe", 2504034831: "DΓ©cimal", 2751113454: "DΓ©crypter?", + 3510912550: "Supprimer %s?", 1016609898: "Supprimer le fichier?", 1364509700: "Supprimer mnΓ©monique", 4102535566: "Profondeur par passage", @@ -607,11 +607,10 @@ 4120536442: "GRBL", 299338213: "Donnez Γ  ce mnΓ©monique un identifiant personnalisΓ©?Sinon l'empreinte actuelle sera utilisΓ©e", 602716148: "Go", - 831562513: "Intervalle de chauffe", - 2300171403: "Temps de chauffe", 3580020863: "ClΓ© public hexadΓ©cimal", 2691246967: "HexadΓ©cimal", 2736309107: "Id existe dΓ©jΓ \n", + 1706005805: "Descripteur de sortie incomplet", 631342955: "EntrΓ©es (%d) : ", 2585599782: "Adresse invalide", 2874529150: "Chargeur de dΓ©marrage invalide", @@ -712,6 +711,7 @@ 3672006076: "PSBT signΓ©", 2281377987: "ClΓ© unique", 4221794628: "CapacitΓ©: ", + 2344747135: "Certains chΓ¨ques ne peuvent pas Γͺtre effectuΓ©s.", 2309020186: "DΓ©pense (%d) : ", 3355862324: "Stackbit 1248", 3303592908: "Stocker sur flash", @@ -738,7 +738,7 @@ 2402455261: "Utilisez l'entropie de la camΓ©ra pour crΓ©er un nouveau mnΓ©monique", 236075140: "UtilisΓ©: ", 4003084591: "Valeur %s hors de portΓ©e: [%s, %s]", - 989428076: "La valeur doit Γͺtre multiple de %s", + 989428076: "La valeur doit Γͺtre un multiple de %s", 4191058607: "Par camΓ©ra", 1254681955: "Via D20", 525309547: "Via D6", @@ -748,7 +748,7 @@ 4232654916: "Descripteur de sortie du portefeuille", 2587172867: "Descripteur de sortie du portefeuille chargΓ©!", 2499782468: "Descripteur de sortie du portefeuille introuvable.", - 1831109430: "Attention:\nDescripteur de sortie incomplet", + 2671738224: "Avertissement:", 797660533: "Mot %d", 3742424146: "NumΓ©ros de mots", 2965123464: "Mots", @@ -777,7 +777,6 @@ 88746165: "Antirreflexo desativado", 1521033296: "Antirreflexo ativado", 1056821534: "Tem certeza?", - 3247612282: "MnemΓ΄nico BIP39", 3455872521: "Voltar", 2541860807: "Backing up bootloader..\n\n%d%%", 2256777600: "Assinatura InvΓ‘lida", @@ -807,6 +806,7 @@ 597912140: "MΓ©todo de Corte", 2504034831: "Decimal", 2751113454: "Descriptografar?", + 3510912550: "Excluir %s?", 1016609898: "Excluir Arquivo?", 1364509700: "Excluir MnemΓ΄nico", 4102535566: "Profundidade da Passagem", @@ -852,11 +852,10 @@ 4120536442: "GRBL", 299338213: "DΓͺ a este mnemΓ΄nico um ID personalizado? Caso contrΓ‘rio, a impressΓ£o digital atual serΓ‘ usada", 602716148: "Ir", - 831562513: "Intervalo de Aquecimento", - 2300171403: "Tempo de Aquecimento", 3580020863: "Chave pΓΊblica hexadecimal", 2691246967: "Hexadecimal", 2736309107: "Id jΓ‘ existe\n", + 1706005805: "Descritor incompleto", 631342955: "Entradas (%d): ", 2585599782: "EndereΓ§o invΓ‘lido", 2874529150: "Bootloader invΓ‘lido", @@ -957,6 +956,7 @@ 3672006076: "PSBT Assinada", 2281377987: "Single-sig", 4221794628: "Total: ", + 2344747135: "Algumas verificaçáes nΓ£o podem ser realizadas.", 2309020186: "Gastos (%d): ", 3355862324: "Stackbit 1248", 3303592908: "Armazene na Flash", @@ -993,7 +993,7 @@ 4232654916: "Descritor da carteira", 2587172867: "Descritor de saΓ­da da carteira carregado!", 2499782468: "O descritor de saΓ­da da carteira nΓ£o foi encontrado.", - 1831109430: "Atenção:\nDescritor de saΓ­da incompleto", + 2671738224: "Aviso:", 797660533: "Palavra %d", 3742424146: "NΓΊmeros das Palavras", 2965123464: "Palavras", @@ -1001,6 +1001,494 @@ 771968845: "Suas alteraçáes serΓ£o mantidas no armazenamento flash do dispositivo.", 2569054451: "Suas alteraçáes serΓ£o mantidas no cartΓ£o SD.", }, + "ru-RU": { + 1185266064: "%d ΠΈΠ· %d ΠΌΡƒΠ»ΡŒΡ‚ΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡŒ", + 2004520398: "%d. Π‘Π΄Π°Ρ‡Π°: \n\n%s\n\n", + 3862364126: "%d. ΠŸΠ΅Ρ€Π΅Π²ΠΎΠ΄ самому сСбС: \n\n%s\n\n", + 3264377309: "%d. Расход: \n\n%s\n\n", + 2399232215: "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½Ρ‹ΠΉ адрСс сдачи!", + 3921290840: "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½Ρ‹ΠΉ адрСс получСния!", + 1808355833: "%s\n\nНЕ НАЙДЕН Π² ΠΏΠ΅Ρ€Π²Ρ‹Ρ… %d адрСсах сдачи", + 1306127065: "%s\n\nНЕ НАЙДЕН Π² ΠΏΠ΅Ρ€Π²Ρ‹Ρ… %d адрСсах получСния", + 3348584292: "(Π­ΠΊΡΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΉ)", + 2739590230: "12 слов", + 1310058127: "24 слова", + 2743272264: "ABC", + 1949634023: "О ΠŸΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ΅", + 1517128857: "Adafruit", + 3270727197: "АдрСс", + 2574498267: "Для Ρ€Π΅ΠΆΠΈΠΌΠ° AES-CBC трСбуСтся Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ энтропия ΠΎΡ‚ ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹", + 283202181: "ΠŸΡ€Π°Π²ΠΈΠ»ΡŒΠ½ΠΎ совмСститС ΠΊΠ°ΠΌΠ΅Ρ€Ρƒ ΠΈ Мини Π‘ΠΈΠ΄-Ρ„Ρ€Π°Π·Ρƒ.", + 88746165: "Антиблик ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½", + 1521033296: "Антиблик Π²ΠΊΠ»ΡŽΡ‡Π΅Π½", + 1056821534: "Π’Ρ‹ ΡƒΠ²Π΅Ρ€Π΅Π½Ρ‹?", + 3455872521: "Назад", + 2541860807: "Π Π΅Π·Π΅Ρ€Π²Π½ΠΎΠ΅ ΠΊΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊΠ°..\n\n%d%%", + 2256777600: "ΠŸΠ»ΠΎΡ…Π°Ρ подпись", + 3937333362: "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠŸΠ΅Ρ€Π΅Π΄Π°Ρ‡ΠΈ Π”Π°Π½Π½Ρ‹Ρ…", + 427617266: "Π‘ΠΈΡ‚ΠΊΠΎΠΈΠ½", + 928727036: "Π—Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π³Ρ€Π°Π½ΠΈΡ†", + 213030954: "CNC", + 1207696150: "Π‘Π΄Π°Ρ‡Π°", + 3126552510: "АдрСс Π‘Π΄Π°Ρ‡ΠΈ", + 1583186953: "Π‘ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ ΠΈ ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ?", + 2697733395: "ИзмСнСния сохранСны Π½Π° SD ΠΊΠ°Ρ€Ρ‚Π΅!", + 388908871: "ИзмСнСния Π±ΡƒΠ΄ΡƒΡ‚ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒΡΡ Π΄ΠΎ Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ.", + 3442025874: "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ SD ΠšΠ°Ρ€Ρ‚Ρƒ", + 3119547911: "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ адрСс ΠΏΡ€ΠΈΠ½Π°Π΄Π»Π΅ΠΆΠΈΡ‚ этому ΠΊΠΎΡˆΠ΅Π»ΡŒΠΊΡƒ?", + 2856261511: "ΠŸΡ€ΠΎΠ²Π΅Ρ€Π΅Π½ΠΎ %d адрСсов сдачи Π±Π΅Π· совпадСний.", + 2788541416: "ΠŸΡ€ΠΎΠ²Π΅Ρ€Π΅Π½ΠΎ %d адрСсов получСния Π±Π΅Π· совпадСний.", + 2446472910: "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ адрСс сдачи %d Π½Π° совпадСниС..", + 2470115694: "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° SD ΠΊΠ°Ρ€Ρ‚Ρ‹..", + 3655273987: "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ адрСс получСния %d Π½Π° совпадСниС..", + 2407028014: "ΠšΠΎΠΌΠΏΠ°ΠΊΡ‚Π½Ρ‹ΠΉ SeedQR", + 4041895036: "ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ?", + 4094072796: "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ QR Код", + 167798282: "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ QR ΠΊΠΎΠ΄ ΠΈΠ· тСкста?", + 2767642191: "Π‘ΠΎΠ·Π΄Π°Π½ΠΎ", + 3513215254: "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ QR Код", + 124617190: "Π“Π»ΡƒΠ±ΠΈΠ½Π° Π Π΅Π·ΠΊΠΈ", + 597912140: "ΠœΠ΅Ρ‚ΠΎΠ΄ Π Π΅Π·ΠΊΠΈ", + 2504034831: "ДСсятичный", + 2751113454: "Π Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ?", + 3510912550: "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ %s?", + 1016609898: "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ Π€Π°ΠΉΠ»?", + 1364509700: "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ ΠœΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + 4102535566: "Π“Π»ΡƒΠ±ΠΈΠ½Π° Π—Π° ΠŸΡ€ΠΎΡ…ΠΎΠ΄", + 2791699253: "ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄Π½Ρ‹ΠΉ ΠΏΡƒΡ‚ΡŒ: %s", + 1230133196: "Ѐлэш ΠΏΠ°ΠΌΡΡ‚ΡŒ устройства Π½Π΅ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π°.", + 3836852788: "Π“ΠΎΡ‚ΠΎΠ²ΠΎ?", + 382368239: "Π”Ρ€Π°ΠΉΠ²Π΅Ρ€", + 3978947916: "ΠšΠΎΠ΄Π΅Ρ€", + 4090746898: "ДСбаунс ΠšΠΎΠ΄Π΅Ρ€Π°", + 374684711: "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ ΠœΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + 1244124409: "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ QR Код", + 2968548114: "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±Ρ‹Π»Π° сохранСна", + 3315319371: "Π—Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π±Ρ‹Π»Π° сохранСна с ID: ", + 350279787: "Π¨ΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΠ΅", + 2601598799: "ΠœΠ΅Ρ‚ΠΎΠ΄ ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΡ", + 3504179008: "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ числа ΠΎΡ‚ 1 Π΄ΠΎ 2048.", + 1100685007: "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ ΡˆΠ΅ΡΡ‚Π½Π°Π΄Ρ†Π°Ρ‚Π΅Ρ€ΠΈΡ‡Π½ΠΎΠ³ΠΎ числа ΠΎΡ‚ 1 Π΄ΠΎ 800.", + 4090266642: "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ Π²ΠΎΡΡŒΠΌΠ΅Ρ€ΠΈΡ‡Π½ΠΎΠ³ΠΎ числа ΠΎΡ‚ 1 Π΄ΠΎ 4000.", + 2780625730: "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ слово вашСй BIP-39 ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ.", + 784361051: "Ошибка:\n%s", + 1505332462: "Π’Ρ‹ΠΉΡ‚ΠΈ", + 3838465623: "Π˜ΡΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚ΡŒ Ρ„Π°ΠΉΠ»Ρ‹?", + 4170881190: "ЭкспортированиС Π½Π° SD ΠΊΠ°Ρ€Ρ‚Ρƒ..", + 1711312434: "Π Π°ΡΡˆΠΈΡ€Π΅Π½Π½Ρ‹ΠΉ ΠŸΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠšΠ»ΡŽΡ‡", + 383371114: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Ρ€Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Ρ‚ΡŒ", + 3048830188: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ PSBT", + 4192663412: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ адрСс", + 1996021743: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΊΠ»ΡŽΡ‡", + 1108715658: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ сообщСниС", + 1081425878: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + 928667220: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор", + 1620572516: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ Ρ„Ρ€Π°Π·Ρƒ-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + 2946146830: "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + 1303554751: "Комиссия: ", + 104500973: "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠΏΠΎΠ΄Π°Ρ‡ΠΈ", + 3313339187: "Имя Ρ„Π°ΠΉΠ»Π°", + 1982637349: "Π€Π°ΠΉΠ» %s сущСствуСт Π½Π° SD ΠΊΠ°Ρ€Ρ‚Π΅, ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ?", + 3737729752: "Π€ΠΈΠ½Π³Π΅Ρ€ΠΏΡ€ΠΈΠ½Ρ‚: %s", + 2542772894: "ΠŸΡ€ΠΎΡˆΠΈΠ²ΠΊΠ° ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Π΅Ρ‚ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€: %d", + 1406590538: "Flute Π”ΠΈΠ°ΠΌΠ΅Ρ‚Ρ€", + 3086093110: "БСсплатно: ", + 1893243331: "Из ΠŸΠ°ΠΌΡΡ‚ΠΈ", + 4120536442: "GRBL", + 299338213: "ΠΠ°Π·Π½Π°Ρ‡ΠΈΡ‚ΡŒ этой ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ кастомный ID? Π’ ΠΈΠ½ΠΎΠΌ случаС Π±ΡƒΠ΄Π΅Ρ‚ использован Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ Ρ„ΠΈΠ½Π³Π΅Ρ€ΠΏΡ€ΠΈΠ½Ρ‚", + 602716148: "OK", + 3580020863: "ШСстнадцатСричный ΠŸΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠšΠ»ΡŽΡ‡", + 2691246967: "ШСстнадцатСричный", + 2736309107: "ID ΡƒΠΆΠ΅ сущСствуСт\n", + 1706005805: "НСполный Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор", + 631342955: "Π’Ρ…ΠΎΠ΄Ρ‹ (%d): ", + 2585599782: "НСвСрный адрСс", + 2874529150: "НСвСрный Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊ", + 4093416954: "НСвСрная Π΄Π»ΠΈΠ½Π° ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ", + 1422874211: "НСвСрный ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡", + 2443867979: "НСвСрный кошСлСк:\n%s", + 4122897393: "Π˜Π½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ", + 3000888649: "ΠšΠ»ΡŽΡ‡", + 2686333978: "ΠšΠ»ΡŽΡ‡: ", + 4123798664: "Krux\n\n\nВСрсия\n%s", + 3835918229: "ВСстовый QR ΠŸΡ€ΠΈΠ½Ρ‚Π΅Ρ€Π° Krux", + 766317539: "Π―Π·Ρ‹ΠΊ", + 972436696: "Π—Π°Π΄Π΅Ρ€ΠΆΠΊΠ° Π›ΠΈΠ½ΠΈΠΈ", + 3596093890: "Линия: ", + 2820726296: "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠœΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + 879727077: "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ с SD ΠΊΠ°Ρ€Ρ‚Ρ‹?", + 669106195: "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΎΠ΄Π½Ρƒ?", + 3330705289: "Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ?", + 2596531078: "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠšΠ°ΠΌΠ΅Ρ€Ρ‹..", + 596389387: "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° адрСса сдачи %d..", + 336702608: "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΏΡ€ΠΈΠ½Ρ‚Π΅Ρ€Π°..", + 2538883522: "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° адрСса получСния %d..", + 3159494909: "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..", + 1177338798: "Π›ΠΎΠΊΠ°Π»ΡŒ", + 2817059741: "РасполоТСниС", + 63976957: "Π£Ρ€ΠΎΠ²Π΅Π½ΡŒ логирования", + 86530918: "Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅", + 2917810189: "Максимальная Π΄Π»ΠΈΠ½Π° ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½Π° (%s)", + 2030045667: "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅", + 3928301843: "ΠžΡ‚ΡΡƒΡ‚ΡΡ‚Π²ΡƒΠ΅Ρ‚ Ρ„Π°ΠΉΠ» подписи", + 1948316555: "МнСмоника", + 2123991188: "ID ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ", + 3911073154: "ID памяти ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ", + 570639842: "МнСмоника Π½Π΅ Π±Ρ‹Π»Π° Ρ€Π°ΡΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π°", + 1746030071: "МнСмоника Π½Π΅ Π±Ρ‹Π»Π° Π·Π°ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½Π°", + 1458925155: "ИзмСнСно: ", + 1845376098: "ΠœΡƒΠ»ΡŒΡ‚ΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡŒ", + 2939797024: "Π‘Π΅Ρ‚ΡŒ", + 73574491: "Новая МнСмоника", + 2792272353: "ΠžΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π° новая ΠΏΡ€ΠΎΡˆΠΈΠ²ΠΊΠ°.\n\nSHA256:\n%s\n\n\n\nΠ£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ?", + 4063104189: "НСт", + 3927838899: "Π‘Π΅Π· BIP39 Ρ„Ρ€Π°Π·Ρ‹-пароля", + 4092516657: "НСдостаточно бросков!", + 1577637745: "Π’ΠΎΡΡŒΠΌΠ΅Ρ€ΠΈΡ‡Π½Ρ‹ΠΉ", + 3312581301: "PBKDF2 Π˜Ρ‚Π΅Ρ€Π°Ρ†ΠΈΠΈ", + 721090621: "PSBT", + 995862913: "Π—Π°ΠΊΡ€Π°ΡΡŒΡ‚Π΅ ΠΏΠ΅Ρ€Ρ„ΠΎΡ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ Ρ‡Π΅Ρ€Π½Ρ‹ΠΌ Ρ†Π²Π΅Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΡ… ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠΈΡ‚ΡŒ.", + 2987800462: "Π¨ΠΈΡ€ΠΈΠ½Π° Π‘ΡƒΠΌΠ°Π³ΠΈ", + 3050763890: "Π§Π°ΡΡ‚ΡŒ\n%d / %d", + 3559456868: "Π Π°Π·ΠΌΠ΅Ρ€ Части", + 4249903283: "Π€Ρ€Π°Π·Π°-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + 3712257341: "Π€Ρ€Π°Π·Π°-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ: ", + 140802882: "ΠŸΠΎΡΡ‚ΠΎΡΠ½Π½Π°Ρ ΠŸΠ°ΠΌΡΡ‚ΡŒ", + 1703779997: "QR ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Ρ‹ΠΌ тСкстом", + 3561756278: "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚Π΅ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька", + 784609464: "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠŸΠΎΠ³Ρ€ΡƒΠΆΠ΅Π½ΠΈΡ", + 3037062877: "ΠΠ°ΠΏΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ ВСстовый QR", + 4278257699: "ΠΠ°ΠΏΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ Π² Π²ΠΈΠ΄Π΅ QR?\n\n%s\n\n", + 516488026: "ΠŸΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ?\n\n%s\n\n", + 1123106929: "ΠŸΡ€ΠΈΠ½Ρ‚Π΅Ρ€", + 3903571079: "Π”Ρ€Π°ΠΉΠ²Π΅Ρ€ ΠŸΡ€ΠΈΠ½Ρ‚Π΅Ρ€Π° Π½Π΅ установлСн!", + 2609799302: "Π˜Π΄Π΅Ρ‚ ΠΏΠ΅Ρ‡Π°Ρ‚ΡŒ\n%d / %d", + 844861889: "Π˜Π΄Π΅Ρ‚ ΠΏΠ΅Ρ‡Π°Ρ‚ΡŒ ...", + 2580599003: "ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ?", + 556126964: "ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ...", + 1848310591: "QR Код", + 710709610: "RX Пин", + 2697857197: "ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ", + 1746677167: "АдрСс ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΡ", + 364354944: "Π Π΅Π³ΠΈΠΎΠ½: ", + 1662254634: "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€ΠΈΡ‚Π΅ отсканированныС Π΄Π°Π½Π½Ρ‹Π΅, ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΏΡ€ΠΈ нСобходимости", + 770350922: "Π‘Ρ€ΠΎΡΡŒΡ‚Π΅ ΠΊΡƒΠ±ΠΈΠΊ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ %d Ρ€Π°Π·, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠ³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ.", + 856795528: "Броски:\n\n%s", + 255086803: "Броски: %d\n", + 3976793317: "SD ΠΊΠ°Ρ€Ρ‚Π°", + 2827687530: "SD ΠΊΠ°Ρ€Ρ‚Π° Π½Π΅ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π°", + 2736513298: "SD ΠΊΠ°Ρ€Ρ‚Π° Π½Π΅ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π°.", + 3593785196: "SHA256 бросков:\n\n%s", + 1143278725: "SHA256 ΡΠ½ΡΠΏΡˆΠΎΡ‚Π°:\n\n%s", + 3338679392: "SHA256:\n%s", + 3531742595: "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π° SD ΠΊΠ°Ρ€Ρ‚Ρƒ?", + 810036588: "Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΎ Π½Π° SD ΠΊΠ°Ρ€Ρ‚Ρƒ:\n%s", + 763824768: "Π¨ΠΊΠ°Π»Π°", + 4117455079: "ΠžΡ‚ΡΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ АдрСс", + 3219991109: "ΠžΡ‚ΡΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ BIP39 Ρ„Ρ€Π°Π·Ρƒ-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + 2537207336: "ΠžΡ‚ΡΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠšΠ»ΡŽΡ‡ QR ΠΊΠΎΠ΄", + 4006316572: "Π‘ΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ слов 1-12 снова", + 2736506158: "Π‘ΠΊΠ°Π½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ слов 13-24", + 266935239: "SeedQR", + 1698829144: "ВрансфСр самому сСбС ΠΈΠ»ΠΈ Π‘Π΄Π°Ρ‡Π° (%d): ", + 473154195: "Настройки", + 1825881236: "Π’Ρ‹ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ", + 2120776272: "Π’Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅..", + 1061961408: "ΠŸΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒ", + 4282338366: "ΠŸΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒ?", + 746161122: "Подпись", + 1988416729: "ПодписанноС Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠ΅", + 3672006076: "ПодписанноС PSBT", + 2281377987: "Одна подпись", + 4221794628: "Π Π°Π·ΠΌΠ΅Ρ€: ", + 2344747135: "НСкоторыС ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π½Π΅ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½Ρ‹.", + 2309020186: "Расход (%d): ", + 3355862324: "Бтэкбит 1248", + 3303592908: "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π° Ѐлэш ΠŸΠ°ΠΌΡΡ‚ΡŒ", + 720041451: "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ Π½Π° SD ΠšΠ°Ρ€Ρ‚Ρƒ", + 3514476519: "Π‘Π²Π°ΠΉΠΏΠ½ΠΈΡ‚Π΅, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ€Π΅ΠΆΠΈΠΌ", + 1898550184: "ΠŸΠ Π˜ΠšΠžΠ‘ΠΠ˜Π’Π•Π‘Π¬ ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡ‚Π΅ Π’Π’ΠžΠ”, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π·Π°Ρ…Π²Π°Ρ‚ΠΈΡ‚ΡŒ", + 4228215415: "TX Пин", + 2612594937: "ВСкст", + 1454688268: "Π’Π΅ΠΌΠ°", + 1180180513: "Π’Π΅Ρ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ", + 4119292117: "Мини Π‘ΠΈΠ΄-Ρ„Ρ€Π°Π·Π°", + 1732872974: "Мини Π‘ΠΈΠ΄-Ρ„Ρ€Π°Π·Π° (Π‘ΠΈΡ‚Ρ‹)", + 725348723: "Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹", + 3684696112: "ΠŸΡ€ΠΈΠΊΠΎΡΠ½ΠΈΡ‚Π΅ΡΡŒ Π“Ρ€Π°Π½ΠΈΡ†Ρ‹", + 2978718564: "Вачскрин", + 2732611775: "ΠŸΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π΅Ρ‰Ρ‘?", + 1487826746: "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ BIP39 Ρ„Ρ€Π°Π·Ρƒ-ΠΏΠ°Ρ€ΠΎΠ»ΡŒ", + 2061556020: "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠšΠ»ΡŽΡ‡", + 2089395053: "ΠΠ½ΠΈΡ‚", + 2845607430: "ОбновлСниС Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊΠ°..\n\n%d%%", + 4164597446: "ОбновлСниС Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΎ.\n\nΠ’Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅..", + 2736001501: "ОбновлСниС ΠΏΡ€ΠΎΡˆΠΈΠ²ΠΊΠΈ..\n\n%d%%", + 2674953168: "Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π½ΡƒΡŽ Ρ„ΠΎΠ½ΠΎΠ²ΡƒΡŽ ΠΏΠΎΠ²Π΅Ρ€Ρ…Π½ΠΎΡΡ‚ΡŒ.", + 2402455261: "Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΡΠ½Ρ‚Ρ€ΠΎΠΏΠΈΡŽ ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡƒ", + 236075140: "Использовано: ", + 4003084591: "Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ %s Π²Π½Π΅ Π΄ΠΈΠ°ΠΏΠΎΠ·ΠΎΠ½Π°: [%s, %s]", + 4191058607: "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠšΠ°ΠΌΠ΅Ρ€Ρ‹", + 1254681955: "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ D20", + 525309547: "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ D6", + 590330112: "Π‘ ΠŸΠΎΠΌΠΎΡ‰ΡŒΡŽ Π ΡƒΡ‡Π½ΠΎΠ³ΠΎ Π’Π²ΠΎΠ΄Π°", + 2504354847: "Π”ΠΎΠΆΠ΄ΠΈΡ‚Π΅ΡΡŒ Π—Π°Ρ…Π²Π°Ρ‚Π°", + 2297028319: "ДСскриптор КошСлька", + 4232654916: "Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька", + 2587172867: "Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½!", + 2499782468: "Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ дСскриптор кошСлька Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½.", + 2671738224: "ΠŸΡ€Π΅Π΄ΡƒΠΏΡ€Π΅ΠΆΠ΄Π΅Π½ΠΈΠ΅:", + 797660533: "Π‘Π»ΠΎΠ²ΠΎ %d", + 3742424146: "Числа Π‘Π»ΠΎΠ²", + 2965123464: "Π‘Π»ΠΎΠ²Π°", + 1303016265: "Π”Π°", + 771968845: "Π’Π°ΡˆΠΈ измСнСния Π±ΡƒΠ΄ΡƒΡ‚ сохранСны Π½Π° Ρ„Π»ΡΡˆ памяти устройства.", + 2569054451: "Π’Π°ΡˆΠΈ измСнСния Π±ΡƒΠ΄ΡƒΡ‚ сохранСны Π½Π° SD ΠΊΠ°Ρ€Ρ‚Π΅.", + }, + "pl-PL": { + 1185266064: "%d z %d multisig", + 2004520398: "%D.Zmiana:\n\n%s\n\n", + 3862364126: "%D.Samo-transfer:\n\n%s\n\n", + 3264377309: "%D.WydaΔ‡:\n\n%s\n\n", + 2399232215: "%s\n\nis prawidΕ‚owy adres zmiany!", + 3921290840: "%s\n\nis waΕΌny adres odbierania!", + 1808355833: "%s\n\nwas nie znaleziono w pierwszych adresach zmiany", + 1306127065: "%s\n\nwas nie znaleziono w pierwszych %D Otrzymuj adresy", + 3348584292: "(Eksperymentalny)", + 2739590230: "12 sΕ‚Γ³w", + 1310058127: "24 sΕ‚owa", + 2743272264: "ABC", + 1949634023: "O", + 1517128857: "Adafruit", + 3270727197: "Adres", + 2574498267: "Dodatkowa entropia z kamery wymaganej dla trybu AES-CBC", + 283202181: "WΕ‚aΕ›ciwie wyrΓ³wnaj kamerΔ™ i maleΕ„kie nasiona.", + 88746165: "NiepeΕ‚nosprawne przeciwblokowane", + 1521033296: "WΕ‚Δ…czona anty-zabawa", + 1056821534: "JesteΕ› pewny?", + 3455872521: "Z powrotem", + 2541860807: "Kopie zapasowe bootloader ..\n\n%d %%", + 2256777600: "zΕ‚y podpis", + 3937333362: "Baudrate", + 427617266: "Bitcoin", + 928727036: "WyΕ›ciΓ³Ε‚ka graniczna", + 213030954: "CNC", + 1207696150: "Zmiana", + 3126552510: "ZmieΕ„ adresy", + 1583186953: "ZmieniΔ‡ motyw i ponownie uruchomiΔ‡?", + 2697733395: "Zmiany utrzymywaΕ‚y siΔ™ na karcie SD!", + 388908871: "Zmiany bΔ™dΔ… trwaΔ‡ do zamkniΔ™cia.", + 3442025874: "SprawdΕΊ kartΔ™ SD", + 3119547911: "SprawdΕΊ, czy adres naleΕΌy do tego portfela?", + 2856261511: "Sprawdzone %d adresy zmiany bez dopasowaΕ„.", + 2788541416: "Sprawdzone %D Otrzymuj adresy bez zapaΕ‚ek.", + 2446472910: "Sprawdzanie Zmieniania Adres %D dla dopasowania.", + 2470115694: "Sprawdzanie karty SD ..", + 3655273987: "Sprawdzanie adresu Otrzymaj %D dla meczu.", + 2407028014: "Kompaktowy seedqr", + 4041895036: "KontynuowaΔ‡?", + 4094072796: "UtwΓ³rz kod QR", + 167798282: "UtwΓ³rz kod QR z tekstu?", + 2767642191: "Utworzony:", + 3513215254: "Niestandardowy kod QR", + 124617190: "Wytnij gΕ‚Δ™bokoΕ›Δ‡", + 597912140: "Metoda ciΔ™cia", + 2504034831: "DziesiΔ™tny", + 2751113454: "OdszyfrowaΔ‡?", + 3510912550: "UsuΕ„ %s?", + 1016609898: "UsunΔ…Δ‡ plik?", + 1364509700: "UsuΕ„ mnemonic", + 4102535566: "GΕ‚Δ™bokoΕ›Δ‡ na przepustkΔ™", + 2791699253: "Pochodzenie: %s", + 1230133196: "Nie wykryto pamiΔ™ci flash urzΔ…dzenia.", + 3836852788: "Zrobione?", + 382368239: "Kierowca", + 3978947916: "Enkoder", + 4090746898: "Encoder Debunet", + 374684711: "Szyfrowanie mnemoniki", + 1244124409: "Zaszyfrowany kod QR", + 2968548114: "Szyfrowana mnemonika nie byΕ‚a przechowywana", + 3315319371: "Szyfrowana mnemonika byΕ‚a przechowywana z ID:", + 350279787: "Szyfrowanie", + 2601598799: "Tryb szyfrowania", + 3504179008: "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39 jako liczbΔ™ od 1 do 2048 r.", + 1100685007: "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39 jako liczbΔ™ w heksadecimal od 1 do 800.", + 4090266642: "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39 jako liczbΔ™ w wysokoΕ›ci od 1 do 4000.", + 2780625730: "WprowadΕΊ kaΕΌde sΕ‚owo swojego mnemonika BIP-39.", + 784361051: "BΕ‚Δ…d:\n%s", + 1505332462: "wyjΕ›cie", + 3838465623: "EksplorowaΔ‡ pliki?", + 4170881190: "Eksportowanie do karty SD ..", + 1711312434: "Rozszerzony klucz publiczny", + 383371114: "Nie udaΕ‚o siΔ™ odszyfrowaΔ‡", + 3048830188: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ PSBT", + 4192663412: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ adresu", + 1996021743: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ klucza", + 1108715658: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ wiadomoΕ›ci", + 1081425878: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ mnemonika", + 928667220: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ deskryptora wyjΕ›ciowego", + 1620572516: "Nie udaΕ‚o siΔ™ zaΕ‚adowaΔ‡ pseudonim", + 2946146830: "Nie udaΕ‚o siΔ™ przechowywaΔ‡ mnemonika", + 1303554751: "OpΕ‚ata:", + 104500973: "SzybkoΕ›Δ‡ pasz", + 3313339187: "Nazwa pliku", + 1982637349: "Nazwa pliku %s istnieje na karcie SD, zastΔ…p?", + 3737729752: "Odcisk palca: %s", + 2542772894: "Oprogramowanie ukΕ‚adowe przekracza maksymalny rozmiar: %D", + 1406590538: "Średnica fletu", + 3086093110: "BezpΕ‚atny:", + 1893243331: "Z przechowywania", + 4120536442: "Grbl", + 299338213: "Podaj temu mnemonicznemu identyfikatorowi?W przeciwnym razie zostanie uΕΌyty obecny odcisk palca", + 602716148: "IΕ›Δ‡", + 3580020863: "Hex Key Public", + 2691246967: "Szesnastkowy", + 2736309107: "ID juΕΌ istnieje\n", + 1706005805: "inComplete Descriptor", + 631342955: "WejΕ›cia (%d):", + 2585599782: "BΕ‚Δ™dny adres", + 2874529150: "NieprawidΕ‚owy bootloader", + 4093416954: "NieprawidΕ‚owa dΕ‚ugoΕ›Δ‡ mnemoniczna", + 1422874211: "NieprawidΕ‚owy klucz publiczny", + 2443867979: "NieprawidΕ‚owy portfel:\n%s", + 4122897393: "OdwracaΔ‡", + 3000888649: "Klucz", + 2686333978: "Klucz:", + 4123798664: "Krux\n\n\neversion\n%s", + 3835918229: "Test drukarki Krux QR", + 766317539: "JΔ™zyk", + 972436696: "OpΓ³ΕΊnienie linii", + 3596093890: "Linia:", + 2820726296: "Ładuj mnemoniczne", + 879727077: "ZaΕ‚aduj z karty SD?", + 669106195: "ZaΕ‚aduj jeden?", + 3330705289: "ObciΔ…ΕΌenie?", + 2596531078: "Ładowanie aparatu ..", + 596389387: "Ładowanie ZMIANY Adres %D ..", + 336702608: "Ładowanie drukarki ..", + 2538883522: "Ładowanie adresu odbierania %d ..", + 3159494909: "Ładowanie..", + 1177338798: "Widownia", + 2817059741: "Lokalizacja", + 63976957: "Poziom dziennika", + 86530918: "Logowanie", + 2917810189: "Maksymalna dΕ‚ugoΕ›Δ‡ przekroczona (%s)", + 2030045667: "WiadomoΕ›Δ‡", + 3928301843: "BrakujΔ…cy plik podpisu", + 1948316555: "Mnemoniczny", + 2123991188: "Mnemoniczne id", + 3911073154: "Mnemoniczny identyfikator przechowywania", + 570639842: "Mnemonik nie zostaΕ‚ odszyfrowany", + 1746030071: "Mnemoniczny nie byΕ‚ szyfrowany", + 1458925155: "Zmodyfikowany:", + 1845376098: "Multisig", + 2939797024: "SieΔ‡", + 73574491: "Nowy Mnemonic", + 2792272353: "Wykryto nowe oprogramowanie.\n\nsha256:\n%s\n\n\n\ninstall?", + 4063104189: "NIE", + 3927838899: "Brak frazy BIP39", + 4092516657: "Za maΕ‚o rolki!", + 1577637745: "Octal", + 3312581301: "PBKDF2 ITER.", + 721090621: "PSBT", + 995862913: "Parzyj kropki czarne, aby moΕΌna je byΕ‚o wykryΔ‡.", + 2987800462: "SzerokoΕ›Δ‡ papieru", + 3050763890: "CzΔ™Ε›Δ‡\n %d / %d", + 3559456868: "Rozmiar czΔ™Ε›ci", + 4249903283: "Fraza", + 3712257341: "FRASSE:", + 140802882: "TrwaΔ‡", + 1703779997: "PlainText QR", + 3561756278: "ZaΕ‚aduj deskryptor wyjΕ›ciowy portfela", + 784609464: "SzybkoΕ›Δ‡ spadku", + 3037062877: "Test wydrukuj QR", + 4278257699: "Drukuj do qr?\n\n%s\n\n", + 516488026: "WydrukowaΔ‡?\n\n%s\n\n", + 1123106929: "Drukarka", + 3903571079: "Sterownik drukarki nie jest ustawiony!", + 2609799302: "Drukowanie\n %d / %d", + 844861889: "Drukowanie ...", + 2580599003: "PrzystΔ™powaΔ‡?", + 556126964: "Przetwarzanie ...", + 1848310591: "Kod QR", + 710709610: "Pin Rx", + 2697857197: "OdbieraΔ‡", + 1746677167: "OdbieraΔ‡ adresy", + 364354944: "Region:", + 1662254634: "Przejrzyj zeskanowane dane, w razie potrzeby edytuj", + 770350922: "RzuΔ‡ kostkΔ™ co najmniej %d, aby wygenerowaΔ‡ mnemoniczny.", + 856795528: "Rolls:\n\n%s", + 255086803: "Rolls: %d\n", + 3976793317: "karta SD", + 2827687530: "Karta SD nie zostaΕ‚a wykryta", + 2736513298: "Karta SD nie zostaΕ‚a wykryta.", + 3593785196: "SHA256 Rolls:\n\n%s", + 1143278725: "SHA256 migawki:\n\n%s", + 3338679392: "SHA256:\n%s", + 3531742595: "Zapisz na karcie SD?", + 810036588: "Zapisano na karcie SD:\n%s", + 763824768: "Skala", + 4117455079: "Adres skanowania", + 3219991109: "Scan BIP39 Passhraz", + 2537207336: "Skanuj kod QR", + 4006316572: "Znowu skanowanie sΕ‚Γ³w 1-12", + 2736506158: "Skanowanie sΕ‚Γ³w 13-24", + 266935239: "Seedqr", + 1698829144: "Samo-transfer lub zmiana (%d):", + 473154195: "Ustawienia", + 1825881236: "ZamkniΔ™cie", + 2120776272: "WyΕ‚Δ…czanie..", + 1061961408: "PodpisaΔ‡", + 4282338366: "PodpisaΔ‡?", + 746161122: "Podpis", + 1988416729: "Podpisana wiadomoΕ›Δ‡", + 3672006076: "Podpisano PSBT", + 2281377987: "Pojedyncze Sig", + 4221794628: "Rozmiar:", + 2344747135: "Nie moΕΌna wykonaΔ‡ niektΓ³rych kontroli.", + 2309020186: "WydaΔ‡ (%d):", + 3355862324: "Stackbit 1248", + 3303592908: "Przechowuj na Flash", + 720041451: "Przechowuj na karcie SD", + 3514476519: "PrzesuΕ„ tryb zmiany", + 1898550184: "Dotknij lub wejdΕΊ do przechwytywania", + 4228215415: "Pin TX", + 2612594937: "Tekst", + 1454688268: "Temat", + 1180180513: "Termiczny", + 4119292117: "MaΕ‚e ziarno", + 1732872974: "MaΕ‚e nasiona (bity)", + 725348723: "NarzΔ™dzia", + 3684696112: "PrΓ³g dotykowy", + 2978718564: "Ekran dotykowy", + 2732611775: "PrΓ³buj bardziej?", + 1487826746: "PassΓ³wka typu BIP39", + 2061556020: "Klucz typu", + 2089395053: "Jednostka", + 2845607430: "Aktualizacja bootloader ..\n\n%d %%", + 4164597446: "Uaktualnienie zakoΕ„czone.\n\nshutting w dΓ³Ε‚ ..", + 2736001501: "Aktualizacja oprogramowania ukΕ‚adowego ..\n\n%d %%", + 2674953168: "UΕΌyj czarnej powierzchni tΕ‚a.", + 2402455261: "UΕΌyj entropii aparatu, aby stworzyΔ‡ nowy mnemonik", + 236075140: "UΕΌywany:", + 4003084591: "WartoΕ›Δ‡ %s z zakresu: [ %s, %s]", + 4191058607: "Za poΕ›rednictwem aparatu", + 1254681955: "Via D20", + 525309547: "Przez D6", + 590330112: "Poprzez rΔ™czne wejΕ›cie", + 2504354847: "Poczekaj na schwytanie", + 2297028319: "Deskryptor portfela", + 4232654916: "Deskryptor wyjΕ›ciowy portfela", + 2587172867: "ZaΕ‚adowany deskryptor wyjΕ›ciowy portfela!", + 2499782468: "Nie znaleziono deskryptora wyjΕ›ciowego portfela.", + 2671738224: "OstrzeΕΌenie:", + 797660533: "SΕ‚owo %d", + 3742424146: "Numery sΕ‚Γ³w", + 2965123464: "SΕ‚owa", + 1303016265: "Tak", + 771968845: "Twoje zmiany bΔ™dΔ… przechowywane w pamiΔ™ci flash urzΔ…dzenia.", + 2569054451: "Twoje zmiany bΔ™dΔ… przechowywane na karcie SD.", + }, "es-MX": { 1185266064: "%d de %d multisig", 2004520398: "%d. Cambio: \n\n%s\n\n", @@ -1022,7 +1510,6 @@ 88746165: "Antideslumbrante desactivado", 1521033296: "Antideslumbrante habilitado", 1056821534: "ΒΏEstas seguro?", - 3247612282: "BIP39 MnemΓ³nico", 3455872521: "AtrΓ‘s", 2541860807: "Copia de seguridad del cargador de arranque..\n\n%d%%", 2256777600: "Mala asignatura", @@ -1052,6 +1539,7 @@ 597912140: "MΓ©todo de corte", 2504034831: "Decimal", 2751113454: "Descifrar?", + 3510912550: "Eliminar %s?", 1016609898: "ΒΏBorrar archivo?", 1364509700: "Eliminar mnemΓ³nico", 4102535566: "Profundidad por pasada", @@ -1097,11 +1585,10 @@ 4120536442: "GRBL", 299338213: "ΒΏDarle a este mnemΓ³nico una identificaciΓ³n personalizada?De lo contrario se utilizarΓ‘ la huella digital actual", 602716148: "Ir", - 831562513: "Intervalo de calor", - 2300171403: "Tiempo de calor", 3580020863: "Clave pΓΊblica hexadecimal", 2691246967: "Hexadecimal", 2736309107: "ID ya existe\n", + 1706005805: "Descriptor incompleto", 631342955: "Entradas (%d): ", 2585599782: "DirecciΓ³n invΓ‘lida", 2874529150: "Cargador de arranque invΓ‘lido", @@ -1202,6 +1689,7 @@ 3672006076: "PSBT firmado", 2281377987: "Single-sig", 4221794628: "Espacio: ", + 2344747135: "No se pueden realizar algunos cheques.", 2309020186: "Gastos (%d): ", 3355862324: "Stackbit 1248", 3303592908: "Almacenar en flash", @@ -1238,7 +1726,7 @@ 4232654916: "Descriptor de salida de billetera", 2587172867: "Β‘Se ha cargado el descriptor de salida de la cartera!", 2499782468: "No se encontrΓ³ el descriptor de salida de la cartera.", - 1831109430: "Advertencia:\nDescriptor de salida incompleto", + 2671738224: "Advertencia:", 797660533: "Palabra %d", 3742424146: "NΓΊmeros de palabra", 2965123464: "Palabras", @@ -1267,7 +1755,6 @@ 88746165: "Blendschutz deaktiviert", 1521033296: "Blendschutz aktiviert", 1056821534: "Bist Du sicher?", - 3247612282: "BIP39 Mnemonic", 3455872521: "ZurΓΌck", 2541860807: "Bootloader wird gesichert..\n\n%d%%", 2256777600: "UngΓΌltige Signatur", @@ -1297,6 +1784,7 @@ 597912140: "Cut-Methode", 2504034831: "Dezimal", 2751113454: "EntschlΓΌsseln?", + 3510912550: "LΓΆschen %s?", 1016609898: "Datei lΓΆschen?", 1364509700: "Mnemonic lΓΆschen", 4102535566: "Tiefe pro Durchgang", @@ -1342,11 +1830,10 @@ 4120536442: "GRBL", 299338213: "Dieser Mnemonic eine benutzerdefinierte ID zuteilen? Andernfalls wird der aktuelle Fingerabdruck verwendet", 602716148: "Go", - 831562513: "WΓ€rmeintervall", - 2300171403: "Hitzezeit", 3580020863: "Hex ΓΆffentlicher SchlΓΌssel", 2691246967: "Hexadezimal", 2736309107: "ID existiert bereits\n", + 1706005805: "UnvollstΓ€ndiger Ausgabedeskriptor", 631342955: "Input (%d): ", 2585599782: "UngΓΌltige Adresse", 2874529150: "UngΓΌltiger Bootloader", @@ -1447,6 +1934,7 @@ 3672006076: "Signierte PSBT", 2281377987: "Single-Sig", 4221794628: "Grâße: ", + 2344747135: "Einige Schecks kΓΆnnen nicht durchgefΓΌhrt werden.", 2309020186: "Ausgabe (%d): ", 3355862324: "Stackbit 1248", 3303592908: "Auf Flash speichern", @@ -1483,7 +1971,7 @@ 4232654916: "Wallet Ausgabedeskriptor", 2587172867: "Wallet Ausgabedeskriptor geladen!", 2499782468: "Wallet Ausgabedeskriptor nicht gefunden.", - 1831109430: "Warnung:\nUnvollstΓ€ndiger Ausgabedeskriptor", + 2671738224: "Warnung:", 797660533: "Wort %d", 3742424146: "Wortnummern", 2965123464: "WΓΆrter", diff --git a/src/krux/wallet.py b/src/krux/wallet.py index 1d9e71c2e..9dfe310de 100644 --- a/src/krux/wallet.py +++ b/src/krux/wallet.py @@ -43,7 +43,7 @@ def __init__(self, key): self.policy = None if not self.key.multisig: self.descriptor = Descriptor.from_string( - "wpkh(%s/{0,1}/*)" % self.key.key_expression() + "wpkh(%s/<0;1>/*)" % self.key.key_expression() ) self.label = t("Single-sig") self.policy = {"type": self.descriptor.scriptpubkey_type()} @@ -206,7 +206,7 @@ def parse_wallet(wallet_data, network): ) else: # Single-sig - descriptor = Descriptor.from_string("wpkh(%s/{0,1}/*)" % keys[0]) + descriptor = Descriptor.from_string("wpkh(%s/<0;1>/*)" % keys[0]) label = ( key_vals[key_vals.index("Name") + 1] if key_vals.index("Name") >= 0 diff --git a/tests/conftest.py b/tests/conftest.py index e94a35f34..bbb9ed37f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ board_dock, board_m5stickv, encode_to_string, + encode, statvfs, ) @@ -26,7 +27,7 @@ def mp_modules(mocker, monkeypatch): monkeypatch.setitem( sys.modules, "qrcode", - mocker.MagicMock(encode_to_string=encode_to_string), + mocker.MagicMock(encode_to_string=encode_to_string, encode=encode), ) monkeypatch.setitem(sys.modules, "secp256k1", mocker.MagicMock(wraps=secp256k1)) monkeypatch.setitem(sys.modules, "urandom", random) diff --git a/tests/pages/new_mnemonic/__init__.py b/tests/pages/new_mnemonic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pages/new_mnemonic/test_dice_rolls.py b/tests/pages/new_mnemonic/test_dice_rolls.py new file mode 100644 index 000000000..399d49ebd --- /dev/null +++ b/tests/pages/new_mnemonic/test_dice_rolls.py @@ -0,0 +1,272 @@ +from ..test_login import create_ctx +from embit import bip39 + + + +def test_new_12w_from_d6(m5stickv, mocker): + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_12W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press to proceed to 12 words + [BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + [ + BUTTON_ENTER, # proceed with poor entropy, + BUTTON_PAGE, # move to Generate Words, while seeing rolls + BUTTON_ENTER, # generate words + BUTTON_ENTER, # 1 press to confirm SHA + ] + ) + MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit" + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx) + entropy = dice_entropy.new_key() + words = bip39.mnemonic_from_bytes(entropy) + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + assert words == MNEMONIC + + +def test_new_24w_from_d6(m5stickv, mocker): + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_24W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press change to 24 words and 1 press to proceed + [BUTTON_PAGE, BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + [ + BUTTON_ENTER, # proceed with poor entropy, + BUTTON_PAGE, # move to Generate Words, while seeing rolls + BUTTON_ENTER, # generate words + BUTTON_ENTER, # 1 press to confirm SHA + ] + ) + MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo" + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx) + entropy = dice_entropy.new_key() + words = bip39.mnemonic_from_bytes(entropy) + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + assert words == MNEMONIC + + + +def test_new_12w_from_d6_on_amigo_device(amigo_tft, mocker): + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_12W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press to proceed to 12 words + [BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + [ + BUTTON_ENTER, # proceed with poor entropy, + BUTTON_PAGE, # move to Generate Words, while seeing rolls + BUTTON_ENTER, # generate words + BUTTON_ENTER, # 1 press to confirm SHA + ] + ) + MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit" + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx) + entropy = dice_entropy.new_key() + words = bip39.mnemonic_from_bytes(entropy) + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + assert words == MNEMONIC + + +def test_new_24w_from_d6_on_amigo_device(amigo_tft, mocker): + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_24W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press change to 24 words and 1 press to proceed + [BUTTON_PAGE, BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + [ + BUTTON_ENTER, # proceed with poor entropy, + BUTTON_PAGE, # move to Generate Words, while seeing rolls + BUTTON_ENTER, # generate words + BUTTON_ENTER, # 1 press to confirm SHA + ] + ) + MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo" + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx, ) + entropy = dice_entropy.new_key() + words = bip39.mnemonic_from_bytes(entropy) + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + assert words == MNEMONIC + + +def test_cancel_new_12w_from_d6_on_amigo_device(amigo_tft, mocker): + "Will test the Esc button on the roll screen" + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy + from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press to proceed to 12 words + [BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 2 press prev and 1 press on btn Esc + [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER] + + + # 1 press to proceed confirm exit msg + [BUTTON_ENTER] + ) + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx, is_d20=True) + dice_entropy.new_key() + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + +def test_new_12w_from_d20(m5stickv, mocker): + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D20_12W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press to proceed to 12 words + [BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + [ + BUTTON_ENTER, # proceed with poor entropy, + BUTTON_PAGE, # move to Generate Words, while seeing rolls + BUTTON_ENTER, # generate words + BUTTON_ENTER, # 1 press to confirm SHA + ] + ) + MNEMONIC = ( + "erupt remain ride bleak year cabin orange sure ghost gospel husband oppose" + ) + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx, is_d20=True) + entropy = dice_entropy.new_key() + words = bip39.mnemonic_from_bytes(entropy) + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + assert words == MNEMONIC + + +def test_new_24w_from_d20(m5stickv, mocker): + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D20_24W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press change to 24 words and 1 press to proceed + [BUTTON_PAGE, BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D20_24W_MIN_ROLLS)] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + [ + BUTTON_ENTER, # proceed with poor entropy, + BUTTON_PAGE, # move to Generate Words, while seeing rolls + BUTTON_ENTER, # generate words + BUTTON_ENTER, # 1 press to confirm SHA + ] + ) + MNEMONIC = "fun island vivid slide cable pyramid device tuition only essence thought gain silk jealous eternal anger response virus couple faculty ozone test key vocal" + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx, is_d20=True) + entropy = dice_entropy.new_key() + words = bip39.mnemonic_from_bytes(entropy) + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) + assert words == MNEMONIC + +def test_cancel_new_12w_from_d20(m5stickv, mocker): + "Will test the Deletion button and the minimum roll on the roll screen" + from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D20_12W_MIN_ROLLS + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + + BTN_SEQUENCE = ( + # 1 press to proceed to 12 words + [BUTTON_ENTER] + + + # 1 press to proceed msg + [BUTTON_ENTER] + + + # 1 presses per roll + [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)] + + + # 3 press prev and 1 press on btn < (delete last roll) + [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER] + + + # 1 press prev and 1 press on btn Go + [BUTTON_PAGE_PREV, BUTTON_ENTER] + + + # 1 press for msg not enough rolls! + [BUTTON_ENTER] + + + # 2 press prev and 1 press on btn Esc + [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER] + + + # 1 press to proceed confirm exit msg + [BUTTON_ENTER] + ) + + ctx = create_ctx(mocker, BTN_SEQUENCE) + dice_entropy = DiceEntropy(ctx, is_d20=True) + dice_entropy.new_key() + + assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) \ No newline at end of file diff --git a/tests/pages/test_addresses.py b/tests/pages/test_addresses.py index 134fa90a7..d1880fa60 100644 --- a/tests/pages/test_addresses.py +++ b/tests/pages/test_addresses.py @@ -9,45 +9,41 @@ def test_scan_address(mocker, m5stickv, tdata): from krux.qr import FORMAT_PMOFN, FORMAT_NONE cases = [ - # Single-sig, loaded, owned address, No print prompt, search successful + # Single-sig, loaded, owned address, search successful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), - # Single-sig, not loaded, owned address, No print prompt, search successful + # Single-sig, not loaded, owned address, search successful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, False, "bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), - # Single-sig, loaded, owned address, Print, search successful + # Single-sig, loaded, owned address, search successful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y", - MockPrinter(), True, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), - # Single-sig, loaded, owned address, Decline to print, search successful + # Single-sig, loaded, owned address, search successful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y", - MockPrinter(), True, - [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER, BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), # Multisig, loaded, owned address, No print prompt, search successful ( @@ -55,167 +51,150 @@ def test_scan_address(mocker, m5stickv, tdata): tdata.SPECTER_MULTISIG_WALLET_DATA, True, "bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), - # Multisig, not loaded, owned address, No print prompt, can't search + # Multisig, not loaded, owned address, can't search ( tdata.MULTISIG_12_WORD_KEY, tdata.SPECTER_MULTISIG_WALLET_DATA, False, "bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65", - None, True, [BUTTON_ENTER], ), - # Multisig, loaded, owned address, Print, search successful + # Multisig, loaded, owned address, search successful ( tdata.MULTISIG_12_WORD_KEY, tdata.SPECTER_MULTISIG_WALLET_DATA, True, "bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65", - MockPrinter(), True, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), - # Multisig, loaded, owned address, Decline to print, search successful + # Multisig, loaded, owned address, search successful ( tdata.MULTISIG_12_WORD_KEY, tdata.SPECTER_MULTISIG_WALLET_DATA, True, "bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65", - MockPrinter(), True, - [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER, BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), - # Single-sig, loaded, unowned address, No print prompt, search unsuccessful + # Single-sig, loaded, unowned address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Multisig, loaded, unowned address, No print prompt, search unsuccessful + # Multisig, loaded, unowned address, search unsuccessful ( tdata.MULTISIG_12_WORD_KEY, tdata.SPECTER_MULTISIG_WALLET_DATA, True, "bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/44 address, No print prompt, skip search + # Single-sig, loaded, unowned m/44 address, skip search ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ", - None, True, [BUTTON_ENTER, BUTTON_PAGE], ), - # Single-sig, loaded, unowned m/44 address, No print prompt, search unsuccessful + # Single-sig, loaded, unowned m/44 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/44 address, No print prompt, 2x search unsuccessful + # Single-sig, loaded, unowned m/44 address, 2x search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/48/0/0/2 address, No print prompt, search unsuccessful + # Single-sig, loaded, unowned m/48/0/0/2 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "1BRwWQ3GHabCV5DP6MfnCpr6dF6GBAwQ7k", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/84 address, No print prompt, search unsuccessful + # Single-sig, loaded, unowned m/84 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/49 address, No print prompt, search unsuccessful + # Single-sig, loaded, unowned m/49 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/0 address, No print prompt, search unsuccessful + # Single-sig, loaded, unowned m/0 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw", - None, True, [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/0 address, Print, search unsuccessful + # Single-sig, loaded, unowned m/0 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw", - MockPrinter(), True, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, unowned m/0 address, Decline to print, search unsuccessful + # Single-sig, loaded, unowned m/0 address, search unsuccessful ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw", - MockPrinter(), True, - [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER], ), - # Single-sig, loaded, fail to capture QR of address, No print prompt, can't search + # Single-sig, loaded, fail to capture QR of address, can't search ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, None, - None, False, [], ), - # Single-sig, loaded, invalid address, No print prompt, can't search + # Single-sig, loaded, invalid address, can't search ( tdata.SINGLESIG_12_WORD_KEY, tdata.SPECTER_SINGLESIG_WALLET_DATA, True, "invalidaddress", - None, False, [], ), @@ -225,28 +204,29 @@ def test_scan_address(mocker, m5stickv, tdata): if case[2]: wallet.load(case[1], FORMAT_PMOFN) - ctx = create_ctx(mocker, case[6], wallet, case[4]) + ctx = create_ctx(mocker, case[5], wallet, None) addresses_ui = Addresses(ctx) mocker.patch.object( addresses_ui, "capture_qr_code", new=lambda: (case[3], FORMAT_NONE) ) + mocker.patch.object( addresses_ui, - "display_qr_codes", - new=lambda data, qr_format, title=None, allow_any_btn=True: ctx.input.wait_for_button(), + "show_address", + new=lambda addr, title="", quick_exit=False: ctx.input.wait_for_button(), ) + mocker.spy(addresses_ui, "show_address") mocker.spy(addresses_ui, "capture_qr_code") - mocker.spy(addresses_ui, "display_qr_codes") addresses_ui.scan_address() addresses_ui.capture_qr_code.assert_called_once() - if case[5]: - addresses_ui.display_qr_codes.assert_called_once() + if case[4]: + addresses_ui.show_address.assert_called_once() else: - addresses_ui.display_qr_codes.assert_not_called() + addresses_ui.show_address.assert_not_called() - assert ctx.input.wait_for_button.call_count == len(case[6]) + assert ctx.input.wait_for_button.call_count == len(case[5]) def test_list_receive_addresses(mocker, m5stickv, tdata): @@ -265,11 +245,14 @@ def test_list_receive_addresses(mocker, m5stickv, tdata): None, [ BUTTON_ENTER, # Show address nΒΊ1 + BUTTON_ENTER, # QR Menu + BUTTON_PAGE_PREV, # Go to "Back to Menu" BUTTON_ENTER, # Leave BUTTON_PAGE_PREV, # Go to "Back" BUTTON_ENTER, # Leave ], ), + # TODO: Add cases for multisig and thermal printing ] for case in cases: wallet = Wallet(case[0]) @@ -278,14 +261,9 @@ def test_list_receive_addresses(mocker, m5stickv, tdata): ctx = create_ctx(mocker, case[5], wallet, case[4]) addresses_ui = Addresses(ctx) - mocker.patch.object( - addresses_ui, - "display_qr_codes", - new=lambda data, qr_format=FORMAT_NONE, title=None, allow_any_btn=True: ctx.input.wait_for_button(), - ) - mocker.spy(addresses_ui, "display_qr_codes") + mocker.spy(addresses_ui, "show_address") addresses_ui.list_address_type() - addresses_ui.display_qr_codes.assert_called_once() + addresses_ui.show_address.assert_called_once() assert ctx.input.wait_for_button.call_count == len(case[5]) diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py index ffeb8d87f..fd6473e47 100644 --- a/tests/pages/test_encryption_ui.py +++ b/tests/pages/test_encryption_ui.py @@ -72,6 +72,7 @@ def test_load_key_from_qr_code(m5stickv, mocker): from krux.pages.encryption_ui import EncryptionKey, ENCRYPTION_KEY_MAX_LEN from krux.input import BUTTON_ENTER, BUTTON_PAGE + print("case 1: load_key_from_qr_code") BTN_SEQUENCE = ( [BUTTON_PAGE] # move to QR code key + [BUTTON_ENTER] # choose QR code key @@ -87,6 +88,7 @@ def test_load_key_from_qr_code(m5stickv, mocker): key = key_generator.encryption_key() assert key == "qr key" + print("case 2: load_key_from_qr_code") # Repeat with too much characters >ENCRYPTION_KEY_MAX_LEN BTN_SEQUENCE = [BUTTON_PAGE] + [ # move to QR code key BUTTON_ENTER @@ -146,7 +148,7 @@ def test_encrypt_to_qrcode_ecb_ui(m5stickv, mocker): from embit.networks import NETWORKS BTN_SEQUENCE = ( - [BUTTON_PAGE] # Move to store on Encrypted QR + [BUTTON_PAGE] * 2 # Move to store on Encrypted QR + [BUTTON_ENTER] # Confirm Encrypted QR + [BUTTON_PAGE] # add custom ID - No + [BUTTON_ENTER] # Confirm and leave @@ -181,7 +183,7 @@ def test_encrypt_to_qrcode_cbc_ui(m5stickv, mocker): from embit.networks import NETWORKS BTN_SEQUENCE = ( - [BUTTON_PAGE] # Move to store on Encrypted QR + [BUTTON_PAGE] * 2 # Move to store on Encrypted QR + [BUTTON_ENTER] # Confirm Encrypted QR + [BUTTON_ENTER] # Confirm add CBC cam entropy + [BUTTON_PAGE] # add custom ID - No diff --git a/tests/pages/test_home.py b/tests/pages/test_home.py index 4ba34ae9d..caf796cd3 100644 --- a/tests/pages/test_home.py +++ b/tests/pages/test_home.py @@ -87,14 +87,15 @@ def create_ctx(mocker, btn_seq, wallet=None, printer=None, touch_seq=None): ctx = mock_context(mocker) ctx.power_manager.battery_charge_remaining.return_value = 1 ctx.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq) + ctx.display.max_lines = mocker.MagicMock(return_value=7) ctx.wallet = wallet ctx.printer = printer if printer is None: - Settings().printer.driver = "none" + Settings().hardware.printer.driver = "none" else: mocker.patch("krux.printers.create_printer", new=mocker.MagicMock()) - Settings().printer.driver = THERMAL_ADAFRUIT_TXT + Settings().hardware.printer.driver = THERMAL_ADAFRUIT_TXT if touch_seq: ctx.input.touch = mocker.MagicMock( @@ -104,7 +105,7 @@ def create_ctx(mocker, btn_seq, wallet=None, printer=None, touch_seq=None): def test_mnemonic_words(mocker, m5stickv, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV from krux.qr import FORMAT_NONE @@ -152,17 +153,19 @@ def test_mnemonic_words(mocker, m5stickv, tdata): print(num) num = num + 1 ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) + mnemonics = MnemonicsView(ctx) - mocker.spy(home, "display_mnemonic") - home.mnemonic() + mocker.spy(mnemonics, "display_mnemonic") + mnemonics.mnemonic() - home.display_mnemonic.assert_called_with(ctx.wallet.key.mnemonic) + mnemonics.display_mnemonic.assert_called_with( + ctx.wallet.key.mnemonic, "Mnemonic" + ) assert ctx.input.wait_for_button.call_count == len(case[2]) def test_mnemonic_standard_qr(mocker, m5stickv, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV from krux.qr import FORMAT_NONE @@ -174,10 +177,12 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): None, # printer [ BUTTON_PAGE, - BUTTON_ENTER, + BUTTON_PAGE, # select Plaintext QR + BUTTON_ENTER, # click BUTTON_ENTER, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -186,10 +191,12 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): None, # printer [ BUTTON_PAGE, - BUTTON_ENTER, + BUTTON_PAGE, # select Plaintext QR + BUTTON_ENTER, # click BUTTON_ENTER, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -199,11 +206,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): MockPrinter(), [ BUTTON_PAGE, - BUTTON_ENTER, + BUTTON_PAGE, # select Plaintext QR + BUTTON_ENTER, # click BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -212,11 +221,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): MockPrinter(), [ BUTTON_PAGE, - BUTTON_ENTER, + BUTTON_PAGE, # select Plaintext QR + BUTTON_ENTER, # click BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -226,11 +237,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): MockPrinter(), [ BUTTON_PAGE, - BUTTON_ENTER, + BUTTON_PAGE, # select Plaintext QR + BUTTON_ENTER, # click BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -239,11 +252,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): MockPrinter(), [ BUTTON_PAGE, - BUTTON_ENTER, + BUTTON_PAGE, # select Plaintext QR + BUTTON_ENTER, # click BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -253,124 +268,165 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata): print(num) num = num + 1 ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) + mnemonics = MnemonicsView(ctx) - mocker.spy(home, "display_qr_codes") - mocker.spy(home, "print_standard_qr") - home.mnemonic() + mocker.spy(mnemonics, "display_qr_codes") + mocker.spy(mnemonics.utils, "print_standard_qr") + mnemonics.mnemonic() title = "Plaintext QR" - home.display_qr_codes.assert_called_with( + mnemonics.display_qr_codes.assert_called_with( ctx.wallet.key.mnemonic, FORMAT_NONE, title ) if case[1] is not None: - home.print_standard_qr.assert_called_with( + mnemonics.utils.print_standard_qr.assert_called_with( ctx.wallet.key.mnemonic, FORMAT_NONE, title ) assert ctx.input.wait_for_button.call_count == len(case[2]) def test_mnemonic_compact_qr(mocker, m5stickv, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV from krux.qr import FORMAT_NONE cases = [ - # No print prompt + # 0 - 12W ( Wallet(tdata.SINGLESIG_12_WORD_KEY), None, [ BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, # Select Compact SeedQR BUTTON_ENTER, # Open Compact SeedQR - BUTTON_ENTER, # Leave - BUTTON_ENTER, # Are you sure? yes + BUTTON_ENTER, # Open QR Menu + BUTTON_PAGE_PREV, # Move to leave QR Viewer + BUTTON_ENTER, # Leave QR Viewer BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), + # 1 - 24W ( Wallet(tdata.SINGLESIG_24_WORD_KEY), None, [ BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, # Select Compact SeedQR BUTTON_ENTER, # Open Compact SeedQR - BUTTON_ENTER, # Leave - BUTTON_ENTER, # Are you sure? yes + BUTTON_ENTER, # Open QR Menu + BUTTON_PAGE_PREV, # Move to leave QR Viewer + BUTTON_ENTER, # Leave QR Viewer BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), - # Print + # 2 - 12W Print ( Wallet(tdata.SINGLESIG_12_WORD_KEY), MockPrinter(), [ BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, # select Compact SeedQR BUTTON_ENTER, # Open Compact SeedQR + BUTTON_ENTER, # Open QR Menu + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, # Move to Print + BUTTON_ENTER, # Print + BUTTON_ENTER, # Print confirm BUTTON_ENTER, # Leave - BUTTON_ENTER, # Are you sure? yes - BUTTON_ENTER, # say yes to print prompt + BUTTON_PAGE_PREV, # Move to leave QR Viewer + BUTTON_ENTER, # Leave QR Viewer BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), + # 3 - 24W Print ( Wallet(tdata.SINGLESIG_24_WORD_KEY), MockPrinter(), [ BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, # select Compact SeedQR BUTTON_ENTER, # Open Compact SeedQR + BUTTON_ENTER, # Open QR Menu + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, # Move to Print + BUTTON_ENTER, # Print + BUTTON_ENTER, # Print confirm BUTTON_ENTER, # Leave - BUTTON_ENTER, # Are you sure? yes - BUTTON_ENTER, # say yes to print prompt + BUTTON_PAGE_PREV, # Move to leave QR Viewer + BUTTON_ENTER, # Leave QR Viewer BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), - # Decline to print + # 4 - 12W Print, Decline to print ( Wallet(tdata.SINGLESIG_12_WORD_KEY), MockPrinter(), [ BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, # select Compact SeedQR BUTTON_ENTER, # Open Compact SeedQR + BUTTON_ENTER, # Open QR Menu + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, # Move to Print + BUTTON_ENTER, # Print + BUTTON_PAGE, # Print decline BUTTON_ENTER, # Leave - BUTTON_ENTER, # Are you sure? yes - BUTTON_PAGE, # say no to print prompt + BUTTON_PAGE_PREV, # Move to leave QR Viewer + BUTTON_ENTER, # Leave QR Viewer BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), + # 5 - 24W Print, Decline to print ( Wallet(tdata.SINGLESIG_24_WORD_KEY), MockPrinter(), [ BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, # select Compact SeedQR BUTTON_ENTER, # Open Compact SeedQR + BUTTON_ENTER, # Open QR Menu + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, # Move to Print + BUTTON_ENTER, # Print + BUTTON_PAGE, # Print decline BUTTON_ENTER, # Leave - BUTTON_ENTER, # Are you sure? yes - BUTTON_PAGE, # say no to print prompt + BUTTON_PAGE_PREV, # Move to leave QR Viewer + BUTTON_ENTER, # Leave QR Viewer BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], ), @@ -380,22 +436,24 @@ def test_mnemonic_compact_qr(mocker, m5stickv, tdata): print(num) num = num + 1 ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) + mnemonics = MnemonicsView(ctx) - mocker.spy(home, "display_seed_qr") - home.mnemonic() + mocker.spy(mnemonics, "display_seed_qr") + mnemonics.mnemonic() - home.display_seed_qr.assert_called_once() + mnemonics.display_seed_qr.assert_called_once() assert ctx.input.wait_for_button.call_count == len(case[2]) def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_TOUCH, BUTTON_PAGE_PREV, BUTTON_ENTER from krux.qr import FORMAT_NONE + position = [2, 0] + cases = [ # No print prompt ( @@ -406,9 +464,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): BUTTON_TOUCH, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], - [1, 0], + position, ), ( Wallet(tdata.SINGLESIG_24_WORD_KEY), @@ -418,9 +477,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): BUTTON_TOUCH, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], - [1, 0], + position, ), # Print ( @@ -432,9 +492,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): BUTTON_TOUCH, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], - [1, 0, 0], + position + [0], ), ( Wallet(tdata.SINGLESIG_24_WORD_KEY), @@ -445,9 +506,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): BUTTON_TOUCH, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], - [1, 0, 0], + position + [0], ), # Decline to print ( @@ -459,9 +521,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): BUTTON_TOUCH, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], - [1, 0, 1], + position + [1], ), ( Wallet(tdata.SINGLESIG_24_WORD_KEY), @@ -472,9 +535,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): BUTTON_TOUCH, BUTTON_PAGE_PREV, # change to btn Back BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, BUTTON_ENTER, # click on back to return to home init screen ], - [1, 0, 1], + position + [1], ), ] num = 0 @@ -482,19 +546,19 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): print(num) num = num + 1 ctx = create_ctx(mocker, case[2], case[0], case[1], touch_seq=case[3]) - home = Home(ctx) + mnemonics = MnemonicsView(ctx) - mocker.spy(home, "display_qr_codes") - mocker.spy(home, "print_standard_qr") + mocker.spy(mnemonics, "display_qr_codes") + mocker.spy(mnemonics.utils, "print_standard_qr") - home.mnemonic() + mnemonics.mnemonic() title = "Plaintext QR" - home.display_qr_codes.assert_called_with( + mnemonics.display_qr_codes.assert_called_with( ctx.wallet.key.mnemonic, FORMAT_NONE, title ) if case[1] is not None: - home.print_standard_qr.assert_called_with( + mnemonics.utils.print_standard_qr.assert_called_with( ctx.wallet.key.mnemonic, FORMAT_NONE, title ) @@ -502,107 +566,119 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata): def test_public_key(mocker, m5stickv, tdata): - from krux.pages.home import Home + from krux.pages.pub_key_view import PubkeyView from krux.wallet import Wallet - from krux.input import BUTTON_ENTER, BUTTON_PAGE + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV from krux.qr import FORMAT_NONE cases = [ - # No print prompt + # Case parameters: [Wallet, Printer, Button Sequence, Show XPUB, Show ZPUB] + # 0 - Singlesig - Show all text and QR codes ( Wallet(tdata.SINGLESIG_12_WORD_KEY), None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - ( - Wallet(tdata.MULTISIG_12_WORD_KEY), - None, - [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], - ), - # Print - ( - Wallet(tdata.SINGLESIG_12_WORD_KEY), - MockPrinter(), [ - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, + BUTTON_ENTER, # Enter XPUB - Text + BUTTON_PAGE, # move to Back + BUTTON_ENTER, # Press Back + BUTTON_PAGE, # move to XPUB - QR Code + BUTTON_ENTER, # Enter XPUB - QR Code + BUTTON_ENTER, # Enter QR Menu + BUTTON_PAGE_PREV, # move to Back to Menu + BUTTON_ENTER, # Press Back to Menu + BUTTON_PAGE, # move to XPUB - QR Code + BUTTON_PAGE, # move to ZPUB - Text + BUTTON_PAGE, # move to ZPUB - QR Code + BUTTON_ENTER, # Enter ZPUB - QR Code + BUTTON_ENTER, # Enter QR Menu + BUTTON_PAGE_PREV, # move to Back to Menu + BUTTON_ENTER, # Press Back to Menu + BUTTON_PAGE_PREV, # Move Back + BUTTON_ENTER, # Press Back to leave ], + True, + True, ), + # 1 - Multisig - Show all text and QR codes ( Wallet(tdata.MULTISIG_12_WORD_KEY), - MockPrinter(), - [ - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - ], - ), - # Decline to print - ( - Wallet(tdata.SINGLESIG_12_WORD_KEY), - MockPrinter(), + None, [ - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_PAGE, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_PAGE, - ], - ), - ( - Wallet(tdata.MULTISIG_12_WORD_KEY), - MockPrinter(), - [ - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_PAGE, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_PAGE, + BUTTON_ENTER, # Enter XPUB - Text + BUTTON_PAGE, # move to Back + BUTTON_ENTER, # Press Back + BUTTON_PAGE, # move to XPUB - QR Code + BUTTON_ENTER, # Enter XPUB - QR Code + BUTTON_ENTER, # Enter QR Menu + BUTTON_PAGE_PREV, # move to Back to Menu + BUTTON_ENTER, # Press Back to Menu + BUTTON_PAGE, # move to XPUB - QR Code + BUTTON_PAGE, # move to ZPUB - Text + BUTTON_PAGE, # move to ZPUB - QR Code + BUTTON_ENTER, # Enter ZPUB - QR Code + BUTTON_ENTER, # Enter QR Menu + BUTTON_PAGE_PREV, # move to Back to Menu + BUTTON_ENTER, # Press Back to Menu + BUTTON_PAGE_PREV, # Move Back + BUTTON_ENTER, # Press Back to leave ], + True, + True, ), + # TODO: Create cases were not all text and QR codes are shown ] + num = 0 + for case in cases: + print(num) + num += 1 + mock_seed_qr_view = mocker.patch( + "krux.pages.qr_view.SeedQRView" + ) # Mock SeedQRView ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) + pub_key_viewer = PubkeyView(ctx) - mocker.spy(home, "display_qr_codes") - mocker.spy(home, "print_standard_qr") - - home.public_key() + pub_key_viewer.public_key() version = "Zpub" if ctx.wallet.key.multisig else "zpub" - display_qr_calls = [ - mocker.call( - ctx.wallet.key.key_expression(None), - FORMAT_NONE, - "XPUB", - ), - mocker.call( - ctx.wallet.key.key_expression(ctx.wallet.key.network[version]), - FORMAT_NONE, - "ZPUB", - ), - ] - print_qr_calls = [ - mocker.call(ctx.wallet.key.key_expression(None), FORMAT_NONE, "XPUB"), - mocker.call( - ctx.wallet.key.key_expression(ctx.wallet.key.network[version]), - FORMAT_NONE, - "ZPUB", - ), - ] - home.display_qr_codes.assert_has_calls(display_qr_calls) - if case[1] is not None: - home.print_standard_qr.assert_has_calls(print_qr_calls) + qr_view_calls = [] + print_qr_calls = [] + + if case[3]: # Show XPUB + qr_view_calls.append( + mocker.call( + ctx, + data=ctx.wallet.key.key_expression(None), + title="XPUB", + ), + ) + print_qr_calls.append( + mocker.call( + ctx.wallet.key.key_expression(None), + FORMAT_NONE, + "XPUB", + ), + ) + if case[4]: # Show ZPUB + qr_view_calls.append( + mocker.call( + ctx, + data=ctx.wallet.key.key_expression(ctx.wallet.key.network[version]), + title="ZPUB", + ), + ) + print_qr_calls.append( + mocker.call( + ctx.wallet.key.key_expression(ctx.wallet.key.network[version]), + FORMAT_NONE, + "ZPUB", + ), + ) + + # Assert SeedQRView was initialized with the correct parameters + mock_seed_qr_view.assert_has_calls(qr_view_calls, any_order=True) + + # TODO: Assert XPUB and ZPUB text was displayed assert ctx.input.wait_for_button.call_count == len(case[2]) @@ -614,7 +690,7 @@ def test_wallet(mocker, m5stickv, tdata): from krux.qr import FORMAT_PMOFN cases = [ - # Don't load + # 0 Don't load ( False, tdata.SINGLESIG_12_WORD_KEY, @@ -622,7 +698,7 @@ def test_wallet(mocker, m5stickv, tdata): None, [BUTTON_PAGE], ), - # Load, good data, accept + # 1 Load, good data, accept ( False, tdata.SINGLESIG_12_WORD_KEY, @@ -630,7 +706,7 @@ def test_wallet(mocker, m5stickv, tdata): None, [BUTTON_ENTER, BUTTON_ENTER], ), - # Load, good data, decline + # 2 Load, good data, decline ( False, tdata.SINGLESIG_12_WORD_KEY, @@ -638,11 +714,11 @@ def test_wallet(mocker, m5stickv, tdata): None, [BUTTON_ENTER, BUTTON_PAGE], ), - # Load, bad capture + # 3 Load, bad capture (False, tdata.SINGLESIG_12_WORD_KEY, None, None, [BUTTON_ENTER]), - # Load, bad wallet data + # 4 Load, bad wallet data (False, tdata.SINGLESIG_12_WORD_KEY, "{}", None, [BUTTON_ENTER, BUTTON_ENTER]), - # No print prompt + # 5 No print prompt ( True, tdata.SINGLESIG_12_WORD_KEY, @@ -650,7 +726,7 @@ def test_wallet(mocker, m5stickv, tdata): None, [BUTTON_ENTER], ), - # Print + # 6 Print ( True, tdata.SINGLESIG_12_WORD_KEY, @@ -658,7 +734,7 @@ def test_wallet(mocker, m5stickv, tdata): MockPrinter(), [BUTTON_ENTER, BUTTON_ENTER], ), - # Decline to print + # 7 Decline to print ( True, tdata.SINGLESIG_12_WORD_KEY, @@ -666,16 +742,20 @@ def test_wallet(mocker, m5stickv, tdata): MockPrinter(), [BUTTON_ENTER, BUTTON_PAGE], ), - # Multisig wallet, no print prompt + # 8 Multisig wallet, no print prompt ( True, tdata.MULTISIG_12_WORD_KEY, tdata.SPECTER_MULTISIG_WALLET_DATA, None, - [BUTTON_ENTER], + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], ), ] + + num = 0 for case in cases: + print("case: %d" % num) + num = num + 1 wallet = Wallet(case[1]) if case[0]: wallet.load(case[2], FORMAT_PMOFN) @@ -690,7 +770,7 @@ def test_wallet(mocker, m5stickv, tdata): "display_qr_codes", new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), ) - mocker.spy(home, "print_standard_qr") + mocker.spy(home.utils, "print_standard_qr") mocker.spy(home, "capture_qr_code") mocker.spy(home, "display_wallet") @@ -698,7 +778,7 @@ def test_wallet(mocker, m5stickv, tdata): if case[0]: home.display_wallet.assert_called_once() - home.print_standard_qr.assert_called_once() + home.utils.print_standard_qr.assert_called_once() else: if case[4][0] == BUTTON_ENTER: home.capture_qr_code.assert_called_once() @@ -993,7 +1073,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): ) mocker.spy(home, "capture_qr_code") mocker.spy(home, "display_qr_codes") - mocker.spy(home, "print_standard_qr") + mocker.spy(home.utils, "print_standard_qr") # case SD available if case[10] is not None: mocker.patch("os.listdir", new=mocker.MagicMock(return_value=["test.psbt"])) @@ -1020,7 +1100,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): home.capture_qr_code.assert_called_once() if case[5]: # signed! home.display_qr_codes.assert_called_once() - home.print_standard_qr.assert_called_once() + home.utils.print_standard_qr.assert_called_once() else: home.display_qr_codes.assert_not_called() else: @@ -1031,7 +1111,7 @@ def test_sign_psbt(mocker, m5stickv, tdata): def test_sign_message(mocker, m5stickv, tdata): import binascii - from krux.pages.home import Home + from krux.pages.sign_message_ui import SignMessage from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV from krux.qr import FORMAT_NONE @@ -1165,14 +1245,14 @@ def test_sign_message(mocker, m5stickv, tdata): wallet = Wallet(tdata.SINGLESIG_SIGNING_KEY) ctx = create_ctx(mocker, case[3], wallet, case[2]) - home = Home(ctx) + home = SignMessage(ctx) mocker.patch.object(home, "capture_qr_code", new=lambda: (case[0], case[1])) mocker.patch.object( home, "display_qr_codes", new=lambda data, qr_format, title=None: ctx.input.wait_for_button(), ) - mocker.spy(home, "print_standard_qr") + mocker.spy(home.utils, "print_standard_qr") mocker.spy(home, "capture_qr_code") mocker.spy(home, "display_qr_codes") if case[6] is not None: @@ -1199,7 +1279,7 @@ def test_sign_message(mocker, m5stickv, tdata): mocker.call(case[5], case[1], "Hex Public Key"), ] ) - home.print_standard_qr.assert_has_calls( + home.utils.print_standard_qr.assert_has_calls( [ mocker.call(case[4], case[1], "Signed Message"), mocker.call(case[5], case[1], "Hex Public Key"), @@ -1207,6 +1287,6 @@ def test_sign_message(mocker, m5stickv, tdata): ) else: home.display_qr_codes.assert_not_called() - home.print_standard_qr.assert_not_called() + home.utils.print_standard_qr.assert_not_called() assert ctx.input.wait_for_button.call_count == len(case[3]) diff --git a/tests/pages/test_login.py b/tests/pages/test_login.py index a28571f7f..2cd3ab536 100644 --- a/tests/pages/test_login.py +++ b/tests/pages/test_login.py @@ -1,13 +1,7 @@ import sys +from unittest import mock import pytest -from Crypto.Cipher import AES from ..shared_mocks import mock_context -from unittest import mock - -if "ucryptolib" not in sys.modules: - sys.modules["ucryptolib"] = mock.MagicMock( - aes=AES.new, MODE_ECB=AES.MODE_ECB, MODE_CBC=AES.MODE_CBC - ) @pytest.fixture @@ -207,89 +201,15 @@ def test_qr_passphrase_fail(m5stickv, mocker): assert test_passphrase == MENU_CONTINUE - -################### New words from dice tests - - -def test_new_12w_from_d6(m5stickv, mocker, mocker_printer): - from krux.pages.login import Login, D6_12W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press to proceed to 12 words - [BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + [ - BUTTON_ENTER, # 1 press to confirm roll string, - BUTTON_ENTER, # 1 press to confirm SHA - BUTTON_ENTER, # 1 press to continue loading key - BUTTON_PAGE, # 1 press to move to Scan passphrase - BUTTON_PAGE, # 1 press to move to No passphrase - BUTTON_ENTER, # 1 press to skip passphrase - BUTTON_ENTER, # 1 press to confirm fingerprint - BUTTON_ENTER, # 1 press to select single-sig - ] - ) - MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit" - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d6() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - assert ctx.wallet.key.mnemonic == MNEMONIC - - -def test_new_24w_from_d6(m5stickv, mocker): - from krux.pages.login import Login, D6_24W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press change to 24 words and 1 press to proceed - [BUTTON_PAGE, BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + [ - BUTTON_ENTER, # 1 press to confirm roll string, - BUTTON_ENTER, # 1 press to confirm SHA - BUTTON_ENTER, # 1 press to see the next 12 words (24 total) - BUTTON_ENTER, # 1 press to continue loading key - BUTTON_PAGE, # 1 press to move to Scan passphrase - BUTTON_PAGE, # 1 press to move to No passphrase - BUTTON_ENTER, # 1 press to skip passphrase - BUTTON_ENTER, # 1 press to confirm fingerprint - BUTTON_ENTER, # 1 press to select single-sig - ] - ) - MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo" - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d6() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - assert ctx.wallet.key.mnemonic == MNEMONIC - - def test_new_12w_from_snapshot(m5stickv, mocker): - from ..shared_mocks import IMAGE_TO_HASH from krux.pages.login import Login - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV + + # mocks a result of a hashed image + mock_capture_entropy = mocker.patch( + "krux.pages.capture_entropy.CameraEntropy.capture", + return_value = b'\x01' * 32 + ) BTN_SEQUENCE = ( # 1 press to proceed to 12 words @@ -317,7 +237,7 @@ def test_new_12w_from_snapshot(m5stickv, mocker): [BUTTON_ENTER] ) MNEMONIC = ( - "credit knee panther note mule luggage attitude era must junior party general" + "absurd amount doctor acoustic avoid letter advice cage absurd amount doctor adjust" ) ctx = create_ctx(mocker, BTN_SEQUENCE) login = Login(ctx) @@ -326,222 +246,6 @@ def test_new_12w_from_snapshot(m5stickv, mocker): assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) assert ctx.wallet.key.mnemonic == MNEMONIC - -def test_new_12w_from_d6_on_amigo_device(amigo_tft, mocker, mocker_printer): - from krux.pages.login import Login, D6_12W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press to proceed to 12 words - [BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + [ - BUTTON_ENTER, # 1 press to confirm roll string, - BUTTON_ENTER, # 1 press to confirm SHA - BUTTON_ENTER, # 1 press to continue loading key - BUTTON_PAGE, # 1 press to move to Scan passphrase - BUTTON_PAGE, # 1 press to move to No passphrase - BUTTON_ENTER, # 1 press to skip passphrase - BUTTON_ENTER, # 1 press to confirm fingerprint - BUTTON_ENTER, # 1 press to select single-sig - ] - ) - MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit" - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d6() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - assert ctx.wallet.key.mnemonic == MNEMONIC - - -def test_new_24w_from_d6_on_amigo_device(amigo_tft, mocker, mocker_printer): - from krux.pages.login import Login, D6_24W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press change to 24 words and 1 press to proceed - [BUTTON_PAGE, BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + [ - BUTTON_ENTER, # 1 press to confirm roll string, - BUTTON_ENTER, # 1 press to confirm SHA - BUTTON_ENTER, # 1 press to continue loading key - BUTTON_PAGE, # 1 press to move to Scan passphrase - BUTTON_PAGE, # 1 press to move to No passphrase - BUTTON_ENTER, # 1 press to skip passphrase - BUTTON_ENTER, # 1 press to confirm fingerprint - BUTTON_ENTER, # 1 press to select single-sig - ] - ) - MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo" - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d6() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - assert ctx.wallet.key.mnemonic == MNEMONIC - - -def test_cancel_new_12w_from_d6_on_amigo_device(amigo_tft, mocker, mocker_printer): - "Will test the Esc button on the roll screen" - from krux.pages.login import Login, D6_12W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press to proceed to 12 words - [BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 2 press prev and 1 press on btn Esc - [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER] - + - # 1 press to proceed confirm exit msg - [BUTTON_ENTER] - ) - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d6() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - - -def test_new_12w_from_d20(m5stickv, mocker, mocker_printer): - from krux.pages.login import Login, D20_12W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press to proceed to 12 words - [BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + [ - BUTTON_ENTER, # 1 press to confirm roll string, - BUTTON_ENTER, # 1 press to confirm SHA - BUTTON_ENTER, # 1 press to continue loading key - BUTTON_PAGE, # 1 press to move to Scan passphrase - BUTTON_PAGE, # 1 press to move to No passphrase - BUTTON_ENTER, # 1 press to skip passphrase - BUTTON_ENTER, # 1 press to confirm fingerprint - BUTTON_ENTER, # 1 press to select single-sig - ] - ) - MNEMONIC = ( - "erupt remain ride bleak year cabin orange sure ghost gospel husband oppose" - ) - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d20() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - assert ctx.wallet.key.mnemonic == MNEMONIC - - -def test_new_24w_from_d20(m5stickv, mocker, mocker_printer): - from krux.pages.login import Login, D20_24W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press change to 24 words and 1 press to proceed - [BUTTON_PAGE, BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D20_24W_MIN_ROLLS)] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + [ - BUTTON_ENTER, # 1 press to confirm roll string, - BUTTON_ENTER, # 1 press to confirm SHA - BUTTON_ENTER, # 1 press to see the next 12 words (24 total) - BUTTON_ENTER, # 1 press to continue loading key - BUTTON_PAGE, # 1 press to move to Scan passphrase - BUTTON_PAGE, # 1 press to move to No passphrase - BUTTON_ENTER, # 1 press to skip passphrase - BUTTON_ENTER, # 1 press to confirm fingerprint - BUTTON_ENTER, # 1 press to select single-sig - ] - ) - MNEMONIC = "fun island vivid slide cable pyramid device tuition only essence thought gain silk jealous eternal anger response virus couple faculty ozone test key vocal" - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d20() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - assert ctx.wallet.key.mnemonic == MNEMONIC - - -def test_cancel_new_12w_from_d20(m5stickv, mocker, mocker_printer): - "Will test the Deletion button and the minimum roll on the roll screen" - from krux.pages.login import Login, D20_12W_MIN_ROLLS - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - - BTN_SEQUENCE = ( - # 1 press to proceed to 12 words - [BUTTON_ENTER] - + - # 1 press to proceed msg - [BUTTON_ENTER] - + - # 1 presses per roll - [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)] - + - # 3 press prev and 1 press on btn < (delete last roll) - [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER] - + - # 1 press prev and 1 press on btn Go - [BUTTON_PAGE_PREV, BUTTON_ENTER] - + - # 1 press for msg not enough rolls! - [BUTTON_ENTER] - + - # 2 press prev and 1 press on btn Esc - [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER] - + - # 1 press to proceed confirm exit msg - [BUTTON_ENTER] - ) - - ctx = create_ctx(mocker, BTN_SEQUENCE) - login = Login(ctx) - login.new_key_from_d20() - - assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE) - - ########## load words from qrcode tests @@ -863,6 +567,7 @@ def test_load_key_from_text(m5stickv, mocker, mocker_printer): [BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + ( # N [BUTTON_PAGE for _ in range(13)] @@ -907,6 +612,7 @@ def test_load_key_from_text(m5stickv, mocker, mocker_printer): [BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + # Go + Confirm word [BUTTON_PAGE for _ in range(28)] @@ -961,6 +667,7 @@ def test_load_key_from_text_on_amigo_tft_with_touch(amigo_tft, mocker, mocker_pr [BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + ( # N [BUTTON_TOUCH] # index 13 -> "n" @@ -1008,6 +715,7 @@ def test_load_key_from_text_on_amigo_tft_with_touch(amigo_tft, mocker, mocker_pr [BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + # Move to Go, press Go, confirm word [BUTTON_PAGE_PREV] + [BUTTON_ENTER] + [BUTTON_ENTER] + @@ -1065,6 +773,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer): ] # 1 press to select and 1 press to confirm ) * 11 # repeat selection of word=2 (ability) eleven times + + [BUTTON_ENTER] # Pick valid checksum final word message + ( # 1 [BUTTON_ENTER] @@ -1088,6 +797,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer): ) + [ BUTTON_ENTER, # Done? + BUTTON_ENTER, # 12 numbers confirm BUTTON_ENTER, # 12 word confirm BUTTON_PAGE, # 1 press to move to Scan passphrase BUTTON_PAGE, # 1 press to move to No passphrase @@ -1108,6 +818,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer): + [BUTTON_ENTER, BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + # Go + Confirm [BUTTON_PAGE for _ in range(11)] @@ -1115,6 +826,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer): + [BUTTON_ENTER] + [ BUTTON_ENTER, # Done? + BUTTON_ENTER, # 12 numbers confirm BUTTON_ENTER, # 12 word confirm BUTTON_PAGE, # 1 press to move to Scan passphrase BUTTON_PAGE, # 1 press to move to No passphrase @@ -1127,7 +839,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer): ] num = 0 for case in cases: - print(num) + print("case:", num) num = num + 1 ctx = create_ctx(mocker, case[0]) login = Login(ctx) @@ -1155,6 +867,7 @@ def test_load_12w_from_hexadecimal(m5stickv, mocker, mocker_printer): + [BUTTON_ENTER] # 1 press to confirm word=FF(255 decimal) cabin ) * 11 # repeat selection of word=FF(255, cabin) eleven times + + [BUTTON_ENTER] # Pick valid checksum final word message + ( [BUTTON_ENTER] # 1 press to number 1 + [BUTTON_ENTER] # 1 press to number 1 @@ -1164,6 +877,7 @@ def test_load_12w_from_hexadecimal(m5stickv, mocker, mocker_printer): ) + [ BUTTON_ENTER, # Done? + BUTTON_ENTER, # 12 numbers confirm BUTTON_ENTER, # 12 word confirm BUTTON_PAGE, # 1 press to move to Scan passphrase BUTTON_PAGE, # 1 press to move to No passphrase @@ -1224,6 +938,7 @@ def test_possible_letters_from_hexadecimal(m5stickv, mocker, mocker_printer): + [BUTTON_ENTER] # 1 press to confirm word=80(128 decimal) avocado ) * 11 # repeat selection of word=80(128, avocado) eleven times + + [BUTTON_ENTER] # Pick valid checksum final word message + ( [BUTTON_PAGE_PREV] # 1 press change to btn Go + [BUTTON_ENTER] # 1 press to select Go @@ -1231,6 +946,7 @@ def test_possible_letters_from_hexadecimal(m5stickv, mocker, mocker_printer): ) + [ BUTTON_ENTER, # Done? + BUTTON_ENTER, # 12 numbers confirm BUTTON_ENTER, # 12 word confirm BUTTON_PAGE, # 1 press to move to Scan passphrase BUTTON_PAGE, # 1 press to move to No passphrase @@ -1264,6 +980,7 @@ def test_load_12w_from_octal(m5stickv, mocker, mocker_printer): + [BUTTON_ENTER] # 1 press to confirm word=777(511 decimal) divert ) * 11 # repeat selection of word=777(511, divert) eleven times + + [BUTTON_ENTER] # Pick valid checksum final word message + ( [BUTTON_ENTER] # 1 press to number 1 + [BUTTON_PAGE for _ in range(4)] # 4 press change to number 5 @@ -1276,6 +993,7 @@ def test_load_12w_from_octal(m5stickv, mocker, mocker_printer): ) + [ BUTTON_ENTER, # Done? + BUTTON_ENTER, # 12 numbers confirm BUTTON_ENTER, # 12 word confirm BUTTON_PAGE, # 1 press to move to Scan passphrase BUTTON_PAGE, # 1 press to move to No passphrase @@ -1329,6 +1047,7 @@ def test_possible_letters_from_octal(m5stickv, mocker, mocker_printer): + [BUTTON_ENTER] # 1 press to confirm word=400(256 decimal) cable ) * 11 # repeat selection of word=400(256, cable) eleven times + + [BUTTON_ENTER] # Pick valid checksum final word message + ( [BUTTON_PAGE_PREV] # 1 press change to btn Go + [BUTTON_ENTER] # 1 press to select Go @@ -1336,6 +1055,7 @@ def test_possible_letters_from_octal(m5stickv, mocker, mocker_printer): ) + [ BUTTON_ENTER, # Done? + BUTTON_ENTER, # 12 numbers confirm BUTTON_ENTER, # 12 word confirm BUTTON_PAGE, # 1 press to move to Scan passphrase BUTTON_PAGE, # 1 press to move to No passphrase @@ -1393,6 +1113,7 @@ def test_no_passphrase_on_amigo(mocker, amigo_tft): [BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + # Move to Go, press Go, confirm word [BUTTON_PAGE_PREV] @@ -1446,6 +1167,7 @@ def test_passphrase(amigo_tft, mocker, mocker_printer): [BUTTON_ENTER] ) * 11 + + [BUTTON_ENTER] # Pick valid checksum final word message + # Move to Go, press Go, confirm word [BUTTON_PAGE_PREV] @@ -1612,333 +1334,8 @@ def test_load_12w_from_1248(m5stickv, mocker, mocker_printer): assert ctx.wallet.key.mnemonic == MNEMONIC -# import unittest -# tc = unittest.TestCase() -# tc.assertEqual(Settings().i18n.locale, 'b') - - -def test_settings_m5stickv(m5stickv, mocker, mocker_printer): - import krux - - from krux.pages.login import Login - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - from krux.krux_settings import Settings, CategorySetting, NumberSetting - from krux.translations import translation_table - from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA - - tlist = list(translation_table) - index_pt = tlist.index("pt-BR") - index_next = (index_pt + 1) % (len(tlist)) - text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt] - text_next = ( - translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next] - ) - - cases = [ - ( # 0 - ( - # Bitcoin - BUTTON_ENTER, - # Change network - BUTTON_PAGE, - BUTTON_ENTER, - # Leave Settings - BUTTON_PAGE_PREV, - BUTTON_ENTER, - ), - [ - mocker.call("Network\nmain", ORANGE), - mocker.call("Network\ntest", GREEN), - ], - lambda: Settings().bitcoin.network == "test", - CategorySetting, - ), - ( # 1 - ( - # Printer - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Thermal - BUTTON_PAGE, - BUTTON_ENTER, - # Change Baudrate - BUTTON_ENTER, - BUTTON_PAGE, - BUTTON_ENTER, - # Back to Thermal - BUTTON_PAGE_PREV, - BUTTON_ENTER, - # Back to Printer - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Leave Settings - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - ), - [ - mocker.call("Baudrate\n9600", WHITE), - mocker.call("Baudrate\n19200", WHITE), - ], - lambda: Settings().printer.thermal.adafruit.baudrate == 19200, - CategorySetting, - ), - ( # 2 - ( - # Language - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Change Locale - BUTTON_PAGE, - BUTTON_ENTER, - ), - [ - mocker.call(text_pt, WHITE), - mocker.call(text_next, WHITE), - ], - lambda: Settings().i18n.locale == tlist[index_next], - CategorySetting, - ), - ( # 3 - ( - # Logging - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Change log level - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Leave Settings - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - ), - [ - mocker.call("Log Level\nNONE", WHITE), - mocker.call("Log Level\nERROR", RED), - mocker.call("Log Level\nWARN", ORANGE), - mocker.call("Log Level\nINFO", GREEN), - mocker.call("Log Level\nDEBUG", MAGENTA), - ], - lambda: Settings().logging.level == "DEBUG", - CategorySetting, - ), - ( # 4 - ( - # Printer - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Thermal - BUTTON_PAGE, - BUTTON_ENTER, - # Paper Width - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Change width - # Remove digit - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - BUTTON_ENTER, - # Add 9 - BUTTON_PAGE_PREV, - BUTTON_ENTER, - # Go - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Back to Thermal - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Back to Printer - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - # Leave Settings - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_ENTER, - ), - [ - mocker.call("Paper Width", 10), - ], - lambda: Settings().printer.thermal.adafruit.paper_width == 389, - NumberSetting, - ), - ] - case_num = 0 - for case in cases: - print("test_settings cases[" + str(case_num) + "]") - case_num = case_num + 1 - - ctx = create_ctx(mocker, case[0]) - login = Login(ctx) - - Settings().i18n.locale = "pt-BR" - login.settings() - - assert ctx.input.wait_for_button.call_count == len(case[0]) - - assert case[2]() - - -def test_settings_on_amigo_tft(amigo_tft, mocker, mocker_printer): - import krux - from krux.pages.login import Login - from krux.input import BUTTON_TOUCH - from krux.krux_settings import Settings, CategorySetting, NumberSetting - from krux.translations import translation_table - from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA - - tlist = list(translation_table) - index_pt = tlist.index("pt-BR") - index_next = (index_pt + 1) % (len(tlist)) - text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt] - text_next = ( - translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next] - ) - - PREV_INDEX = 0 - GO_INDEX = 1 - NEXT_INDEX = 2 - - LOCALE_INDEX = 2 - LOGGING_INDEX = 3 - PRINTER_INDEX = 5 - LEAVE_INDEX = 8 - - cases = [ - ( - ( - # Bitcoin - 0, - # Change network - NEXT_INDEX, - GO_INDEX, - # Leave Settings - LEAVE_INDEX, - ), - [ - mocker.call("Network\nmain", ORANGE), - mocker.call("Network\ntest", GREEN), - ], - lambda: Settings().bitcoin.network == "test", - CategorySetting, - ), - ( - ( - # Printer - PRINTER_INDEX, - # Thermal - 1, - # Change Baudrate - 0, - NEXT_INDEX, - GO_INDEX, - # Back to Thermal - 8, - # Back to Printer - 3, - # Leave Settings - LEAVE_INDEX, - ), - [ - mocker.call("Baudrate\n9600", WHITE), - mocker.call("Baudrate\n19200", WHITE), - ], - lambda: Settings().printer.thermal.adafruit.baudrate == 19200, - CategorySetting, - ), - ( - ( - # Language - LOCALE_INDEX, - # Change Locale - NEXT_INDEX, - GO_INDEX, - ), - [ - mocker.call(text_pt, WHITE), - mocker.call(text_next, WHITE), - ], - lambda: Settings().i18n.locale == tlist[index_next], - CategorySetting, - ), - ( - ( - # Logging - LOGGING_INDEX, - # Change log level - NEXT_INDEX, - NEXT_INDEX, - NEXT_INDEX, - NEXT_INDEX, - GO_INDEX, - # Leave Settings - LEAVE_INDEX, - ), - [ - mocker.call("Log Level\nNONE", WHITE), - mocker.call("Log Level\nERROR", RED), - mocker.call("Log Level\nWARN", ORANGE), - mocker.call("Log Level\nINFO", GREEN), - mocker.call("Log Level\nDEBUG", MAGENTA), - ], - lambda: Settings().logging.level == "DEBUG", - CategorySetting, - ), - ] - case_num = 0 - for case in cases: - print("test_settings_on_amigo_tft cases[" + str(case_num) + "]") - case_num = case_num + 1 - - ctx = mock_context(mocker) - ctx.power_manager.battery_charge_remaining.return_value = 1 - ctx.input.wait_for_button = mocker.MagicMock(return_value=BUTTON_TOUCH) - ctx.input.touch = mocker.MagicMock( - current_index=mocker.MagicMock(side_effect=case[0]) - ) - - mocker.patch.object(ctx.input.touch, "x_regions", (0, 100, 200, 300)) - mocker.patch.object(ctx.input.touch, "y_regions", (100, 200)) - - login = Login(ctx) - - Settings().i18n.locale = "pt-BR" - login.settings() - - assert ctx.input.wait_for_button.call_count == len(case[0]) - - assert case[2]() - - def test_about(mocker, m5stickv): import krux - from krux.pages.login import Login from krux.metadata import VERSION from krux.input import BUTTON_ENTER diff --git a/tests/pages/test_page.py b/tests/pages/test_page.py index f9160cace..eb26e6884 100644 --- a/tests/pages/test_page.py +++ b/tests/pages/test_page.py @@ -37,6 +37,19 @@ def test_init(mocker, m5stickv, mock_page_cls): assert isinstance(page, Page) +def test_flash_text(mocker, m5stickv, mock_page_cls): + import krux + + ctx = mock_context(mocker) + mocker.patch("time.ticks_ms", new=lambda: 0) + page = mock_page_cls(ctx) + page.flash_text("Hello world", krux.display.lcd.WHITE, 0, 1000) + + assert ctx.display.clear.call_count == 2 + ctx.display.draw_centered_text.assert_called_once() + krux.pages.time.sleep_ms.assert_called_with(1000) + + def test_capture_qr_code(mocker, m5stickv, mock_page_cls): mocker.patch( "krux.camera.sensor.snapshot", new=snapshot_generator(outcome=SNAP_SUCCESS) @@ -50,6 +63,7 @@ def test_capture_qr_code(mocker, m5stickv, mock_page_cls): mocker.patch("time.ticks_ms", new=lambda: 0) page = mock_page_cls(ctx) + ctx.input.flush_events() qr_code, qr_format = page.capture_qr_code() assert qr_code == "12345678910" @@ -60,7 +74,7 @@ def test_capture_qr_code(mocker, m5stickv, mock_page_cls): ctx.display.draw_centered_text.assert_has_calls([mocker.call("Loading Camera..")]) -def test_camera_antiglare_light(mocker, m5stickv, mock_page_cls): +def test_camera_antiglare(mocker, m5stickv, mock_page_cls): from krux.camera import OV7740_ID from krux.input import PRESSED, RELEASED @@ -75,12 +89,12 @@ def test_camera_antiglare_light(mocker, m5stickv, mock_page_cls): from krux.camera import Camera ctx = mock_context(mocker) - ENTER_SEQ = [RELEASED] + [PRESSED] + [PRESSED] + [RELEASED] - PAGE_PREV_SEQ = [RELEASED] + [RELEASED] + [RELEASED] + [PRESSED] + ENTER_SEQ = [False] + [True] + [True] + [False] + PAGE_PREV_SEQ = [False] + [False] + [False] + [True] mocker.patch("time.ticks_ms", time_mocker.tick) - ctx.input.enter_value = mocker.MagicMock(side_effect=ENTER_SEQ) - ctx.input.page_value = mocker.MagicMock(side_effect=ENTER_SEQ) - ctx.input.page_prev_value = mocker.MagicMock(side_effect=PAGE_PREV_SEQ) + ctx.input.enter_event = mocker.MagicMock(side_effect=ENTER_SEQ) + ctx.input.page_event = mocker.MagicMock(side_effect=ENTER_SEQ) + ctx.input.page_prev_event = mocker.MagicMock(side_effect=PAGE_PREV_SEQ) ctx.camera = Camera() ctx.camera.cam_id = OV7740_ID mocker.spy(ctx.camera, "disable_antiglare") diff --git a/tests/pages/test_qr_view.py b/tests/pages/test_qr_view.py index 0114c167d..1c67a8551 100644 --- a/tests/pages/test_qr_view.py +++ b/tests/pages/test_qr_view.py @@ -3,7 +3,7 @@ def test_load_qr_view(amigo_tft, mocker): from krux.pages.qr_view import SeedQRView - from krux.input import BUTTON_ENTER, SWIPE_LEFT, SWIPE_RIGHT + from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV, SWIPE_LEFT, SWIPE_RIGHT BTN_SEQUENCE = [ SWIPE_LEFT, # lines mode @@ -15,7 +15,7 @@ def test_load_qr_view(amigo_tft, mocker): SWIPE_LEFT, # lines mode again SWIPE_RIGHT, # back to standard mode BUTTON_ENTER, # leave - BUTTON_ENTER, # confirm + BUTTON_PAGE_PREV, # move to Back to Main Menu BUTTON_ENTER, # confirm ] @@ -23,13 +23,13 @@ def test_load_qr_view(amigo_tft, mocker): ctx.input.wait_for_button = mocker.MagicMock(side_effect=BTN_SEQUENCE) data = "test code" seed_qr_view = SeedQRView(ctx, data=data, title="Test QR Code") - seed_qr_view.display_seed_qr() + seed_qr_view.display_qr() assert ctx.display.draw_qr_code.call_count == 8 def test_loop_through_regions(amigo_tft, mocker): from krux.pages.qr_view import SeedQRView - from krux.input import BUTTON_ENTER, SWIPE_LEFT, SWIPE_RIGHT + from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV, SWIPE_LEFT, SWIPE_RIGHT from ..test_encryption import CBC_ENCRYPTED_QR BTN_SEQUENCE = ( @@ -46,7 +46,7 @@ def test_loop_through_regions(amigo_tft, mocker): SWIPE_LEFT, # lines mode again SWIPE_RIGHT, # back to standard mode BUTTON_ENTER, # leave - BUTTON_ENTER, # confirm + BUTTON_PAGE_PREV, # move to Back to Main Menu BUTTON_ENTER, # confirm ] ) @@ -55,5 +55,5 @@ def test_loop_through_regions(amigo_tft, mocker): ctx.input.wait_for_button = mocker.MagicMock(side_effect=BTN_SEQUENCE) data = CBC_ENCRYPTED_QR # Will produce an QR code with 48 regions, max=G7 seed_qr_view = SeedQRView(ctx, data=data, title="Test QR Code") - seed_qr_view.display_seed_qr() + seed_qr_view.display_qr() assert ctx.display.draw_qr_code.call_count == 57 diff --git a/tests/pages/test_settings_page.py b/tests/pages/test_settings_page.py new file mode 100644 index 000000000..9622deaf5 --- /dev/null +++ b/tests/pages/test_settings_page.py @@ -0,0 +1,391 @@ +import pytest +from ..shared_mocks import mock_context +import sys + + +@pytest.fixture +def mocker_printer(mocker): + mocker.patch("krux.printers.thermal.AdafruitPrinter", new=mocker.MagicMock()) + + +@pytest.fixture +def mocker_ucryptolib(mocker): + sys.modules["ucryptolib"] = mocker.MagicMock() + + +def create_ctx(mocker, btn_seq, touch_seq=None): + """Helper to create mocked context obj""" + + ctx = mock_context(mocker) + ctx.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq) + + if touch_seq: + ctx.input.touch = mocker.MagicMock( + current_index=mocker.MagicMock(side_effect=touch_seq) + ) + return ctx + + +################### Test menus + + +def test_settings_m5stickv(m5stickv, mocker, mocker_printer, mocker_ucryptolib): + import krux + from krux.pages.settings_page import SettingsPage + from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV + from krux.krux_settings import Settings, CategorySetting, NumberSetting + from krux.translations import translation_table + from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA + + tlist = list(translation_table) + index_pt = tlist.index("pt-BR") + index_next = (index_pt + 1) % (len(tlist)) + text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt] + text_next = ( + translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next] + ) + + cases = [ + ( # 0 + ( + # Bitcoin + BUTTON_ENTER, + # Change network + BUTTON_PAGE, + BUTTON_ENTER, + # Leave Settings + BUTTON_PAGE_PREV, + BUTTON_ENTER, + ), + [ + mocker.call("Network\nmain", ORANGE), + mocker.call("Network\ntest", GREEN), + ], + lambda: Settings().bitcoin.network == "test", + CategorySetting, + ), + ( # 1 + ( + # Hardware + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # TODO: Identify it's printer settings + # Thermal + BUTTON_PAGE, + BUTTON_ENTER, + # Change Baudrate + BUTTON_ENTER, + BUTTON_PAGE, + BUTTON_ENTER, + # Back to Thermal + BUTTON_PAGE_PREV, + BUTTON_ENTER, + # Back to Printer + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Leave Settings + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + ), + [ + mocker.call("Baudrate\n9600", WHITE), + mocker.call("Baudrate\n19200", WHITE), + ], + lambda: Settings().hardware.printer.thermal.adafruit.baudrate == 19200, + CategorySetting, + ), + ( # 2 + ( + # Language + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Change Locale + BUTTON_PAGE, + BUTTON_ENTER, + ), + [ + mocker.call(text_pt, WHITE), + mocker.call(text_next, WHITE), + ], + lambda: Settings().i18n.locale == tlist[index_next], + CategorySetting, + ), + ( # 3 + ( + # Logging + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Change log level + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Leave Settings + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + ), + [ + mocker.call("Log Level\nNONE", WHITE), + mocker.call("Log Level\nERROR", RED), + mocker.call("Log Level\nWARN", ORANGE), + mocker.call("Log Level\nINFO", GREEN), + mocker.call("Log Level\nDEBUG", MAGENTA), + ], + lambda: Settings().logging.level == "DEBUG", + CategorySetting, + ), + ( # 4 + ( + # Printer + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Thermal + BUTTON_PAGE, + BUTTON_ENTER, + # Paper Width + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Change width + # Remove digit + BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, + BUTTON_PAGE_PREV, + BUTTON_ENTER, + # Add 9 + BUTTON_PAGE_PREV, + BUTTON_ENTER, + # Go + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Back to Thermal + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Back to Printer + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + # Leave Settings + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_PAGE, + BUTTON_ENTER, + ), + [ + mocker.call("Paper Width", 10), + ], + lambda: Settings().hardware.printer.thermal.adafruit.paper_width == 389, + NumberSetting, + ), + ] + case_num = 0 + for case in cases: + print("test_settings cases[" + str(case_num) + "]") + case_num = case_num + 1 + + ctx = create_ctx(mocker, case[0]) + settings_page = SettingsPage(ctx) + + Settings().i18n.locale = "pt-BR" + settings_page.settings() + + assert ctx.input.wait_for_button.call_count == len(case[0]) + + assert case[2]() + + +def test_settings_on_amigo_tft(amigo_tft, mocker, mocker_printer): + import krux + from krux.pages.settings_page import SettingsPage + from krux.input import BUTTON_TOUCH + from krux.krux_settings import Settings, CategorySetting, NumberSetting + from krux.translations import translation_table + from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA + + tlist = list(translation_table) + index_pt = tlist.index("pt-BR") + index_next = (index_pt + 1) % (len(tlist)) + text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt] + text_next = ( + translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next] + ) + + PREV_INDEX = 0 + GO_INDEX = 1 + NEXT_INDEX = 2 + + HARDWARE_INDEX = 2 + LOCALE_INDEX = 3 + LOGGING_INDEX = 4 + PRINTER_INDEX = 0 + LEAVE_INDEX = 8 + + cases = [ + ( + # Case 0 + ( + # Bitcoin + 0, + # Change network + NEXT_INDEX, + GO_INDEX, + # Leave Settings + LEAVE_INDEX, + ), + [ + mocker.call("Network\nmain", ORANGE), + mocker.call("Network\ntest", GREEN), + ], + lambda: Settings().bitcoin.network == "test", + CategorySetting, + ), + ( + # Case 1 + ( + # Hardware + HARDWARE_INDEX, + # Printer + PRINTER_INDEX, + # Thermal + 1, + # Change Baudrate + 0, + NEXT_INDEX, + GO_INDEX, + # Back from Thermal + 6, + # Back from Printer + 3, + # Back from Hardware + 2, + # Leave Settings + LEAVE_INDEX, + ), + [ + mocker.call("Baudrate\n9600", WHITE), + mocker.call("Baudrate\n19200", WHITE), + ], + lambda: Settings().hardware.printer.thermal.adafruit.baudrate == 19200, + CategorySetting, + ), + ( + # Case 2 + ( + # Language + LOCALE_INDEX, + # Change Locale + NEXT_INDEX, + GO_INDEX, + ), + [ + mocker.call(text_pt, WHITE), + mocker.call(text_next, WHITE), + ], + lambda: Settings().i18n.locale == tlist[index_next], + CategorySetting, + ), + ( + # Case 3 + ( + # Logging + LOGGING_INDEX, + # Change log level + NEXT_INDEX, + NEXT_INDEX, + NEXT_INDEX, + NEXT_INDEX, + GO_INDEX, + # Leave Settings + LEAVE_INDEX, + ), + [ + mocker.call("Log Level\nNONE", WHITE), + mocker.call("Log Level\nERROR", RED), + mocker.call("Log Level\nWARN", ORANGE), + mocker.call("Log Level\nINFO", GREEN), + mocker.call("Log Level\nDEBUG", MAGENTA), + ], + lambda: Settings().logging.level == "DEBUG", + CategorySetting, + ), + ] + case_num = 0 + for case in cases: + print("test_settings_on_amigo_tft cases[" + str(case_num) + "]") + case_num = case_num + 1 + + ctx = mock_context(mocker) + ctx.power_manager.battery_charge_remaining.return_value = 1 + ctx.input.wait_for_button = mocker.MagicMock(return_value=BUTTON_TOUCH) + ctx.input.touch = mocker.MagicMock( + current_index=mocker.MagicMock(side_effect=case[0]) + ) + + mocker.patch.object(ctx.input.touch, "x_regions", (0, 100, 200, 300)) + mocker.patch.object(ctx.input.touch, "y_regions", (100, 200)) + + settings_page = SettingsPage(ctx) + + Settings().i18n.locale = "pt-BR" + settings_page.settings() + + assert ctx.input.wait_for_button.call_count == len(case[0]) + + assert case[2]() + + +def test_encryption_pbkdf2_setting(m5stickv, mocker, mocker_ucryptolib): + from krux.pages.settings_page import SettingsPage + from krux.krux_settings import Settings, EncryptionSettings + from krux.settings import NumberSetting + + ctx = mock_context(mocker) + settings_page = SettingsPage(ctx) + + enc_setting = EncryptionSettings() + + # pbkdf2_iterations has default value + assert Settings().encryption.pbkdf2_iterations == 100000 + + # try to change the value + settings_page.capture_from_keypad = mocker.MagicMock(return_value=100001) + settings_page.number_setting( + EncryptionSettings(), EncryptionSettings.pbkdf2_iterations + ) + + # continue with default value because it must be multiple of 10000 + assert Settings().encryption.pbkdf2_iterations == 100000 + + # try to change the value to a multiple of 10000 + settings_page.capture_from_keypad = mocker.MagicMock(return_value=110000) + settings_page.number_setting( + EncryptionSettings(), EncryptionSettings.pbkdf2_iterations + ) + + # value changed! + assert Settings().encryption.pbkdf2_iterations == 110000 diff --git a/tests/pages/test_stackbit.py b/tests/pages/test_stackbit.py index 03c51bd34..718bd0e92 100644 --- a/tests/pages/test_stackbit.py +++ b/tests/pages/test_stackbit.py @@ -2,7 +2,7 @@ def test_export_mnemonic_stackbit(mocker, m5stickv, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE @@ -14,6 +14,7 @@ def test_export_mnemonic_stackbit(mocker, m5stickv, tdata): BUTTON_PAGE, BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, BUTTON_ENTER, # Open Stackbit BUTTON_ENTER, # PG2 BUTTON_ENTER, # PG3 @@ -25,15 +26,15 @@ def test_export_mnemonic_stackbit(mocker, m5stickv, tdata): ], ] ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) - mocker.spy(home, "stackbit") - home.mnemonic() - home.stackbit.assert_called_once() + mnemonics = MnemonicsView(ctx) + mocker.spy(mnemonics, "stackbit") + mnemonics.mnemonic() + mnemonics.stackbit.assert_called_once() assert ctx.input.wait_for_button.call_count == len(case[2]) def test_export_mnemonic_stackbit_amigo(mocker, amigo_tft, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE @@ -45,6 +46,7 @@ def test_export_mnemonic_stackbit_amigo(mocker, amigo_tft, tdata): BUTTON_PAGE, BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, BUTTON_ENTER, # Open Stackbit BUTTON_ENTER, # PG2 BUTTON_ENTER, # PG3 @@ -56,10 +58,10 @@ def test_export_mnemonic_stackbit_amigo(mocker, amigo_tft, tdata): ], ] ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) - mocker.spy(home, "stackbit") - home.mnemonic() - home.stackbit.assert_called_once() + mnemonics = MnemonicsView(ctx) + mocker.spy(mnemonics, "stackbit") + mnemonics.mnemonic() + mnemonics.stackbit.assert_called_once() assert ctx.input.wait_for_button.call_count == len(case[2]) diff --git a/tests/pages/test_tiny_seed.py b/tests/pages/test_tiny_seed.py index 60d9f226a..463daac09 100644 --- a/tests/pages/test_tiny_seed.py +++ b/tests/pages/test_tiny_seed.py @@ -3,7 +3,7 @@ def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata): - from krux.pages.home import Home + from krux.pages.mnemonic_view import MnemonicsView from krux.wallet import Wallet from krux.input import BUTTON_ENTER, BUTTON_PAGE @@ -18,6 +18,7 @@ def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata): BUTTON_PAGE, BUTTON_PAGE, BUTTON_PAGE, + BUTTON_PAGE, BUTTON_ENTER, # Open TinySeed BUTTON_ENTER, # go to page 2 BUTTON_ENTER, # Leave @@ -27,10 +28,10 @@ def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata): ], ] ctx = create_ctx(mocker, case[2], case[0], case[1]) - home = Home(ctx) - mocker.spy(home, "tiny_seed") - home.mnemonic() - home.tiny_seed.assert_called_once() + mnemonics = MnemonicsView(ctx) + mocker.spy(mnemonics, "tiny_seed") + mnemonics.mnemonic() + mnemonics.tiny_seed.assert_called_once() assert ctx.input.wait_for_button.call_count == len(case[2]) @@ -276,10 +277,10 @@ def test_scan_tiny_seed_24w(m5stickv, mocker): # Seed will be returned as its word index import time from krux.pages.tiny_seed import TinyScanner - from krux.input import BUTTON_ENTER, PRESSED, RELEASED + from krux.input import BUTTON_ENTER BTN_SEQUENCE = [BUTTON_ENTER] + [BUTTON_ENTER] # Intro # Check OK - ENTER_SEQ = [RELEASED] + [PRESSED] + [RELEASED] * 3 + ENTER_SEQ = [False] + [True] + [False] * 3 TIME_STAMPS = (0, 1, 1000, 2000, 3000, 4000, 5000) TINYSEED_RECTANGLE = (10, 10, 100, 100) TEST_WORDS_NUMBERS_1_12 = [ @@ -314,7 +315,9 @@ def test_scan_tiny_seed_24w(m5stickv, mocker): TEST_24_WORDS = "market glass laugh warm cream either robot end blood awful escape fan palm waste surge kick display shoe remove achieve shoulder siren loop gate" mocker.patch.object(time, "ticks_ms", mocker.MagicMock(side_effect=TIME_STAMPS)) ctx = create_ctx(mocker, BTN_SEQUENCE) - ctx.input.enter_value = mocker.MagicMock(side_effect=ENTER_SEQ) + mocker.patch.object(ctx.input, "page_event", new=lambda: False) + mocker.patch.object(ctx.input, "page_prev_event", new=lambda: False) + ctx.input.enter_event = mocker.MagicMock(side_effect=ENTER_SEQ) tiny_seed = TinyScanner(ctx) mocker.patch.object( tiny_seed, "_detect_tiny_seed", new=lambda image: TINYSEED_RECTANGLE @@ -335,7 +338,7 @@ def test_scan_tiny_seed_24w_amigo(amigo_tft, mocker): from krux.input import BUTTON_ENTER, PRESSED, RELEASED BTN_SEQUENCE = [BUTTON_ENTER] + [BUTTON_ENTER] # Intro # Check OK - ENTER_SEQ = [RELEASED] + [PRESSED] + [RELEASED] * 3 + ENTER_SEQ = [False] + [True] + [False] * 3 TIME_STAMPS = (0, 1, 1000, 2000, 3000, 4000, 5000) TINYSEED_RECTANGLE = (10, 10, 100, 100) TEST_WORDS_NUMBERS_1_12 = [ @@ -370,7 +373,10 @@ def test_scan_tiny_seed_24w_amigo(amigo_tft, mocker): TEST_24_WORDS = "market glass laugh warm cream either robot end blood awful escape fan palm waste surge kick display shoe remove achieve shoulder siren loop gate" mocker.patch.object(time, "ticks_ms", mocker.MagicMock(side_effect=TIME_STAMPS)) ctx = create_ctx(mocker, BTN_SEQUENCE) - ctx.input.enter_value = mocker.MagicMock(side_effect=ENTER_SEQ) + mocker.patch.object(ctx.input, "page_event", new=lambda: False) + mocker.patch.object(ctx.input, "page_prev_event", new=lambda: False) + mocker.patch.object(ctx.input, "touch_event", new=lambda: False) + ctx.input.enter_event = mocker.MagicMock(side_effect=ENTER_SEQ) tiny_seed = TinyScanner(ctx) mocker.patch.object( tiny_seed, "_detect_tiny_seed", new=lambda image: TINYSEED_RECTANGLE diff --git a/tests/pages/test_tools.py b/tests/pages/test_tools.py index cd187ec34..34abfa514 100644 --- a/tests/pages/test_tools.py +++ b/tests/pages/test_tools.py @@ -89,9 +89,9 @@ def test_sd_check_no_sd(m5stickv, mocker): ctx = mock_context(mocker) ctx.input.wait_for_button = mocker.MagicMock(side_effect=BTN_SEQUENCE) tool = Tools(ctx) + tool.flash_text = mocker.MagicMock() tool.sd_check() - - ctx.display.flash_text.assert_has_calls([mocker.call("SD card not detected", ANY)]) + tool.flash_text.assert_has_calls([mocker.call("SD card not detected", ANY)]) def test_sd_check(m5stickv, mocker, mock_file_operations): @@ -136,4 +136,5 @@ def test_delete_mnemonic_from_sd(m5stickv, mocker, mock_file_operations): tool.del_stored_mnemonic() # First mnemonic in the list (ECB) will be deleted # Assert only CBC remains - m().write.assert_called_once_with(CBC_ONLY_JSON) + padding_size = len(SEEDS_JSON) - len(CBC_ONLY_JSON) + m().write.assert_called_once_with(CBC_ONLY_JSON + " " * padding_size) \ No newline at end of file diff --git a/tests/printers/test_cnc.py b/tests/printers/test_cnc.py index 4d4f73e2e..eb48dc49e 100644 --- a/tests/printers/test_cnc.py +++ b/tests/printers/test_cnc.py @@ -81,8 +81,8 @@ def test_print_qr_code_with_row_cutmethod(mocker, m5stickv, mocker_sd_card): ) ) - Settings().printer.cnc.cut_method = "row" - Settings().printer.cnc.invert = False + Settings().hardware.printer.cnc.cut_method = "row" + Settings().hardware.printer.cnc.invert = False p = FilePrinter() @@ -112,8 +112,8 @@ def test_print_qr_code_with_spiral_cutmethod(mocker, m5stickv, mocker_sd_card): ) ) - Settings().printer.cnc.cut_method = "spiral" - Settings().printer.cnc.invert = False + Settings().hardware.printer.cnc.cut_method = "spiral" + Settings().hardware.printer.cnc.invert = False p = FilePrinter() @@ -143,8 +143,8 @@ def test_print_qr_code_inverted(mocker, m5stickv, mocker_sd_card): ) ) - Settings().printer.cnc.cut_method = "spiral" - Settings().printer.cnc.invert = True + Settings().hardware.printer.cnc.cut_method = "spiral" + Settings().hardware.printer.cnc.invert = True p = FilePrinter() diff --git a/tests/shared_mocks.py b/tests/shared_mocks.py index 60b75fada..8eb78e42e 100644 --- a/tests/shared_mocks.py +++ b/tests/shared_mocks.py @@ -32,6 +32,24 @@ def encode_to_string(data): return new_code_str +def encode(data): + # Uses string encoded qr as it already cleaned up the frames + # PyQRcode also doesn't offer any binary output + + frame_less_qr = encode_to_string(data) + size = 0 + while frame_less_qr[size] != "\n": + size += 1 + binary_qr = bytearray(b"\x00" * ((size * size + 7) // 8)) + for y in range(size): + for x in range(size): + bit_index = y * size + x + bit_string_index = y * (size + 1) + x + if frame_less_qr[bit_string_index] == "1": + binary_qr[bit_index >> 3] |= 1 << (bit_index % 8) + return binary_qr + + def get_mock_open(files: dict[str, str]): def open_mock(filename, *args, **kwargs): for expected_filename, content in files.items(): @@ -99,6 +117,9 @@ def total_count(self): def parsed_count(self): return len(self.parts) + def processed_parts_count(self): + return self.parsed_count() + def parse(self, part): if part not in self.parts: self.parts.append(part) @@ -285,6 +306,7 @@ def board_amigo_tft(): "BUTTON_A": 16, "BUTTON_B": 20, "BUTTON_C": 23, + "TOUCH_IRQ": 33, "LED_W": 32, "I2C_SDA": 27, "I2C_SCL": 24, @@ -333,24 +355,38 @@ def mock_context(mocker): if board.config["type"] == "m5stickv": return mocker.MagicMock( - input=mocker.MagicMock(touch=None), + input=mocker.MagicMock( + touch=None, + enter_event=mocker.MagicMock(return_value=False), + page_event=mocker.MagicMock(return_value=False), + page_prev_event=mocker.MagicMock(return_value=False), + touch_event=mocker.MagicMock(return_value=False), + ), display=mocker.MagicMock( font_width=8, font_height=14, width=mocker.MagicMock(return_value=135), height=mocker.MagicMock(return_value=240), to_lines=mocker.MagicMock(return_value=[""]), + max_lines=mocker.MagicMock(return_value=7), ), ) elif board.config["type"] == "dock": return mocker.MagicMock( - input=mocker.MagicMock(touch=None), + input=mocker.MagicMock( + touch=None, + enter_event=mocker.MagicMock(return_value=False), + page_event=mocker.MagicMock(return_value=False), + page_prev_event=mocker.MagicMock(return_value=False), + touch_event=mocker.MagicMock(return_value=False), + ), display=mocker.MagicMock( font_width=8, font_height=16, width=mocker.MagicMock(return_value=240), height=mocker.MagicMock(return_value=320), to_lines=mocker.MagicMock(return_value=[""]), + max_lines=mocker.MagicMock(return_value=9), ), ) elif board.config["type"].startswith("amigo"): @@ -361,5 +397,6 @@ def mock_context(mocker): width=mocker.MagicMock(return_value=320), height=mocker.MagicMock(return_value=480), to_lines=mocker.MagicMock(return_value=[""]), + max_lines=mocker.MagicMock(return_value=9), ), ) diff --git a/tests/test_context.py b/tests/test_context.py index b58ad19ea..dfbe16c41 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -4,9 +4,9 @@ def mock_modules(mocker): mocker.patch("krux.context.logger", new=mocker.MagicMock()) mocker.patch("krux.context.Display", new=mocker.MagicMock()) - mocker.patch("krux.context.Input", new=mocker.MagicMock()) mocker.patch("krux.context.Camera", new=mocker.MagicMock()) mocker.patch("krux.context.Light", new=mocker.MagicMock()) + mocker.patch("krux.context.Input", new=mocker.MagicMock()) def test_init(mocker, m5stickv): @@ -51,3 +51,58 @@ def test_clear_clears_printer(mocker, m5stickv): assert c.wallet is None c.printer.clear.assert_called() + + +def test_screensaver(mocker, m5stickv): + """Test whether the screensaver is animating and changing color over time""" + mock_modules(mocker) + from krux.context import Context, SCREENSAVER_ANIMATION_TIME + from krux.themes import theme + from krux.input import BUTTON_ENTER + import time + + logo = """ + β–ˆβ–ˆ + β–ˆβ–ˆ + β–ˆβ–ˆ + β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ + β–ˆβ–ˆβ–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ + β–ˆβ–ˆ β–ˆβ–ˆ +"""[ + 1:-1 + ].split( + "\n" + ) + c = Context(logo) + + # a sequence of events to simulate users waiting and after some time press BUTTON_ENTER + btn_seq = [] + time_seq = [] + tmp = SCREENSAVER_ANIMATION_TIME + 1 + for _ in range(28): + time_seq.append(tmp) + time_seq.append(tmp) + tmp += SCREENSAVER_ANIMATION_TIME + 1 + btn_seq.append(None) + + time_seq.append(tmp) + time_seq.append(tmp) + btn_seq.append(BUTTON_ENTER) + + c.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq) + time.ticks_ms = mocker.MagicMock(side_effect=time_seq) + + c.screensaver() + + c.display.draw_line_hcentered_with_fullw_bg.assert_any_call( + logo[10], 10, theme.fg_color, theme.bg_color + ) + c.display.draw_line_hcentered_with_fullw_bg.assert_any_call( + logo[5], 5, theme.bg_color, theme.fg_color + ) + c.input.wait_for_button.assert_called() diff --git a/tests/test_display.py b/tests/test_display.py index e054aace3..058af6c31 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -1,78 +1,6 @@ -TEST_QR = """ -111111100111000100010011001111111 -100000101001100100101111001000001 -101110101001100001000001001011101 -101110100010101000110011101011101 -101110101101001110000101001011101 -100000101010100111011110101000001 -111111101010101010101010101111111 -000000001111101101101111000000000 -110100110101111010010110101110110 -001101000100111011101100101000001 -010101101010100000011000111110101 -001101010101010100111100010011001 -010110110010110011001100001000000 -011100011010100010010110101010001 -100110100001101011101101000110110 -000100001001011001110100010011011 -001011111100100011011110001010001 -111100011111010010100001110000000 -000111111110011000000010000110111 -101101000111100111001100101010001 -100010111100110010110100110100011 -011001011000110011001010111000011 -110111100110111010111000111000101 -011000000100011110011100001111001 -100101110101110011011111111110000 -000000001000101000011111100010001 -111111101111110010000010101010110 -100000100101110111011100100010011 -101110100010000101010110111110000 -101110101111101010100100001111111 -101110100100001000000010110110011 -100000101001000011110101100111000 -111111101000011010001101101011010 -""".strip() - -TEST_QR_WITH_BORDER = """ -00000000000000000000000000000000000 -01111111001110001000100110011111110 -01000001010011001001011110010000010 -01011101010011000010000010010111010 -01011101000101010001100111010111010 -01011101011010011100001010010111010 -01000001010101001110111101010000010 -01111111010101010101010101011111110 -00000000011111011011011110000000000 -01101001101011110100101101011101100 -00011010001001110111011001010000010 -00101011010101000000110001111101010 -00011010101010101001111000100110010 -00101101100101100110011000010000000 -00111000110101000100101101010100010 -01001101000011010111011010001101100 -00001000010010110011101000100110110 -00010111111001000110111100010100010 -01111000111110100101000011100000000 -00001111111100110000000100001101110 -01011010001111001110011001010100010 -01000101111001100101101001101000110 -00110010110001100110010101110000110 -01101111001101110101110001110001010 -00110000001000111100111000011110010 -01001011101011100110111111111100000 -00000000010001010000111111000100010 -01111111011111100100000101010101100 -01000001001011101110111001000100110 -01011101000100001010101101111100000 -01011101011111010101001000011111110 -01011101001000010000000101101100110 -01000001010010000111101011001110000 -01111111010000110100011011010110100 -00000000000000000000000000000000000 -""".strip() - -DEFAULT_BG_COLOR = 0x0000 # Black +TEST_QR = bytearray( + b"\x7fn\xfd\x830\x08v9\xd6\xedj\xa0\xdbUU7\xc8\xa0\xe0_U\x7f\x00i\x00\xe3\xd61P\x08\xf5Q\xef^\xfe`\xe8\xc1\x7f\xdex\x936Y\x91\xb8\xeb\xd29c\xd5\xd4\x7f\x00\n#\xfe\xcd\xd7\rJ\x8e\xd9\xe5\xf8\xb9K\xe6x\x17\xb9\xca\xa0\x9a\x9a\x7f\xbb\x1b\x01" +) def test_init(mocker, m5stickv): @@ -182,6 +110,9 @@ def test_to_lines(mocker, m5stickv): cases = [ (135, "Two Words", ["Two Words"]), + (135, "Two Words", ["Two Words"]), + (135, "Two Words", ["Two Words"]), + (135, "Two Words", ["Two Words"]), (135, "Two\nWords", ["Two", "Words"]), (135, "Two\n\nWords", ["Two", "", "Words"]), (135, "Two\n\n\nWords", ["Two", "", "", "Words"]), @@ -238,6 +169,7 @@ def test_to_lines(mocker, m5stickv): "52aad7bd7dcce121", "", "", + "", "Install?", ], ), @@ -307,6 +239,50 @@ def test_to_lines(mocker, m5stickv): assert lines == case[2] +def test_to_lines_exact_match_amigo(mocker, amigo_tft): + from krux.display import Display + + cases = [ + (320, "01234 0123456789012345678", ["01234 0123456789012345678"]), + (320, "0123456789 01234567890 01234", ["0123456789", "01234567890 01234"]), + (320, "01234567890123456789012345", ["0123456789012345678901234", "5"]), + ( + 320, + "01234 0123456789012345678\n01234 0123456789012345678", + ["01234 0123456789012345678", "01234 0123456789012345678"], + ), + ( + 320, + "01 34 0123456789012345678\n01234 0123456789012345678", + ["01 34 0123456789012345678", "01234 0123456789012345678"], + ), + ( + 320, + "01 01 01 01 01 01 01 01 0\n01 01 01 01 01 01 01 0123", + ["01 01 01 01 01 01 01 01 0", "01 01 01 01 01 01 01 0123"], + ), + ( + 320, + "0 0 0 0 0 0 0 0 0 0 0 0 0\n01 01 01 01 01 01 01 0123", + ["0 0 0 0 0 0 0 0 0 0 0 0 0", "01 01 01 01 01 01 01 0123"], + ), + ( + 320, + "01 345 0123456789012345678\n01234 0123456789012345678", + ["01 345", "0123456789012345678", "01234 0123456789012345678"], + ), + ] + for case in cases: + mocker.patch( + "krux.display.lcd", + new=mocker.MagicMock(width=mocker.MagicMock(return_value=case[0])), + ) + d = Display() + lines = d.to_lines(case[1]) + print(lines) + assert lines == case[2] + + def test_outline(mocker, m5stickv): mocker.patch("krux.display.lcd", new=mocker.MagicMock()) import krux @@ -403,40 +379,44 @@ def test_draw_hcentered_text(mocker, m5stickv): ) -def test_draw_centered_text(mocker, m5stickv): +def test_draw_line_hcentered_with_fullw_bg(mocker, m5stickv): mocker.patch("krux.display.lcd", new=mocker.MagicMock()) import krux from krux.display import Display d = Display() mocker.patch.object(d, "width", new=lambda: 135) - mocker.patch.object(d, "height", new=lambda: 240) - mocker.spy(d, "draw_hcentered_text") + mocker.spy(d, "draw_string") - d.draw_centered_text("Hello world", krux.display.lcd.WHITE, 0) + d.draw_line_hcentered_with_fullw_bg( + "Hello world", 10, krux.display.lcd.WHITE, krux.display.lcd.BLACK + ) - d.draw_hcentered_text.assert_called_with( - "Hello world", 113, krux.display.lcd.WHITE, 0 + d.draw_string.assert_called_with( + 23, + d.font_height * 10, + "Hello world", + krux.display.lcd.WHITE, + krux.display.lcd.BLACK, ) + krux.display.lcd.fill_rectangle.assert_called() -def test_flash_text(mocker, m5stickv): +def test_draw_centered_text(mocker, m5stickv): mocker.patch("krux.display.lcd", new=mocker.MagicMock()) - mocker.patch("krux.display.time", new=mocker.MagicMock()) import krux from krux.display import Display d = Display() mocker.patch.object(d, "width", new=lambda: 135) mocker.patch.object(d, "height", new=lambda: 240) - mocker.spy(d, "draw_centered_text") - mocker.spy(d, "clear") + mocker.spy(d, "draw_hcentered_text") - d.flash_text("Hello world", krux.display.lcd.WHITE, 0, 1000) + d.draw_centered_text("Hello world", krux.display.lcd.WHITE, 0) - assert d.clear.call_count == 2 - d.draw_centered_text.assert_called_once() - krux.display.time.sleep_ms.assert_called_with(1000) + d.draw_hcentered_text.assert_called_with( + "Hello world", 113, krux.display.lcd.WHITE, 0 + ) def test_draw_qr_code(mocker, m5stickv): @@ -449,6 +429,6 @@ def test_draw_qr_code(mocker, m5stickv): d.draw_qr_code(0, TEST_QR) - krux.display.lcd.draw_qr_code.assert_called_with( - 0, TEST_QR_WITH_BORDER, 135, QR_DARK_COLOR, QR_LIGHT_COLOR, DEFAULT_BG_COLOR + krux.display.lcd.draw_qr_code_binary.assert_called_with( + 0, TEST_QR, 135, QR_DARK_COLOR, QR_LIGHT_COLOR, QR_LIGHT_COLOR ) diff --git a/tests/test_encryption.py b/tests/test_encryption.py index e1d540478..52c7c249c 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -61,6 +61,9 @@ def mock_file_operations(mocker): mocker.patch("builtins.open", mocker.mock_open(read_data=SEEDS_JSON)) +# ------------------------- + + def test_ecb_encryption(m5stickv): from krux.encryption import AESCipher @@ -117,7 +120,7 @@ def test_load_decrypt_cbc(m5stickv, mock_file_operations): assert words_sd == CBC_WORDS -def test_encrypt_ecb_flash(mocker): +def test_encrypt_ecb_flash(m5stickv, mocker): from krux.krux_settings import Settings from krux.encryption import MnemonicStorage @@ -129,7 +132,7 @@ def test_encrypt_ecb_flash(mocker): m().write.assert_called_once_with(ECB_ONLY_JSON) -def test_encrypt_cbc_flash(mocker): +def test_encrypt_cbc_flash(m5stickv, mocker): from krux.krux_settings import Settings from krux.encryption import MnemonicStorage @@ -143,7 +146,7 @@ def test_encrypt_cbc_flash(mocker): m().write.assert_called_once_with(CBC_ONLY_JSON) -def test_encrypt_ecb_sd(mocker, mock_file_operations): +def test_encrypt_ecb_sd(m5stickv, mocker, mock_file_operations): from krux.krux_settings import Settings from krux.encryption import MnemonicStorage @@ -155,7 +158,7 @@ def test_encrypt_ecb_sd(mocker, mock_file_operations): m().write.assert_called_once_with(ECB_ONLY_JSON) -def test_encrypt_cbc_sd(mocker, mock_file_operations): +def test_encrypt_cbc_sd(m5stickv, mocker, mock_file_operations): from krux.krux_settings import Settings from krux.encryption import MnemonicStorage @@ -169,7 +172,7 @@ def test_encrypt_cbc_sd(mocker, mock_file_operations): m().write.assert_called_once_with(CBC_ONLY_JSON) -def test_delet_from_flash(mocker): +def test_delete_from_flash(m5stickv, mocker): from krux.encryption import MnemonicStorage # Loads a file with 2 mnemonics, one with ID="ecbID", other with ID="cbcID" @@ -180,7 +183,7 @@ def test_delet_from_flash(mocker): m().write.assert_called_once_with(CBC_ONLY_JSON) -def test_delet_from_sd(mocker, mock_file_operations): +def test_delete_from_sd(m5stickv, mocker, mock_file_operations): from krux.encryption import MnemonicStorage # Loads a file with 2 mnemonics, one with ID="ecbID", other with ID="cbcID" @@ -188,10 +191,12 @@ def test_delet_from_sd(mocker, mock_file_operations): with patch("krux.sd_card.open", new=mocker.mock_open(read_data=SEEDS_JSON)) as m: storage = MnemonicStorage() storage.del_mnemonic("ecbID", sd_card=True) - m().write.assert_called_once_with(CBC_ONLY_JSON) + # Calculate padding size + padding_size = len(SEEDS_JSON) - len(CBC_ONLY_JSON) + m().write.assert_called_once_with(CBC_ONLY_JSON + " " * padding_size) -def test_create_ecb_encrypted_qr_code(): +def test_create_ecb_encrypted_qr_code(m5stickv): from krux.encryption import EncryptedQRCode from krux.krux_settings import Settings @@ -201,7 +206,7 @@ def test_create_ecb_encrypted_qr_code(): assert qr_data == ECB_ENCRYPTED_QR -def test_create_cbc_encrypted_qr_code(): +def test_create_cbc_encrypted_qr_code(m5stickv): from krux.encryption import EncryptedQRCode from krux.krux_settings import Settings @@ -212,7 +217,7 @@ def test_create_cbc_encrypted_qr_code(): assert qr_data == CBC_ENCRYPTED_QR -def test_decode_ecb_encrypted_qr_code(): +def test_decode_ecb_encrypted_qr_code(m5stickv): from krux.encryption import EncryptedQRCode from embit import bip39 @@ -224,7 +229,7 @@ def test_decode_ecb_encrypted_qr_code(): assert words == TEST_WORDS -def test_decode_cbc_encrypted_qr_code(): +def test_decode_cbc_encrypted_qr_code(m5stickv): from krux.encryption import EncryptedQRCode from embit import bip39 @@ -237,14 +242,24 @@ def test_decode_cbc_encrypted_qr_code(): assert words == TEST_WORDS -def create_ctx(mocker, btn_seq, touch_seq=None): - """Helper to create mocked context obj""" - ctx = mock_context(mocker) - ctx.power_manager.battery_charge_remaining.return_value = 1 - ctx.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq) +def test_customize_pbkdf2_iterations_create_and_decode(m5stickv): + from krux.encryption import EncryptedQRCode + from krux.krux_settings import Settings + from embit import bip39 - if touch_seq: - ctx.input.touch = mocker.MagicMock( - current_index=mocker.MagicMock(side_effect=touch_seq) - ) - return ctx + print("case Encode: customize_pbkdf2_iterations") + Settings().encryption.version = "AES-ECB" + Settings().encryption.pbkdf2_iterations = 99999 + encrypted_qr = EncryptedQRCode() + qr_data = encrypted_qr.create(TEST_KEY, TEST_MNEMONIC_ID, TEST_WORDS) + print(qr_data) + print(ECB_ENCRYPTED_QR) + + print("case Decode: customize_pbkdf2_iterations") + public_data = encrypted_qr.public_data(qr_data) + assert public_data == ( + "Encrypted QR Code:\nID: test ID\nVersion: AES-ECB\nKey iter.: 90000" + ) + word_bytes = encrypted_qr.decrypt(TEST_KEY) + words = bip39.mnemonic_from_bytes(word_bytes) + assert words == TEST_WORDS diff --git a/tests/test_input.py b/tests/test_input.py index e0487d51f..fc3812119 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -7,18 +7,22 @@ def reset_input_states(mocker, input): if input.enter: mocker.patch.object(input, "enter_value", new=lambda: RELEASED) + mocker.patch.object(input, "enter_event", new=lambda: False) if input.page: mocker.patch.object(input, "page_value", new=lambda: RELEASED) + mocker.patch.object(input, "page_event", new=lambda: False) if input.page_prev: mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED) + mocker.patch.object(input, "page_prev_event", new=lambda: False) if input.touch: mocker.patch.object(input.touch.touch_driver, "current_point", new=lambda: None) + mocker.patch.object(input.touch, "event", new=lambda: False) return input def test_init(mocker, m5stickv): - mocker.patch("krux.input.fm.register", new=mocker.MagicMock()) - mocker.patch("krux.input.GPIO", new=mocker.MagicMock()) + mocker.patch("krux.buttons.fm.register", new=mocker.MagicMock()) + mocker.patch("krux.buttons.GPIO", new=mocker.MagicMock()) import krux from krux.input import Input import board @@ -26,37 +30,37 @@ def test_init(mocker, m5stickv): input = Input() assert isinstance(input, Input) - krux.input.fm.register.assert_has_calls( + krux.buttons.fm.register.assert_has_calls( [ mocker.call(board.config["krux"]["pins"]["BUTTON_A"], mocker.ANY), mocker.call(board.config["krux"]["pins"]["BUTTON_B"], mocker.ANY), ] ) assert ( - krux.input.fm.register.call_args_list[0].args[1]._extract_mock_name() + krux.buttons.fm.register.call_args_list[0].args[1]._extract_mock_name() == "mock.fm.fpioa.GPIOHS21" ) assert ( - krux.input.fm.register.call_args_list[1].args[1]._extract_mock_name() + krux.buttons.fm.register.call_args_list[1].args[1]._extract_mock_name() == "mock.fm.fpioa.GPIOHS22" ) assert input.enter is not None assert input.page is not None - assert krux.input.GPIO.call_count == 2 + assert krux.buttons.GPIO.call_count == 2 assert ( - krux.input.GPIO.call_args_list[0].args[0]._extract_mock_name() + krux.buttons.GPIO.call_args_list[0].args[0]._extract_mock_name() == "mock.GPIOHS21" ) assert ( - krux.input.GPIO.call_args_list[1].args[0]._extract_mock_name() + krux.buttons.GPIO.call_args_list[1].args[0]._extract_mock_name() == "mock.GPIOHS22" ) def test_init_amigo_tft(mocker, amigo_tft): - mocker.patch("krux.input.fm.register", new=mocker.MagicMock()) - mocker.patch("krux.input.GPIO", new=mocker.MagicMock()) + mocker.patch("krux.buttons.fm.register", new=mocker.MagicMock()) + mocker.patch("krux.buttons.GPIO", new=mocker.MagicMock()) import krux from krux.input import Input import board @@ -64,7 +68,7 @@ def test_init_amigo_tft(mocker, amigo_tft): input = Input() assert isinstance(input, Input) - krux.input.fm.register.assert_has_calls( + krux.buttons.fm.register.assert_has_calls( [ mocker.call(board.config["krux"]["pins"]["BUTTON_A"], mocker.ANY), mocker.call(board.config["krux"]["pins"]["BUTTON_B"], mocker.ANY), @@ -72,38 +76,39 @@ def test_init_amigo_tft(mocker, amigo_tft): ] ) assert ( - krux.input.fm.register.call_args_list[0].args[1]._extract_mock_name() + krux.buttons.fm.register.call_args_list[0].args[1]._extract_mock_name() == "mock.fm.fpioa.GPIOHS21" ) assert ( - krux.input.fm.register.call_args_list[1].args[1]._extract_mock_name() + krux.buttons.fm.register.call_args_list[1].args[1]._extract_mock_name() == "mock.fm.fpioa.GPIOHS22" ) assert ( - krux.input.fm.register.call_args_list[2].args[1]._extract_mock_name() + krux.buttons.fm.register.call_args_list[2].args[1]._extract_mock_name() == "mock.fm.fpioa.GPIOHS0" ) assert input.enter is not None assert input.page is not None assert input.page_prev is not None - assert krux.input.GPIO.call_count == 3 + assert krux.buttons.GPIO.call_count == 3 assert ( - krux.input.GPIO.call_args_list[0].args[0]._extract_mock_name() + krux.buttons.GPIO.call_args_list[0].args[0]._extract_mock_name() == "mock.GPIOHS21" ) assert ( - krux.input.GPIO.call_args_list[1].args[0]._extract_mock_name() + krux.buttons.GPIO.call_args_list[1].args[0]._extract_mock_name() == "mock.GPIOHS22" ) assert ( - krux.input.GPIO.call_args_list[2].args[0]._extract_mock_name() == "mock.GPIOHS0" + krux.buttons.GPIO.call_args_list[2].args[0]._extract_mock_name() + == "mock.GPIOHS0" ) def test_init_dock(mocker, dock): - mocker.patch("krux.input.fm.register", new=mocker.MagicMock()) - mocker.patch("krux.input.GPIO", new=mocker.MagicMock()) + mocker.patch("krux.buttons.fm.register", new=mocker.MagicMock()) + mocker.patch("krux.buttons.GPIO", new=mocker.MagicMock()) import krux from krux.input import Input import board @@ -111,7 +116,7 @@ def test_init_dock(mocker, dock): input = Input() assert isinstance(input, Input) - krux.input.fm.register.assert_has_calls( + krux.buttons.fm.register.assert_has_calls( [ mocker.call(board.config["krux"]["pins"]["BUTTON_A"], mocker.ANY), ] @@ -123,7 +128,7 @@ def test_init_dock(mocker, dock): ] ) assert ( - krux.input.fm.register.call_args_list[0].args[1]._extract_mock_name() + krux.buttons.fm.register.call_args_list[0].args[1]._extract_mock_name() == "mock.fm.fpioa.GPIOHS21" ) assert ( @@ -138,10 +143,10 @@ def test_init_dock(mocker, dock): assert input.page is not None assert input.page_prev is not None - assert krux.input.GPIO.call_count == 1 + assert krux.buttons.GPIO.call_count == 1 assert krux.rotary.GPIO.call_count == 2 assert ( - krux.input.GPIO.call_args_list[0].args[0]._extract_mock_name() + krux.buttons.GPIO.call_args_list[0].args[0]._extract_mock_name() == "mock.GPIOHS21" ) assert ( @@ -225,19 +230,21 @@ def test_wait_for_release(mocker, m5stickv): input = Input() input = reset_input_states(mocker, input) + mocker.patch.object(input, "enter_event", new=lambda: True) mocker.patch.object(input, "enter_value", new=lambda: PRESSED) def release(): - mocker.patch.object(time, "ticks_ms", new=lambda: 0) + mocker.patch.object(time, "ticks_ms", new=lambda: 1000) + time.sleep(0.1) + mocker.patch.object(time, "ticks_ms", new=lambda: 2000) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 100) mocker.patch.object(input, "enter_value", new=lambda: RELEASED) assert input.entropy == 0 t = threading.Thread(target=release) t.start() - input.wait_for_release() + input.wait_for_button() t.join() assert input.entropy > 0 @@ -252,13 +259,14 @@ def test_wait_for_button_blocks_until_enter_released(mocker, m5stickv): input = Input() input = reset_input_states(mocker, input) + mocker.patch.object(input, "enter_event", new=lambda: True) + mocker.patch.object(input, "enter_value", new=lambda: PRESSED) + def release(): - mocker.patch.object(time, "ticks_ms", new=lambda: 0) + mocker.patch.object(time, "ticks_ms", new=lambda: 1000) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 100) - mocker.patch.object(input, "enter_value", new=lambda: PRESSED) + mocker.patch.object(time, "ticks_ms", new=lambda: 1100) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 200) mocker.patch.object(input, "enter_value", new=lambda: RELEASED) assert input.entropy == 0 @@ -281,13 +289,14 @@ def test_wait_for_button_blocks_until_page_released(mocker, m5stickv): input = Input() input = reset_input_states(mocker, input) + mocker.patch.object(input, "page_event", new=lambda: True) + mocker.patch.object(input, "page_value", new=lambda: PRESSED) + def release(): - mocker.patch.object(time, "ticks_ms", new=lambda: 0) + mocker.patch.object(time, "ticks_ms", new=lambda: 1000) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 100) - mocker.patch.object(input, "page_value", new=lambda: PRESSED) + mocker.patch.object(time, "ticks_ms", new=lambda: 1100) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 200) mocker.patch.object(input, "page_value", new=lambda: RELEASED) assert input.entropy == 0 @@ -302,7 +311,7 @@ def release(): krux.input.wdt.feed.assert_called() -def test_wait_for_button_blocks_until_page_prev_released(mocker, amigo_tft): +def test_wait_for_button_blocks_until_page_prev_released(mocker, m5stickv): import threading import krux from krux.input import Input, RELEASED, PRESSED, BUTTON_PAGE_PREV @@ -310,27 +319,19 @@ def test_wait_for_button_blocks_until_page_prev_released(mocker, amigo_tft): input = Input() input = reset_input_states(mocker, input) - def click(): - mocker.patch.object(time, "ticks_ms", new=lambda: 0) + mocker.patch.object(input, "page_prev_event", new=lambda: True) + mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED) + + def release(): + mocker.patch.object(time, "ticks_ms", new=lambda: 1000) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 100) - # if input.page_prev, "value" is mocked, in what apparently is bug, - # other input buttons get the same "value", invalidating the test - mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED) + mocker.patch.object(time, "ticks_ms", new=lambda: 1100) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 200) mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED) assert input.entropy == 0 - # first one click to enable physical buttons - t = threading.Thread(target=click) - t.start() - btn = input.wait_for_button(True) - t.join() - - # than other click to be counted - t = threading.Thread(target=click) + t = threading.Thread(target=release) t.start() btn = input.wait_for_button(True) t.join() @@ -343,51 +344,20 @@ def click(): def test_wait_for_button_blocks_until_touch_released(mocker, amigo_tft): import threading import krux - from krux.input import Input, BUTTON_TOUCH + from krux.input import Input, BUTTON_TOUCH, PRESSED, RELEASED input = Input() input = reset_input_states(mocker, input) - def click(): - mocker.patch.object(time, "ticks_ms", new=lambda: 0) - time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 100) - mocker.patch.object(input.touch, "current_state", new=lambda: 1) - time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 200) - mocker.patch.object(input.touch, "current_state", new=lambda: 0) - - assert input.entropy == 0 - - t = threading.Thread(target=click) - t.start() - btn = input.wait_for_button(True) - t.join() - - assert btn == BUTTON_TOUCH - assert input.entropy > 0 - krux.input.wdt.feed.assert_called() - - -def test_wait_for_button_waits_for_existing_press_to_release(mocker, m5stickv): - import threading - import krux - from krux.input import Input, RELEASED, PRESSED, BUTTON_ENTER - - input = Input() - input = reset_input_states(mocker, input) + mocker.patch.object(input.touch, "event", new=lambda: True) + mocker.patch.object(input, "touch_value", new=lambda: PRESSED) def release(): - mocker.patch.object(time, "ticks_ms", new=lambda: 0) - time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 100) - mocker.patch.object(input, "page_value", new=lambda: RELEASED) + mocker.patch.object(time, "ticks_ms", new=lambda: 1000) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 200) - mocker.patch.object(input, "enter_value", new=lambda: PRESSED) + mocker.patch.object(time, "ticks_ms", new=lambda: 1100) time.sleep(0.1) - mocker.patch.object(time, "ticks_ms", new=lambda: 300) - mocker.patch.object(input, "enter_value", new=lambda: RELEASED) + mocker.patch.object(input, "touch_value", new=lambda: RELEASED) assert input.entropy == 0 @@ -396,7 +366,7 @@ def release(): btn = input.wait_for_button(True) t.join() - assert btn == BUTTON_ENTER + assert btn == BUTTON_TOUCH assert input.entropy > 0 krux.input.wdt.feed.assert_called() @@ -435,8 +405,11 @@ def release(): mocker.patch.object(time, "ticks_ms", new=lambda: 0) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 100) + mocker.patch.object(input, "page_event", new=lambda: True) mocker.patch.object(input, "page_value", new=lambda: PRESSED) time.sleep(0.1) + mocker.patch.object(input, "page_event", new=lambda: False) + time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: LONG_PRESS_PERIOD + 500) mocker.patch.object(input, "page_value", new=lambda: RELEASED) @@ -464,8 +437,11 @@ def release(): mocker.patch.object(time, "ticks_ms", new=lambda: 0) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 100) + mocker.patch.object(input, "page_prev_event", new=lambda: True) mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED) time.sleep(0.1) + mocker.patch.object(input, "page_prev_event", new=lambda: False) + time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: LONG_PRESS_PERIOD + 500) mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED) @@ -493,28 +469,32 @@ def test_touch_indexing(mocker, amigo_tft): def time_control(point1, point2): nonlocal elapsed_time - mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) time.sleep(0.1) - elapsed_time += 500 + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) # touch on 3ΒΊ quadrant + mocker.patch.object(input.touch, "event", new=lambda: True) + mocker.patch.object(input.touch.touch_driver, "irq_point", new=lambda: point1) mocker.patch.object( input.touch.touch_driver, "current_point", new=lambda: point1 ) time.sleep(0.1) - elapsed_time += 500 + mocker.patch.object(input.touch, "event", new=lambda: False) + time.sleep(0.1) + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) # touch slightly sideways before release mocker.patch.object( input.touch.touch_driver, "current_point", new=lambda: point2 ) time.sleep(0.1) - elapsed_time += 500 + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) + # release touch mocker.patch.object(input.touch.touch_driver, "current_point", new=lambda: None) time.sleep(0.1) - elapsed_time += 500 + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) # full screen as single touch button @@ -557,27 +537,31 @@ def test_touch_gestures(mocker, amigo_tft): def time_control(point1, point2): nonlocal elapsed_time + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) - time.sleep(0.1) - elapsed_time += 500 - mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) - # touch on 3ΒΊ quadrant + mocker.patch.object(input.touch, "event", new=lambda: True) + mocker.patch.object(input.touch.touch_driver, "irq_point", new=lambda: point1) mocker.patch.object( input.touch.touch_driver, "current_point", new=lambda: point1 ) time.sleep(0.1) - elapsed_time += 500 + # Detect press event + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) - # swipe + time.sleep(0.1) + mocker.patch.object(input.touch.touch_driver, "event", new=lambda: False) + time.sleep(0.1) + # Swipe mocker.patch.object( input.touch.touch_driver, "current_point", new=lambda: point2 ) time.sleep(0.1) - elapsed_time += 500 + # Release + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) mocker.patch.object(input.touch.touch_driver, "current_point", new=lambda: None) time.sleep(0.1) - elapsed_time += 500 + elapsed_time += 200 mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time) # Swipe Right @@ -723,9 +707,12 @@ def release(): mocker.patch.object(time, "ticks_ms", new=lambda: 0) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 100) + mocker.patch.object(input, "enter_event", new=lambda: True) mocker.patch.object(input, "enter_value", new=lambda: PRESSED) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 200) + mocker.patch.object(input, "enter_event", new=lambda: False) + time.sleep(0.1) mocker.patch.object(input, "enter_value", new=lambda: RELEASED) assert input.entropy == 0 @@ -754,9 +741,11 @@ def release(): mocker.patch.object(time, "ticks_ms", new=lambda: 0) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 100) + mocker.patch.object(input, "page_event", new=lambda: True) mocker.patch.object(input, "page_value", new=lambda: PRESSED) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 200) + mocker.patch.object(input, "page_event", new=lambda: False) mocker.patch.object(input, "page_value", new=lambda: RELEASED) assert input.entropy == 0 @@ -785,9 +774,11 @@ def release(): mocker.patch.object(time, "ticks_ms", new=lambda: 0) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 100) + mocker.patch.object(input, "page_prev_event", new=lambda: True) mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED) time.sleep(0.1) mocker.patch.object(time, "ticks_ms", new=lambda: 200) + mocker.patch.object(input, "page_prev_event", new=lambda: False) mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED) assert input.entropy == 0 @@ -801,3 +792,31 @@ def release(): assert btn is None assert input.entropy > 0 krux.input.wdt.feed.assert_called() + + +def test_wait_for_press_screensaver(mocker, m5stickv): + from krux.input import Input + from krux.krux_settings import Settings + import krux + + input = Input(screensaver_fallback=mocker.MagicMock()) + input = reset_input_states(mocker, input) + input.buttons_active = False + + # Make test faster + Settings().appearance.screensaver_time = 0.0001 + + time_seq = [] + tmp = 100 + for _ in range(10): + time_seq.append(tmp) + tmp += 100 + + time.ticks_ms = mocker.MagicMock(side_effect=time_seq) + + input.wait_for_press(True, enable_screensaver=True) + input.screensaver_fallback.assert_called() + + Settings().appearance.screensaver_time = 0 + input.wait_for_button(block=False) + input.screensaver_fallback.assert_called_once() diff --git a/tests/test_qr.py b/tests/test_qr.py index 212aa4961..6993efc83 100644 --- a/tests/test_qr.py +++ b/tests/test_qr.py @@ -79,7 +79,10 @@ def test_parser(mocker, m5stickv, tdata): (FORMAT_UR, tdata.TEST_PARTS_FORMAT_SINGLEPART_UR), (FORMAT_UR, tdata.TEST_PARTS_FORMAT_MULTIPART_UR), ] + num = 0 for case in cases: + print("case: ", num) + num += 1 fmt = case[0] parts = case[1] @@ -89,7 +92,10 @@ def test_parser(mocker, m5stickv, tdata): assert parser.format == fmt - assert parser.total_count() == len(parts) + if num == 4: + assert parser.total_count() == len(parts) * 2 + else: + assert parser.total_count() == len(parts) if parser.format == FORMAT_UR: assert parser.parsed_count() > 0 else: @@ -103,7 +109,11 @@ def test_parser(mocker, m5stickv, tdata): # Re-parse the first part to test that redundant parts are ignored parser.parse(parts[0]) - assert parser.total_count() == len(parts) + if num == 4: + assert parser.total_count() == len(parts) * 2 + else: + assert parser.total_count() == len(parts) + if parser.format == FORMAT_UR: assert parser.parsed_count() > 0 else: @@ -120,19 +130,31 @@ def test_parser(mocker, m5stickv, tdata): def test_to_qr_codes(mocker, m5stickv, tdata): from krux.qr import to_qr_codes, FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR + from krux.display import Display cases = [ - (FORMAT_NONE, tdata.TEST_DATA_B58, 1), - (FORMAT_PMOFN, tdata.TEST_DATA_B58, 3), - (FORMAT_UR, tdata.TEST_DATA_UR, 3), + # Test 135 pixels wide display + (FORMAT_NONE, tdata.TEST_DATA_B58, 135, 1), + (FORMAT_PMOFN, tdata.TEST_DATA_B58, 135, 9), + (FORMAT_UR, tdata.TEST_DATA_UR, 135, 26), + # Test 320 pixels wide display + (FORMAT_NONE, tdata.TEST_DATA_B58, 320, 1), + (FORMAT_PMOFN, tdata.TEST_DATA_B58, 320, 3), + (FORMAT_UR, tdata.TEST_DATA_UR, 320, 6), ] for case in cases: + mocker.patch( + "krux.display.lcd", + new=mocker.MagicMock(width=mocker.MagicMock(return_value=case[2])), + ) + display = Display() + qr_data_width = display.qr_data_width() fmt = case[0] data = case[1] - expected_parts = case[2] + expected_parts = case[3] codes = [] - code_generator = to_qr_codes(data, 135, fmt) + code_generator = to_qr_codes(data, qr_data_width, fmt) i = 0 while True: try: @@ -141,7 +163,8 @@ def test_to_qr_codes(mocker, m5stickv, tdata): assert total == expected_parts if i == total - 1: break - except: + except Exception as e: + print("Error:", e) break i += 1 assert len(codes) == expected_parts @@ -155,3 +178,13 @@ def test_detect_plaintext_qr(mocker, m5stickv): ) detect_format(PLAINTEXT_QR_DATA) + + +def test_find_min_num_parts(m5stickv): + from krux.qr import find_min_num_parts + + with pytest.raises(ValueError) as raised_ex: + find_min_num_parts("", 10, "format unknown") + + assert raised_ex.type is ValueError + assert raised_ex.value.args[0] == "Invalid format type" diff --git a/tests/test_settings.py b/tests/test_settings.py index 46f1d28fd..99d8aac6f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -18,14 +18,6 @@ def test_init(mocker, m5stickv): assert isinstance(s, Settings) -# @pytest.fixture -# def mocker_sd_card(mocker): -# mocker.patch( -# "os.listdir", -# new=mocker.MagicMock(return_value=["somefile", "otherfile"]), -# ) - - def test_store_init(mocker, m5stickv): from krux.settings import Store, SETTINGS_FILENAME, SD_PATH @@ -43,9 +35,7 @@ def test_store_init(mocker, m5stickv): assert s.settings == case[1] -def test_store_get(mocker, m5stickv): - mo = mocker.mock_open() - mocker.patch("builtins.open", mo) +def test_store_get(): from krux.settings import Store s = Store() @@ -55,16 +45,20 @@ def test_store_get(mocker, m5stickv): ("ns1.ns2", "setting", "call1_defaultvalue2", "call2_defaultvalue2"), ("ns1.ns2.ns3", "setting", "call1_defaultvalue3", "call2_defaultvalue3"), ] - for case in cases: + for i, case in enumerate(cases): # First call, setting does not exist, so default value becomes the value assert s.get(case[0], case[1], case[2]) == case[2] + + # Getter does not populate settings dict. if nothing is set then settings is empty + if i == 0: + assert s.settings == {} + # Second call, setting does exist, so default value is ignored + s.set(case[0], case[1], case[2]) assert s.get(case[0], case[1], case[3]) == case[2] -def test_store_set(mocker, m5stickv): - mo = mocker.mock_open() - mocker.patch("builtins.open", mo) +def test_store_set(): from krux.settings import Store s = Store() @@ -74,6 +68,9 @@ def test_store_set(mocker, m5stickv): ("ns1.ns2", "setting", "call1_value2", 2, "call3_value2"), ("ns1.ns2.ns3", "setting", "call1_value3", 3, "call3_value3"), ] + + assert s.dirty == False + for case in cases: s.set(case[0], case[1], case[2]) assert s.get(case[0], case[1], "default") == case[2] @@ -84,6 +81,101 @@ def test_store_set(mocker, m5stickv): s.set(case[0], case[1], case[4]) assert s.get(case[0], case[1], "default") == case[4] + assert s.dirty == True + + +def test_store_delete(): + from krux.settings import Store + + s = Store() + + cases = [ + ("ns1", "setting1", "value1"), + ("ns1.ns2", "setting2", "value2"), + ] + + assert s.dirty == False + + for case in cases: + s.set(case[0], case[1], case[2]) + + for i, case in enumerate(cases): + s.delete(case[0], case[1]) + + # value of deleted setting is its default + assert s.get(case[0], case[1], "default") == "default" + + # when deleting a setting, its empty namespaces are deleted too + if i == len(cases) - 1: + assert s.settings == {} + + assert s.dirty == True + + +def test_store_update_file_location(mocker): + ms, mr = mocker.Mock(), mocker.Mock() + mocker.patch("os.stat", ms) + mocker.patch("os.remove", mr) + from krux.settings import Store, SD_PATH, FLASH_PATH, SETTINGS_FILENAME + + s = Store() + + # default is /flash/ + assert s.file_location == "/" + FLASH_PATH + "/" + + # from flash to sd removes /flash/settings.json + s.update_file_location(SD_PATH) + assert s.file_location == "/" + SD_PATH + "/" + ms.assert_called_once_with("/" + FLASH_PATH + "/" + SETTINGS_FILENAME) + mr.assert_called_once_with("/" + FLASH_PATH + "/" + SETTINGS_FILENAME) + ms.reset_mock() + mr.reset_mock() + + # from sd to flash removes /sd/settings.json + s.update_file_location(FLASH_PATH) + assert s.file_location == "/" + FLASH_PATH + "/" + ms.assert_called_once_with("/" + SD_PATH + "/" + SETTINGS_FILENAME) + mr.assert_called_once_with("/" + SD_PATH + "/" + SETTINGS_FILENAME) + ms.reset_mock() + mr.reset_mock() + + # updating with existing file_location does nothing + s.update_file_location(FLASH_PATH) + assert s.file_location == "/" + FLASH_PATH + "/" + ms.assert_not_called() + mr.assert_not_called() + + +def test_store_save_settings(mocker): + mo = mocker.mock_open() + mocker.patch("builtins.open", mo) + from krux.settings import Store, SETTINGS_FILENAME + + s = Store() + filename = s.file_location + SETTINGS_FILENAME + + # new setting change: is dirty, save_settings() persists, file written + s.set("name.space", "setting", "custom_value") + assert s.dirty == True + assert s.save_settings() == True + mo.assert_called_with(filename, "w") + + # no setting change: not dirty, save_settings() doesn't persist, file not even read + mo.reset_mock() + assert s.dirty == False + assert s.save_settings() == False + mo.assert_not_called() + + # settings changed to original: dirty but save_settings() doesn't persist, file read -- not written + mo = mocker.mock_open(read_data='{"name": {"space": {"setting": "custom_value"}}}') + mocker.patch("builtins.open", mo) + s.set("name.space", "setting", "new_custom_value") + s.set("name.space", "setting", "custom_value") + assert s.dirty == True + assert s.save_settings() == False + mo.assert_called_once() + assert s.dirty == False + def test_setting(mocker, m5stickv): mo = mocker.mock_open() @@ -101,103 +193,48 @@ class TestClass: assert t.some_setting == 2 -def test_encryption_pbkdf2_setting(m5stickv, mocker): - from krux.pages.login import Login - from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV - from krux.krux_settings import Settings, NumberSetting - - cases = [ - ( # 0 - ( - BUTTON_PAGE, - # Encryption - BUTTON_ENTER, - # pbkdf2 - BUTTON_ENTER, - # go to "<" keypad position - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - # delete last 0 - BUTTON_ENTER, - # go to "1" keypad - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - # Enter "1" - BUTTON_ENTER, - # go to "Go" keypad position - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - # Go - BUTTON_ENTER, - # Leave Encryption settings - BUTTON_PAGE_PREV, - BUTTON_ENTER, - # Leave Settings - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - BUTTON_ENTER, - ), - # Assert iterations didn't change to 100,001 (not multiple of 10,000) - lambda: Settings().encryption.pbkdf2_iterations == 100000, - NumberSetting, - ), - ( # 0 - ( - BUTTON_PAGE, - # Encryption - BUTTON_ENTER, - # pbkdf2 - BUTTON_ENTER, - # go to "<" keypad position - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - # delete last 5 zeros - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - # go to "1" keypad - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - BUTTON_PAGE, - # Enter "1" - BUTTON_ENTER, - # Go to Zero - BUTTON_PAGE_PREV, - # Enter 4 zeros - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - BUTTON_ENTER, - # go to "Go" keypad position - BUTTON_PAGE_PREV, - # Go - BUTTON_ENTER, - # Leave Encryption settings - BUTTON_PAGE_PREV, - BUTTON_ENTER, - # Leave Settings - BUTTON_PAGE_PREV, - BUTTON_PAGE_PREV, - BUTTON_ENTER, - ), - # Assert iterations changed to a multiple of 10,000 - lambda: Settings().encryption.pbkdf2_iterations == 110000, - NumberSetting, - ), - ] - for case in cases: - ctx = create_ctx(mocker, case[0]) - login = Login(ctx) - - login.settings() - - assert ctx.input.wait_for_button.call_count == len(case[0]) +def test_all_labels(mocker, m5stickv): + from krux.krux_settings import ( + BitcoinSettings, + I18nSettings, + LoggingSettings, + EncryptionSettings, + PrinterSettings, + ThermalSettings, + AdafruitPrinterSettings, + CNCSettings, + GRBLSettings, + PersistSettings, + ThemeSettings, + TouchSettings, + EncoderSettings, + ) - assert case[1]() + bitcoin = BitcoinSettings() + i18n = I18nSettings() + logging = LoggingSettings() + encryption = EncryptionSettings() + printer = PrinterSettings() + thermal = ThermalSettings() + adafruit = AdafruitPrinterSettings() + cnc = CNCSettings() + gbrl = GRBLSettings() + persist = PersistSettings() + appearance = ThemeSettings() + touch = TouchSettings() + encoder = EncoderSettings() + + assert bitcoin.label("network") + assert i18n.label("locale") + assert logging.label("level") + assert encryption.label("version") + assert printer.label("thermal") + assert thermal.label("adafruit") + assert adafruit.label("tx_pin") + assert cnc.label("invert") + assert gbrl.label("tx_pin") + assert persist.label("location") + assert appearance.label("theme") + assert appearance.label("screensaver_time") + assert touch.label("threshold") + assert encoder.label("debounce") diff --git a/tests/test_wallet.py b/tests/test_wallet.py index a97d3e589..2cd42d059 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -118,16 +118,16 @@ def tdata(mocker): ), ) - UNAMBIGUOUS_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/{0,1}/*)" + UNAMBIGUOUS_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/<0;1>/*)" AMBIGUOUS_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA)" - UNAMBIGUOUS_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/{0,1}/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/{0,1}/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/{0,1}/*))" + UNAMBIGUOUS_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))" AMBIGUOUS_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv))" UNRELATED_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB)" UNRELATED_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv))" - UNSORTED_MULTISIG_DESCRIPTOR = "wsh(multi(2,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/{0,1}/*,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/{0,1}/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/{0,1}/*))" + UNSORTED_MULTISIG_DESCRIPTOR = "wsh(multi(2,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))" return namedtuple( "TestData", @@ -346,7 +346,7 @@ def test_load_multisig(mocker, m5stickv, tdata): ], }, ), - # TODO: Fix + # TODO: Fix - ValueError: xpub not a cosigner # (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR, tdata.UR_OUTPUT_MULTISIG_DESCRIPTOR, '2 of 3', { # 'type': 'p2wsh', 'm': 2, 'n': 3, 'cosigners': [ # tdata.MULTISIG_KEY1.xpub(), @@ -436,8 +436,7 @@ def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata): cases = [ (tdata.SPECTER_MULTISIG_WALLET_DATA, FORMAT_PMOFN), (tdata.BLUEWALLET_MULTISIG_WALLET_DATA, FORMAT_NONE), - # TODO: Fix - # (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR), + (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR), (tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR), ] for case in cases: @@ -537,7 +536,10 @@ def test_parse_wallet(mocker, m5stickv, tdata): ), ] + case_n = 1 for case in cases: + print(case_n) + case_n += 1 descriptor, label = parse_wallet(case[0], NETWORKS["main"]) assert descriptor.to_string() == case[1] assert label == case[2] diff --git a/tests/touchscreens/test_ft6x36.py b/tests/touchscreens/test_ft6x36.py index 45df99fde..f2895458f 100644 --- a/tests/touchscreens/test_ft6x36.py +++ b/tests/touchscreens/test_ft6x36.py @@ -13,7 +13,7 @@ def test_point_read(mocker, m5stickv): # four bytes reg_point = [0x00, 0x09, 0x00, 0x09] point = (0x09, 0x09) - touch.i2c.readfrom_mem = mocker.MagicMock(side_effect=([0x01], reg_point)) + mocker.patch("krux.i2c.i2c_bus.readfrom_mem",side_effect=([0x01], reg_point)) data = touch.current_point() assert data == point @@ -24,7 +24,7 @@ def test_gesture_discarded(mocker, m5stickv): touch = FT6X36() # four bytes reg_point = [0x00, 0x09, 0x00, 0x09] - touch.i2c.readfrom_mem = mocker.MagicMock(side_effect=([0x02], reg_point)) + mocker.patch("krux.i2c.i2c_bus.readfrom_mem",side_effect=([0x02], reg_point)) data = touch.current_point() assert data is None @@ -35,6 +35,6 @@ def test_point_read_error(mocker, m5stickv): touch = FT6X36() # four bytes e = Exception("mocked error") - touch.i2c.readfrom_mem = mocker.MagicMock(side_effect=([0x01], e)) + mocker.patch("krux.i2c.i2c_bus.readfrom_mem",side_effect=([0x01], e)) data = touch.current_point() assert data == e diff --git a/vendor/embit b/vendor/embit index be13c6c97..605ca51e9 160000 --- a/vendor/embit +++ b/vendor/embit @@ -1 +1 @@ -Subproject commit be13c6c9789055f39f515389ab3fbf2e1c6beaf3 +Subproject commit 605ca51e96d28b1deed96f790a6ef67c15262adc diff --git a/vendor/urtypes b/vendor/urtypes index 8ff8e6ebe..9b84f87c2 160000 --- a/vendor/urtypes +++ b/vendor/urtypes @@ -1 +1 @@ -Subproject commit 8ff8e6ebe484d7a0f98ad73f4441708704998b43 +Subproject commit 9b84f87c2b127baea0d665ddcbe4f677ee4f45be