From f0dd1e00769f78d49344a85e31a168f7d5425d84 Mon Sep 17 00:00:00 2001 From: GlassOnTin <63980135+GlassOnTin@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:14:29 +0100 Subject: [PATCH 1/4] Finished poetry packaging. Added webserver_logger example. --- install.sh | 4 + poetry.lock | 555 ++++++++++++----------- pyproject.toml | 19 +- radiacode_examples/README.md | 31 ++ radiacode_examples/README_ru.md | 25 + radiacode_examples/__init__.py | 22 + radiacode_examples/basic.py | 67 +++ radiacode_examples/narodmon.py | 88 ++++ radiacode_examples/radiacode_exporter.py | 46 ++ radiacode_examples/show_spectrum.py | 380 ++++++++++++++++ radiacode_examples/webserver.html | 142 ++++++ radiacode_examples/webserver.py | 108 +++++ radiacode_examples/webserver_logger.py | 123 +++++ 13 files changed, 1342 insertions(+), 268 deletions(-) create mode 100755 install.sh create mode 100644 radiacode_examples/README.md create mode 100644 radiacode_examples/README_ru.md create mode 100644 radiacode_examples/__init__.py create mode 100644 radiacode_examples/basic.py create mode 100644 radiacode_examples/narodmon.py create mode 100644 radiacode_examples/radiacode_exporter.py create mode 100755 radiacode_examples/show_spectrum.py create mode 100644 radiacode_examples/webserver.html create mode 100644 radiacode_examples/webserver.py create mode 100644 radiacode_examples/webserver_logger.py diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..9b99015 --- /dev/null +++ b/install.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash +version=0.3.4 +poetry build +sudo pip install --force-reinstall dist/radiacode-${version}-py3-none-any.whl diff --git a/poetry.lock b/poetry.lock index 7d6c2ad..f20c020 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,91 +1,103 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.3.4" +description = "Happy Eyeballs for asyncio" +optional = true +python-versions = "<4.0,>=3.8" +files = [ + {file = "aiohappyeyeballs-2.3.4-py3-none-any.whl", hash = "sha256:40a16ceffcf1fc9e142fd488123b2e218abc4188cf12ac20c67200e1579baa42"}, + {file = "aiohappyeyeballs-2.3.4.tar.gz", hash = "sha256:7e1ae8399c320a8adec76f6c919ed5ceae6edd4c3672f4d9eae2b27e37c80ff6"}, +] [[package]] name = "aiohttp" -version = "3.9.5" +version = "3.10.0" description = "Async http client/server framework (asyncio)" optional = true python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, - {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, - {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, - {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, - {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, - {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, - {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, - {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, - {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, - {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, - {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, - {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, - {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, - {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, + {file = "aiohttp-3.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:68ab608118e212f56feef44d4785aa90b713042da301f26338f36497b481cd79"}, + {file = "aiohttp-3.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:64a117c16273ca9f18670f33fc7fd9604b9f46ddb453ce948262889a6be72868"}, + {file = "aiohttp-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54076a25f32305e585a3abae1f0ad10646bec539e0e5ebcc62b54ee4982ec29f"}, + {file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71c76685773444d90ae83874433505ed800e1706c391fdf9e57cc7857611e2f4"}, + {file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdda86ab376f9b3095a1079a16fbe44acb9ddde349634f1c9909d13631ff3bcf"}, + {file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6dcd1d21da5ae1416f69aa03e883a51e84b6c803b8618cbab341ac89a85b9e"}, + {file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06ef0135d7ab7fb0284342fbbf8e8ddf73b7fee8ecc55f5c3a3d0a6b765e6d8b"}, + {file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccab9381f38c669bb9254d848f3b41a3284193b3e274a34687822f98412097e9"}, + {file = "aiohttp-3.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:947da3aee057010bc750b7b4bb65cbd01b0bdb7c4e1cf278489a1d4a1e9596b3"}, + {file = "aiohttp-3.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5268b35fee7eb754fb5b3d0f16a84a2e9ed21306f5377f3818596214ad2d7714"}, + {file = "aiohttp-3.10.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff25d988fd6ce433b5c393094a5ca50df568bdccf90a8b340900e24e0d5fb45c"}, + {file = "aiohttp-3.10.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:594b4b4f1dfe8378b4a0342576dc87a930c960641159f5ae83843834016dbd59"}, + {file = "aiohttp-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8820dad615cd2f296ed3fdea8402b12663ac9e5ea2aafc90ef5141eb10b50b8"}, + {file = "aiohttp-3.10.0-cp310-cp310-win32.whl", hash = "sha256:ab1d870403817c9a0486ca56ccbc0ebaf85d992277d48777faa5a95e40e5bcca"}, + {file = "aiohttp-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:563705a94ea3af43467167f3a21c665f3b847b2a0ae5544fa9e18df686a660da"}, + {file = "aiohttp-3.10.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13679e11937d3f37600860de1f848e2e062e2b396d3aa79b38c89f9c8ab7e791"}, + {file = "aiohttp-3.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c66a1aadafbc0bd7d648cb7fcb3860ec9beb1b436ce3357036a4d9284fcef9a"}, + {file = "aiohttp-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7e3545b06aae925f90f06402e05cfb9c62c6409ce57041932163b09c48daad6"}, + {file = "aiohttp-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:effafe5144aa32f0388e8f99b1b2692cf094ea2f6b7ceca384b54338b77b1f50"}, + {file = "aiohttp-3.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a04f2c8d41821a2507b49b2694c40495a295b013afb0cc7355b337980b47c546"}, + {file = "aiohttp-3.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6dbfac556219d884d50edc6e1952a93545c2786193f00f5521ec0d9d464040ab"}, + {file = "aiohttp-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a65472256c5232681968deeea3cd5453aa091c44e8db09f22f1a1491d422c2d9"}, + {file = "aiohttp-3.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941366a554e566efdd3f042e17a9e461a36202469e5fd2aee66fe3efe6412aef"}, + {file = "aiohttp-3.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:927b4aca6340301e7d8bb05278d0b6585b8633ea852b7022d604a5df920486bf"}, + {file = "aiohttp-3.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:34adb8412e736a5d0df6d1fccdf71599dfb07a63add241a94a189b6364e997f1"}, + {file = "aiohttp-3.10.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:43c60d9b332a01ee985f080f639f3e56abcfb95ec1320013c94083c3b6a2e143"}, + {file = "aiohttp-3.10.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3f49edf7c5cd2987634116e1b6a0ee2438fca17f7c4ee480ff41decb76cf6158"}, + {file = "aiohttp-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9784246431eaf9d651b3cc06f9c64f9a9f57299f4971c5ea778fa0b81074ef13"}, + {file = "aiohttp-3.10.0-cp311-cp311-win32.whl", hash = "sha256:bec91402df78b897a47b66b9c071f48051cea68d853d8bc1d4404896c6de41ae"}, + {file = "aiohttp-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:25a9924343bf91b0c5082cae32cfc5a1f8787ac0433966319ec07b0ed4570722"}, + {file = "aiohttp-3.10.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:21dab4a704c68dc7bc2a1219a4027158e8968e2079f1444eda2ba88bc9f2895f"}, + {file = "aiohttp-3.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:872c0dcaccebd5733d535868fe2356aa6939f5827dcea7a8b9355bb2eff6f56e"}, + {file = "aiohttp-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f381424dbce313bb5a666a215e7a9dcebbc533e9a2c467a1f0c95279d24d1fa7"}, + {file = "aiohttp-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ca48e9f092a417c6669ee8d3a19d40b3c66dde1a2ae0d57e66c34812819b671"}, + {file = "aiohttp-3.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbe2f6d0466f5c59c7258e0745c20d74806a1385fbb7963e5bbe2309a11cc69b"}, + {file = "aiohttp-3.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:03799a95402a7ed62671c4465e1eae51d749d5439dbc49edb6eee52ea165c50b"}, + {file = "aiohttp-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5549c71c35b5f057a4eebcc538c41299826f7813f28880722b60e41c861a57ec"}, + {file = "aiohttp-3.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6fa7a42b78d8698491dc4ad388169de54cca551aa9900f750547372de396277"}, + {file = "aiohttp-3.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77bbf0a2f6fefac6c0db1792c234f577d80299a33ce7125467439097cf869198"}, + {file = "aiohttp-3.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:34eaf5cfcc979846d73571b1a4be22cad5e029d55cdbe77cdc7545caa4dcb925"}, + {file = "aiohttp-3.10.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4f1de31a585344a106db43a9c3af2e15bb82e053618ff759f1fdd31d82da38eb"}, + {file = "aiohttp-3.10.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3a1ea61d96146e9b9e5597069466e2e4d9e01e09381c5dd51659f890d5e29e7"}, + {file = "aiohttp-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:73c01201219eb039a828bb58dcc13112eec2fed6eea718356316cd552df26e04"}, + {file = "aiohttp-3.10.0-cp312-cp312-win32.whl", hash = "sha256:33e915971eee6d2056d15470a1214e4e0f72b6aad10225548a7ab4c4f54e2db7"}, + {file = "aiohttp-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2dc75da06c35a7b47a88ceadbf993a53d77d66423c2a78de8c6f9fb41ec35687"}, + {file = "aiohttp-3.10.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f1bc4d68b83966012813598fe39b35b4e6019b69d29385cf7ec1cb08e1ff829b"}, + {file = "aiohttp-3.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9b8b31c057a0b7bb822a159c490af05cb11b8069097f3236746a78315998afa"}, + {file = "aiohttp-3.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10f0d7894ddc6ff8f369e3fdc082ef1f940dc1f5b9003cd40945d24845477220"}, + {file = "aiohttp-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72de8ffba4a27e3c6e83e58a379fc4fe5548f69f9b541fde895afb9be8c31658"}, + {file = "aiohttp-3.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd36d0f0afc2bd84f007cedd2d9a449c3cf04af471853a25eb71f28bc2e1a119"}, + {file = "aiohttp-3.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f64d503c661864866c09806ac360b95457f872d639ca61719115a9f389b2ec90"}, + {file = "aiohttp-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31616121369bc823791056c632f544c6c8f8d1ceecffd8bf3f72ef621eaabf49"}, + {file = "aiohttp-3.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f76c12abb88b7ee64b3f9ae72f0644af49ff139067b5add142836dab405d60d4"}, + {file = "aiohttp-3.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6c99eef30a7e98144bcf44d615bc0f445b3a3730495fcc16124cb61117e1f81e"}, + {file = "aiohttp-3.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:39e7ec718e7a1971a5d98357e3e8c0529477d45c711d32cd91999dc8d8404e1e"}, + {file = "aiohttp-3.10.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1cef548ee4e84264b78879de0c754bbe223193c6313beb242ce862f82eab184"}, + {file = "aiohttp-3.10.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f98f036eab11d2f90cdd01b9d1410de9d7eb520d070debeb2edadf158b758431"}, + {file = "aiohttp-3.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc4376ff537f7d2c1e98f97f6d548e99e5d96078b0333c1d3177c11467b972de"}, + {file = "aiohttp-3.10.0-cp38-cp38-win32.whl", hash = "sha256:ebedc51ee6d39f9ea5e26e255fd56a7f4e79a56e77d960f9bae75ef4f95ed57f"}, + {file = "aiohttp-3.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:aad87626f31a85fd4af02ba7fd6cc424b39d4bff5c8677e612882649da572e47"}, + {file = "aiohttp-3.10.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1dc95c5e2a5e60095f1bb51822e3b504e6a7430c9b44bff2120c29bb876c5202"}, + {file = "aiohttp-3.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c83977f7b6f4f4a96fab500f5a76d355f19f42675224a3002d375b3fb309174"}, + {file = "aiohttp-3.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8cedc48d36652dd3ac40e5c7c139d528202393e341a5e3475acedb5e8d5c4c75"}, + {file = "aiohttp-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b099fbb823efed3c1d736f343ac60d66531b13680ee9b2669e368280f41c2b8"}, + {file = "aiohttp-3.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d583755ddb9c97a2da1322f17fc7d26792f4e035f472d675e2761c766f94c2ff"}, + {file = "aiohttp-3.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a03a4407bdb9ae815f0d5a19df482b17df530cf7bf9c78771aa1c713c37ff1f"}, + {file = "aiohttp-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb6e65f6ea7caa0188e36bebe9e72b259d3d525634758c91209afb5a6cbcba7"}, + {file = "aiohttp-3.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6612c6ed3147a4a2d6463454b94b877566b38215665be4c729cd8b7bdce15b4"}, + {file = "aiohttp-3.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b0c0148d2a69b82ffe650c2ce235b431d49a90bde7dd2629bcb40314957acf6"}, + {file = "aiohttp-3.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0d85a173b4dbbaaad1900e197181ea0fafa617ca6656663f629a8a372fdc7d06"}, + {file = "aiohttp-3.10.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:12c43dace645023583f3dd2337dfc3aa92c99fb943b64dcf2bc15c7aa0fb4a95"}, + {file = "aiohttp-3.10.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:33acb0d9bf12cdc80ceec6f5fda83ea7990ce0321c54234d629529ca2c54e33d"}, + {file = "aiohttp-3.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:91e0b76502205484a4d1d6f25f461fa60fe81a7987b90e57f7b941b0753c3ec8"}, + {file = "aiohttp-3.10.0-cp39-cp39-win32.whl", hash = "sha256:1ebd8ed91428ffbe8b33a5bd6f50174e11882d5b8e2fe28670406ab5ee045ede"}, + {file = "aiohttp-3.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:0433795c4a8bafc03deb3e662192250ba5db347c41231b0273380d2f53c9ea0b"}, + {file = "aiohttp-3.10.0.tar.gz", hash = "sha256:e8dd7da2609303e3574c95b0ec9f1fd49647ef29b94701a2862cceae76382e1d"}, ] [package.dependencies] +aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" @@ -94,7 +106,7 @@ multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aiosignal" @@ -230,53 +242,53 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "fonttools" -version = "4.51.0" +version = "4.53.1" description = "Tools to manipulate font files" optional = true python-versions = ">=3.8" files = [ - {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"}, - {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"}, - {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"}, - {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"}, - {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"}, - {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"}, - {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"}, - {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"}, - {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"}, - {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"}, - {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"}, - {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"}, - {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"}, - {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"}, - {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"}, - {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"}, - {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"}, - {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"}, - {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"}, - {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"}, - {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"}, - {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"}, - {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"}, - {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"}, - {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"}, - {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"}, - {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"}, - {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"}, - {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"}, - {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"}, - {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"}, - {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"}, - {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"}, - {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"}, - {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"}, - {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"}, - {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"}, - {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"}, - {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"}, - {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"}, - {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"}, - {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, + {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, + {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, + {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, + {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, + {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, + {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, + {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, + {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, + {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, + {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, + {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, + {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, + {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, + {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, + {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, + {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, + {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, + {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, + {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, + {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, + {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, + {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, + {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, + {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, + {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, + {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, + {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, ] [package.extras] @@ -523,39 +535,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.8.4" +version = "3.9.1" description = "Python plotting package" optional = true python-versions = ">=3.9" files = [ - {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"}, - {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"}, - {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"}, - {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"}, - {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"}, - {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"}, - {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"}, - {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"}, - {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"}, - {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"}, - {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"}, - {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"}, - {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"}, - {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"}, - {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"}, - {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"}, - {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"}, - {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"}, - {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"}, - {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"}, - {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"}, - {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"}, - {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"}, - {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"}, - {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"}, - {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"}, - {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"}, - {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, + {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, + {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, + {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, + {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, + {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, + {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, + {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, + {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, + {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, + {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, + {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, + {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, + {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, + {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, + {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, + {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, + {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, + {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, ] [package.dependencies] @@ -564,12 +577,15 @@ cycler = ">=0.10" fonttools = ">=4.22.0" importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} kiwisolver = ">=1.3.1" -numpy = ">=1.21" +numpy = ">=1.23" packaging = ">=20.0" pillow = ">=8" pyparsing = ">=2.3.1" python-dateutil = ">=2.7" +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + [[package]] name = "multidict" version = "6.0.5" @@ -671,44 +687,44 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -774,95 +790,106 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" description = "Python Imaging Library (Fork)" optional = true python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -1032,13 +1059,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1146,18 +1173,18 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.18.1" +version = "3.19.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = true python-versions = ">=3.8" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] examples = ["aiohttp", "matplotlib", "numpy", "prometheus-client", "pyyaml"] diff --git a/pyproject.toml b/pyproject.toml index 9b99b87..4645db9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "radiacode" -version = "0.3.3" -description = "Library for RadiaCode-101" +version = "0.3.4" +description = "Library for RadiaCode-10x" authors = ["Maxim Andreev "] license = "MIT" readme = "README.md" @@ -11,7 +11,10 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", ] -include = ["radiacode-examples/*"] +packages = [ + { include = "radiacode" }, + { include = "radiacode_examples" } +] [tool.poetry.dependencies] python = "^3.9" @@ -30,6 +33,14 @@ mypy = "^1.7" [tool.poetry.extras] examples = ["aiohttp", "prometheus-client", "matplotlib", "numpy", "pyyaml"] +[tool.poetry.scripts] +radiacode_basic = "radiacode_examples.basic:main" +radiacode_exporter = "radiacode_examples.radiacode_exporter:main" +radiacode_show_spectrum = "radiacode_examples.show_spectrum:main" +radiacode_webserver = "radiacode_examples.webserver:main" +radiacode_webserver_logger = "radiacode_examples.webserver_logger:main" +radiacode_narodmon = "radiacode_examples.narodmon:main" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" @@ -46,4 +57,4 @@ inline-quotes = "single" quote-style = "single" [tool.ruff.per-file-ignores] -"__init__.py" = ["F401", "F403"] +"__init__.py" = ["F401", "F403"] \ No newline at end of file diff --git a/radiacode_examples/README.md b/radiacode_examples/README.md new file mode 100644 index 0000000..08078e9 --- /dev/null +++ b/radiacode_examples/README.md @@ -0,0 +1,31 @@ +# RadiaCode Library - Examples + +[Описание на русском языке](README_ru.md) + +These example projects are installed with the library if you specify `pip install radiacode[examples]` (instead of `pip install radiacode`). +Each example project provides information when using the parameter `--help`. + +### 1. [basic.py](./basic.py) +Minimal example connecting to a device via USB or Bluetooth, obtaining serial number, spectrum and particle/dose measurements. +``` +$ python3 -m radiacode-examples.basic --bluetooth-mac 52:43:01:02:03:04 +``` + +### 2. [webserver.py](./webserver.py) & [webserver.html](./webserver.html) +Shows spectrum and particles/dose measurements in the web interface with automatic updates. +``` +$ python3 -m radiacode-examples.webserver --bluetooth-mac 52:43:01:02:03:04 --listen-port 8080 +``` + +### 3. [narodmon.py](./narodmon.py) +Sends measurements to the service [public monitoring project narodmon.ru](https://narodmon.ru). +``` +$ python3 -m radiacode-examples.narodmon --bluetooth-mac 52:43:01:02:03:04 +``` + +### 3. [radiacode-exporter.py](./radiacode-exporter.py) +Exports metrics for [prometheus](https://prometheus.io/) +``` +$ python3 -m radiacode-examples.radiacode-exporter --bluetooth-mac 52:43:01:02:03:04 --port 5432 +$ curl http://127.0.0.1:5432/metrics +``` diff --git a/radiacode_examples/README_ru.md b/radiacode_examples/README_ru.md new file mode 100644 index 0000000..871d1b3 --- /dev/null +++ b/radiacode_examples/README_ru.md @@ -0,0 +1,25 @@ +# Примеры использования библиотеки + +Устанавливаются с пакетом если указать `pip install radiacode[examples]` (вместо `pip install radiacode`) + +У каждого примера есть справка по `--help` + + +### 1. [basic.py](./basic.py) +Минимальный пример, показывающий соединение с устройством по USB или Bluetooth и получение серийного номера, спектра и измерений числа частиц/дозы +``` +$ python3 -m radiacode-examples.basic --bluetooth-mac 52:43:01:02:03:04 +``` + +### 2. [webserver.py](./webserver.py) & [webserver.html](./webserver.html) +Спектр и число частиц/доза в веб интерфейсе с автоматическим обновлением +``` +$ python3 -m radiacode-examples.webserver --bluetooth-mac 52:43:01:02:03:04 --listen-port 8080 +``` + + +### 3. [narodmon.py](./narodmon.py) +Отправка измерений в сервис [народный мониторинг narodmon.ru](https://narodmon.ru) +``` +$ python3 -m radiacode-examples.narodmon --bluetooth-mac 52:43:01:02:03:04 +``` \ No newline at end of file diff --git a/radiacode_examples/__init__.py b/radiacode_examples/__init__.py new file mode 100644 index 0000000..ff0bcff --- /dev/null +++ b/radiacode_examples/__init__.py @@ -0,0 +1,22 @@ +""" +RadiaCode Examples + +This package contains example scripts for using the RadiaCode library. + +Available examples: +- basic: A simple command-line interface for RadiaCode +- show_spectrum: Reads spectrum data from Radiacode 102 device, displays and + stores the count rate history and the spectrum of deposited energies. +- webserver: A web-based interface for RadiaCode data +- webserver_logger: ...with rotating file logging +- radiacode_exporter: Stores radiacode data in a Prometheus database +- narodmon: Script for sending data to the narodmon.ru monitoring project + +To run an example, use: +python -m radiacode_examples. + +For instance: +python -m radiacode_examples.webserver +""" + +__all__ = ['basic', 'show_spectrum', 'webserver', 'webserver_logger', 'radiacode_exporter', 'narodmon'] \ No newline at end of file diff --git a/radiacode_examples/basic.py b/radiacode_examples/basic.py new file mode 100644 index 0000000..018aeee --- /dev/null +++ b/radiacode_examples/basic.py @@ -0,0 +1,67 @@ +import argparse +import time +import platform + +from radiacode import RadiaCode +from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB +from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT + + +def main(): + parser = argparse.ArgumentParser() + + if platform.system() != 'Darwin': + parser.add_argument( + '--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device (e.g. 00:11:22:33:44:55)' + ) + + parser.add_argument( + '--serial', + type=str, + required=False, + help='serial number of radiascan device (e.g. "RC-10x-xxxxxx"). Useful in case of multiple devices.', + ) + + args = parser.parse_args() + + if hasattr(args, 'bluetooth_mac') and args.bluetooth_mac: + print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') + + try: + rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) + except DeviceNotFoundBT as e: + print(e) + return + except ValueError as e: + print(e) + return + else: + print('Connecting to Radiacode via USB' + (f' (serial number: {args.serial})' if args.serial else '')) + + try: + rc = RadiaCode(serial_number=args.serial) + except DeviceNotFoundUSB: + print('Device not found, check your USB connection') + return + + serial = rc.serial_number() + print(f'### Serial number: {serial}') + print('--------') + + fw_version = rc.fw_version() + print(f'### Firmware: {fw_version}') + print('--------') + + spectrum = rc.spectrum() + print(f'### Spectrum: {spectrum}') + print('--------') + + print('### DataBuf:') + while True: + for v in rc.data_buf(): + print(v.dt.isoformat(), v) + time.sleep(2) + + +if __name__ == '__main__': + main() diff --git a/radiacode_examples/narodmon.py b/radiacode_examples/narodmon.py new file mode 100644 index 0000000..e4b26ad --- /dev/null +++ b/radiacode_examples/narodmon.py @@ -0,0 +1,88 @@ +import argparse +import asyncio +import time + +import aiohttp + +from radiacode import RealTimeData, RadiaCode + + +def sensors_data(rc_conn): + databuf = rc_conn.data_buf() + + last = None + for v in databuf: + if isinstance(v, RealTimeData): + if last is None or last.dt < v.dt: + last = v + + if last is None: + return [] + + ts = int(last.dt.timestamp()) + return [ + { + 'id': 'S1', + 'name': 'CountRate', + 'value': last.count_rate, + 'unit': 'CPS', + 'time': ts, + }, + { + 'id': 'S2', + 'name': 'R_DoseRate', + 'value': 1000000 * last.dose_rate, + 'unit': 'μR/h', + 'time': ts, + }, + ] + + +async def send_data(d): + # use aiohttp because we already have it as dependency in webserver.py, don't want add 'requests' here + async with aiohttp.ClientSession() as session: + async with session.post('https://narodmon.ru/json', json=d) as resp: + return await resp.text() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--bluetooth-mac', type=str, required=True, help='MAC address of radiascan device') + parser.add_argument('--connection', choices=['usb', 'bluetooth'], default='bluetooth', help='device connection type') + parser.add_argument('--interval', type=int, required=False, default=600, help='send interval, seconds') + args = parser.parse_args() + + if args.connection == 'usb': + print('will use USB connection') + rc_conn = RadiaCode() + else: + print('will use Bluetooth connection') + rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) + + device_data = { + 'mac': args.bluetooth_mac.replace(':', '-'), + 'name': 'RadiaCode-101', + } + + while True: + d = { + 'devices': [ + { + **device_data, + 'sensors': sensors_data(rc_conn), + }, + ], + } + print(f'Sending {d}') + + try: + r = asyncio.run(send_data(d)) + print(f'NarodMon Response: {r}') + except Exception as ex: + print(f'NarodMon send error: {ex}') + + time.sleep(args.interval) + + +if __name__ == '__main__': + main() diff --git a/radiacode_examples/radiacode_exporter.py b/radiacode_examples/radiacode_exporter.py new file mode 100644 index 0000000..5934310 --- /dev/null +++ b/radiacode_examples/radiacode_exporter.py @@ -0,0 +1,46 @@ +import argparse +import time + +import prometheus_client + +from radiacode import RealTimeData, RadiaCode + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') + parser.add_argument('--update-interval', type=int, default=3, required=False, help='update interval (seconds)') + parser.add_argument('--port', type=int, default=5432, required=False, help='prometheus http port') + args = parser.parse_args() + + if args.bluetooth_mac: + print('will use Bluetooth connection') + rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) + else: + print('will use USB connection') + rc = RadiaCode() + + serial = rc.serial_number() + + metric_count_rate = prometheus_client.Gauge('radiacode_count_rate', 'count rate, CPS', ['device']).labels(serial) + metric_count_rate_error = prometheus_client.Gauge('radiacode_count_rate_error', 'count rate error, %', ['device']).labels( + serial + ) + metric_dose_rate = prometheus_client.Gauge('radiacode_dose_rate', 'dose rate, μSv/h', ['device']).labels(serial) + metric_dose_rate_error = prometheus_client.Gauge('radiacode_dose_rate_error', 'dose rate error, %', ['device']).labels(serial) + + prometheus_client.start_http_server(args.port) + + while True: + for v in rc.data_buf(): + if isinstance(v, RealTimeData): + metric_count_rate.set(v.count_rate) + metric_count_rate_error.set(v.count_rate_err) + metric_dose_rate.set(10000 * v.dose_rate) # convert to μSv/h + metric_dose_rate_error.set(v.dose_rate_err) + + time.sleep(args.update_interval) + + +if __name__ == '__main__': + main() diff --git a/radiacode_examples/show_spectrum.py b/radiacode_examples/show_spectrum.py new file mode 100755 index 0000000..9b6895d --- /dev/null +++ b/radiacode_examples/show_spectrum.py @@ -0,0 +1,380 @@ +#! /usr/bin/env python3 +"""script show-spectrum.py + + Reads spectrum data from Radiacode 102 device and displays and stores + the count rate history and the spectrum of deposited energies. + Data is stored in a file in human-readable yaml format. + + Calculates and shows in an animated display: + + - counts: accumulated number of counts/sec + - count rate: count rate + - dose rate: energy deposit in crystal, i.e. sum(counts*energies). + - total dose: total sum of deposited energies + + + Command line options: + + Usage: show-spectrum.py [-h] [-b BLUETOOTH_MAC] [-r] [-R] [-q] + [-i INTERVAL] [-f FILE] [-t TIME] [-H HISTORY] + + Read and display gamma energy spectrum from RadioCode 102, + show differential and updated cumulative spectrum, + optionally store data to file in yaml format. + + Options: + -h, --help show this help message and exit + -b BLUETOOTH_MAC, --bluetooth-mac BLUETOOTH_MAC bluetooth MAC address of device + -s SERIAL_NUMBER, --serial-number SERIAL_NUMBER serial number of device + -r, --restart restart spectrum accumulation + -R, --Reset reset spectrum stored in device + -q, --quiet no status output to terminal + -i INTERVAL, --interval INTERVAL update interval + -f FILE, --file FILE file to store results + -t TIME, --time TIME run time in seconds + -H HISTORY, --history HISTORY number of rate-history points to store in file + + Hint: use option -R to reset spectrum data in RadiaCode device + +""" + +import argparse +import sys +import time +import numpy as np +import yaml +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.patches as patches +from radiacode import RadiaCode + +# set backend and matplotlib style +mpl.use('Qt5Agg') +plt.style.use('dark_background') + +# some constants +rho_CsJ = 4.51 # density of CsJ in g/cm^3 +m_sensor = rho_CsJ * 1e-3 # Volume is 1 cm^3, mass in kg +keV2J = 1.602e-16 # conversion factor keV to Joule +depositedE2doserate = keV2J * 3600 * 1e6 / m_sensor # dose rate in µGy/h +depositedE2dose = keV2J * 1e6 / m_sensor # dose rate in µGy/h + + +class appColors: + """Define colors used in this app""" + + title = 'goldenrod' + text1 = 'blue' + text2 = 'green' + text3 = 'lightgreen' + text4 = 'red' + bg = 'black' + line1 = '#F0F0C0' + marker1 = 'orange' + auxline = 'red' + + +def plot_RC102Spectrum(): + # Helper functions for conversion of channel numbers to energies + global a0, a1, a2 # calibration constants + # approx. calibration, overwritten by first retrieved spectrum + a0 = 0.17 + a1 = 2.42 + a2 = 0.0004 + + global mpl_active # flag indicating that matplotlib figure exists + mpl_active = False + + def Chan2En(C): + # convert Channel number to Energy + # E = a0 + a1*C + a2 C^2 + return a0 + a1 * C + a2 * C**2 + + def En2Chan(E): + # convert Energies to Channel Numbers + # inverse E = a0 + a1*C + a2 C^2 + c = a0 - E + return (np.sqrt(a1**2 - 4 * a2 * c) - a1) / (2 * a2) + + def on_mpl_window_closed(ax): + # detect when matplotlib window is closed + global mpl_active + print(' !!! Graphics window closed') + mpl_active = False + + # end helpers --------------------------------------- + + # ------ + # parse command line arguments + # ------ + parser = argparse.ArgumentParser( + description='Read and display gamma energy spectrum from RadioCode 102, ' + + 'show differential and updated cumulative spectrum, ' + + 'optionally store data to file in yaml format.' + ) + parser.add_argument('-b', '--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of device') + parser.add_argument('-s', '--serial-number', type=str, required=False, help='serial number of device') + parser.add_argument('-r', '--restart', action='store_true', help='restart spectrum accumulation') + parser.add_argument('-R', '--Reset', action='store_true', help='reset spectrum stored in device') + parser.add_argument('-q', '--quiet', action='store_true', help='no status output to terminal') + parser.add_argument('-i', '--interval', type=float, default=1.0, help='update interval') + parser.add_argument('-f', '--file', type=str, default='', help='file to store results') + parser.add_argument('-t', '--time', type=int, default=36000, help='run time in seconds') + parser.add_argument('-H', '--history', type=int, default=500, help='number of rate-history points to store in file') + args = parser.parse_args() + + bluetooth_mac = args.bluetooth_mac + serial_number = args.serial_number + restart_accumulation = args.restart + reset_device_spectrum = args.Reset + quiet = args.quiet + dt_wait = args.interval + timestamp = time.strftime('%y%m%d-%H%M', time.localtime()) + print(args.file) + filename = args.file + '_' + timestamp + '.yaml' if args.file != '' else '' + NHistory = args.history + run_time = args.time + rate_history = np.zeros(NHistory) + + if not quiet: + print(f'\n *==* script {sys.argv[0]} executing') + if bluetooth_mac is not None: + print(f' connecting via Bluetooth, MAC {bluetooth_mac}') + elif serial_number is not None: + print(f' connect via USB to device with SN {serial_number}') + else: + print(' connect via USB') + + # ------ + # initialize and connect to RC10x device + # ------ + rc = RadiaCode(bluetooth_mac=bluetooth_mac, serial_number=serial_number) + serial = rc.serial_number() + fw_version = rc.fw_version() + status_flags = eval(rc.status().split(':')[1])[0] + a0, a1, a2 = rc.energy_calib() + # get initial spectrum and meta-data + if reset_device_spectrum: + rc.spectrum_reset() + spectrum = rc.spectrum() + # print(f'### Spectrum: {spectrum}') + counts0 = np.asarray(spectrum.counts) + NChannels = len(counts0) + Channels = np.asarray(range(NChannels)) + 0.5 + Energies = Chan2En(Channels) + duration_s = spectrum.duration.total_seconds() + _t0 = time.time() + t_start = _t0 # start time of acquisition from device + + print(f'### Found device with serial number: {serial}') + print(f' Firmware: {fw_version}') + print(f' Status flags: 0x{status_flags:x}') + print(f' Calibration coefficientes: a0={a0:.6f}, a1={a1:.6f}, a2={a2:.6f}') + print(f' Number of spectrum channels: {NChannels}') + print(f' Spectrum accumulation since {spectrum.duration}') + + # ------ + # initialize graphics display + # ------- + # create a figure with two sub-plots + fig = plt.figure('GammaSpectrum', figsize=(8.0, 8.0)) + fig.suptitle('Radiacode: $\gamma$-ray spectrum ' + time.asctime(), size='large', color=appColors.title) + fig.subplots_adjust(left=0.12, bottom=0.1, right=0.95, top=0.88, wspace=None, hspace=0.1) # + gs = fig.add_gridspec(nrows=15, ncols=1) + mpl_active = True + fig.canvas.mpl_connect('close_event', on_mpl_window_closed) + + # 1st plot for cumulative spectrum + axE = fig.add_subplot(gs[:-6, :]) + axE.set_ylabel('Cumulative counts', size='large') + axE.set_xlim(0.0, Energies[NChannels - 1]) + plt.locator_params(axis='x', nbins=12) + axE.grid(linestyle='dotted', which='both') + axE.set_yscale('log') + axE.set_xticklabels([]) + # second x-axis for channels + axC = axE.secondary_xaxis('top', functions=(En2Chan, Chan2En)) + axC.set_xlabel('Channel #') + + # 2nd, smaller plot for differential spectrum + axEdiff = fig.add_subplot(gs[-6:-4, :]) + axEdiff.set_xlabel('Energy (keV)', size='large') + axEdiff.set_ylabel('Counts', size='large') + axEdiff.set_xlim(0.0, Energies[NChannels - 1]) + plt.locator_params(axis='x', nbins=12) + axEdiff.grid(linestyle='dotted', which='both') + + # 3rd, small plot for rate history + axRate = fig.add_subplot(gs[-2:, :]) + axRate.set_xlabel('History [s]', size='large') + axRate.set_ylabel('Rate (Hz)', size='large') + axRate.grid(linestyle='dotted', which='both') + num_history_points = 300 + axRate.set_xlim(-num_history_points * dt_wait, 0.0) + + # create and initialize graph elements + (line,) = axE.plot([1], [0.5], color=appColors.line1, lw=1) + line.set_xdata(Energies) + (line_diff,) = axEdiff.plot([1], [0.5], color=appColors.line1) + line_diff.set_xdata(Energies) + hrates = num_history_points * [None] + _xplt = np.linspace(-num_history_points * dt_wait, 0.0, num_history_points) + (line_rate,) = axRate.plot(_xplt, hrates, '.--', lw=1, markersize=4, color=appColors.line1, mec=appColors.marker1) + line_avrate = axRate.axhline(0.0, linestyle='--', lw=1, color=appColors.auxline) + + # text for active time, cumulative and differential statistiscs + text_active = axE.text( + 0.66, + 0.94, + ' ', + transform=axE.transAxes, + color=appColors.text1, + # backgroundcolor='white', + alpha=0.7, + ) + text_cum_statistics = axE.text( + 0.7, + 0.75, + ' ', + transform=axE.transAxes, + color=appColors.text2, + alpha=0.7, + ) + + # textbox and background + rect = patches.Rectangle((0.65, 0.73), 0.34, 0.26, angle=0.0, color='white', alpha=0.7, transform=axE.transAxes) + axE.add_patch(rect) + + text_diff_statistics = axEdiff.text( + 0.75, + 0.55, + ' ', + transform=axEdiff.transAxes, + color=appColors.text3, + alpha=0.7, + ) + + # plot in non-blocking mode + plt.ion() # interactive mode, non-blocking + plt.show() + + # --- + # initialize and start read-out loop + # --- + print(f'### Collecting data for {run_time:d} s') + print(' type +c to stop ', end='\r') + + toggle = [' \\ ', ' | ', ' / ', ' - '] + itoggle = 0 + icount = -1 + total_time = 0 + previous_counts = counts0.copy() + if restart_accumulation: + counts = np.zeros(len(counts0)) + T0 = t_start + else: + counts = counts0.copy() + T0 = t_start - duration_s # start time of accumulation + + countsum0 = np.sum(counts) + + time.sleep(dt_wait - time.time() + t_start) + try: + while total_time < run_time and mpl_active: + _t = time.time() # start time of loop + icount += 1 + # dt = _t - _t0 # last time interval + _t0 = _t + total_time = int(10 * (_t - T0)) / 10 # active time rounded to 0.1s + spectrum = rc.spectrum() + actual_counts = np.asarray(spectrum.counts) + if not any(actual_counts): + time.sleep(dt_wait) + print(' accumulation time:', total_time, ' s', ' !!! waiting for data', end='\r') + continue + counts_diff = actual_counts - previous_counts + previous_counts[:] = actual_counts + counts += counts_diff + # some statistics + countsum = np.sum(counts) + rate = (countsum - countsum0) / dt_wait + rate_history[icount % NHistory] = rate + rate_av = countsum / total_time + hrates[icount % num_history_points] = rate + depE = np.sum(counts_diff * Energies) # in keV + doserate = depE * depositedE2doserate / dt_wait + # dose in µGy/h = µJ/(kg*h) + deposited_energy = np.sum(counts * Energies) # in keV + total_dose = deposited_energy * depositedE2dose + av_doserate = deposited_energy * depositedE2doserate / total_time + + countsum0 = countsum + # update graphics + line.set_ydata(counts) + axE.relim() + axE.autoscale_view() + line_diff.set_ydata(counts_diff) + axEdiff.relim() + axEdiff.autoscale_view() + k = icount % num_history_points + line_rate.set_ydata(np.concatenate((hrates[k + 1 :], hrates[: k + 1]))) + axRate.relim() + axRate.autoscale_view() + line_avrate.set_ydata([rate_av]) + + text_active.set_text('accumulation time: ' + str(total_time) + 's') + text_cum_statistics.set_text( + f'counts: {countsum:.5g}\n' + + f'av. rate: {rate_av:.3g} Hz\n' + + f'dose: {total_dose:.3g} µGy \n' + + f'av. doserate: {av_doserate:.3g} µGy/h' + ) + text_diff_statistics.set_text(f'rate: {rate:.3g} Hz\n' + f'dose: {doserate:.3g} µGy/h') + # draw data + fig.canvas.draw_idle() + # update status text in terminal + if not quiet: + print( + toggle[itoggle], + ' active:', + total_time, + 's ', + f'counts: {countsum:.5g}, rate: {rate:.3g} Hz, dose: {doserate:.3g} µGy/h', + ' (+c to stop) ', + end='\r', + ) + itoggle = itoggle + 1 if itoggle < 3 else 0 + # wait for corrected wait interval) + fig.canvas.start_event_loop(max(0.9 * dt_wait, dt_wait * (icount + 2) - (time.time() - t_start))) + # --> end while true + + print('\n' + sys.argv[0] + ': exit after ', total_time, ' s of data accumulation ...') + + except KeyboardInterrupt: + print('\n' + sys.argv[0] + ': keyboard interrupt - ending ...') + + finally: # store data + if filename != '': + print(22 * ' ' + '... storing data to yaml file -> ', filename) + d = dict( + active_time=total_time, + interval=dt_wait, + rates=rate_history[: icount + 1].tolist() + if icount < NHistory + else np.concatenate((rate_history[icount + 1 :], rate_history[: icount + 1])).tolist(), + ecal=[a0, a1, a2], + spectrum=counts.tolist(), + ) + with open(filename, 'w') as f: + f.write(yaml.dump(d, default_flow_style=None)) + + if mpl_active: + input(' type to close down graphics window --> ') + + ### get dose info from device + # for v in rc.data_buf(): + # print(v.dt.isoformat(), v) + + +if __name__ == '__main__': + plot_RC102Spectrum() diff --git a/radiacode_examples/webserver.html b/radiacode_examples/webserver.html new file mode 100644 index 0000000..879acf9 --- /dev/null +++ b/radiacode_examples/webserver.html @@ -0,0 +1,142 @@ + + + + +RadiaCode demo + + + + + + +
+
+ +
+
+ + +
+
+ + + + + +
+
+ + + + +
+
+ + +
+
+ + +
+
+ + + + diff --git a/radiacode_examples/webserver.py b/radiacode_examples/webserver.py new file mode 100644 index 0000000..b36ee01 --- /dev/null +++ b/radiacode_examples/webserver.py @@ -0,0 +1,108 @@ +import argparse +import asyncio +import json +import pathlib + +from aiohttp import web + +from radiacode import RadiaCode, RealTimeData + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') + parser.add_argument('--listen-host', type=str, required=False, default='0.0.0.0', help='listen host for webserver') + parser.add_argument('--listen-port', type=int, required=False, default=8102, help='listen port for webserver') + args = parser.parse_args() + + app = web.Application() + app.ws_clients = [] + if args.bluetooth_mac: + print('will use Bluetooth connection') + app.rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) + else: + print('will use USB connection') + app.rc_conn = RadiaCode() + + app.on_startup.append(on_startup) + app.add_routes( + [ + web.get('/', handle_index), + web.get('/spectrum', handle_spectrum), + web.post('/spectrum/reset', handle_spectrum_reset), + web.get('/ws', handle_ws), + ], + ) + web.run_app(app, host=args.listen_host, port=args.listen_port) + +async def handle_index(request): + return web.FileResponse(pathlib.Path(__file__).parent.absolute() / 'webserver.html') + + +async def handle_ws(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + request.app.ws_clients.append(ws) + async for _ in ws: + pass + request.app.ws_clients.remove(ws) + return ws + + +async def handle_spectrum(request): + cn = request.app.rc_conn + accum = request.query.get('accum') == 'true' + spectrum = cn.spectrum_accum() if accum else cn.spectrum() + # apexcharts can't handle 0 in logarithmic view + spectrum_data = [(channel, cnt if cnt > 0 else 0.5) for channel, cnt in enumerate(spectrum.counts)] + print('Spectrum updated') + return web.json_response( + { + 'coef': [spectrum.a0, spectrum.a1, spectrum.a2], + 'duration': spectrum.duration.total_seconds(), + 'series': [{'name': 'spectrum', 'data': spectrum_data}], + }, + ) + + +async def handle_spectrum_reset(request): + cn = request.app.rc_conn + cn.spectrum_reset() + print('Spectrum reset') + return web.json_response({}) + + +async def process(app): + max_history_size = 128 + history = [] + while True: + databuf = app.rc_conn.data_buf() + for v in databuf: + if isinstance(v, RealTimeData): + history.append(v) + + history.sort(key=lambda x: x.dt) + history = history[-max_history_size:] + jdata = json.dumps( + { + 'series': [ + { + 'name': 'countrate', + 'data': [(int(1000 * x.dt.timestamp()), x.count_rate) for x in history], + }, + { + 'name': 'doserate', + 'data': [(int(1000 * x.dt.timestamp()), 10000 * x.dose_rate) for x in history], + }, + ], + }, + ) + print(f'Rates updated, sending to {len(app.ws_clients)} connected clients') + await asyncio.gather(*[ws.send_str(jdata) for ws in app.ws_clients], asyncio.sleep(1.0)) + + +async def on_startup(app): + asyncio.create_task(process(app)) + + +if __name__ == '__main__': + main() diff --git a/radiacode_examples/webserver_logger.py b/radiacode_examples/webserver_logger.py new file mode 100644 index 0000000..c8a9102 --- /dev/null +++ b/radiacode_examples/webserver_logger.py @@ -0,0 +1,123 @@ +import argparse +import asyncio +import json +import pathlib +import logging +from logging.handlers import RotatingFileHandler + +from aiohttp import web + +from radiacode import RadiaCode, RealTimeData + +# Set up logging for webserver +webserver_log_file = '/var/log/radiacode_webserver.log' +webserver_logger = logging.getLogger('radiacode.webserver') +webserver_logger.setLevel(logging.INFO) +webserver_handler = RotatingFileHandler(webserver_log_file, maxBytes=10*1024*1024, backupCount=5) +webserver_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +webserver_handler.setFormatter(webserver_formatter) +webserver_logger.addHandler(webserver_handler) + +# Set up logging for new readings +readings_log_file = '/var/log/radiacode_readings.log' +readings_logger = logging.getLogger('radiacode.readings') +readings_logger.setLevel(logging.INFO) +readings_handler = RotatingFileHandler(readings_log_file, maxBytes=10*1024*1024, backupCount=5) +readings_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +readings_handler.setFormatter(readings_formatter) +readings_logger.addHandler(readings_handler) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') + parser.add_argument('--listen-host', type=str, required=False, default='0.0.0.0', help='listen host for webserver') + parser.add_argument('--listen-port', type=int, required=False, default=8102, help='listen port for webserver') + args = parser.parse_args() + + app = web.Application() + app.ws_clients = [] + if args.bluetooth_mac: + webserver_logger.info(f'Using Bluetooth connection with MAC: {args.bluetooth_mac}') + app.rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) + else: + webserver_logger.info('Using USB connection') + app.rc_conn = RadiaCode() + + app.on_startup.append(on_startup) + app.add_routes( + [ + web.get('/', handle_index), + web.get('/spectrum', handle_spectrum), + web.post('/spectrum/reset', handle_spectrum_reset), + web.get('/ws', handle_ws), + ], + ) + webserver_logger.info(f'Starting webserver on {args.listen_host}:{args.listen_port}') + web.run_app(app, host=args.listen_host, port=args.listen_port) + +async def handle_index(request): + return web.FileResponse(pathlib.Path(__file__).parent.absolute() / 'webserver.html') + +async def handle_ws(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + request.app.ws_clients.append(ws) + async for _ in ws: + pass + request.app.ws_clients.remove(ws) + return ws + +async def handle_spectrum(request): + cn = request.app.rc_conn + accum = request.query.get('accum') == 'true' + spectrum = cn.spectrum_accum() if accum else cn.spectrum() + spectrum_data = [(channel, cnt if cnt > 0 else 0.5) for channel, cnt in enumerate(spectrum.counts)] + webserver_logger.info(f"Spectrum updated: {'accumulated' if accum else 'current'}") + return web.json_response( + { + 'coef': [spectrum.a0, spectrum.a1, spectrum.a2], + 'duration': spectrum.duration.total_seconds(), + 'series': [{'name': 'spectrum', 'data': spectrum_data}], + }, + ) + +async def handle_spectrum_reset(request): + cn = request.app.rc_conn + cn.spectrum_reset() + webserver_logger.info("Spectrum reset") + return web.json_response({}) + +async def process(app): + max_history_size = 128 + history = [] + while True: + databuf = app.rc_conn.data_buf() + for v in databuf: + if isinstance(v, RealTimeData): + history.append(v) + readings_logger.info(f"{v.count_rate} cps, {v.dose_rate} Sv/h") + + history.sort(key=lambda x: x.dt) + history = history[-max_history_size:] + jdata = json.dumps( + { + 'series': [ + { + 'name': 'countrate', + 'data': [(int(1000 * x.dt.timestamp()), x.count_rate) for x in history], + }, + { + 'name': 'doserate', + 'data': [(int(1000 * x.dt.timestamp()), 10000 * x.dose_rate) for x in history], + }, + ], + }, + ) + webserver_logger.info(f"Rates updated, sending to {len(app.ws_clients)} connected clients") + await asyncio.gather(*[ws.send_str(jdata) for ws in app.ws_clients], asyncio.sleep(1.0)) + +async def on_startup(app): + asyncio.create_task(process(app)) + +if __name__ == '__main__': + main() \ No newline at end of file From 1db63bbcb7fb4b79aaaf3b9c097d43b9d2d766cd Mon Sep 17 00:00:00 2001 From: GlassOnTin <63980135+GlassOnTin@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:16:13 +0100 Subject: [PATCH 2/4] Python modules cannot have hyphen, so "radiacode_example" --- radiacode-examples/README.md | 31 -- radiacode-examples/README_ru.md | 25 -- radiacode-examples/basic.py | 67 ---- radiacode-examples/narodmon.py | 88 ------ radiacode-examples/radiacode-exporter.py | 46 --- radiacode-examples/show-spectrum.py | 380 ----------------------- radiacode-examples/webserver.html | 142 --------- radiacode-examples/webserver.py | 106 ------- 8 files changed, 885 deletions(-) delete mode 100644 radiacode-examples/README.md delete mode 100644 radiacode-examples/README_ru.md delete mode 100644 radiacode-examples/basic.py delete mode 100644 radiacode-examples/narodmon.py delete mode 100644 radiacode-examples/radiacode-exporter.py delete mode 100755 radiacode-examples/show-spectrum.py delete mode 100644 radiacode-examples/webserver.html delete mode 100644 radiacode-examples/webserver.py diff --git a/radiacode-examples/README.md b/radiacode-examples/README.md deleted file mode 100644 index 08078e9..0000000 --- a/radiacode-examples/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# RadiaCode Library - Examples - -[Описание на русском языке](README_ru.md) - -These example projects are installed with the library if you specify `pip install radiacode[examples]` (instead of `pip install radiacode`). -Each example project provides information when using the parameter `--help`. - -### 1. [basic.py](./basic.py) -Minimal example connecting to a device via USB or Bluetooth, obtaining serial number, spectrum and particle/dose measurements. -``` -$ python3 -m radiacode-examples.basic --bluetooth-mac 52:43:01:02:03:04 -``` - -### 2. [webserver.py](./webserver.py) & [webserver.html](./webserver.html) -Shows spectrum and particles/dose measurements in the web interface with automatic updates. -``` -$ python3 -m radiacode-examples.webserver --bluetooth-mac 52:43:01:02:03:04 --listen-port 8080 -``` - -### 3. [narodmon.py](./narodmon.py) -Sends measurements to the service [public monitoring project narodmon.ru](https://narodmon.ru). -``` -$ python3 -m radiacode-examples.narodmon --bluetooth-mac 52:43:01:02:03:04 -``` - -### 3. [radiacode-exporter.py](./radiacode-exporter.py) -Exports metrics for [prometheus](https://prometheus.io/) -``` -$ python3 -m radiacode-examples.radiacode-exporter --bluetooth-mac 52:43:01:02:03:04 --port 5432 -$ curl http://127.0.0.1:5432/metrics -``` diff --git a/radiacode-examples/README_ru.md b/radiacode-examples/README_ru.md deleted file mode 100644 index 871d1b3..0000000 --- a/radiacode-examples/README_ru.md +++ /dev/null @@ -1,25 +0,0 @@ -# Примеры использования библиотеки - -Устанавливаются с пакетом если указать `pip install radiacode[examples]` (вместо `pip install radiacode`) - -У каждого примера есть справка по `--help` - - -### 1. [basic.py](./basic.py) -Минимальный пример, показывающий соединение с устройством по USB или Bluetooth и получение серийного номера, спектра и измерений числа частиц/дозы -``` -$ python3 -m radiacode-examples.basic --bluetooth-mac 52:43:01:02:03:04 -``` - -### 2. [webserver.py](./webserver.py) & [webserver.html](./webserver.html) -Спектр и число частиц/доза в веб интерфейсе с автоматическим обновлением -``` -$ python3 -m radiacode-examples.webserver --bluetooth-mac 52:43:01:02:03:04 --listen-port 8080 -``` - - -### 3. [narodmon.py](./narodmon.py) -Отправка измерений в сервис [народный мониторинг narodmon.ru](https://narodmon.ru) -``` -$ python3 -m radiacode-examples.narodmon --bluetooth-mac 52:43:01:02:03:04 -``` \ No newline at end of file diff --git a/radiacode-examples/basic.py b/radiacode-examples/basic.py deleted file mode 100644 index 018aeee..0000000 --- a/radiacode-examples/basic.py +++ /dev/null @@ -1,67 +0,0 @@ -import argparse -import time -import platform - -from radiacode import RadiaCode -from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB -from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT - - -def main(): - parser = argparse.ArgumentParser() - - if platform.system() != 'Darwin': - parser.add_argument( - '--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device (e.g. 00:11:22:33:44:55)' - ) - - parser.add_argument( - '--serial', - type=str, - required=False, - help='serial number of radiascan device (e.g. "RC-10x-xxxxxx"). Useful in case of multiple devices.', - ) - - args = parser.parse_args() - - if hasattr(args, 'bluetooth_mac') and args.bluetooth_mac: - print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') - - try: - rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) - except DeviceNotFoundBT as e: - print(e) - return - except ValueError as e: - print(e) - return - else: - print('Connecting to Radiacode via USB' + (f' (serial number: {args.serial})' if args.serial else '')) - - try: - rc = RadiaCode(serial_number=args.serial) - except DeviceNotFoundUSB: - print('Device not found, check your USB connection') - return - - serial = rc.serial_number() - print(f'### Serial number: {serial}') - print('--------') - - fw_version = rc.fw_version() - print(f'### Firmware: {fw_version}') - print('--------') - - spectrum = rc.spectrum() - print(f'### Spectrum: {spectrum}') - print('--------') - - print('### DataBuf:') - while True: - for v in rc.data_buf(): - print(v.dt.isoformat(), v) - time.sleep(2) - - -if __name__ == '__main__': - main() diff --git a/radiacode-examples/narodmon.py b/radiacode-examples/narodmon.py deleted file mode 100644 index e4b26ad..0000000 --- a/radiacode-examples/narodmon.py +++ /dev/null @@ -1,88 +0,0 @@ -import argparse -import asyncio -import time - -import aiohttp - -from radiacode import RealTimeData, RadiaCode - - -def sensors_data(rc_conn): - databuf = rc_conn.data_buf() - - last = None - for v in databuf: - if isinstance(v, RealTimeData): - if last is None or last.dt < v.dt: - last = v - - if last is None: - return [] - - ts = int(last.dt.timestamp()) - return [ - { - 'id': 'S1', - 'name': 'CountRate', - 'value': last.count_rate, - 'unit': 'CPS', - 'time': ts, - }, - { - 'id': 'S2', - 'name': 'R_DoseRate', - 'value': 1000000 * last.dose_rate, - 'unit': 'μR/h', - 'time': ts, - }, - ] - - -async def send_data(d): - # use aiohttp because we already have it as dependency in webserver.py, don't want add 'requests' here - async with aiohttp.ClientSession() as session: - async with session.post('https://narodmon.ru/json', json=d) as resp: - return await resp.text() - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--bluetooth-mac', type=str, required=True, help='MAC address of radiascan device') - parser.add_argument('--connection', choices=['usb', 'bluetooth'], default='bluetooth', help='device connection type') - parser.add_argument('--interval', type=int, required=False, default=600, help='send interval, seconds') - args = parser.parse_args() - - if args.connection == 'usb': - print('will use USB connection') - rc_conn = RadiaCode() - else: - print('will use Bluetooth connection') - rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) - - device_data = { - 'mac': args.bluetooth_mac.replace(':', '-'), - 'name': 'RadiaCode-101', - } - - while True: - d = { - 'devices': [ - { - **device_data, - 'sensors': sensors_data(rc_conn), - }, - ], - } - print(f'Sending {d}') - - try: - r = asyncio.run(send_data(d)) - print(f'NarodMon Response: {r}') - except Exception as ex: - print(f'NarodMon send error: {ex}') - - time.sleep(args.interval) - - -if __name__ == '__main__': - main() diff --git a/radiacode-examples/radiacode-exporter.py b/radiacode-examples/radiacode-exporter.py deleted file mode 100644 index 5934310..0000000 --- a/radiacode-examples/radiacode-exporter.py +++ /dev/null @@ -1,46 +0,0 @@ -import argparse -import time - -import prometheus_client - -from radiacode import RealTimeData, RadiaCode - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') - parser.add_argument('--update-interval', type=int, default=3, required=False, help='update interval (seconds)') - parser.add_argument('--port', type=int, default=5432, required=False, help='prometheus http port') - args = parser.parse_args() - - if args.bluetooth_mac: - print('will use Bluetooth connection') - rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) - else: - print('will use USB connection') - rc = RadiaCode() - - serial = rc.serial_number() - - metric_count_rate = prometheus_client.Gauge('radiacode_count_rate', 'count rate, CPS', ['device']).labels(serial) - metric_count_rate_error = prometheus_client.Gauge('radiacode_count_rate_error', 'count rate error, %', ['device']).labels( - serial - ) - metric_dose_rate = prometheus_client.Gauge('radiacode_dose_rate', 'dose rate, μSv/h', ['device']).labels(serial) - metric_dose_rate_error = prometheus_client.Gauge('radiacode_dose_rate_error', 'dose rate error, %', ['device']).labels(serial) - - prometheus_client.start_http_server(args.port) - - while True: - for v in rc.data_buf(): - if isinstance(v, RealTimeData): - metric_count_rate.set(v.count_rate) - metric_count_rate_error.set(v.count_rate_err) - metric_dose_rate.set(10000 * v.dose_rate) # convert to μSv/h - metric_dose_rate_error.set(v.dose_rate_err) - - time.sleep(args.update_interval) - - -if __name__ == '__main__': - main() diff --git a/radiacode-examples/show-spectrum.py b/radiacode-examples/show-spectrum.py deleted file mode 100755 index 9b6895d..0000000 --- a/radiacode-examples/show-spectrum.py +++ /dev/null @@ -1,380 +0,0 @@ -#! /usr/bin/env python3 -"""script show-spectrum.py - - Reads spectrum data from Radiacode 102 device and displays and stores - the count rate history and the spectrum of deposited energies. - Data is stored in a file in human-readable yaml format. - - Calculates and shows in an animated display: - - - counts: accumulated number of counts/sec - - count rate: count rate - - dose rate: energy deposit in crystal, i.e. sum(counts*energies). - - total dose: total sum of deposited energies - - - Command line options: - - Usage: show-spectrum.py [-h] [-b BLUETOOTH_MAC] [-r] [-R] [-q] - [-i INTERVAL] [-f FILE] [-t TIME] [-H HISTORY] - - Read and display gamma energy spectrum from RadioCode 102, - show differential and updated cumulative spectrum, - optionally store data to file in yaml format. - - Options: - -h, --help show this help message and exit - -b BLUETOOTH_MAC, --bluetooth-mac BLUETOOTH_MAC bluetooth MAC address of device - -s SERIAL_NUMBER, --serial-number SERIAL_NUMBER serial number of device - -r, --restart restart spectrum accumulation - -R, --Reset reset spectrum stored in device - -q, --quiet no status output to terminal - -i INTERVAL, --interval INTERVAL update interval - -f FILE, --file FILE file to store results - -t TIME, --time TIME run time in seconds - -H HISTORY, --history HISTORY number of rate-history points to store in file - - Hint: use option -R to reset spectrum data in RadiaCode device - -""" - -import argparse -import sys -import time -import numpy as np -import yaml -import matplotlib as mpl -import matplotlib.pyplot as plt -import matplotlib.patches as patches -from radiacode import RadiaCode - -# set backend and matplotlib style -mpl.use('Qt5Agg') -plt.style.use('dark_background') - -# some constants -rho_CsJ = 4.51 # density of CsJ in g/cm^3 -m_sensor = rho_CsJ * 1e-3 # Volume is 1 cm^3, mass in kg -keV2J = 1.602e-16 # conversion factor keV to Joule -depositedE2doserate = keV2J * 3600 * 1e6 / m_sensor # dose rate in µGy/h -depositedE2dose = keV2J * 1e6 / m_sensor # dose rate in µGy/h - - -class appColors: - """Define colors used in this app""" - - title = 'goldenrod' - text1 = 'blue' - text2 = 'green' - text3 = 'lightgreen' - text4 = 'red' - bg = 'black' - line1 = '#F0F0C0' - marker1 = 'orange' - auxline = 'red' - - -def plot_RC102Spectrum(): - # Helper functions for conversion of channel numbers to energies - global a0, a1, a2 # calibration constants - # approx. calibration, overwritten by first retrieved spectrum - a0 = 0.17 - a1 = 2.42 - a2 = 0.0004 - - global mpl_active # flag indicating that matplotlib figure exists - mpl_active = False - - def Chan2En(C): - # convert Channel number to Energy - # E = a0 + a1*C + a2 C^2 - return a0 + a1 * C + a2 * C**2 - - def En2Chan(E): - # convert Energies to Channel Numbers - # inverse E = a0 + a1*C + a2 C^2 - c = a0 - E - return (np.sqrt(a1**2 - 4 * a2 * c) - a1) / (2 * a2) - - def on_mpl_window_closed(ax): - # detect when matplotlib window is closed - global mpl_active - print(' !!! Graphics window closed') - mpl_active = False - - # end helpers --------------------------------------- - - # ------ - # parse command line arguments - # ------ - parser = argparse.ArgumentParser( - description='Read and display gamma energy spectrum from RadioCode 102, ' - + 'show differential and updated cumulative spectrum, ' - + 'optionally store data to file in yaml format.' - ) - parser.add_argument('-b', '--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of device') - parser.add_argument('-s', '--serial-number', type=str, required=False, help='serial number of device') - parser.add_argument('-r', '--restart', action='store_true', help='restart spectrum accumulation') - parser.add_argument('-R', '--Reset', action='store_true', help='reset spectrum stored in device') - parser.add_argument('-q', '--quiet', action='store_true', help='no status output to terminal') - parser.add_argument('-i', '--interval', type=float, default=1.0, help='update interval') - parser.add_argument('-f', '--file', type=str, default='', help='file to store results') - parser.add_argument('-t', '--time', type=int, default=36000, help='run time in seconds') - parser.add_argument('-H', '--history', type=int, default=500, help='number of rate-history points to store in file') - args = parser.parse_args() - - bluetooth_mac = args.bluetooth_mac - serial_number = args.serial_number - restart_accumulation = args.restart - reset_device_spectrum = args.Reset - quiet = args.quiet - dt_wait = args.interval - timestamp = time.strftime('%y%m%d-%H%M', time.localtime()) - print(args.file) - filename = args.file + '_' + timestamp + '.yaml' if args.file != '' else '' - NHistory = args.history - run_time = args.time - rate_history = np.zeros(NHistory) - - if not quiet: - print(f'\n *==* script {sys.argv[0]} executing') - if bluetooth_mac is not None: - print(f' connecting via Bluetooth, MAC {bluetooth_mac}') - elif serial_number is not None: - print(f' connect via USB to device with SN {serial_number}') - else: - print(' connect via USB') - - # ------ - # initialize and connect to RC10x device - # ------ - rc = RadiaCode(bluetooth_mac=bluetooth_mac, serial_number=serial_number) - serial = rc.serial_number() - fw_version = rc.fw_version() - status_flags = eval(rc.status().split(':')[1])[0] - a0, a1, a2 = rc.energy_calib() - # get initial spectrum and meta-data - if reset_device_spectrum: - rc.spectrum_reset() - spectrum = rc.spectrum() - # print(f'### Spectrum: {spectrum}') - counts0 = np.asarray(spectrum.counts) - NChannels = len(counts0) - Channels = np.asarray(range(NChannels)) + 0.5 - Energies = Chan2En(Channels) - duration_s = spectrum.duration.total_seconds() - _t0 = time.time() - t_start = _t0 # start time of acquisition from device - - print(f'### Found device with serial number: {serial}') - print(f' Firmware: {fw_version}') - print(f' Status flags: 0x{status_flags:x}') - print(f' Calibration coefficientes: a0={a0:.6f}, a1={a1:.6f}, a2={a2:.6f}') - print(f' Number of spectrum channels: {NChannels}') - print(f' Spectrum accumulation since {spectrum.duration}') - - # ------ - # initialize graphics display - # ------- - # create a figure with two sub-plots - fig = plt.figure('GammaSpectrum', figsize=(8.0, 8.0)) - fig.suptitle('Radiacode: $\gamma$-ray spectrum ' + time.asctime(), size='large', color=appColors.title) - fig.subplots_adjust(left=0.12, bottom=0.1, right=0.95, top=0.88, wspace=None, hspace=0.1) # - gs = fig.add_gridspec(nrows=15, ncols=1) - mpl_active = True - fig.canvas.mpl_connect('close_event', on_mpl_window_closed) - - # 1st plot for cumulative spectrum - axE = fig.add_subplot(gs[:-6, :]) - axE.set_ylabel('Cumulative counts', size='large') - axE.set_xlim(0.0, Energies[NChannels - 1]) - plt.locator_params(axis='x', nbins=12) - axE.grid(linestyle='dotted', which='both') - axE.set_yscale('log') - axE.set_xticklabels([]) - # second x-axis for channels - axC = axE.secondary_xaxis('top', functions=(En2Chan, Chan2En)) - axC.set_xlabel('Channel #') - - # 2nd, smaller plot for differential spectrum - axEdiff = fig.add_subplot(gs[-6:-4, :]) - axEdiff.set_xlabel('Energy (keV)', size='large') - axEdiff.set_ylabel('Counts', size='large') - axEdiff.set_xlim(0.0, Energies[NChannels - 1]) - plt.locator_params(axis='x', nbins=12) - axEdiff.grid(linestyle='dotted', which='both') - - # 3rd, small plot for rate history - axRate = fig.add_subplot(gs[-2:, :]) - axRate.set_xlabel('History [s]', size='large') - axRate.set_ylabel('Rate (Hz)', size='large') - axRate.grid(linestyle='dotted', which='both') - num_history_points = 300 - axRate.set_xlim(-num_history_points * dt_wait, 0.0) - - # create and initialize graph elements - (line,) = axE.plot([1], [0.5], color=appColors.line1, lw=1) - line.set_xdata(Energies) - (line_diff,) = axEdiff.plot([1], [0.5], color=appColors.line1) - line_diff.set_xdata(Energies) - hrates = num_history_points * [None] - _xplt = np.linspace(-num_history_points * dt_wait, 0.0, num_history_points) - (line_rate,) = axRate.plot(_xplt, hrates, '.--', lw=1, markersize=4, color=appColors.line1, mec=appColors.marker1) - line_avrate = axRate.axhline(0.0, linestyle='--', lw=1, color=appColors.auxline) - - # text for active time, cumulative and differential statistiscs - text_active = axE.text( - 0.66, - 0.94, - ' ', - transform=axE.transAxes, - color=appColors.text1, - # backgroundcolor='white', - alpha=0.7, - ) - text_cum_statistics = axE.text( - 0.7, - 0.75, - ' ', - transform=axE.transAxes, - color=appColors.text2, - alpha=0.7, - ) - - # textbox and background - rect = patches.Rectangle((0.65, 0.73), 0.34, 0.26, angle=0.0, color='white', alpha=0.7, transform=axE.transAxes) - axE.add_patch(rect) - - text_diff_statistics = axEdiff.text( - 0.75, - 0.55, - ' ', - transform=axEdiff.transAxes, - color=appColors.text3, - alpha=0.7, - ) - - # plot in non-blocking mode - plt.ion() # interactive mode, non-blocking - plt.show() - - # --- - # initialize and start read-out loop - # --- - print(f'### Collecting data for {run_time:d} s') - print(' type +c to stop ', end='\r') - - toggle = [' \\ ', ' | ', ' / ', ' - '] - itoggle = 0 - icount = -1 - total_time = 0 - previous_counts = counts0.copy() - if restart_accumulation: - counts = np.zeros(len(counts0)) - T0 = t_start - else: - counts = counts0.copy() - T0 = t_start - duration_s # start time of accumulation - - countsum0 = np.sum(counts) - - time.sleep(dt_wait - time.time() + t_start) - try: - while total_time < run_time and mpl_active: - _t = time.time() # start time of loop - icount += 1 - # dt = _t - _t0 # last time interval - _t0 = _t - total_time = int(10 * (_t - T0)) / 10 # active time rounded to 0.1s - spectrum = rc.spectrum() - actual_counts = np.asarray(spectrum.counts) - if not any(actual_counts): - time.sleep(dt_wait) - print(' accumulation time:', total_time, ' s', ' !!! waiting for data', end='\r') - continue - counts_diff = actual_counts - previous_counts - previous_counts[:] = actual_counts - counts += counts_diff - # some statistics - countsum = np.sum(counts) - rate = (countsum - countsum0) / dt_wait - rate_history[icount % NHistory] = rate - rate_av = countsum / total_time - hrates[icount % num_history_points] = rate - depE = np.sum(counts_diff * Energies) # in keV - doserate = depE * depositedE2doserate / dt_wait - # dose in µGy/h = µJ/(kg*h) - deposited_energy = np.sum(counts * Energies) # in keV - total_dose = deposited_energy * depositedE2dose - av_doserate = deposited_energy * depositedE2doserate / total_time - - countsum0 = countsum - # update graphics - line.set_ydata(counts) - axE.relim() - axE.autoscale_view() - line_diff.set_ydata(counts_diff) - axEdiff.relim() - axEdiff.autoscale_view() - k = icount % num_history_points - line_rate.set_ydata(np.concatenate((hrates[k + 1 :], hrates[: k + 1]))) - axRate.relim() - axRate.autoscale_view() - line_avrate.set_ydata([rate_av]) - - text_active.set_text('accumulation time: ' + str(total_time) + 's') - text_cum_statistics.set_text( - f'counts: {countsum:.5g}\n' - + f'av. rate: {rate_av:.3g} Hz\n' - + f'dose: {total_dose:.3g} µGy \n' - + f'av. doserate: {av_doserate:.3g} µGy/h' - ) - text_diff_statistics.set_text(f'rate: {rate:.3g} Hz\n' + f'dose: {doserate:.3g} µGy/h') - # draw data - fig.canvas.draw_idle() - # update status text in terminal - if not quiet: - print( - toggle[itoggle], - ' active:', - total_time, - 's ', - f'counts: {countsum:.5g}, rate: {rate:.3g} Hz, dose: {doserate:.3g} µGy/h', - ' (+c to stop) ', - end='\r', - ) - itoggle = itoggle + 1 if itoggle < 3 else 0 - # wait for corrected wait interval) - fig.canvas.start_event_loop(max(0.9 * dt_wait, dt_wait * (icount + 2) - (time.time() - t_start))) - # --> end while true - - print('\n' + sys.argv[0] + ': exit after ', total_time, ' s of data accumulation ...') - - except KeyboardInterrupt: - print('\n' + sys.argv[0] + ': keyboard interrupt - ending ...') - - finally: # store data - if filename != '': - print(22 * ' ' + '... storing data to yaml file -> ', filename) - d = dict( - active_time=total_time, - interval=dt_wait, - rates=rate_history[: icount + 1].tolist() - if icount < NHistory - else np.concatenate((rate_history[icount + 1 :], rate_history[: icount + 1])).tolist(), - ecal=[a0, a1, a2], - spectrum=counts.tolist(), - ) - with open(filename, 'w') as f: - f.write(yaml.dump(d, default_flow_style=None)) - - if mpl_active: - input(' type to close down graphics window --> ') - - ### get dose info from device - # for v in rc.data_buf(): - # print(v.dt.isoformat(), v) - - -if __name__ == '__main__': - plot_RC102Spectrum() diff --git a/radiacode-examples/webserver.html b/radiacode-examples/webserver.html deleted file mode 100644 index 879acf9..0000000 --- a/radiacode-examples/webserver.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - -RadiaCode demo - - - - - - -
-
- -
-
- - -
-
- - - - - -
-
- - - - -
-
- - -
-
- - -
-
- - - - diff --git a/radiacode-examples/webserver.py b/radiacode-examples/webserver.py deleted file mode 100644 index 687e8ac..0000000 --- a/radiacode-examples/webserver.py +++ /dev/null @@ -1,106 +0,0 @@ -import argparse -import asyncio -import json -import pathlib - -from aiohttp import web - -from radiacode import RadiaCode, RealTimeData - - -async def handle_index(request): - return web.FileResponse(pathlib.Path(__file__).parent.absolute() / 'webserver.html') - - -async def handle_ws(request): - ws = web.WebSocketResponse() - await ws.prepare(request) - request.app.ws_clients.append(ws) - async for _ in ws: - pass - request.app.ws_clients.remove(ws) - return ws - - -async def handle_spectrum(request): - cn = request.app.rc_conn - accum = request.query.get('accum') == 'true' - spectrum = cn.spectrum_accum() if accum else cn.spectrum() - # apexcharts can't handle 0 in logarithmic view - spectrum_data = [(channel, cnt if cnt > 0 else 0.5) for channel, cnt in enumerate(spectrum.counts)] - print('Spectrum updated') - return web.json_response( - { - 'coef': [spectrum.a0, spectrum.a1, spectrum.a2], - 'duration': spectrum.duration.total_seconds(), - 'series': [{'name': 'spectrum', 'data': spectrum_data}], - }, - ) - - -async def handle_spectrum_reset(request): - cn = request.app.rc_conn - cn.spectrum_reset() - print('Spectrum reset') - return web.json_response({}) - - -async def process(app): - max_history_size = 128 - history = [] - while True: - databuf = app.rc_conn.data_buf() - for v in databuf: - if isinstance(v, RealTimeData): - history.append(v) - - history.sort(key=lambda x: x.dt) - history = history[-max_history_size:] - jdata = json.dumps( - { - 'series': [ - { - 'name': 'countrate', - 'data': [(int(1000 * x.dt.timestamp()), x.count_rate) for x in history], - }, - { - 'name': 'doserate', - 'data': [(int(1000 * x.dt.timestamp()), 10000 * x.dose_rate) for x in history], - }, - ], - }, - ) - print(f'Rates updated, sending to {len(app.ws_clients)} connected clients') - await asyncio.gather(*[ws.send_str(jdata) for ws in app.ws_clients], asyncio.sleep(1.0)) - - -async def on_startup(app): - asyncio.create_task(process(app)) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') - parser.add_argument('--listen-host', type=str, required=False, default='0.0.0.0', help='listen host for webserver') - parser.add_argument('--listen-port', type=int, required=False, default=8080, help='listen port for webserver') - args = parser.parse_args() - - app = web.Application() - app.ws_clients = [] - if args.bluetooth_mac: - print('will use Bluetooth connection') - app.rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) - else: - print('will use USB connection') - app.rc_conn = RadiaCode() - - app.on_startup.append(on_startup) - app.add_routes( - [ - web.get('/', handle_index), - web.get('/spectrum', handle_spectrum), - web.post('/spectrum/reset', handle_spectrum_reset), - web.get('/ws', handle_ws), - ], - ) - web.run_app(app, host=args.listen_host, port=args.listen_port) From 2aa97c3c40ec76b21c9512f011c311dab7caa496 Mon Sep 17 00:00:00 2001 From: GlassOnTin Date: Mon, 26 May 2025 17:01:17 +0100 Subject: [PATCH 3/4] Improve Bluetooth permission error handling - Detect when user is not in bluetooth group - Provide clear instructions for permanent fix (usermod command) - Show exact sudo command as temporary workaround - Enhanced error messages for common Bluetooth failures - Check permissions before attempting Bluetooth scan - Improve user experience when running without proper permissions This makes it much clearer to users why Bluetooth scanning fails and provides actionable steps to resolve the issue. --- device_reader.py | 251 +++++++++++++++++++++++++++++++++++++++++ radiacode/discovery.py | 120 ++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 device_reader.py create mode 100644 radiacode/discovery.py diff --git a/device_reader.py b/device_reader.py new file mode 100644 index 0000000..1d357f3 --- /dev/null +++ b/device_reader.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +Standalone RadiaCode device reader +Publishes data to a JSON file that the webserver can read +""" +import asyncio +import json +import argparse +import signal +import time +from pathlib import Path +from datetime import datetime, timezone +from collections import deque + +class DeviceReader: + def __init__(self, output_path="/tmp/radiacode_data.json", mock=False): + self.output_path = Path(output_path) + self.mock = mock + self.running = True + self.device = None + self.data_buffer = deque(maxlen=300) # 5 minutes at 1Hz + self.last_spectrum_time = 0 + self.spectrum_interval = 30 # Update spectrum every 30s + + async def connect(self, bluetooth_mac=None): + """Connect to device with retry logic""" + retry_count = 0 + while self.running and retry_count < 3: + try: + if self.mock: + print("Using mock device") + from radiacode_examples.mock_data_generator import MockRadiaCode + self.device = MockRadiaCode() + return True + + from radiacode import RadiaCode + if bluetooth_mac: + print(f"Connecting to Bluetooth {bluetooth_mac}...") + self.device = RadiaCode(bluetooth_mac=bluetooth_mac) + else: + print("Connecting via USB...") + self.device = RadiaCode() + + serial = self.device.serial_number() + print(f"✓ Connected to {serial}") + return True + + except Exception as e: + retry_count += 1 + print(f"Connection attempt {retry_count} failed: {e}") + if retry_count < 3: + await asyncio.sleep(5) + + return False + + async def read_loop(self): + """Main reading loop""" + while self.running: + try: + # Read real-time data + data_points = [] + for data in self.device.data_buf(): + if not self.running: + break + # Only process RealTimeData or DoseRateDB objects + if hasattr(data, 'count_rate'): + data_point = { + 'timestamp': data.dt.isoformat(), + 'count_rate': data.count_rate, + 'dose_rate': data.dose_rate, + } + # Add optional fields if they exist + if hasattr(data, 'count_rate_err'): + data_point['count_rate_err'] = data.count_rate_err + if hasattr(data, 'dose_rate_err'): + data_point['dose_rate_err'] = data.dose_rate_err + + data_points.append(data_point) + self.data_buffer.append(data_points[-1]) + + # Get spectrum periodically + spectrum_data = None + now = time.time() + if now - self.last_spectrum_time > self.spectrum_interval: + spectrum = self.device.spectrum() + spectrum_accum = self.device.spectrum_accum() + spectrum_data = { + 'current': { + 'duration': spectrum.duration.total_seconds(), + 'coefficients': [spectrum.a0, spectrum.a1, spectrum.a2], + 'counts': spectrum.counts[:100] # First 100 for preview + }, + 'accumulated': { + 'duration': spectrum_accum.duration.total_seconds(), + 'coefficients': [spectrum_accum.a0, spectrum_accum.a1, spectrum_accum.a2], + 'counts': spectrum_accum.counts + } + } + self.last_spectrum_time = now + + # Write to file atomically + output = { + 'device': { + 'connected': True, + 'serial': getattr(self.device, 'serial_number', lambda: 'mock')(), + 'last_update': datetime.now(timezone.utc).isoformat() + }, + 'realtime': { + 'latest': data_points[-1] if data_points else None, + 'buffer': list(self.data_buffer) + }, + 'spectrum': spectrum_data + } + + # Atomic write + temp_path = self.output_path.with_suffix('.tmp') + with open(temp_path, 'w') as f: + json.dump(output, f) + temp_path.replace(self.output_path) + + print(f"✓ Updated {len(data_points)} readings, " + f"latest: {data_points[-1]['count_rate']:.1f} CPS" if data_points else "No new data") + + await asyncio.sleep(1) + + except Exception as e: + print(f"Read error: {e}") + # Write disconnected state + error_output = { + 'device': { + 'connected': False, + 'error': str(e), + 'last_update': datetime.now(timezone.utc).isoformat() + } + } + with open(self.output_path, 'w') as f: + json.dump(error_output, f) + + await asyncio.sleep(5) # Wait before retry + + async def run(self, bluetooth_mac=None): + """Main run method""" + # Setup signal handlers + loop = asyncio.get_event_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, self.shutdown) + + # Connect + if not await self.connect(bluetooth_mac): + print("Failed to connect to device") + return + + # Run read loop + try: + await self.read_loop() + finally: + if self.device and hasattr(self.device._connection, 'close'): + self.device._connection.close() + print("Device reader stopped") + + def shutdown(self): + """Graceful shutdown""" + print("\nShutting down...") + self.running = False + +async def main(): + parser = argparse.ArgumentParser(description='RadiaCode device reader service') + parser.add_argument('--bluetooth-mac', '-b', help='Bluetooth MAC address (auto-discover if not specified)') + parser.add_argument('--output', '-o', default='/tmp/radiacode_data.json', + help='Output JSON file path') + parser.add_argument('--mock', action='store_true', help='Use mock device') + parser.add_argument('--list', action='store_true', help='List available devices and exit') + args = parser.parse_args() + + # Handle device listing + if args.list: + from radiacode.discovery import list_all_radiacode_devices + list_all_radiacode_devices() + return + + # Auto-discover if no MAC specified and not using mock + bluetooth_mac = args.bluetooth_mac + if not bluetooth_mac and not args.mock: + print("No MAC address specified, scanning for RadiaCode devices...") + + # Check permissions before scanning + import os + import grp + import sys + + if os.geteuid() != 0: + try: + bluetooth_gid = grp.getgrnam('bluetooth').gr_gid + user_groups = os.getgroups() + if bluetooth_gid not in user_groups: + print("\n⚠️ Permission issue detected:") + print(" You are not in the 'bluetooth' group") + print("\n To fix this permanently:") + print(f" sudo usermod -a -G bluetooth {os.getlogin()}") + print(" Then logout and login again") + print("\n Or run with sudo:") + print(f" sudo {' '.join(sys.argv)}") + return + except KeyError: + # bluetooth group doesn't exist + print("\n⚠️ Bluetooth access requires root privileges") + print(f" Run with: sudo {' '.join(sys.argv)}") + return + except OSError: + # getlogin() can fail in some environments + pass + + from radiacode.discovery import find_first_radiacode + bluetooth_mac = find_first_radiacode() + if not bluetooth_mac: + print("No RadiaCode devices found. Specify MAC address or use --mock") + return + + reader = DeviceReader(output_path=args.output, mock=args.mock) + await reader.run(bluetooth_mac) + +if __name__ == "__main__": + # Check if we need to restart with sudo for discovery + import os + import sys + import grp + + if '--list' in sys.argv and os.geteuid() != 0: + # Check if user is in bluetooth group + try: + bluetooth_gid = grp.getgrnam('bluetooth').gr_gid + user_groups = os.getgroups() + if bluetooth_gid not in user_groups: + print("⚠️ Permission issue: You are not in the 'bluetooth' group") + print("\nTo fix this permanently:") + print(f" sudo usermod -a -G bluetooth {os.getlogin()}") + print(" Then logout and login again") + print("\nOr run with sudo:") + print(f" sudo {' '.join(sys.argv)}") + else: + print("Note: Bluetooth scanning may require root privileges.") + print(f"If scanning fails, try: sudo {' '.join(sys.argv)}") + except KeyError: + print("Note: Bluetooth scanning requires root privileges.") + print(f"Please run with: sudo {' '.join(sys.argv)}") + except OSError: + print("Note: Bluetooth scanning requires root privileges.") + print(f"Please run with: sudo {' '.join(sys.argv)}") + sys.exit(1) + + asyncio.run(main()) \ No newline at end of file diff --git a/radiacode/discovery.py b/radiacode/discovery.py new file mode 100644 index 0000000..0a2580a --- /dev/null +++ b/radiacode/discovery.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +RadiaCode device discovery utilities +""" +import time +import typing +from bluepy.btle import Scanner, DefaultDelegate, BTLEException + + +class ScanDelegate(DefaultDelegate): + """Delegate to handle BLE scan results""" + def __init__(self): + DefaultDelegate.__init__(self) + + +def discover_radiacode_devices(timeout: float = 5.0) -> typing.List[typing.Tuple[str, str]]: + """ + Scan for RadiaCode devices via Bluetooth LE + + Args: + timeout: Scan timeout in seconds + + Returns: + List of tuples (mac_address, device_name) + """ + devices = [] + + try: + scanner = Scanner().withDelegate(ScanDelegate()) + entries = scanner.scan(timeout) + + for entry in entries: + name = None + # Check for device name in scan data + for (adtype, desc, value) in entry.getScanData(): + if desc == "Complete Local Name": + name = value + break + + # RadiaCode devices typically have names starting with "RadiaCode" or "RC-" + if name and ("RadiaCode" in name or name.startswith("RC-")): + devices.append((entry.addr, name)) + + except BTLEException as e: + # If scanning fails (e.g., no permissions), return empty list + import os + error_msg = str(e) + + if "Permission denied" in error_msg or "Operation not permitted" in error_msg: + if os.geteuid() != 0: + print(f"⚠️ Bluetooth scanning failed: {e}") + print("\nThis is likely a permission issue. Try:") + print(" 1. Run with sudo") + print(" 2. Add your user to the bluetooth group:") + print(f" sudo usermod -a -G bluetooth {os.environ.get('USER', '$USER')}") + print(" Then logout and login again") + else: + print(f"Bluetooth scanning failed: {e}") + else: + print(f"Bluetooth scanning failed: {e}") + if "No such device" in error_msg: + print(" - Check that Bluetooth is enabled") + print(" - Verify Bluetooth adapter is present") + + return devices + + +def find_first_radiacode(timeout: float = 5.0) -> typing.Optional[str]: + """ + Find the first available RadiaCode device + + Args: + timeout: Scan timeout in seconds + + Returns: + MAC address of first device found, or None + """ + devices = discover_radiacode_devices(timeout) + if devices: + mac, name = devices[0] + print(f"Found RadiaCode device: {name} ({mac})") + return mac + return None + + +def list_all_radiacode_devices(timeout: float = 10.0) -> None: + """ + List all RadiaCode devices found + + Args: + timeout: Scan timeout in seconds + """ + print(f"Scanning for RadiaCode devices ({timeout}s)...") + devices = discover_radiacode_devices(timeout) + + if not devices: + print("No RadiaCode devices found.") + print("\nTroubleshooting:") + print("1. Ensure device is powered on") + print("2. Check Bluetooth is enabled on device") + print("3. Disconnect from phone app if connected") + print("4. Try running with sudo") + else: + print(f"\nFound {len(devices)} RadiaCode device(s):") + for i, (mac, name) in enumerate(devices, 1): + print(f" {i}. {name} - {mac}") + + +if __name__ == "__main__": + # Test discovery when run directly + import sys + + if len(sys.argv) > 1 and sys.argv[1] == "--list": + list_all_radiacode_devices() + else: + mac = find_first_radiacode() + if mac: + print(f"First device MAC: {mac}") + else: + print("No devices found") \ No newline at end of file From fd7419a25fbe9cbe3c14d1bf18ababc7551b19a4 Mon Sep 17 00:00:00 2001 From: GlassOnTin Date: Tue, 27 May 2025 00:21:56 +0100 Subject: [PATCH 4/4] Add automatic reconnection logic for device_reader.py - Implement connection state tracking (disconnected/connecting/connected/reconnecting) - Add exponential backoff for reconnection attempts (1s to 60s max) - Enhance error handling to detect Bluetooth disconnections - Add comprehensive logging for connection state changes - Ensure device continues attempting reconnection until manually stopped This improves reliability when Bluetooth connections drop due to: - Device timeouts after prolonged streaming - Bluetooth interference - Device power cycling - Other processes connecting to the device --- device_reader.py | 150 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 35 deletions(-) diff --git a/device_reader.py b/device_reader.py index 1d357f3..dd6f858 100644 --- a/device_reader.py +++ b/device_reader.py @@ -11,6 +11,22 @@ from pathlib import Path from datetime import datetime, timezone from collections import deque +from enum import Enum +import logging + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' +) +logger = logging.getLogger(__name__) + +class ConnectionState(Enum): + DISCONNECTED = "disconnected" + CONNECTING = "connecting" + CONNECTED = "connected" + RECONNECTING = "reconnecting" class DeviceReader: def __init__(self, output_path="/tmp/radiacode_data.json", mock=False): @@ -22,40 +38,87 @@ def __init__(self, output_path="/tmp/radiacode_data.json", mock=False): self.last_spectrum_time = 0 self.spectrum_interval = 30 # Update spectrum every 30s + # Connection state tracking + self.connection_state = ConnectionState.DISCONNECTED + self.bluetooth_mac = None + self.device_serial = None + + # Reconnection parameters + self.reconnect_delay = 1.0 # Start with 1 second + self.max_reconnect_delay = 60.0 # Max 60 seconds + self.reconnect_backoff_factor = 2.0 + + def _set_connection_state(self, state: ConnectionState): + """Update connection state and log changes""" + if self.connection_state != state: + logger.info(f"Connection state: {self.connection_state.value} → {state.value}") + self.connection_state = state + async def connect(self, bluetooth_mac=None): - """Connect to device with retry logic""" - retry_count = 0 - while self.running and retry_count < 3: - try: - if self.mock: - print("Using mock device") - from radiacode_examples.mock_data_generator import MockRadiaCode - self.device = MockRadiaCode() - return True - + """Connect to device""" + self._set_connection_state(ConnectionState.CONNECTING) + self.bluetooth_mac = bluetooth_mac + + try: + if self.mock: + logger.info("Using mock device") + from radiacode_examples.mock_data_generator import MockRadiaCode + self.device = MockRadiaCode() + self.device_serial = "RC-MOCK-000001" + else: from radiacode import RadiaCode if bluetooth_mac: - print(f"Connecting to Bluetooth {bluetooth_mac}...") + logger.info(f"Connecting to Bluetooth {bluetooth_mac}...") self.device = RadiaCode(bluetooth_mac=bluetooth_mac) else: - print("Connecting via USB...") + logger.info("Connecting via USB...") self.device = RadiaCode() - serial = self.device.serial_number() - print(f"✓ Connected to {serial}") - return True + self.device_serial = self.device.serial_number() + + self._set_connection_state(ConnectionState.CONNECTED) + logger.info(f"✓ Connected to {self.device_serial}") + + # Reset reconnect delay on successful connection + self.reconnect_delay = 1.0 + return True + + except Exception as e: + self._set_connection_state(ConnectionState.DISCONNECTED) + logger.error(f"Connection failed: {e}") + return False + + async def reconnect(self): + """Attempt to reconnect with exponential backoff""" + self._set_connection_state(ConnectionState.RECONNECTING) + + while self.running and self.connection_state != ConnectionState.CONNECTED: + logger.info(f"Reconnection attempt in {self.reconnect_delay:.1f} seconds...") + await asyncio.sleep(self.reconnect_delay) + + if not self.running: + break - except Exception as e: - retry_count += 1 - print(f"Connection attempt {retry_count} failed: {e}") - if retry_count < 3: - await asyncio.sleep(5) + if await self.connect(self.bluetooth_mac): + return True + + # Exponential backoff + self.reconnect_delay = min( + self.reconnect_delay * self.reconnect_backoff_factor, + self.max_reconnect_delay + ) return False async def read_loop(self): - """Main reading loop""" + """Main reading loop with automatic reconnection""" while self.running: + if self.connection_state != ConnectionState.CONNECTED: + # Try to reconnect + if not await self.reconnect(): + break + continue + try: # Read real-time data data_points = [] @@ -102,8 +165,9 @@ async def read_loop(self): output = { 'device': { 'connected': True, - 'serial': getattr(self.device, 'serial_number', lambda: 'mock')(), - 'last_update': datetime.now(timezone.utc).isoformat() + 'serial': self.device_serial, + 'last_update': datetime.now(timezone.utc).isoformat(), + 'connection_state': self.connection_state.value }, 'realtime': { 'latest': data_points[-1] if data_points else None, @@ -118,25 +182,42 @@ async def read_loop(self): json.dump(output, f) temp_path.replace(self.output_path) - print(f"✓ Updated {len(data_points)} readings, " - f"latest: {data_points[-1]['count_rate']:.1f} CPS" if data_points else "No new data") + if data_points: + logger.debug(f"Updated {len(data_points)} readings, " + f"latest: {data_points[-1]['count_rate']:.1f} CPS") await asyncio.sleep(1) except Exception as e: - print(f"Read error: {e}") + # Check for specific Bluetooth disconnect error + error_msg = str(e) + if "BTLEDisconnectError" in str(type(e)) or "disconnected" in error_msg.lower(): + logger.error("Bluetooth connection lost") + else: + logger.error(f"Read error: {e}") + + self._set_connection_state(ConnectionState.DISCONNECTED) + + # Close existing connection + try: + if self.device and hasattr(self.device._connection, 'close'): + self.device._connection.close() + except: + pass + self.device = None + # Write disconnected state error_output = { 'device': { 'connected': False, 'error': str(e), - 'last_update': datetime.now(timezone.utc).isoformat() + 'last_update': datetime.now(timezone.utc).isoformat(), + 'connection_state': self.connection_state.value, + 'reconnect_delay': self.reconnect_delay } } with open(self.output_path, 'w') as f: json.dump(error_output, f) - - await asyncio.sleep(5) # Wait before retry async def run(self, bluetooth_mac=None): """Main run method""" @@ -145,22 +226,21 @@ async def run(self, bluetooth_mac=None): for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, self.shutdown) - # Connect + # Initial connection attempt if not await self.connect(bluetooth_mac): - print("Failed to connect to device") - return + logger.warning("Initial connection failed, will keep trying...") - # Run read loop + # Run read loop (handles reconnection) try: await self.read_loop() finally: if self.device and hasattr(self.device._connection, 'close'): self.device._connection.close() - print("Device reader stopped") + logger.info("Device reader stopped") def shutdown(self): """Graceful shutdown""" - print("\nShutting down...") + logger.info("Shutting down...") self.running = False async def main():